Как мы перебрали бэкенд, и для 20 миллионов юзеров всё прошло гладко

Логотип компании Дзен
Отредактировано

За два года мы переписали инфраструктуру приложения так, что пользователи не заметили перехода. В статье расскажем, как это удалось.

5К открытий6К показов

Дзен — это платформа для создания и просмотра контента. Сотни тысяч авторов публикуют посты, лонгриды, длинные видео и короткие ролики, а умные алгоритмы подстраивают ленту под интересы миллионов пользователей. За два года мы полностью переписали инфраструктуру — и ни юзеры, ни авторы контента не заметили перехода. В статье расскажем, как это удалось.

Что было до

Раньше Дзен состоял из двух бэкендов — так называемые фронт и рекомендер — и балансеров над ними. Фронт принимал весь пользовательский трафик. Нижний, рекомендер, реализовывал систему выдачи и рекомендаций для пользователей контента (500 миллисекунд в 99-м перцентиле) и хранил в себе сотни миллионов документов.

Раз в день мы проводили единые большие релизы, куда все продуктовые команды сгружали тикеты. Такие релизы было долго и сложно тестировать, они часто ломались и откатывались обратно. Приходилось и подстраиваться под другие команды – если в соседней фиче находили баг, ваша также откатывалась. Нужно было ждать следующего релиза.

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

При этом интеграции с разными браузерами и мессенджерами и 20 миллионов активных ежедневных пользователей оказывали нагрузку до 150 тысяч RPS на сервера. И Дзен развивался. Появлялись новые типы контента — статьи и видео разных форматов, — и отдельные команды, каждая со своими продуктовыми требованиями и стеком.

Мы поняли: чтобы поддерживать работу и быстро расти, пора переписывать инфраструктуру.

Что нужно было учесть

Высокая нагрузка, постоянный поток юзеров и нового контента диктовали требования к платформе и нашей работе.

  • Durability. Если пользователь написал и сохранил статью, он должен быть уверен, что через неделю она останется на том же месте. Вне зависимости от того, переписали мы что-то или нет.
  • Доступность. У Дзена есть концепция четырёх девяток. За отчётный период мы должны быть доступны 99,99% времени, то есть можем простаивать примерно 52 минуты в год.
  • Консистентность внутри данных. Если пользователь написал статью, она должна корректно попасть в ленту канала, карусель с подборками новых статей автора, подписки читателя и так далее. И не должна внезапно пропадать из рекомендаций. То есть данные о новой единице контента, подписке или лайке должны консистентно «разойтись» по сервисам, продуктам и счётчикам.
  • Скорость. Во-первых, само приложение, все вкладки и окна должны открываться быстро и плавно. Во-вторых, контент должен выгружаться на платформу как можно быстрее. Для этого мы постоянно:оптимизируем алгоритмы рекомендаций;работаем над новыми классификаторами и скоростью разметки контента;переписываем код и ищем, что можно запускать параллельно.
  • Микросервисы. Мы обслуживаем большой RPS, разрабатываем множество фич — от студии блогера до ленты комментариев — разными командами. И поддерживать всё это на монолите сложно.
  • Железо. Чтобы поддерживать рекомендательный сервис и обрабатывать сотни миллионов документов за миллисекунды, нужно 80 тысяч ядер.
  • Плавность. Все рефакторинги должны были происходить для пользователя бесшовно. Для этого мы выделяли ответственность, ту же геобазу, деплоили её в отдельный сервис и затем переставали хранить эту ответственность в монолите.

Что мы сделали с бэкендом приложения

Выделили ответственности внутри монолита

Ответственность — это часть монолита, которую можно выделить в отдельный сервис или фичу. Например:

  • SSR — набор сервисов, которые рендерят наш сайт;
  • Клиентский API, куда приходит пользователь, чтобы посмотреть или выложить контент, и все взаимодействия с бэкендом по протоколу клиент-сервер;
  • Система авторизации пользователя;
  • Система сбора Big Data о пользователе;
  • Сервис пользователя — сервис, который резолвит пользователя в конкретную страну/город/район и так далее;
  • Рекомендер;
  • Сервис, который реализует A/B-тесты (мы можем проводить тысячи таких одновременно) и фиксирует, какие пользователи в какие эксперименты попадают;
  • Сервис рекламы — отвечает за то, какую рекламу и в каких количествах показать пользователю.
  • И ещё десятки других ответственностей.

