Эволюция архитектуры проекта. Из монолита в микросервисы

Kate

Administrator
Команда форума
Здравствуйте, меня зовут Денис, и сейчас я работаю на позиции Team Lead в компании Fiverr. Уже 10 лет я занимаюсь разработкой веб-сервисов. За это время я участвовал в техническом развитии нескольких крупных компаний, таких как криптобиржа EXMO, автоматизировал работу складов компании Westwing и так далее.

У каждой компании своя история масштабирования и свой период времени для этого. Чем быстрее развиваемся, тем сложнее перестроиться из-за нехватки времени. Для начала нужно понять, каким образом вести масштабирование. На примере компании Fiverr я бы хотел разобрать эти шаги для бэкенд-части. Статья может быть полезной бэкенд-разработчикам, которые планируют расширять свой проект или интересуются, как работают компании с большим количеством трафика.

Шаг 1. Монолит. Кеширование​

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

Но при росте у монолита много недостатков. Например, логики приложения постепенно становится все больше, тесты начинают идти вечность. В какой-то момент разработчики меняют общую часть кода и должны поддержать это изменение еще в 20 местах, а тесты больше не проходят, и их тоже нужно чинить.

Хорошо, кстати, если тесты кто-то вообще писал. В этот момент далеко не все проджект-менеджеры понимают, почему такая простая задача занимает столько времени. Или в приложении начинает тормозить один участок, и приходится выливать несколько инстансов, чтобы распределить нагрузку именно на него, хотя для всего проекта это не нужно.

Как было в Fiverr​

Мы начали создание сайта как монолитного приложения Ruby on Rails, поддерживаемого базой данных MySQL.

По мере того как сайт набирал популярность, а трафик увеличивался, необходимо было добавлять дополнительный слой кеширования в виде Memcached и Rails Action кэширования. Чтобы избежать вычислений данных для поиска и просмотра каталогов в реальном времени, были добавлены некоторые задания cron, в которых для разогрева кеша попадала заранее подготовленная информация.

image1_q5YImJX.png
Ранняя архитектура, состоящая из Rails, MySQL и Memcached

Но рост и масштабирование имеет свою стоимость, и в течение следующих 3 лет увеличение трафика заставило акцентироваться на оригинальной архитектуре.

Монолитная природа RoR означала, что любая ошибка или авария приведет к падению сайта, в то время как наличие единой базы данных в основном подразумевало, что с течением времени и по мере роста данных и объединения таблиц становилась все более и более продолжительной сборка для все большего количества запросов, и система стала работать медленнее с течением времени, заставляя каждого пользователя синхронно ждать получения, обновления и загрузки страниц. Беспорядок.

Добавим к этому растущую инженерную организацию с постоянно увеличивающимся количеством команд, и вы получите очень сложную в обслуживании кодовую базу.

Шаг 2. Сделать из монолита микросервисы​

Если же не разделять монолит, то хотя бы новый функционал нужно писать в микросервисе. Один из исходов событий, который мне нравиться больше всего, это когда разработчик говорит проджекту: «Давай я сделаю эту задачу в отдельном проекте, и это будет раза в 2 быстрее». Конечно, он согласится, а разработчик заодно попробует еще новую технологию или хотя бы будет работать в чистом проекте без легаси-кода. По ощущениям это как пленочку с телефона снять.

Также микросервисы становиться намного легче масштабировать горизонтально.

Шаг 3. Выделить отдельные задачи, которые можно передать в асинхронную обработку​

Если пользователю не нужно показать какие-то данные сразу после ответа сервера, то эту задачу в большинстве случаев можно поставить в очередь и сделать потом. Отправка email, создание отчетов и, возможно, даже обработка всего респонса целиком.

Fiverr в свое время объединил шаги № 2 и № 3.

Наряду с большой шумихой вокруг микросервисов мы начали детские шаги в путешествии, чтобы сломать монолит.

При разработке первого микросервиса были важные понятия, которые мы хотели продвинуть:

  • Простота начальной разработки — наша команда инженеров на бэкенде говорила на Ruby, и мы полюбили его за простоту и удобство для разработчиков. Мы хотели сохранить Ruby в качестве нашего языка (который позже эволюционировал в добавление Golang в наш стек, но об этом чуть позже).
  • Вирусное решение — усилия по масштабированию начинаются в первую очередь с инженеров, и мы должны были убедиться, что наша команда создает решение, которое является одновременно и глубоко стабильным, надежным, масштабируемым, и вирусным — это означает, что его принятие среди инженеров будет простым.
  • Асинхронный обмен сообщениями между сервисами — по мере увеличения трафика возникла необходимость в быстром и отзывчивом сайте, и мы поняли, что, позволяя пользователю ждать синхронного завершения обновлений, замедляли не только его сессии, но и всю платформу. Так как меньше запросов может быть обработано в то время, как другие пользователи используют сессию.

Шаг 4. Добавить события​

Когда сервисов становится много и им нужно оперировать общими данными, то лучше всего передавать в очереди сами события о том, что произошло в системе, а микросервис уже сам решит, что ему делать с этими данными. Заодно в случае ошибки можно повторно отправлять событие на обработку.

