Архитектурные паттерны в распределенных высоконагруженных системах

Kate

Administrator
Команда форума
Всякая сложная инфраструктура, поступательно развивавшаяся на протяжении длительного времени, содержит в себе набор разных архитектурных неоптимальностей, а то и откровенных недостатков. Порой эти недостатки становятся неожиданным препятствием для внедрения новых сервисов. Инфраструктура М.Видео-Эльдорадо в этом отношении не является исключением, в чем мы признаемся без излишней рефлексии. Но что с этим делать? Как сделать систему надежной и пригодной для дальнейшего развития? За ответами мы пришли к Александру Алехину, директору по развитию ИТ архитектуры.

Масштабируемость​

Масштабировать систему можно по нескольким традиционным направлениям. Инженеров интересует в первую очередь масштабируемость нагрузки при одновременной экономии ресурсов.
На самом же деле этот термин шире и включает в себя т.н. «масштабируемость поколений» (Generation scalability) – способность системы принимать в себя будущие новые компоненты, которые сегодня еще не существуют и не описаны. Сюда же можно отнести «гетерогенное масштабирование» (Heterogeneous scalability) – возможность взаимодействия с другими архитектурами и компонентами, например, продуктами сторонних производителей или разработчиков.
Масштабируемость нагрузки принято разделять на вертикальную и горизонтальную. При вертикальной мы добавляем процессоры, оперативную память, накопители, маршрутизаторы и пр. На этом пути процесс в итоге упирается в расходы на аппаратные компоненты и их эксплуатацию. Это вполне работоспособная схема, но эффективна она лишь до тех пор, пока стоимость наращивания ресурсов для выполнения бизнес-процессов не начнет превышать экономический эффект от их внедрения.
По модели вертикального масштабирования часто развиваются, например, базы данных, которые невозможно разбить на разделы или каким-то образом шардировать. Такие монолитные системы проще в создании, их удобно применять в процессах «пилотирования», так как не нужно задумываться о тонкостях архитектуры, а надо лишь проверить функциональность.
Более современный подход к архитектуре совсем иной и построен вокруг понятия микросервисов. Самой важной особенностью микросервисов является их практически неограниченная способность к горизонтальному масштабированию. Если архитектура реализована качественно, то при росте нагрузки достаточно просто добавлять в систему однородные вычислительные ресурсы, что обходится значительно дешевле, чем в случае с вертикальным масштабированием.
3385da4992e9df80041a73d8c4237c5f.png

Дьявол в деталях​

К сожалению, на практике все оказывается не так просто. Неконтролируемый рост количества микросервисов, создаваемых под любую функциональность, в конце концов приводит к тому, что перед нами оказывается неуправляемая «вещь в себе». Она запросто может оказаться даже еще более сложной в управлении, чем «монолит». Такая инфраструктура даже получила собственное название – «микролит» (microlith). Не допустить этого помогают правильные подходы к реализации микросервисной архитектуры.
Когда мы движемся от «монолита» к слабосвязанным микросервисным архитектурам, можно выделить три основные «оси масштабирования». Первая – функциональная декомпозиция. Именно растаскивание функциональности на отдельные процессы, обеспечивает возможность масштабирования в будущем.
Когда функциональное разделение завершено, можно уже говорить о «масштабировании экземпляров». Если нагрузка на отдельный сервис растет, для решения проблемы достаточно запустить в параллельную работу необходимое количество его экземпляров. В монолитных системах такое практически невозможно или крайне дорого.
Наконец, масштабирование данных в микросервисной архитектуре удобно делать путем разделения БД на разделы по какому-то признаку.
На этом пути очень легко превратить свою микросервисную архитектуру в «большой комок грязи» (Big Ball of Mud) с недоступной пониманию запутанной структурой. Мы в своей практике долго изучали эти процессы и пришли к пониманию правильной структуры микросервисов в понятиях паттернов и антипаттернов.
2a14a7634c161cacbaba55a755421481.png

Архитектурные паттерны и антипаттерны​

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

Структура приложения​

