Кейс: новостная лента (news feed)

Лента ваших подписок — задача про то, как собрать персональный список постов для миллионов людей быстро.

Снова шаблон: требования → оценки → схема → ключевой компромисс → узкие места. Сердце задачи — стратегия формирования ленты.

1. Требования

Функциональные: опубликовать пост; подписаться на пользователя; видеть ленту постов тех, на кого подписан, в обратном хронологическом порядке. Вне скоупа: реклама, личные сообщения, сложное ранжирование.

Нефункциональные: лента грузится быстро (p99 < 200 мс); чтений ≫ записей; допустимо небольшое отставание (лента может обновиться через пару секунд — eventual ок); высокая доступность.

2. Оценки масштаба

300 млн DAU, каждый открывает ленту 10 раз/день
Чтений ленты: 3 млрд / день ≈ 35 000 QPS (пик ~70 000)
Публикаций: каждый постит ~ раз в день → 300 млн / день ≈ 3500 QPS
→ чтений в ~10 раз больше; оптимизируем чтение ленты

3. Высокоуровневая схема

[клиент] → [LB] → [сервис ленты] ─┬─ [сервис постов + БД постов]
                                  ├─ [сервис графа подписок]
                                  └─ [кэш лент (Redis): user → список post_id]

4. Главный компромисс: где собирать ленту

Вопрос на миллион: формировать ленту в момент записи поста или в момент чтения ленты?

АспектFan-out on write (push)Fan-out on read (pull)
Когда работаемпри публикации: разносим пост в ленты всех подписчиковпри открытии ленты: собираем посts тех, на кого подписан
Чтение лентыочень быстро (уже готова)медленнее (собираем на лету)
Публикациядорого (миллион вставок у популярных)дёшево (одна запись)
Памятьмного (ленты материализованы)экономно
Проблемазнаменитости с миллионами подписчиковмедленное чтение для активных

Поскольку чтений в 10 раз больше, базовый выбор — fan-out on write: при публикации кладём post_id в кэш-ленту каждого подписчика, и открытие ленты становится простым чтением готового списка.

5. Проблема знаменитостей и гибридный подход

У fan-out on write есть боль: если у пользователя 50 млн подписчиков, один его пост порождает 50 млн вставок — это и долго, и дорого. Решение — гибрид:

  • обычные пользователи — push (разносим пост по лентам подписчиков заранее);
  • знаменитости — pull (их посты не разносим; при чтении ленты подмешиваем свежие посты тех немногих знаменитостей, на кого подписан читатель).

Так мы не делаем миллионы вставок на каждый пост звезды и не замедляем чтение для всех. Это типичный приём «лучшее из двух миров» под конкретную асимметрию нагрузки.

6. Модель данных ленты в кэше

{
  "feed:user-42": ["post-9001", "post-8997", "post-8990"],
  "post-9001": {"author": "user-7", "text": "...", "ts": 1718380800}
}

В ленте храним только id постов (список), сами посты — отдельно. Имя и аватар автора денормализуем в пост, чтобы при отрисовке не ходить за ними отдельно (лента читается миллионы раз).

7. Узкие места

Узкое местоРешение
Огромный fan-out у звёздгибрид push/pull
70 000 QPS чтенияленты в Redis, готовы заранее
Разнос постов асинхроннопубликация → событие в очередь → воркеры разносят (eventual ок)
Объём лент в памятихранить только последние N постов на пользователя

Итог

  • Главный компромисс ленты — fan-out on write (быстрое чтение, дорогая запись) против fan-out on read (наоборот).
  • При перевесе чтений берут push, но для знаменитостей переходят на pull — гибрид.
  • Разнос делают асинхронно через очередь; данные автора денормализуют в пост.
Проверьте себя
1. В чём суть fan-out on write (push) при формировании ленты?
AЛента собирается в момент чтения из постов подписок
BПри публикации пост сразу разносится в готовые ленты всех подписчиков — чтение становится быстрым
CПосты вообще не сохраняются
DЛента формируется случайно
2. Какую проблему fan-out on write решают гибридным подходом?
AЛента слишком короткая
BЗнаменитости с миллионами подписчиков порождают слишком много вставок на каждый пост
CНевозможность хранить посты
DСлишком медленное чтение у всех
3. Почему имя и аватар автора денормализуют прямо в пост?
AЧтобы сэкономить место
BЧтобы при отрисовке ленты не делать отдельный запрос за автором — лента читается миллионы раз
CПотому что иначе пост не сохранится
DЧтобы усложнить запись без причины
4. Почему разнос постов по лентам делают асинхронно через очередь?
AЧтобы публикация была мгновенной для автора, а тяжёлый разнос шёл в фоне (небольшое отставание допустимо)
BПотому что синхронно это сделать невозможно
CЧтобы потерять часть постов
DЭто требование базы данных
Поддержать проект