Привет, Хабр. В этой статье я кратко расскажу о деталях реализации микросервисной архитектуры с использованием инструментов, которые предоставляет Spring Cloud на примере простого концепт-пруф приложения. Код доступен для ознакомления на гитхабе. Образы опубликованы на докерхабе, весь зоопарк стартует одной командой.

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

 Попробуем декомпозировать монолит на несколько основных микросервисов, каждый из которых будет отвечать за определенную бизнес-задачу.  Реализует логику и валидацию по сохранению доходов, расходов, накоплений и настроек аккаунта. Производит расчет основных статистических параметров аккаунта, приводит их значения к базовой валюте и периоду, сохраняет данные в виде, удобном для последующего анализа. Полученный временной ряд будет использован для отображения пользователю статистики и показателей за прошедшее время и экстраполяции для простейших прогнозов на будущее. Хранит настройки уведомлений (частоты напоминаний, периодичность бекапов). По расписанию производит рассылку e-mail сообщений, предварительно собирая и агрегируя нужные данные у первых двух сервисов, если требуется. Для обеспечения совместной работы описанных выше сервисов будем использовать набор основных паттернов и практик Микросервисной архитектуры. Многие из них реализованы в Spring Cloud (в частности, посредством интеграции с продуктами Netflix OSS) — на деле это зависимости, расширяющие возможности Spring Boot в ту или иную сторону. Ниже кратко рассмотрен каждый из компонентов.

 Spring Cloud Config — это горизонтально масштабируемое хранилище конфигураций для распределенной системы. В качестве источника данных на данный момент поддерживаются Git, Subversion и простые файлы, хранящиеся локально. По умолчанию Spring Cloud Config отдает файлы, соответствующие имени запрашивающего Spring приложения (но можно забирать проперти под конкретный Spring profile и из определенной ветки системы контроля версий).

На практике наибольший интерес представляет загрузка конфигураций из систем котроля версий, но здесь для простоты будем использовать локальные файлы. Поместим дирректорию shared в класспасе приложения, в которой будут хранится конфигурационные файлы для всех приложений в кластере. Например, если Notification service запросит конфигурацию, Config server ответит ему содержимым файла shared/notification-service.yml, смердженным с shared/application.yml (который является общим для всех).

На стороне клиентского приложения теперь не требуются никаких конфигурационных файлов, кроме bootstrap.yml с именем приложения и адресом Config server:

 Spring Cloud Config позволяет изменять конфигурацию динамически. Например, бин EmailService, помеченный аннотацией @RefreshScope, может начать рассылать измененный текст e-mail сообщения без пересборки.

Для этого следует внести правки в конфигурационный файл Config server, а затем выполнить следующий запрос к Notification service: Этот процесс можно автоматизировать, если использовать загрузку конфигураций из систем контроля версий, настроив вебхук из Github, Gitlub или Bitbucket.

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

На самом деле, здесь описан только один из возможных подходов. Spring Cloud и Spring Security позволяют достаточно гибко настраивать конфигурацию под ваши нужды (например, имеет смысл проводить авторизацию на стороне API Gateway, а внутрь инфраструктуры передавать запрос с уже заполненными данными пользователя).

В этом проекте я использую Password credential grant type для авторизации пользователей и Client credentials grant type — для авторизации между сервисами.

Spring Cloud Security предоставляет удобные аннотации и автоконфигурацию, что позволяет достаточно просто реализовать описанный функционал как со стороны клиента, так и со стороны авторизационного сервера.

Со стороны клиента это ничем не отличается от традиционной авторизации с помощью сессий. Из запроса можно получить объект Principal, проверить роли и другие параметры с использованием аннотации @PreAuthorize. Кроме того, каждое OAuth2-приложение имеет scope: для бэкенд-сервисов — server, для браузера — ui. Так мы можем ограничить доступ к некоторым эндпоинтам извне: Все три основных сервиса, которые мы обсудили выше, предоставляют для внешнего пользователя некоторый API. В промышленных системах, построенных на Микросервисной архитектуре, число компонентов растет быстро — поговаривают, что в Амазоне в рендеринг страницы вовлечены порядка 150 сервисов.

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

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

Netflix заопенсорсила свою имплементацию API Gateway — Zuul. Spring Cloud нативно интегрирован с ним и включается добавлением одной зависимости и аннотациии @EnableZuulProxy в Spring Boot приложение. В этом проекте Zuul используется для самых элементарных задач — отдачи статики (веб-приложение) и роутинга запросов.

Пример префиксной маршрутизации для Notification service: Теперь каждый запрос, uri которого начинается на /notifications, будет направлен в соответствующий сервис.

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

Ключевым звеном здесь является Registry service. В этом проекте я использую Netflix Eureka (но есть еще Consul, Zookeeper, Etcd и другие). Eureka — пример client-side discovery паттерна, что означает клиент должен запросить адреса доступных инстансов и осуществлять балансировку между ними самостоятельно.

