Микросервисная архітектура, Spring Cloud і Docker

Привіт, Хабр. У цій статті я коротко розповім про деталі реалізації микросервисной архітектури з використанням інструментів, які надає Spring Cloud на прикладі простого концепт-пруф програми.

Код доступний для ознайомлення на гітхабі. Образи опубліковані на докерхабе, весь зоопарк стартує однією командою.

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




Функціональні сервіси
Спробуємо декомпозировать моноліт на кілька основних микросервисов, кожен з яких буде відповідати за певну бізнес-завдання.


service Account
Реалізує логіку і валідацію по збереженню доходів, витрат, накопичень і налаштувань.








Метод Шлях Опис авторизований Користувач Доступно UI GET /accounts/{account} Отримати дані зазначеного облікового запису GET /accounts/current Отримати дані поточного облікового запису × × GET /accounts/demo Отримати дані демо запису × PUT /accounts/current Зберегти дані поточного облікового запису × × POST /accounts/ Зареєструвати новий обліковий запис ×
Statistics service
Виробляє розрахунок основних статистичних параметрів облікового запису, приводить їх значення до базової валюті та періоду, зберігає дані у вигляді, зручному для подальшого аналізу. Отриманий часовий ряд буде використаний для відображення користувачеві статистики і показників за минулий час та екстраполяції для найпростіших прогнозів на майбутнє.







Метод Шлях Опис авторизований Користувач Доступно UI GET /statistics/{account} Отримати статистику зазначеного облікового запису GET /statistics/current Отримати статистику поточного облікового запису × × GET /statistics/demo Отримати статистику демо запису × PUT /statistics/{account} Створити/оновити дата-поінт для
зазначеного облікового запису
Notification service
Зберігає налаштування сповіщень (частоти нагадувань, періодичність бекапів). За розкладом виробляє розсилку e-mail повідомлень, попередньо збираючи і агрегуючи потрібні дані у перших двох сервісів, якщо потрібно.





Метод Шлях Опис авторизований Користувач Доступно UI GET /notifications/settings/current Отримати налаштування нотифікацій
для поточного облікового запису
× × PUT /notifications/settings/current Зберегти налаштування нотифікацій
для поточного облікового запису
× ×

Примітки

  • Всі микросервисы мають свою власну БД, відповідно будь доступ до даних можна отримати тільки через API додатка.
  • У цьому проекті для простоти я використовував тільки MongoDB як основну БД для кожного з сервісів. На практиці може виявитися корисним підхід, званий Polyglot persistence — вибір сховища, найбільш підходящого для завдань конкретного сервісу.
  • Комунікація між сервісами також суттєво спрощено: використовуються тільки синхронні rest-запити. Загальноприйнятою практикою є комбінування різних способів взаємодії. Наприклад, синхронні GET-запити для отримання інформації і асинхронні запити з використанням сервера черг — для create/update операцій. Що, до речі, переносить нас у світ eventual consistency — один з важливих аспектів розподілених систем, з якими доводиться жити.