Как это было в Fiverr​

image3_GLK7iTY.png

Мы создали «химеру», вдохновленную мифологическим двуглавым зверем, которая представляет собой Ruby-шаблон микросервиса, сочетающий в себе:

  • READ SIDE Grape API — для получения данных из ограниченного контекста, представленного сервисом, если это информация о пользователях, ценах на билеты или аналитика конкретного заказа.
  • WRITE SIDE RabbitMQ consumer — прослушивание темы сообщения и асинхронное выполнение обновлений модели ограниченного контекста сервиса.
«Химера» также пользуется:

Shared Core — так как обе «головы» находятся в одном репозитории, они наслаждаются повторным использованием кода и делятся бизнес-логикой домена, утилитами и так далее.

image2_ifZIUY4.png


Общий набор коннекторов БД — переход на микросервисы инициировал использование специфических для сервиса баз данных, таких как MongoDB или Redis, которые мы сейчас широко используем. Сохраняя источник истины в виде реляционного кластера MySQL, мы хотели продвигать каждый сервис для доступа к собственной базе данных, оптимизированной для домена, и нам нужны были утилиты для упрощения подключения ко всем этим различным хранилищам.

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

  1. Пользователь выполнит POST для создания заказа.
  2. REST API будет проверять запрос во время сеансов, пока ничего не сохраняя в базе данных. Это выполняется очень быстро, так как валидация поверхностная и включает в себя базовые входные валидации, а также некоторые проверки целостности данных с БД, которые выполняются без какой-либо тяжелой обработки.
  3. В этот момент, если валидация пройдена, в почтовый брокер (RabbitMQ) посылается сообщение с ключом маршрутизации, указывающим на собственного работника «химеры». Сообщение содержит всю информацию, переданную методом POST, и будет выполняться асинхронно, а пользователю не нужно будет ждать.
  4. Затем пользователю возвращается быстрый 201 статус, и он может сразу же продолжить использование платформы.
  5. Сообщение получает потребитель. RabbitMQ (работник) «химеры» имеет доступ к той же модели домена и может использовать его для выполнения команды и сохранения порядка в БД.
После представления «химеры», которая является разновидностью микросервиса, наша обновленная архитектура бэкенда выглядит примерно так:

image5_9mXwFUZ.png


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

Как показано на этой иллюстрации, новая шина обмена сообщениями использовалась для двух типов событий:

  • Командные события — события, посылаемые «химерой» внутрь своего работника (потребителя RabbitMQ) для асинхронной обработки обновлений.
  • Доменные события (Domain Events) — события, посылаемые химерой в подшаблоне pub любой другой химере, зарегистрированной для их прослушивания, информируя систему о том, что что-то случилось внутри ограниченного контекста «химерой».

Шаг 6. CQRS-подход и Event Sourcing​

Теперь, когда в системе появились события, можно создавать отдельные хранилища со своей структурой. А если хранить историю всех событий, можно в любой момент построить наполненную базу данных или заново обработать ошибки. Или даже совершить откат базы данных до любого момента времени! Это технологически затратный процесс, но он дает неоспоримое преимущество в критических ситуациях, особенно в финансовом секторе.

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

По сути, CQRS предназначен для создания оптимизированных для чтения представлений с целью повышения производительности приложений.

Рассмотрим один сценарий использования CQRS, который нам очень пригодился — аналитика! Простым примером для демонстрации будет приборная панель заказов продавцов:

image4_X99xkFr.png


Получение общего количества заказов на разных этапах потребует простой группы по команде в SQL. Но, когда дело доходит до больших масштабов, мы не хотим каждый раз при обновлении приборной панели «Управление продажами» выполнять такой объемный запрос для каждого из продавцов. CQRS на помощь!

image7_1qSdIZF.png


Получение статистики заказа теперь уменьшено с o(n) до o(1). Прекрасно!

CQRS позволяет обновлять контекст ограниченных заказов, сохранять изменение статуса заказа в реляционной таблице MySQL, затем, используя доменное событие, информирующее любой заинтересованный компонент системы об этом изменении, мы фиксируем изменение в совершенно ином ограниченном контексте — аналитической «химере». И обновляем наш прочитанный оптимизированный MongoDB-документ, увеличивая конкретное значение bucket.

Эту модель мы продолжаем использовать для масштабирования и оптимизации производительности, которые мы проводим на протяжении всего пути в мире микроуслуг.

Итак, наша первая эволюция платформы в итоге выглядела так:

image6_0r1I4w1.png


Вывод​

Конечно, у такой архитектуры есть свои проблемы — это более сложная инфраструктура, более сложное взаимодействие сервисов, а распределенные транзакции — это еще то веселье.

Зато с таким подходом компания может быть готова к рекламе на Super Bowl, внезапному росту биткоина до 60К, порождающему массовые торги, увеличению количества товара на складе в несколько раз за ночь, огромному баннеру на главной странице App Store. А это все реальные истории. Когда выпадает шанс вырасти в несколько раз, нужно быть готовым, чтобы продукт это выдержал.

 
Сверху