Кейс: новостная лента (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 — гибрид.
- Разнос делают асинхронно через очередь; данные автора денормализуют в пост.