Real-Time Backend

Fan-out Patterns

Пользователь с 200 млн подписчиков публикует один пост - как система доставляет его всем, не падая?

  • Twitter home timeline: fan-out-on-write для обычных аккаунтов (< 1M фолловеров) через Redis-списки, celebrity-инжекция при чтении. Переход к гибриду после того, как несколько celebrity-твитов буквально клали Redis-кластер.
  • Instagram Feed: асинхронный fan-out через Celery + RabbitMQ при < 10M подписчиков на аккаунт, отдельный путь для mega-influencers. В 2018 году описали переход к гибридной архитектуре в Engineering Blog.
  • Facebook News Feed: эволюция от чистого fan-out-on-read к многоэтапному pipeline (candidate generation -> ML ranking -> delivery). Сейчас это не классический fan-out, а граф вычислений с предрасчётом candidates.
  • Discord guild messages: не персонализированная лента, а broadcast через Elixir-процессы. 800K пользователей в популярных guild получают сообщения через WebSocket fan-out без N inbox-копий.

Fanout Write (Push)

**Fan-out-on-write** - это паттерн доставки контента, при котором запись одного события немедленно распространяется во все целевые хранилища. Когда пользователь публикует пост, система синхронно (или через очередь) записывает его в inbox каждого из N подписчиков.

Twitter использовал именно этот подход для большинства аккаунтов: при публикации твита сервис записывал tweet_id в Redis-список home timeline каждого фолловера. Чтение ленты сводилось к O(1) - просто достать список из Redis.

  • **Запись:** O(N) операций, где N - количество подписчиков
  • **Чтение:** O(1) - лента уже готова, нет join'ов
  • **Latency чтения:** минимальная (< 1 мс из Redis)
  • **Хранилище:** N копий каждого tweet_id во всех inbox'ах

Instagram Fan-out: при публикации фото в 2013 году система помещала событие в очередь, воркеры распараллеливали запись в inbox подписчиков. При 10 млн подписчиков у знаменитости одна публикация порождала 10 млн write-операций.

Twitter публикует твит пользователя с 500K подписчиков через fan-out-on-write. Сколько write-операций в Redis происходит?

Fanout Read (Pull)

**Fan-out-on-read** - обратный паттерн: при публикации данные пишутся один раз, а сборка ленты происходит в момент чтения. Сервис агрегирует посты из всех источников, на которые подписан пользователь, прямо в момент запроса.

Facebook News Feed первоначально строился именно так: при открытии ленты сервер делал fan-out запросы ко всем друзьям и страницам, агрегировал результаты, ранжировал и отдавал. При 200 друзьях - 200 read-запросов на каждое открытие ленты.

  • **Запись:** O(1) - один раз в источник
  • **Чтение:** O(N) join/merge по N подпискам
  • **Latency чтения:** высокая при большом N (параллельные запросы спасают, но не полностью)
  • **Хранилище:** минимальное - один экземпляр каждого поста

Discord использует pull-подход для истории сообщений в guild-каналах. Когда пользователь открывает канал, он читает последние сообщения из одного источника - нет N копий для N участников. Это работает, потому что Discord-каналы - это pull по одному источнику, а не агрегация N лент.

Какой главный недостаток fan-out-on-read при сборке ленты пользователя с 1000 подписок?

Гибридный Fan-out

Ни push, ни pull не работают универсально при наличии пользователей с радикально разным числом подписчиков. Twitter решил это **гибридной стратегией**: fan-out-on-write для обычных аккаунтов (< 1M фолловеров) и fan-out-on-read для celebrities.

Логика разделения: Kylie Jenner с 200M подписчиков - fan-out-on-write означает 200M Redis-операций на каждый твит. Вместо этого: обычные аккаунты пушатся в ленту предварительно, а celebrity-твиты добавляются к уже готовой ленте в момент чтения - injecting at read time.

Instagram применял схожий гибрид при масштабировании до 1 млрд пользователей: аккаунты с большой аудиторией (бренды, знаменитости) хранились отдельно и инжектировались в ленту при чтении, тогда как обычные аккаунты работали через асинхронный push через очереди на базе Celery + RabbitMQ.

  1. Определить порог celebrity (например, 1M фолловеров)
  2. Обычные пользователи - push через очередь при публикации
  3. Celebrity-аккаунты - store once, inject at read
  4. При чтении: merge pre-built timeline + celebrity injections
  5. Кешировать merged result на короткое время (10-30 сек)

