Ctrl K
Урок

Разбор: лента новостей

Интуиция: почтальон или киоск

Представь газеты. Можно доставлять их почтальоном: как только выпуск напечатан, его раскладывают по ящикам всех подписчиков. Утром ты открываешь ящик, и всё уже там. Это push, или fan-out on write. А можно ходить в киоск: ты приходишь, и продавец собирает тебе свежие номера всех изданий, на которые ты подписан. Это pull, или fan-out on read.

Почтальон хорош, пока у издания тысяча подписчиков. Если подписчиков 50 миллионов, он не успеет разнести выпуск. С этой разницы и начинается весь кейс.

Сначала цифры

На собесе сразу прикинь масштаб. Без него выбор между push и pull не обоснуешь.

Параметр Оценка
DAU 300 млн
Новых постов в день 100 млн → ~1 200 постов/с
Средний подписчик-граф 200 подписчиков на автора
Записей в ленты при push 1 200 × 200 ≈ 240 тыс. вставок/с
Открытий ленты 300 млн × 10 в день ≈ 35 тыс. чтений/с
Пост звезды (50 млн подписчиков) 50 млн вставок на один пост

Чтений ленты много, и пользователь ждёт ответа за ~100–200 мс. Поэтому хочется отдавать ленту готовой. Но звёзды делают push взрывным.

Fan-out on write (push)

Пост сохраняется, и событие уходит в очередь. Воркеры берут из графа подписок список подписчиков и дописывают post_id в ленту каждого из них в Redis. При чтении сервис ленты берёт готовый список id и подтягивает тексты постов.

HTTPSновый постGET /feedсохранитьсобытиекто подписан?LPUSH в лентычитать idгидрацияКлиентклиентБалансировщикLBPost ServiceсервисFeed ServiceсервисОчередь событий постовочередьPosts DBБДFan-out воркерыворкерГраф подписокБДRedis: ленты (списки post_id)кэш
Push: работа делается при записи, чтение — дешёвое
  • Плюсы: быстрое чтение, простая выдача.
  • Минусы: дорогая запись. У звёзд она катастрофически дорогая. Ленты неактивных пользователей тоже обновляются, а это зря потраченная работа.

Fan-out on read (pull)

Ничего не раскладываем заранее. Когда ты открываешь ленту, сервис берёт список тех, на кого ты подписан, достаёт последние посты каждого автора и сливает их по времени (merge K отсортированных списков).

GET /feedна кого подписанпосты N авторовпромах кэшаКлиентклиентБалансировщикLBFeed Service (merge)сервисГраф подписокБДRedis: последние посты авторакэшPosts DBБД
Pull: запись дешёвая, вся работа при чтении
  • Плюсы: запись стоит O(1), никакой лишней работы, звёзды не страшны.
  • Минусы: чтение тяжёлое. Если подписок 500, нужно 500 обращений и merge, и всё это на каждые 35 тыс. запросов в секунду. Растёт задержка.

Гибрид: так делают на практике

Порог задаём по числу подписчиков, например 10 тыс.:

  • Обычные авторы идут через push: их посты раскладываются по лентам заранее.
  • Звёзды идут через pull: их посты лежат только в кэше «последние посты автора».
  • При чтении берём предвычисленную ленту из Redis, добавляем к ней свежие посты звёзд, на которых ты подписан (их обычно единицы), сливаем и отдаём.

Совет: скажи вслух «push для большинства, pull для звёзд, merge при чтении». Интервьюер почти всегда ждёт именно этой фразы и вопроса про порог.

Сюда же отнеси неактивных пользователей. Тому, кто не заходил месяц, ленту не раскладываем, а при входе собираем её через pull.

Хранение ленты в кэше

  • В Redis храним только id постов, а не тексты: feed:{user_id} → список из ~800 последних post_id.
  • Объём: 8 байт × 800 ≈ 6,4 КБ на пользователя. Для 300 млн пользователей это ~2 ТБ сырых данных, а с накладными расходами Redis заметно больше. Поэтому держим ленты только активных и шардируем по user_id.
  • Тексты, лайки и авторов подтягиваем отдельно (гидрация) из кэша постов. Так правка поста не требует переписывать миллионы лент.

Пагинация курсором

Пагинация через offset здесь ломается. Пока ты листаешь, сверху появляются новые посты, всё съезжает, и ты видишь дубли. Вместо этого передаём курсор: последний увиденный post_id, или пару «время + id».

GET /v1/feed?limit=20&cursor=eyJsYXN0X2lkIjoxODQ0Njc0fQ
→ 200 { "items": [...], "next_cursor": "eyJsYXN0X2lkIjoxODQ0NjU0fQ" }

Курсор стабилен при вставках сверху, и запрос «дай 20 постов старше X» дешёвый.

Ранжирование кратко

Хронология — базовый вариант. Ранжированная лента устроена в две ступени. Сначала собираем кандидатов: те же push и pull, несколько сотен постов. Затем скорим их моделью по признакам: близость к автору, вовлечённость, свежесть, тип контента. Скоринг можно делать при чтении. Есть и альтернатива: периодически пересчитывать ленту офлайн. Компромисс тут в том, что чем умнее модель, тем дороже каждое чтение.

Частые ошибки

  • Выбрать push и забыть про звёзд.
  • Хранить в ленте полные посты вместо id.
  • Пагинировать через offset.
  • Не упомянуть очередь: fan-out синхронно в запросе на публикацию блокирует автора.
  • Строить ленты всем пользователям, включая «мёртвых».

Твоя очередь

Интервьюер спрашивает: «Хорошо, гибрид. А что будет, если обычный автор внезапно набрал 2 млн подписчиков за сутки? И как ты обработаешь удаление поста, который уже разложен по лентам?»

Ответь ей сам. Продумай три вещи:

  1. Как и когда автор переключается между push и pull.
  2. Что происходит с уже разложенными id при удалении поста: чистить ленты или фильтровать при гидрации.
  3. Какую согласованность ты здесь готов принять.