Привет! В этой статье я хочу рассказать, как из модного микросервисного приложения можно сделать рабочую, управляемую систему с помощью трех проверенных годами методик: на примере проекта внутренней performance-based рекламы Joom.



The Project


Проект JoomAds предлагает продавцам инструменты продвижения товаров в Joom. Для продавца процесс продвижения начинается с создания рекламной кампании, которая состоит из:


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

Рекламная кампания — это одна из частей состояния рекламного товара (см. Рис. 1), в которое также входят метаданные товара (наименование, доступные варианты, принадлежность к категории, и т.д.) и данные ранжирования (например, оценки эффективности показов).



Рис. 1


JoomAds API использует это состояние при обработке запросов в поиске, каталоге или в разделе «Лучшее» для показа оптимального товара каждому пользователю с учетом региональной доступности и персонализации.


JoomAds API может изменять часть состояния при регистрации покупок успешно прорекламированных товаров, корректируя остаток бюджета рекламных кампаний (Рис. 1). Настройками кампаний управляет сервис кампаний — JoomAds Campaign, метаданными продукта — сервис Inventory, данные ранжирования расположены в хранилище аналитики (Рис. 2).


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



Рис. 2
JoomAds API выступает в роли медиатора данной микросервисной системы.


Pure Microservices equals Problems


Вы можете задать справедливый вопрос: «Есть ли проблема в таком количестве внешних коммуникаций? Всего три вызова...».


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


Быстродействие


Любые внешние коммуникации (например, поход за метаданными товара в Inventory) — это дополнительные накладные расходы, увеличивающие время ответа медиатора. Такие расходы — не проблема на ранних этапах развития проекта: последовательные походы в JoomAds Campaign, Inventory и хранилище аналитики вносили небольшой вклад во время ответа JoomAds API, т.к. количество рекламируемых товаров было небольшим, а рекламная выдача присутствовала только в разделе «Лучшее».


Однако с ростом количества товаров в рекламе и подключением трафика других разделов Joom, 95-й перцентиль времени JoomAds API достиг сотен миллисекунд вместо желаемых десятков. Такая ситуация является результатом несоответствия текущих условий эксплуатации исходным требованиям, использованным при разработке отдельных компонентов.


Например, поиск товаров Inventory не был рассчитан на высокие частоты запросов, но он нам нужен именно таким.


Отказоустойчивость


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


Отказ любой зависимости JoomAds API ведет к некорректной или неповторяемой рекламной выдаче, либо к ее полному отсутствию.


Сложность поддержки


Микросервисная архитектура позволяет снизить сложность поддержки узкоспециализированных приложений, таких как Inventory, но значительно усложняет разработку приложений-медиаторов, таких как JoomAds API.


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


Осложняется отладка приложений-медиаторов в процессе разработки, т.к. для этого требуются либо репрезентативные копии микросервисов, либо точные mock-объекты, поддержка которых тоже требует времени.


Эти наблюдения привели нас к осознанию необходимости изменений. Новый JoomAds должен генерировать экономически эффективную и согласованную рекламную выдачу при отказе JoomAds Campaign, Inventory или хранилища аналитики, а также иметь предсказуемое быстродействие и отвечать на входящие запросы быстрее 100 мс в 95% случаев.


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


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


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


Monolith over microservices (kind of)


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


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


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


Materialization


Совместить лучшие качества микросервисов и монолитной архитектуры нам позволил подход, именуемый Materialized View. Материализованные представления часто встречаются в реализациях СУБД. Основной целью их внедрения является оптимизация доступа к данным на чтение при выполнении конкретных запросов.


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


Например, для запросов состояния продукта по его идентификатору (см. Рис. 3) или запросов состояния множества продуктов по идентификатору рекламной кампании.



Рис. 3