Почему Twitter перешёл на гибридный fan-out вместо чистого fan-out-on-write для всех?

Tradeoffs и выбор подхода

Выбор между push и pull определяется соотношением write-amplification vs read-amplification. Каждый паттерн имеет свою точку, за которой начинается деградация.

Discord пошёл другим путём: 800K участников guild видят одни и те же сообщения в канале. Это не персонализированная лента - это единый поток. Fan-out здесь работает через WebSocket broadcast: сервер рассылает событие всем подключённым клиентам через Elixir/Erlang процессы, а не пишет в 800K inbox'ов.

  • **Push (write):** читают часто, пишут редко, N_followers умеренный
  • **Pull (read):** пишут часто, читают редко, N_following умеренный
  • **Hybrid:** неравномерное распределение аудитории (Pareto-распределение фолловеров)
  • **Broadcast (WebSocket):** все видят одно и то же - нет персонализации, нет inbox

Facebook News Feed прошёл эволюцию: от fan-out-on-read (ранние версии) к гибриду с предрасчётом ленты (Multifeed) к полному ML-ранжированию в реальном времени. Сейчас это pipeline: candidate generation (pull) -> feature extraction -> ML ranking -> delivery - это уже не чистый fan-out, а многоэтапный граф обработки.

Fan-out-on-write всегда быстрее для чтения, значит это лучший выбор для любой социальной сети

Fan-out-on-write оптимален только при умеренном N_followers. При celebrity с миллионами подписчиков одна публикация убивает кластер write-операциями.

Write-amplification при fan-out-on-write растёт линейно с N_followers. При 200M подписчиков и 10 твитов в день это 2 млрд write-операций ежедневно от одного аккаунта. Поэтому Twitter, Instagram и Facebook независимо пришли к одному и тому же решению - гибриду с celebrity-threshold.

Сервис социальной сети: большинство пользователей имеют < 500 подписчиков, но 0.1% аккаунтов имеют > 5M подписчиков. Какой fan-out выбрать?

Ключевые идеи

  • **Fan-out-on-write (push):** дорогая запись O(N_followers), дешёвое чтение O(1). Работает до ~1M подписчиков на аккаунт.
  • **Fan-out-on-read (pull):** дешёвая запись O(1), дорогое чтение O(N_following). Работает при умеренном числе подписок и редком чтении.
  • **Гибрид:** celebrity-threshold делит аккаунты на два пути. Обычные - push, знаменитости - pull-injection при чтении. Twitter, Instagram и Facebook пришли к этому независимо.
  • **Broadcast (WebSocket):** отдельный паттерн для публичных каналов без персонализации - Discord, Slack. Не требует N inbox-копий.

Связанные темы

Fan-out паттерны пересекаются с несколькими смежными областями распределённых систем:

  • Message Queues — Async fan-out-on-write реализуется через очереди (RabbitMQ, Kafka) - воркеры разбирают N write-операций без блокировки publisher
  • Caching Strategies — Предрасчитанные ленты (fan-out-on-write) хранятся в Redis. Гибридный fan-out требует merge кешированной ленты с celebrity-инжекцией при чтении
  • WebSocket & Real-time — Discord-style broadcast реализован через WebSocket fan-out: событие рассылается всем подключённым клиентам без записи в inbox

Вопросы для размышления

  • Если бы N_followers одного аккаунта вырос с 1M до 100M - как изменилась бы нагрузка при fan-out-on-write? Что стало бы узким местом первым?
  • Как гибридный fan-out влияет на консистентность ленты: пользователь одновременно видит pre-built timeline и celebrity-инжекцию - могут ли появиться аномалии порядка?
  • Discord обслуживает guild с 800K участниками через broadcast. Что произойдёт, если 10% участников будут offline - нужно ли хранить missed messages и как это меняет архитектуру?

Связанные уроки

  • net-37-load-balancing
Fan-out Patterns

0

1

Войти