Давайте разберем большие группы паттернов для слабосвязанных микросервисных архитектур. Одним из базовых является многоуровневая архитектура, когда все компоненты приложения группируются по слоям, внутри которых содержатся только однородные функции (фронтенд, интеграционные адаптеры, бизнес-логика и т.д.)
Для этого примера антипаттерном является подход, когда мы функциональность разной природы смешиваем в рамках одного компонента. Например, бизнес-логика присутствует в UI-компонентах. Такие решения плохи с точки зрения прозрачности разработки. Они ведут к путанице и проблемам эксплуатации.
Еще один паттерн связан с движением от монолита к микросервисным архитектурам. Мы уже упоминали о функциональной декомпозиции, но сейчас остановимся на ней чуть подробнее. Сервисы внутри приложения должны быть однородны по функциональности в соответствии с подходами DDD (Domain-Driven Design). Это требование заложено в самих принципах микросервисной архитектуры.
В роли антипаттерна здесь можно привести ситуацию, когда функциональность внутри микросервиса разнородна, в то время как однородная функциональность распределена между несколькими связанными между собой микросервисами.
Наконец, на абстрактном уровне мы можем рассмотреть паттерн «бекенд для фронтенда» (Backend For Frontend, BFF). В рамках этой схемы мы вводим между фронтендом и бэкендом промежуточный слой, содержащий различные вспомогательные инструменты, в основном выполняющие задачу агрегации и форматирования данных, поступающих в те или иные фронтенд-приложения.
Рассмотрим на примере страницы товара, для составления которой фронтенд-приложению надо обратиться к десятку бекенд-сервисов. Слой BFF, отдельный для каждой фронтенд-системы, но использующий одинаковые библиотеки, избавляет от этой необходимости, самостоятельно агрегируя и кешируя нужные сведения перед передачей фронтенду.
Антипаттерн для этого сценария очевиден. Он подразумевает агрегацию и форматирование данных на уровне фронтенда. BFF, даже при его наличии, может иметь монолитную структуру и обслуживать несколько приложений. Минусом будет и ситуация, когда реализация одинаковых задач для разных фронтенд-систем выполнена без использования общих библиотек.

Работа с данными​

Для сохранения возможности масштабирования данных, особенно в высоконагруженных системах, имеет смысл уделить внимание микросервисам, владеющим собственными данными. Они не должны храниться в общей БД, разделяемой с другими микросервисами. Здесь уместнее раздельное хранение, причем доступ к данным одного микросервиса со стороны другого должен предоставляться только по API.
Этим правилом можно пренебречь, если речь идет о «пилоте» или MVP, но по мере роста нагрузки задача будет становиться все более актуальной.
Второй паттерн не такой очевидный, но он тоже важен. При решении ряда задач необходимо хранить не сами данные, а историю их изменений. Типичный пример – хранение остатка товара, когда нас интересует в первую очередь изменение, а не абсолютное значение. Данные при этом хранятся в таблицах «за час», «за день» и т.д.
При решении задачи масштабирования возможностей БД по чтению данных, мы используем паттерн CQRS (The Command and Query Responsibility Segregation). В этом случае между БД и BFF вводится еще один промежуточный слой, который работает по одному из двух сценариев – прием или отдача данных.
CQRS работает асинхронно, а после извлечения нужных данных хранит их в собственной БД или индексе, которые обновляются по мере обновления основной БД. Это оправдано в случае, когда интенсивность запросов на чтение в два и более раз превышает интенсивность запросов на запись.
Антипаттерном можно считать ситуацию, когда для чтения и записи используется одна и та же БД, обработка запросов на чтение и запись производится с использованием одного и того же сценария или при выполнении запросов на запись используется чтение.
2cea47162d245271585d5ce936e72c4a.png

Распределенные транзакции​