Материализованное представление данных расположено во внутреннем хранилище JoomAds API, поэтому замыкание входящих коммуникаций на него положительно сказывается на производительности и отказоустойчивости системы, т.к. доступ на чтение теперь зависит только от доступности / производительности хранилища данных JoomAds, а не от аналогичных характеристик внешних ресурсов. JoomAds API является надежным монолитным приложением!


Но как обновлять данные Materialized View?


Data Sourcing


Разработку стратегии обновления данных мы начали с формализации требований, которым она должна соответствовать:


  • Изолировать клиентскую сторону от проблем доступа к внешним ресурсам.
  • Учитывать возможность высокого времени ответа компонентов инфраструктуры JoomAds.
  • Предоставлять механизм восстановления на случай утраты текущего состояния Materialized View.

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


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


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


Обработка набора событий конкретного товара в порядке их поступления дает одинаковый результат при идентичном нулевом состоянии — это свойство отлично подходит для реализации плана восстановления Materialized View в случае нештатных ситуаций.


Event Sourcing


Описанный в предыдущем разделе способ управления данными известен под названием Event Sourcing. Он предполагает наличие объектов-событий, содержащих информацию об изменениях в системе с возможностью установления хронологического порядка их наступления, благодаря чему мы можем восстановить состояние системы на определенный момент времени.


В результате адаптации Event Sourcing подхода в инфраструктуре JoomAds появились три новых компонента: хранилище материализованного представления (MAT View Storage), конвейер материализации (Materialization Pipeline), а так же конвейер ранжирования (Ranking Pipeline), реализующий поточное вычисление потоварных score'ов ранжирования (см. Рис. 4).



Рис. 4


Discussion, Technologies


Materialized View и Event Sourcing позволили нам решить основные проблемы ранней архитектуры проекта JoomAds.


Специализированные Materialized View значительно повысили надежность и быстродействие клиентских запросов. Обновление данных с использованием Event Sourcing подхода повысило надежность коммуникации с внешними сервисами, предоставило инструменты контроля консистентности данных и позволило избавиться от неэффективных запросов к внешним ресурсам.


Однако у всех решений есть цена. Чем больше несовместимых классов запросов реализует ваше приложение, тем больше материализованных представлений вам требуется собрать. Такой подход увеличивает потребление ресурсов по памяти, системе хранения данных и CPU. Материализованные представления JoomAds располагаются в хранилище Apache Cassandra, поэтому процесс порождения новых представлений, удаления старых или модификации существующих можно назвать безболезненным.


В нашем случае MAT View целиком хранится в одной таблице Cassandra: добавление колонок в таблицы Cassandra — безболезненная операция, удаление MAT View осуществляется удалением таблицы. Таким образом, крайне важно выбрать удачное хранилище для реализации Materialized View в вашем проекте.


Event Sourcing предъявляет серьезные требования к своим пользователям. Генерация событий изменения данных во всех интересующих подсистемах в заданном формате с возможностью установления хронологического порядка следования — это сложная организационно-техническая задача, которую крайне трудно реализовать в середине жизненного цикла ПО. Можно назвать удачей, что хранилище данных Inventory уже имело функцию генерации событий на обновление метаданных продуктов.


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


Вместо этого мы воспользовались популярными open-source решениями, развивающимися при участии Apache Software Foundation: Apache Kafka и Apache Flink.


Помимо организационно-технологических сложностей, событийно-ориентированным системам свойственно т.н. eventually consistent состояние данных, что может вносить дополнительную сложность в процессы обработки информации.


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


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


Takeaway


Обмен данными на базе событий в совокупности с поточной обработкой позволяют преодолеть несоответствие контрактов различных систем, повысить контроль над отказоустойчивостью и производительностью, но требуют серьезного анализа компромиссов. Кроме того, внедрение event sourcing подхода лучше всего подходит системам, находящимся на ранних стадиях своего развития.


P.S. Этот пост был впервые опубликован в блоге Joom на vc, вы могли его встречать там. Так делать можно.