[Перевод] Микросервисные паттерны проектирования

Здравствуйте, Хабр!

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

Основные характеристики микросервисных приложений описаны в статье «Microservices, Monoliths and NoOps». Эти характеристики — функциональная декомпозиция или предметно-ориентированное проектирование, четко определенные интерфейсы, явно публикуемый интерфейс, принцип единственной обязанности и потенциальная многоязычность. Каждый сервис полностью автономный и полностековый. Соответственно, изменение реализации одного сервиса никак не сказывается на остальных, а обмен информацией происходит через четко определенные интерфейсы. У такого приложения есть ряд преимуществ, но они даром не даются, а требуют серьезной работы, связанной с NoOps.

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

71054732a3c34fce82a1baafb7986ca8.png

Для создания качественной микросервисной архитектуры необходимо четко разделить функции в вашем приложении и команде. Так можно достичь слабого связывания (REST-интерфейсы) и сильного сцепления (множество сервисов могут компоноваться вместе, определяя более высокоуровневые сервисы или приложение).

Создание «глаголов» (напр. Checkout) или «существительных» (Product) в составе приложения — один из эффективных способов декомпозиции имеющегося кода. Например, product, catalog и checkout могут быть реализованы как три отдельных микросервиса, а затем взаимодействовать друг с другом, обеспечивая полный функционал корзины заказов.

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

Об этом и пойдет речь в статье

Паттерн Агрегатор (Aggregator)

Первый и, пожалуй, наиболее распространенный паттерн проектирования при работе с микросервисами — «агрегатор».

В простейшей форме агрегатор представляет собой обычную веб-страницу, вызывающую множество сервисов для реализации функционала, требуемого в приложении. Поскольку все сервисы (Service A, Service B и Service C) предоставляются при помощи легковесного REST-механизма, веб-страница может извлечь данные и обработать/отобразить их как нужно. Если требуется какая-либо обработка, например, применить бизнес-логику к данным, полученным от отдельных сервисов, то для этого у вас может быть CDI-компонент, преобразующий данные таким образом, чтобы их можно было вывести на веб-странице.

0940e1d1593b4254baed594640a051b9.png

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

Этот паттерн следует принципу DRY. Если существует множество сервисов, которые должны обращаться к сервисам A, B и C, то рекомендуется абстрагировать эту логику в составной микросервис и агрегировать ее в виде отдельного сервиса. Преимущество абстрагирования на этом уровне заключается в том, что отдельные сервисы, скажем, A, B и C, могут развиваться независимо, а бизнес-логику будет по-прежнему выполнять составной микросервис.

Обратите внимание: каждый отдельный микросервис (опционально) имеет собственные уровни кэширования и базы данных. Если агрегатор — это составной микросервис, то и у него могут быть такие уровни.

Агрегатор также может независимо масштабироваться как по горизонтали, так и по вертикали. То есть, если речь идет о веб-странице, то к ней можно прикрутить дополнительные веб-серверы, а если это составной микросервис, использующий Java EE, то к нему прикручиваются дополнительные экземпляры WildFly, позволяющие удовлетворить растущие потребности.

Паттерн Посредник (Proxy)

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

a5e6996d11e544ea8ebf48f6cddfe906.png

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

Посредник может быть формальным (dumb), в таком случае он просто делегирует запрос одному из сервисов. Он может быть и интеллектуальным (smart), в таком случае данные перед отправкой клиенту подвергаются тем или иным преобразованиям. Например, уровень представления для различных устройств может быть инкапсулирован в интеллектуальный посредник.

Паттерн проектирования «Цепочка» (Chained)

Микросервисный паттерн проектирования «Цепочка» выдает единый консолидированный ответ на запрос. В данном случае сервис A получает запрос от клиента, связывается с сервисом B, который, в свою очередь, может связаться с сервисом C. Все эти сервисы, скорее всего, будут обмениваться синхронными сообщениями «запрос/отклик» по протоколу HTTP.

8766050f52f84c328a48201c495b94e3.png

Здесь важнее всего запомнить, что клиент блокируется до тех пор, пока не выполнится вся коммуникационная цепочка запросов и откликов, т.е. Service <-> Service B и Service B <-> Service C. Запрос от Service B к Service C может выглядеть совершенно иначе, нежели от Service A к Service B. Аналогично, отклик от Service B к Service A может принципиально отличаться от отклика Service C к Service B. Это наиболее важно во всех случаях, когда бизнес-ценность нескольких сервисов суммируется.

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

Цепочка, состоящая из единственного микросервиса, называется «цепочка-одиночка». Впоследствии ее можно расширить.

Паттерн проектирования «Ветка» (Branch)

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

173257bcaed04d97831a0691ec0c37ca.png

Сервис A, будь то веб-страница или составной микросервис, может конкурентно вызывать две различные цепочки — и в этом случае будет напоминать агрегатор. В другом случае сервис А может вызывать лишь одну цепочку в зависимости от того, какой запрос получит от клиента.

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

Паттерн «Разделяемые данные» (Shared Data)

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

Однако, типичная проблема, особенно при рефакторинге имеющегося монолитного приложения, связана с нормализацией базы данных — так, чтобы у каждого микросервиса был строго определенный объем информации, ни больше, ни меньше. Даже если в монолитном приложении используется только база данных SQL, ее денормализация приводит к дублированию данных, а возможно — и к несогласованности. На переходном этапе в некоторых приложениях бывает очень полезно применить паттерн «Разделяемые данные».

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

3b06e8f29f9f4a78a09e8afff9c79935.png

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

Паттерн «Асинхронные сообщения» (Asynchronous Messaging)

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

bec4a42ec9d7476f90c7f8047957c87c.png

В этом паттерне сервис А может синхронно вызывать сервис C, который затем будет асинхронно связываться с сервисами B и В при помощи разделяемой очереди сообщений. Коммуникация Service A → Service C может быть асинхронной, скажем, с использованием веб-сокетов; так достигается желаемая масштабируемость.
Комбинация модели REST запрос/отклик и обмена сообщениями публикатор/подписчик также могут использоваться для достижения поставленных целей.

Рекомендую также прочитать статью Coupling vs Autonomy in Microservices, в которой описано, какие паттерны коммуникации удобно применять с микросервисами.

© Habrahabr.ru