Чтобы превратить Spring Boot приложение в Registry server, достаточно добавить зависимость на spring-cloud-starter-eureka-server и аннотацию @EnableEurekaServer. На стороне клиентов — зависимость spring-cloud-starter-eureka, аннотацию @EnableDiscoveryClient и имя приложения (serviceId) в bootstrap.yml: Теперь инстанс приложения при старте будет регистрироваться в Eureka, предоставляя мета-данные (такие как хост, порт и прочее). Eureka будет принимать хартбит-сообщения, и если их нет в течении сконфигурированного времени — инстанс будет удален из реестра. Кроме того, Eureka предоставляет дашборд, на котором видны зарегистрированные приложения с количеством инстансов и другая техническая информация: http://localhost:8761

 Следующий набор инструментов тоже разработан в Netflix и нативно интегрирован в Spring Cloud. Все они работают совместно и используются в микросервисах, которым нужно общаться с внешним миром или внутренней инфраструктурой.

 Ribbon — это client-side балансировщик. По сравнению с традиционным, здесь запросы проходят напрямую по нужному адресу, что исключает лишний узел при вызове. Из коробки он интегрирован с механизмом Service Discovery, который предоставляет динамический список доступных инстансов для балансировки между ними.

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

Помимо контроля за размыканием цепи, Hystrix позволяет определить fallback-метод, который будет вызван при неуспешном вызове. Тем самым можно отдавать дефолтный ответ, сообщение об ошибке, и др.

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

 Feign — простой и гибкий http-клиент, который нативно интегрирован с Ribbon и Hystrix. Проще говоря, имея в класспасе зависимость spring-cloud-starter-feign и активировав клиент аннотацией @EnableFeignClients, вы получаете полный набор из балансировщика, предохранителя и клиента, готовый к бою с разумной дефолтной конфигурацией.

Вот пример из Account Service:

 Метрики, которые генерит Hystrix, можно отдавать наружу, включив в класспас зависимость spring-boot-starter-actuator. Помимо прочих интересных вещей, будет выставлен специальный эндпоинт — /hystrix.stream. Этот стрим можно визуализировать с помощью Hystrix dashboard, о котором мы подробно поговорим ниже. Для включения Hystrix Dashboard понадобится зависимость spring-cloud-starter-hystrix-dashboard и аннотация @EnableHystrixDashboard. Hystrix Dashboard можно натравить на стрим любого микросервиса для наблюдения живой картины происходящего в этом конкретном сервисе.

Однако в нашем случае сервисов несколько, и было бы здорово видеть все их метрики в одном месте. Для этого существует специальное решение. Каждый из наших сервисов будет пушить свои стримы в AMQP брокер (RabbitMQ), откуда аггрегатор стримов, Turbine, будет преобразовывать их, выставляя единый эндпоинт для Hystrix Dashboard.

Рассмотрим поведение системы под нагрузкой: Account service вызывает Statistics Service, и тот отвечает с варьируемой имитационной задержкой. Пороговое значение времени запроса установлено в 1 секунду.
 В инфраструктуре, состоящей из большого количества движущихся частей (каждая из которых может иметь по нескольку экземпляров), очень важно использовать систему централизированного сбора, обработки и анализа логов. Elasticsearch, Logstash и Kibana составляют стeк, с помощью которого можно эффективно решать такую задачу.

Готовая к запуску Docker-конфигурация ELK с Куратором и шаблонами для шипперов доступна на гитхабе. Именно такая конфигурация, с небольшой кастомизацией и масштабированием, успешно работает в продакшене на моем текущем проекте для анализа логов, сетевой активности и мониторинга производительности серверов.


Картинка с официального сайта Elastic, просто для примера

 Развертывание микросервисной системы с большим количеством движущихся частей и взаимосвязанностью — задача очевидно более комплексная, нежели деплой монолитного приложения. Без автоматизированной инфраструктуры вся история превратится в бесконечную боль и трату времени. Это тема для совершенно отдельного разговора, я лишь покажу простейший Сontinuous delivery воркфлоу, реализованный в этом проекте на бесплатных версиях сервисов:

 Последний этап — это образно, продакшена для проекта не предполагается.
К корне репозитория находится .travis.yml файл с указаниями для CI сервера — что делать после удачной сборки. В данной конфигурации на каждый успешный пуш в Github, Travis CI соберет докер-образы, пометит их тегом и запушит в Docker Hub. Теперь получается, что у нас всегда есть готовые к деплою контейнеры, помеченные тегом latest, а также контейнеры со старыми версиями, версиями из любых веток.

 Если вы дочитали до этого места, возможно вам будет интересно запустить все это своими руками. Хочу отметить, что инфраструктура состоит из 8 Spring Boot приложений, 4 инстансов MongoDB и одного RabbitMQ. Убедитесь, что в системе доступны 3-4 Гб памяти. Всегда можно запустить ограничиться самым необходимым функционалом — отказаться от Statistics service, Notification Service и Monitoring.

 В этом режиме все предварительно собранные образы загружаются из центрального репозитория (в данном случае Docker Hub), порты проброшены наружу докера только для API Gateway, Service Discovery, Monitoring и RabbitMQ management. Все что вам понадобится — это docker-compose файл и команда docker-compose up -d. В режиме разработки предполагается строить образы, а не забирать их из репозитория. Все контейнеры выставлены наружу для удобного дебага. Эта конфигурация наследуется от приведенной выше, перезаписывая и расширяя указанные моменты. Запускается командой docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d Всем Spring Boot приложениям в этом проекте для старта необходим доступный Config Server. Благодаря опции fail-fast в bootstrap.yml каждого приложения и опции restart: always в докере, контейнеры можно запускать одновременно (они будут автоматически продолжать попытки старта, пока не поднимется Config Server).

Механизм Service Discovery так же требует некоторого времени для начала полноценной работы. Сервис не доступен для вызова, пока он сам, Eureka и клиент не имеют одну и ту же мета-информацию у себя локально — на это требуется 3 хартбита. По умолчанию период времени между хартбитами составляет 30 секунд.

 Software Engineer