В итоге мы получили «пирог», в котором каждый слой — это отдельная ответственность. И начали искать способ этот пирог порезать.

Распилили монолит по этим ответственностям и выделили отдельные сервисы

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

В итоге каждая ответственность расползалась по разным сервисам — как это было раньше — чего мы и пытались избежать.

Затем попробовали делить монолит «по слоям»: вынесли SSR, клиентский API и другие — и получили дерево зависимостей этих ответственностей друг от друга.

На следующем этапе мы переписали дерево. Начали с «листьев». Например, сервис пользователя не имеет своих зависимостей, а является только зависимостью для других сервисов. Мы вынесли её, переписали на новый стек, создали под неё отдельный сервис и выдали его отдельной команде.

И затем пошли вниз, постепенно выделяя и переписывая те ответственности, которые зависят только от того, что мы уже вынесли в отдельный сервис. Так, у нас получилось примерно 200 сервисов.

Часть сервисов — например, авторизацию — пришлось писать «сбоку», и потом постепенно переносить коммиты.

Чтобы организовать работу, мы прописали полноценный роадмап и указали майлстоуны: сегодня вынесли сервис пользователя, через месяц — авторизацию, ещё через месяц — рекомендер. А также проводили регулярные встречи, где фиксировали, на каком этапе распил монолита, какие сервисы будут к концу недели и так далее.

Прописали взаимодействие между сервисами

Для этого использовали Service Mesh и его реализацию Apphost, написанную Яндексом. У неё несколько плюсов.

  1. Apphost включает декларативный конфиг зависимостей между сервисами. То есть у него есть граф зависимостей, который получает request пользователя на входе и выдаёт response на выходе. Между ними — облако выделенных нами сервисов, которые зависят друг от друга. Описание графа находится в конфиге Service Mesh нашего Apphost и описано декларативно.
Как мы перебрали бэкенд, и для 20 миллионов юзеров всё прошло гладко 1
  1. Apphost ходит в бэкенд приложения, минуя слой балансеров, которые роутят трафик (тех же сайдкаров с HTTP-прокси, через которые сервисы ходят друг в друга). Это даёт преимущество в latency, скорости и надёжности.
  2. Форсятся правильные политики отказоустойчивости. И разработчику не нужно указывать, с какими ретраями, с каким таймаутом он будет ходить в другой сервис. Так, исключается каскадное падение системы, и никто не может её задедосить (разве что сам Apphost).
  3. Появился сервисный трейсинг запроса. Благодаря ему мы видим, в какой момент упало выполнение запроса и что конкретно отвалилось. К тому же в микросервисах без него невозможно расследовать инциденты и дебажить.

Что нам это дало

Мы перешли на архитектуру Service Mesh, выделили сервисы, отдали их продуктовым командам. Так, любой из 200 сервисов смог релизиться отдельно, может быть выкачен и не заблочится на релиз другого сервиса. Также у нас поддерживается обратная совместимость в протоколе их взаимодействия друг с другом.

Благодаря такому распилу мы смогли сделать чанковую загрузку сайта (HTTP Chunked Encoding). Поисковая строка, рекомендательная лена подгружаются на главную страницу dzen.ru отдельными чанками, за каждый из которых отвечает свой бэкенд. И пока подгружаются тяжёлые рекомендации, более лёгкие элементы сайта уже доступны пользователю. И отображение в целом происходит быстрее, что положительно сказывается на UX пользователя.

Поначалу копаться в бэкенде приложения было сложно, потому что, по сути, это — большой пласт легаси. В нём оставались наслоения логики, которые складывались годами, невозможно было выяснить, почему конкретный if делает то, что делает. И чтобы убрать этот if, три строчки кода, мы тратили полтора часа на анализ логов.

Но чем больше сервисов мы выносили из монолита, чем больше задач решали вне его, тем проще было работать. Потому что в нём остались только legacy-ручки для старых клиентов, старые приложения, которые мы редко используем и редко коммитим.

К тому же помогала мотивация и желание сделать что-то крутое и полезное из большой и тяжёлой структуры.

Следите за новыми постами
Следите за новыми постами по любимым темам
5К открытий6К показов