Real-Time Backend
Activity Feeds
Instagram открывается за 300 мс. За это время система должна показать персонализированную ленту из сотен событий от сотен подписок, отсортированных по релевантности, а не по времени. Как отобрать и отранжировать нужные события за миллисекунды?
- **Instagram:** pre-computed inbox в Cassandra с партицированием по user_id. Глубина хранения - 1000 событий на пользователя с автоматическим TTL-удалением более старых. Гибридный fan-out с порогом ~10k followers.
- **Facebook:** EdgeRank (затем ML-модель) ранжирует события по affinity, weight, time decay. Переход на ML поднял вовлечённость на 16% - алгоритм понял паттерны, которые люди не заложили явно.
- **GitHub:** агрегирует события в ленте по 1-часовым окнам. Коммиты в один репозиторий за час сворачиваются в одну строку. Снижение числа строк в ленте в 10-20x для активных разработчиков.
- **Pinterest:** динамический threshold для fan-out - граница между push/pull меняется в реальном времени в зависимости от текущей нагрузки системы, а не фиксирована на 10k.
Feed Timeline Architecture
Activity feed - это персонализированная лента событий: 'Иван лайкнул ваше фото', 'Мария начала подписываться', 'Команда закрыла задачу'. В отличие от notification fan-out (одно событие -> N уведомлений), feed работает в обратную сторону: один пользователь открывает ленту и видит агрегированные события от N источников. Это фундаментальное различие определяет всю архитектуру.
Instagram хранит activity feed в Cassandra с партицированием по user_id. Каждый пользователь имеет свой 'inbox' - список событий, предварительно записанных при action других пользователей. Глубина хранения - последние 1000 активностей на пользователя. Более старые автоматически удаляются через TTL.
Пользователь подписан на 5000 аккаунтов. При открытии ленты используется flat storage без pre-computation. Что произойдёт с производительностью?
Aggregation событий в ленте
Без агрегации лента превращается в спам: 'Иван лайкнул фото', 'Иван лайкнул фото', 'Иван лайкнул фото' - 50 строк подряд. Aggregation сворачивает однотипные события в одно: 'Иван и ещё 49 человек лайкнули ваше фото'. Это и UX-улучшение, и экономия трафика.
GitHub агрегирует события в ленте активности по окну 1 час. Коммиты в один репозиторий за час сворачиваются в 'X pushed Y commits to repo'. Это снижает количество строк в ленте в 10-20x для активных разработчиков. Алгоритм агрегации хранится на бэкенде, фронтенд получает уже свёрнутые данные.
Временное окно агрегации - критический параметр. Слишком узкое (5 мин) - пользователь видит дубли, если несколько людей лайкнули подряд. Слишком широкое (24 часа) - агрегат устаревает, актуальные события теряются в общей куче. Практика: 1-4 часа для социальных событий, 24 часа для сводных дайджестов.
50 пользователей лайкнули одно фото за 30 минут. Как правильно отобразить это в ленте владельца фото?
Ranking - хронология vs relevance
Простая хронология (reverse chronological) - честная, предсказуемая, но неэффективная. Если пользователь не заходил 3 дня, он видит кучу старых событий и пропускает важное. Ranking - ранжирование событий по relevance score - показывает сначала то, что реально важно пользователю, независимо от времени.
Facebook EdgeRank (сейчас ML-модель) использовал три фактора: affinity (близость), weight (тип события), time decay (время). После перехода на ML-ранжирование вовлечённость пользователей выросла на 16%. Instagram при переходе от хронологической к алгоритмической ленте в 2016 году: пользователи стали видеть на 50% больше постов от людей, с которыми реально взаимодействуют.
В алгоритмической ленте пользователь не видит посты от лучшего друга, зато видит вирусные посты от малознакомых людей. В чём проблема алгоритма?
Feed Fan-out стратегии
Feed fan-out решает ту же задачу, что и notification fan-out, но с другими компромиссами. При записи (push) - быстрое чтение ценой дорогой записи. При чтении (pull) - медленное чтение, но дешёвая запись. Выбор зависит от соотношения reads/writes и распределения размера аудитории.
Pinterest использует гибридный подход с интересным twist: fan-out на запись для пинов популярных досок, fan-out на чтение для нишевых авторов с малой аудиторией. Threshold определяется динамически на основе текущей нагрузки системы, а не фиксированным числом подписчиков.
Для activity feed достаточно одной стратегии fan-out - выбрать push или pull и применять для всех пользователей.
Гибридная стратегия обязательна при наличии пользователей с сильно различающимся числом подписчиков. Один threshold (обычно 10k followers) разделяет аккаунты на push и pull категории.
Обычный пользователь с 200 подписчиками и знаменитость с 10M создают принципиально разные нагрузки на систему. Единая стратегия либо убьёт запись (push для знаменитостей), либо сделает чтение медленным (pull для всех).
У пользователя 50k подписчиков. При публикации поста используется fan-out на запись. Сколько записей в БД создаётся?
Итоги
- **Pre-computed inbox ускоряет чтение**: хранить готовый feed каждого пользователя вместо merge при запросе - классический трейдоф write amplification vs read speed, который выбирают Instagram и Facebook.
- **Aggregation убирает шум**: 50 лайков на одно фото → одна строка 'Иван и ещё 49 лайкнули', временное окно 1-4 часа - стандарт индустрии для социальных событий.
- **Гибридный fan-out обязателен**: push для обычных пользователей, pull для знаменитостей - единая стратегия ломается при неоднородном распределении аудитории.
Связанные темы
Activity feeds сочетают несколько паттернов из разных областей:
- Notification Fan-out — Та же задача распределения событий по N получателям, но feed строит персонализированную ленту, а не отправляет push-уведомления
- Cassandra / Wide-column stores — Cassandra с партицированием по user_id - стандартное хранилище для pre-computed feed благодаря O(1) чтению по partition key
- Redis Sorted Sets — Sorted Set по score = timestamp используется для хранения feed в кеше с автоматической сортировкой и эффективным range-запросом
Вопросы для размышления
- Пользователь удалил пост. Как эффективно удалить его из pre-computed feed всех подписчиков, у которых этот пост уже присутствует?
- Как реализовать 'видел/не видел' для событий в ленте при гибридном fan-out, где часть событий берётся из pull-запроса в реальном времени?
- LinkedIn показывает и хронологическую, и алгоритмическую ленту. Как хранить одни данные, но поддерживать оба режима отображения?