Когда, к примеру, в систему приходит оплата заказа, нам нужно проделать множество действий – изменить статус заказа, создать заявку на комплектацию и т.д. Здесь применяется паттерн Saga, который можно реализовать в двух видах – хореографии и оркестрации.
Принцип хореографии подразумевает, что сервисы выполняют свою работу последовательно, по аналогии с эстафетой. Этот подход удобен, когда транзакция не слишком сложная – 2-3 шага с минимумом вариаций.
Оркестрация – более сложный путь. В этом случае запускается специальный микросервис, в котором может быть прописана сложнейшая бизнес-логика. В процесс могут вовлекаться разные сервисы, выполняться разные операции, применяется асинхронная обработка и пр. Важным моментом, который должен быть реализован в рамках Saga, является возможность отката к предыдущему состоянию в случае любой ошибки.
Антипаттерном здесь будет являться отсутствие механизма отката. Кроме того, плохо, когда при сложной логике транзакции ее маршрутизация не централизована, а разбросана по разным сервисам. В этом случае можно легко оказаться невозможно найти источник сбоя или провести необходимые изменения.

Кэширование данных​

Мы используем три базовых сценария кеширования. Оно может выполняться на стороне поставщика данных (Read-Through cache или Write-Through cache), который таким образом закрывается от высокой нагрузки. При этом поставщик сам определяет стратегию заполнения кеша, выбирая, какие данные держать в нем продолжительное время, а какие больше не понадобятся.
Кэшироваться данные могут и на стороне потребителя. Но к этому надо подходить с осторожностью, четко отделяя данные, устаревание которых в фронтенд-сервисе недопустимо. Например, это может быть состояние корзины покупок.
Антипаттерном можно считать отсутствие кэширования высокоинтенсивных запросов и отсутствие синхронизации кэша и БД в отношении данных, которые нельзя потерять. Кроме того, неудачной следует признать реализацию кеша, при которой cache hit ratio составляет менее 90%.

Предоставление данных сервиса​

Надежность сервиса во многом определяется его способностью вовремя и корректно предоставлять данные. В реализации этой функции есть ряд нюансов, облегчающих управление нагрузкой. В первую очередь речь идет о неблокирующей обработке запросов, когда сервис, выполняющий работу по запросу, не сбрасывает следующий, а запускает его выполнение параллельно с предыдущим. Это можно реализовать, например, через Connection pool.
Второй важный нюанс – создание механизма квотирования запросов, позволяющего установить максимальное количество обращений в единицу времени, которое сервис реально сможет отработать. Оно обычно реализуется на основе API Gateway. Проблема лишь в том, что простые инструменты этого типа требуют серьезных доработок, а лицензия на более совершенные стоит очень серьезных денег.
Антипаттерн заключается либо в блокировке вызывающего потока при обработке запроса, либо в настройках лимитов на уровне приложения, а не API Gateway. Плохой практикой также является возвращение не вошедших в лимит запросов по таймауту.
С точки зрения недопущения потери данных очень важно реализовать на уровне микросервиса механизм повтора соединения. Если при асинхронном взаимодействии сервис поменял свои данные, он должен обратиться к брокеру сообщений. Может оказаться, что брокер в данный момент недоступен. В этом случае сервис должен иметь возможность копить свои данные до тех пор, пока не восстановится соединение с брокером, и параллельно слать предупреждения системе мониторинга о сложившейся ситуации.
Даже в том случае, если данные отправить так и не удалось, их следует отправлять в очередь DLQ (Dead Letter Queue) для дальнейшей диагностики. Отказ от вышеперечисленных принципов может привести – и, скорее всего, приведет – к потере важной информации, что, конечно, является очень серьезным антипаттерном.
f66b9180289ec1a10a49107b5de78960.png

Потребление данных сервисом​

Если говорить о потреблении данных сервисами, хорошей практикой, помогающей снизить нагрузку и трафик, является наличие в системе проксирующего сервера Circuit Breaker. При накоплении ошибок он автоматически отключает передачу запросов в неисправный процесс, тем самым снижая нагрузку на систему.
Аналогичные действия следует предпринимать и в случае, когда превышено время ожидания ответа. Для высоконагруженных систем это недопустимо, поэтому в случае отсутствия ответа соединение должно обрываться по таймауту.
Наконец, в вопросах потребления данных сервисом оказаться полезной и упомянутая выше очередь DLQ (Dead Letter Queue), которая позволит эти данные сохранить.

Заключение​

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

 
Сверху