Інфраструктурні сервіси
Для забезпечення спільної роботи описаних вище сервісів будемо використовувати набір основних патернів і практик Микросервисной архітектури. Багато з них реалізовані Spring Cloud (зокрема, за допомогою інтеграції з продуктами Netflix OSS — на ділі це залежно, що розширюють можливості Spring Boot в ту або іншу сторону. Нижче коротко розглянуто кожний з компонентів.



Config Server
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:
application:
name: notification service
cloud:
config:
uri: http://config:8888
fail-fast: true

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

Для цього слід внести правки в конфігураційний файл Config server, а потім виконати наступний запит до
Notification service
:
`curl -H "Authorization: Bearer #token#" -XPOST http://127.0.0.1:8000/notifications/refresh`

Цей процес можна автоматизувати, якщо використовувати завантаження конфігурацій з систем контролю версій, налаштувавши вебхук з Github, Gitlub або Bitbucket.

Примітки
  • На жаль, є істотні обмеження на динамічне оновлення конфігурації.
    @RefreshScope
    не працює для
    @Configuration
    класів і методів, зазначених анотацією
    @Scheduled
  • Властивість
    fail-fast
    , згадане в
    bootstrap.yml
    означає, що додаток зупинить завантаження відразу ж, якщо немає з'єднання з Config Server. Це знадобиться нам при одночасному старті всієї інфраструктури.
  • Просунуті налаштування безпеки виходять за рамки цього концепт-пруф програми. Spring Security надає широкі можливості для реалізації механізмів забезпечення безпеки. Використання JCE keystore для шифрування паролів микросервисов та інформації в конфігураційних файлах докладно описані в документації.


Auth Server
Обов'язки за авторизації повністю винесені в окремий додаток, який видає 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
. Так ми можемо обмежити доступ до деяких эндпоинтам ззовні:
@PreAuthorize("#oauth2.hasScope('server')")
@RequestMapping(value = "accounts/{name}", method = RequestMethod.GET)
public List<DataPoint> getStatisticsByAccountName(@PathVariable String name) {
return statisticsService.findByAccountName(name);
}


API Gateway
Всі три основних сервісу, які ми обговорили вище, надають для зовнішнього користувача деякий API. У промислових системах, побудованих на Микросервисной архітектурі, число компонентів росте швидко — подейкують, що в Амазоні в рендеринг сторінок залучені близько 150 сервісів.

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

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

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

Приклад префиксной маршрутизації для Notification service:
zuul:
routes:
notification service:
path: /notifications/**
serviceId: notification service
stripPrefix: false

Тепер кожен запит, uri якого починається на
/notifications
, буде спрямований у відповідний сервіс.


Service discovery
Ще один широко відомий патерн для розподілених систем. Service discovery дозволяє автоматично визначати мережеві адреси для доступних инстансов додатків, які можуть динамічно змінюватися з причин масштабування, падінь і оновлень.

Ключовою ланкою тут є Registry service. У цьому проекті я використовую Netflix Eureka (але є ще Consul, Zuukeeper, Etcd та інші). Eureka — приклад client-side discovery патерну, що означає клієнт повинен запросити адреси доступних инстансов і здійснювати балансування між ними самостійно.

Щоб перетворити Spring Boot додаток в Registry server, досить додати в залежність
spring-cloud-starter-eureka-server
та анотацію
@EnableEurekaServer
. На стороні клієнтів — залежність
spring-cloud-starter-eureka
, анотацію
@EnableDiscoveryClient
та ім'я програми (serviceId)
bootstrap.yml
:
spring:
application:
name: notification service

Тепер інстанси програми при старті буде реєструватися в Eureka, надаючи мета-дані (такі як хост, порт та інше). Eureka буде приймати хартбит-повідомлення, і якщо їх немає в перебігу настроєний часу — інстанси буде видалений з реєстру. Крім того, Eureka надає дашборд, на якому видно зареєстровані програми з кількістю инстансов та інша технічна інформація:
http://localhost:8761





Клієнтський балансувальник, Запобіжник і Http-клієнт
Наступний набір інструментів теж розроблений в Netflix і нативно інтегрований в Spring Cloud. Всі вони працюють спільно і використовуються в микросервисах, яким потрібно спілкуватися із зовнішнім світом або внутрішньою інфраструктурою.


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


Hystrix
Hystrix — це імплементація патерну Circuit Breaker — запобіжника, який дає контроль над затримками та помилками при дзвінки з мережі. Основна ідея полягає в тому, щоб зупинити каскадний відмову в розподіленій системі, що складається з великої кількості компонентів. Це дозволяє віддавати помилку як можна швидше, не затримуючись при запиті до завислому сервісу (даючи йому відновиться).

Крім контролю за розмиканням кола, Hystrix дозволяє визначити fallback-метод, який буде викликаний при неуспішним виклик. Тим самим можна віддавати дефолтний відповідь, повідомлення про помилку, і ін

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


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

Ось приклад з Service Account:


@FeignClient(name = "statistics service")
public interface StatisticsServiceClient {

@RequestMapping(method = RequestMethod.PUT, value = "/statistics/{accountName}", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
void updateStatistics(@PathVariable("accountName") String accountName, Account account);

}


  • Все що потрібно — оголосити інтерфейс
  • Як зазвичай, ми просто вказуємо ім'я сервісу (завдяки механізму Service Discovery), але звичайно можна звертатися і до безпідставного url
  • @RequestMapping
    разом з вмістом можна залишати єдиної для
    @FeignClient
    та
    @Controller
    в Spring MVC

Панель моніторингу
Метрики, які генерує 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 секунду.







затримка 0 мс
затримка 500 мс
затримка 800 мс
затримка 1100 мс
Система працює без помилок. Пропускна здатність близько 22 з/с. Невелике число активних потоків в Statistics service. Середній час отримання відповіді —
50 мс.
Кількість активних потоків збільшується. Фіолетова цифра показує кількість відхилених запитів, відповідно до порядку 30-40% помилок, але ланцюг все ще замкнута. Напіввідкритий стан: відсоток помилок більше 50%, запобіжник розмикає ланцюг. Після певного таймауту, ланцюг замикається, але знову ненадовго. 100% запитів з помилками. Розімкнути ланцюг постійно, спроби пропустити запит через таймаут нічого не міняють — кожний окремий запит занадто повільний.


Аналіз логів
В інфраструктурі, що складається з великої кількості рухомих частин (кожна з яких може мати по декілька екземплярів), дуже важливо використовувати систему централізованого збору, обробки і аналізу логів. Elasticsearch, Logstash і Kibana складають стек, за допомогою якого можна ефективно вирішувати таку задачу.

Готова до запуску Docker-конфігурація ELK з Куратором і шаблонами для шипперов доступна на гітхабі. Саме така конфігурація, з невеликою кастомізацією і масштабуванням, успішно працює в продакшені на моєму поточному проекті для аналізу логів, мережевої активності і моніторингу продуктивності серверів.


Картинка з офіційного сайту Elastic, просто для прикладу


Автоматизація інфраструктури
Розгортання микросервисной системи з великою кількістю рухомих частин і взаємопов'язаністю — завдання очевидно більш комплексна, ніж деплой монолітного програми. Без автоматизованої інфраструктури вся історія перетвориться на нескінченну біль і витрату часу. Це тема для окремої розмови, я лише покажу найпростіший Сontinuous delivery воркфлоу, реалізований в цьому проекті на безкоштовних версіях сервісів:




Останній етап — це образно, продакшену для проекту не передбачається.
До корені репозиторію перебуває .travis.yml файл з вказівками для CI сервера — що робити після збірки. У даній конфігурації на кожен успішний пуш в Github, Travis CI збере докер-образи, позначить їх тегом і запушит в Docker Hub. Тепер виходить, що у нас завжди є готові до деплою контейнери, позначені тегом
latest
, а також контейнери зі старими версіями, версіях з будь-яких гілок.


Запуск
Якщо ви дочитали до цього місця, можливо вам буде цікаво запустити все це своїми руками. Хочу зазначити, що інфраструктура складається з 8 Spring Boot додатків, 4 инстансов MongoDB і одного RabbitMQ. Переконайтеся, що у системі доступні 8 Гб пам'яті. В іншому випадку можна запустити систему з обмеженим функціоналом (відмовитися від Statistics service, Notification Service Monitoring).


Перш ніж почати

  • Виберіть Docker і Docker Compose
  • Експортуйте змінні оточення:
    CONFIG_SERVICE_PASSWORD
    ,
    NOTIFICATION_SERVICE_PASSWORD
    ,
    STATISTICS_SERVICE_PASSWORD
    ,
    ACCOUNT_SERVICE_PASSWORD
    ,
    MONGODB_PASSWORD

Production mode

У цьому режимі всі попередньо зібрані образи завантажуються з центрального сховища (в даному випадку Docker Hub), порти проброшены назовні докера тільки для API Gateway Service Discovery, Monitoring і RabbitMQ management. Все що вам знадобиться — це docker-compose файл і команда
docker-compose up -d
.
docker-compose.yml
version: '2'
services:
rabbitmq:
image: rabbitmq:3-management
restart: always
ports:
- 15672:15672
logging:
options:
max-size: "10m"
max-file: "10"

config:
environment:
CONFIG_SERVICE_PASSWORD: $CONFIG_SERVICE_PASSWORD
image: sqshq/piggymetrics-config
restart: always
logging:
options:
max-size: "10m"
max-file: "10"

registry:
environment:
CONFIG_SERVICE_PASSWORD: $CONFIG_SERVICE_PASSWORD
image: sqshq/piggymetrics-registry
restart: always
ports:
- 8761:8761
logging:
options:
max-size: "10m"
max-file: "10"

gateway:
environment:
CONFIG_SERVICE_PASSWORD: $CONFIG_SERVICE_PASSWORD
image: sqshq/piggymetrics-gateway
restart: always
ports:
- 80:4000
logging:
options:
max-size: "10m"
max-file: "10"

auth-service:
environment:
CONFIG_SERVICE_PASSWORD: $CONFIG_SERVICE_PASSWORD
NOTIFICATION_SERVICE_PASSWORD: $NOTIFICATION_SERVICE_PASSWORD
STATISTICS_SERVICE_PASSWORD: $STATISTICS_SERVICE_PASSWORD
ACCOUNT_SERVICE_PASSWORD: $ACCOUNT_SERVICE_PASSWORD
MONGODB_PASSWORD: $MONGODB_PASSWORD
image: sqshq/piggymetrics-auth-service
restart: always
logging:
options:
max-size: "10m"
max-file: "10"

auth-mongodb:
environment:
MONGODB_PASSWORD: $MONGODB_PASSWORD
image: sqshq/piggymetrics-mongodb
restart: always
logging:
options:
max-size: "10m"
max-file: "10"

account-service:
environment:
CONFIG_SERVICE_PASSWORD: $CONFIG_SERVICE_PASSWORD
ACCOUNT_SERVICE_PASSWORD: $ACCOUNT_SERVICE_PASSWORD
MONGODB_PASSWORD: $MONGODB_PASSWORD
image: sqshq/piggymetrics-account-service
restart: always
logging:
options:
max-size: "10m"
max-file: "10"

account-mongodb:
environment:
INIT_DUMP: account-service-dump.js
MONGODB_PASSWORD: $MONGODB_PASSWORD
image: sqshq/piggymetrics-mongodb
restart: always
logging:
options:
max-size: "10m"
max-file: "10"

statistics-service:
environment:
CONFIG_SERVICE_PASSWORD: $CONFIG_SERVICE_PASSWORD
MONGODB_PASSWORD: $MONGODB_PASSWORD
STATISTICS_SERVICE_PASSWORD: $STATISTICS_SERVICE_PASSWORD
image: sqshq/piggymetrics-statistics-service
restart: always
logging:
options:
max-size: "10m"
max-file: "10"

statistics-mongodb:
environment:
MONGODB_PASSWORD: $MONGODB_PASSWORD
image: sqshq/piggymetrics-mongodb
restart: always
logging:
options:
max-size: "10m"
max-file: "10"

notification service:
environment:
CONFIG_SERVICE_PASSWORD: $CONFIG_SERVICE_PASSWORD
MONGODB_PASSWORD:$MONGODB_PASSWORD
NOTIFICATION_SERVICE_PASSWORD: $NOTIFICATION_SERVICE_PASSWORD
image: sqshq/piggymetrics-notification-service
restart: always
logging:
options:
max-size: "10m"
max-file: "10"

notification-mongodb:
image: sqshq/piggymetrics-mongodb
restart: always
environment:
MONGODB_PASSWORD: $MONGODB_PASSWORD
logging:
options:
max-size: "10m"
max-file: "10"

monitoring:
environment:
CONFIG_SERVICE_PASSWORD: $CONFIG_SERVICE_PASSWORD
image: sqshq/piggymetrics-monitoring
restart: always
ports:
- 9000:8080
- 8989:8989
logging:
options:
max-size: "10m"
max-file: "10"

Development mode

В режимі розробки передбачається будувати образи, а не забирати їх з репозиторію. Всі контейнери виставлені назовні для зручного дебага. Ця конфігурація успадковується від наведеної вище, переписуючи і розширюючи зазначені моменти. Запускається командою
docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d

docker-compose.dev.yml
version: '2'
services:
rabbitmq:
ports:
- 5672:5672

config:
build: config
ports:
- 8888:8888

registry:
build: registry

gateway:
build: gateway

auth-service:
build: auth-service
ports:
- 5000:5000

auth-mongodb:
build: mongodb
ports:
- 25000:27017

account-service:
build: account-service
ports:
- 6000:6000

account-mongodb:
build: mongodb
ports:
- 26000:27017

statistics-service:
build: statistics-service
ports:
- 7000:7000

statistics-mongodb:
build: mongodb
ports:
- 27000:27017

notification service:
build: notification service
ports:
- 8000:8000

notification-mongodb:
build: mongodb
ports:
- 28000:27017

monitoring:
build: monitoring

Примітки

Всім Spring Boot додатками в цьому проекті для старту необхідний доступний Config Server. Завдяки опції
fail-fast
на
bootstrap.yml
кожного програми та опції restart: always в докері, контейнери можна запускати одночасно (вони будуть автоматично продовжувати спроби старту, поки не підніметься Config Server).

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


Посилання по темі
  • Статті про микросервисах Мартіна Фаулера
  • Building Microservices Сема Ньюмана — книга, в деталях охоплює всі основні поняття Микросервисной архітектури
  • Ціла брошура з описом відмінностей Микросервисов і SOA
  • Серія статей про Микросервисах в блозі NGINX від Кріса Річардсона, засновника CloudFoundry
  • Дуже крутий і докладний доповідь Кирила Толкачова tolkkv і Олександра Тарасова aatarasoff з Альфа-Лабораторії, двух частях. Про микросервисах, Spring Boot, Spring Cloud, інструментах Netflix OSS, Apache Thrift і в чому іншому.



Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.