В социальной ленте один автор имеет миллион подписчиков. Почему публикация одной записи может создать миллион операций записи?
Это происходит при стратегии fan-out on write: публикация заранее раскладывается в персональные ленты всех подписчиков. Поэтому одна запись автора превращается примерно в миллион операций записи, а не остаётся одной записью, которую читатели находят при запросе.
Такая схема ускоряет чтение, но переносит основную стоимость и нагрузку на момент публикации. Для автора с большой аудиторией обычно применяют гибрид: записи обычных авторов раскладывают заранее, а публикации крупных авторов добавляют в ленты при чтении.
Ранние и высоконагруженные социальные системы столкнулись с противоположными требованиями: лента должна быстро открываться, но число подписок и публикаций постоянно растёт. Если собирать ленту каждого пользователя при каждом чтении, приходится выполнять много операций объединения, сортировки и фильтрации.
Fan-out on write появился как способ заранее подготовить результат чтения. Цена запроса публикации возрастает, зато чтение ленты становится предсказуемым и быстрым, особенно когда чтений значительно больше, чем публикаций.
При публикации запись автора может быть добавлена в отдельные списки идентификаторов для каждого подписчика. Автор с миллионом подписчиков создаёт не одну логическую запись, а до миллиона физических операций, часто ещё с дополнительной индексацией, репликацией и передачей данных между узлами.
Это создаёт несколько рисков: всплеск нагрузки при публикации, рост очередей фоновых задач, увеличение объёма хранения и задержку появления записи у части подписчиков. Если выполнять все операции синхронно в запросе публикации, API может отвечать слишком долго или завершаться по тайм-ауту.
Обратный вариант — fan-out on read — уменьшает стоимость публикации, но делает чтение дорогим: сервис должен находить записи всех авторов, на которых подписан пользователь, объединять их и выбирать последние элементы. При большом числе подписок это ухудшает задержку и увеличивает нагрузку на хранилище.
Сначала в оценку нагрузки включают коэффициент распространения: нагрузка на публикацию зависит не только от числа публикаций, но и от размера аудитории каждого автора. Для автора с миллионом подписчиков одна публикация потенциально порождает миллион элементов распространения; если публикаций десять в секунду, верхняя оценка составит около десяти миллионов таких операций в секунду до учёта повторов, батчинга и оптимизаций.
Практичная архитектура обычно разделяет исходную запись и её распространение. Исходная запись сохраняется один раз, а событие публикации передаётся в очередь. Рабочие процессы порциями обновляют ленты подписчиков, поэтому пользовательский запрос не ждёт завершения миллиона операций.
Для обычных авторов можно использовать fan-out on write, потому что число получателей умеренное. Для авторов с очень большой аудиторией применяют fan-out on read: исходная запись хранится один раз, а при чтении ленты она присоединяется к результату отдельно. Это уменьшает всплеск записи, но усложняет чтение и может потребовать кэширования или специального ранжирования.
Важно сделать распространение идемпотентным. Повторная доставка события не должна создавать дубликат в ленте; для этого используют уникальность по паре «идентификатор пользователя — идентификатор записи» или эквивалентную проверку. Между сохранением исходной записи и распространением обычно возможна задержка, поэтому система должна явно допускать eventual consistency.
Компромисс выбирают по профилю нагрузки. Fan-out on write даёт быстрые и простые чтения, но дорогие публикации и большой объём производных данных. Fan-out on read экономит записи и хранение, но увеличивает сложность чтения, его задержку и требования к кэшам.
В сервисе коротких публикаций обычные авторы имели от десятков до нескольких тысяч подписчиков, но несколько популярных аккаунтов — миллионы. Полное распространение каждой записи в персональные ленты приводило к резким всплескам очереди и задержке публикаций для всех пользователей.
Рассматривались два варианта. Полный fan-out on read снизил бы нагрузку на запись, но сделал бы каждое чтение ленты зависимым от числа подписок и усложнил сортировку. Полный fan-out on write сохранял бы быстрые чтения, но отдельная публикация популярного автора могла породить слишком много фоновых операций.
Выбрали гибридный подход: записи обычных авторов распространялись асинхронно, а записи авторов выше заданного порога аудитории не копировались во все ленты и добавлялись при чтении. Исходные записи хранились отдельно, а результаты для часто читаемых лент кэшировались. Это уменьшило пиковую нагрузку на запись, сохранив предсказуемое чтение для большинства пользователей.
Нет. Запрос должен подтвердить долговременное сохранение исходной записи, а распространение лучше передать асинхронному обработчику. Иначе время ответа зависит от размера аудитории и временной доступности всех затронутых узлов.
При этом асинхронность означает, что публикация может появиться у подписчиков не одновременно. Контракт должен описывать такую задержку, а мониторинг — отслеживать возраст необработанных событий и долю отставших лент.
Очередь сглаживает всплеск, но не уменьшает общее число операций. Если публикации поступают быстрее, чем обработчики успевают распространять их, очередь продолжит расти.
Нужно ограничивать скорость обработки, масштабировать потребителей, использовать пакетные операции и отдельно обрабатывать популярных авторов. Для них смена стратегии на fan-out on read устраняет сам множитель числа подписчиков, а не только временно скрывает его за очередью.
Теоретически да, но стоимость чтения тогда зависит от числа подписок, активности авторов и сложности ранжирования. Сервису придётся получать данные из множества источников, объединять их по времени или оценке релевантности и выдерживать нагрузку при одновременном чтении.
Такой вариант подходит, если публикаций мало, подписок немного или требования к задержке чтения мягкие. При большом числе чтений и строгом времени ответа выгоднее заранее подготовить хотя бы часть ленты и использовать гибридную стратегию для исключительных авторов.