Spring Cloud Netflix Microservices — start project (серия статей) — часть 2

Kirill Sereda
Nov 1 · 11 min read

Продолжаем серию статей по теме Spring Cloud: Netflix.

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

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

Поехали.

Давайте вспомним наш план:

  1. В первой части статьи мы настраивали Eureka Server, Eureka Client(User-Service, Gallery-Service), использовали для всего этого базу MongoDB, и написали наши сервисы на Spring 5 (пусть будут реактивными, так интересней). Также подружили сервисы и заставили их общаться посредством RestTemplate, Feign Clientи WebClient. Наш user-service получает данные из БД через gallery-service. В случае выхода из строя БД или gallery-service мы используем библиотеку Hystrix.
  2. В этой части мы добавим Zuul API Gateway,Ribbon - балансировщик нагрузки. У нас будет Movie-service с запасной репликой, и мы настроим Load Balancer. Настроим Config Serverи Config Client. Также мы прикрутим сюда JWT Security.
  3. В третьей части сделаем несколько реплик для нашего Eureka Server и остальных сервисов и заставим их работать вместе. Также добавимELK (Elastic Search Kibana) для просмотра логов. Познакомимся более детально с Hystrix и Hystrix Dashboard.
  4. Затем добавим Spring Cloud Sleutchи попробуем Spring Cloud Stream вместе с Kafka и RabbitMQ. И наконец познакомимся с протоколамиWebSocket и RSocketи попробуем с их помощью построить связь между сервисами.
  5. Разберем Saga Patternи внедрим ее в наш “мини” проект. Познакомимся с Axon.

Netflix OSS

Used: Eureka, Config Server, Config Client, Zuul Api Gateway, Feign Client, RestTemplate, WebClient, Hystrix, Ribbon, Security (JWT) + MongoDB + Docker + Reactive (Spring WebFlux).


Zuul

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

У нас может быть несколько служб (экземпляров), работающих на разных портах. Zuul нужен для приема внешних запросов и маршрутизации в нужные сервисы внутренней инфраструктуры

Spring Cloud нативно интегрирован с ним.

Zuul-service по ссылке на github:

Zuul github

Используется с аннотацией @EnableZuulProxy в основном классе.

Также нам надо пометить этот сервис как @EnableEurekaClient, чтобы наш Eureka Server обнаружил его и они подружились. При старте сервиса он пошлет эхо запрос на Eureka Server и получит подтверждение от Eureka.

@SpringBootApplication
@EnableEurekaClient
@EnableZuulProxy
public class ZuulServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulServiceApplication.class, args);
}
}

Zuul автоматически выберет список серверов в Eureka. Он хорошо работает в связке с Hystrix, Ribbon и Turbine.

Он запускает предварительные фильтры (pre-filters), затем передает запрос с помощью клиента Netty, а затем возвращает ответ после запуска постфильтров (post-filters).

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

Я также написал статью про Zuul для более подробной информации:

Статья Zuul API Gateway

Как работают фильтры ?

Фильтры написаны на Groovy, но Zuul поддерживает любой язык на основе JVM. Исходный код каждого фильтра записывается в указанный набор каталогов на сервере Zuul, которые автоматически обновляются в случае каких-либо нововведений. Обновленные фильтры считываются, динамически компилируются в работающий сервер и вызываются Zuul для каждого последующего запроса.

Пул подключений

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

Повтор отправки запроса

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

  • ошибка таймаута
  • ошибка в случае кода статуса (например статус 503)

Повторный запрос отправлен не будет, в случае:

  • если утеряна часть body запроса
  • если уже был начат ответ клиенту

Push Notifications

Начиная с версии 2.0 Zuul поддерживает отправку push-сообщения — отправку сообщений с сервера на клиент (Push-соединения отличаются от обычных HTTP-запросов тем, что они постоянны и долговечны).

Он поддерживает два протокола, WebSockets и Server Sent Events (SSE).

В нашем примере в файле application.yml

spring:
application:
name: zuul-service
server:
port: 8766
eureka:
client:
serviceUrl:
defaultZone: ${EUREKA_URI:http://localhost:8761/eureka}
instance:
preferIpAddress: true

Также мы должны настроить роутинг маршрутов на наши сервисы. Т.е. чтобы мы смогли попасть на gallery-service например не через

http://localhost:8081

А непосредственно через сам zuul!

http://localhost:8766/gallery

или на user-service

http://localhost:8766/users

Т.е. наши сервисы user-service и gallery-service ничего не знают друг о друге и не знают ничего о zuul. Это слабая связанность наших сервисов, что является одним из преимуществом использования данной архитектуры. Нам вообще не важно где находится например gallery-service. Нам даже не надо знать на каком порту, его адрес и прочее. Нам надо знать только его имя, и все!

Это очень удобно, правда ?

Для этого добавим такие настройки

zuul:
routes:
auth-service:
strip-prefix: false
sensitive-headers: Cookie,Set-Cookie
path: /auth/**
service-id: security-service
gallery-service:
path: /gallery/**
service-id: gallery-service
user-service:
path: /users/**
service-id: user-service

Также необходимо указать вот такой параметр

zuul.ignored-services=*

которая означает, что если мы запустим наш zuul-service и перейдем по адресу

http://localhost:8766/gallery

мы попадем на наш gallery-service и дальше сможем ходить по всем его урлам без проблем. Наш zuul будет автоматически перенаправлять на все его урлы.

Также в zuul-service я настроил JWT Security.

Сейчас перейдем непосредственно к самому security-service.


Security-Service

Настроим здесь JWT Security для нашего user-service.

Также помечаем наш сервис как @EnableEurekaClient.

В файле application.yml указываем имя нашему сервисы, порт, и информацию про Eureka Server. Этого достаточно.

spring:
application:
name: security-service
server:
port: 9100
eureka:
client:
serviceUrl:
defaultZone: ${EUREKA_URI:http://localhost:8761/eureka}
instance:
preferIpAddress: true

Каков принцип аутентификации ?

  1. Пользователь отправляет запрос на получение токена, передающего его учетные данные.
  2. Сервер проверяет учетные данные и отправляет обратно токен.
  3. При каждом запросе пользователь должен предоставить токен, и сервер будет проверять этот токен.

Для начала нам надо проверить токен.

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

Вместо этого мы можем проверить токены на уровне zuul и позволить службе аутентификации проверять учетные данные пользователя и выдавать токены.

Мы будем блокировать запросы, если они не аутентифицированы (кроме запросов на генерацию токенов).

Давайте как раз это и сделаем.

Как я писал выше, мы настроили кое-что связанное с security в модуле zull-service. Вы можете увидеть все это в коде на github:

Zuul github

В zuul-service нам нужно сделать две вещи:

  1. проверять токены с каждым запросом
  2. предотвращать все неаутентифицированные запросы к нашим службам.

В классе SecurityTokenConfig мы определяем наши конфигурации безопасности.

В классе JwtTokenAuthenticationFilter мы реализовываем наш фильтр, который проверяет токены (используем OncePerRequestFilter)

Теперь перейдем к нашему security-service:

Здесь нам надо:

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

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

Также нам надо сделать фильтр:

Мы используем JwtUsernameAndPasswordAuthenticationFilter. Он используется для проверки учетных данных пользователя и создания токенов. Имя пользователя и пароль должны быть отправлены в запросе POST.

Тестируем:

Запустите вначале Eureka Server. Затем запустите gallery-service, user-service, zuul-service, security-service.

Сначала попробуем получить доступ к gallery-service без токена (через наш zuul-service, а не напрямую в gallery-service)

localhost:8766/gallery

Вы должны получить несанкционированную ошибку

{ 
"timestamp": "...",
"status": 401,
"error": "Unauthorized",
"message": "No message available",
"path": "/ gallery /"
}

Для того, чтобы получить токен, отправьте POST запрос на наш zuul-service через Postman (например)

POST
localhost:8766/auth
{
"username":"admin",
"password":"admin"
}

При условии что у вас установлено

Content-Type — application/json Accept — application/json

В ответе в Headers вы получите сгенерированный токен.

Скопируйте и вставьте его в Authorization — и выберите там Bearer token

А теперь отправьте запрос на gallery-service через наш zuul-service (как было сделано выше)

localhost:8766/gallery

Теперь вы не получите ошибку аутентификации. Все должно быть в порядке.

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

Ссылка на user-service на Github:

User-Service github

Config-Server и Config-Client

Создадим общий сервис config-server, который будет содержать ссылки на хранилище с общими настройками. Чтобы не писать одинаковый код в разных сервисах, будем использовать общее хранилище настроек.

Мы рассмотрим оба варианта:

  • хранение настроек локально
  • хранение настроек на github

По умолчанию будем использовать локальный способ хранения (откуда другие сервисы будут считывать настройки — коннекшен к базе).

Используется с аннотацией

@EnableConfigServer

Также необходимо добавить зависимость

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>

В файле application.yml

server:
port: 8888
spring:
application:
name: config-server

Указываем 2 способа хранения

spring:
profiles:
active: native
---
spring:
profiles: native
cloud:
config:
server:
native:
search-locations:
/home/ks/IdeaProjects/Gallery-Service/ms-config-properties/{application}/{profile},
---
spring:
profiles: git
cloud:
config:
server:
git:
uri: https://github.com/ksereda/Gallery-Service/
search-paths:
- "ms-config-properties/{application}/{profile}"

По умолчанию — локальный.

И создадим папку ms-config-properties в которой будут лежать сами настройки.

Рассмотрим на примере gallery-service.

Создаем внутри папку с идентичным названием нашего сервиса (gallery-service), в которой мы используем разные профили. По умолчанию у нас профиль default.

Внутри должен быть файл с названием сервиса — gallery-service.yml, в котором будем прописывать настройки (какие хотим).

Теперь вернемся в наш gallery-service: в нем должна быть указана аннотация

@EnableConfigClient

чтобы указать, что он также является конфиг клиентом.

Также в application.yml файл нашего gallery-service необходимо добавить следующие настройки

cloud:
config:
discovery:
enabled: true
service-id: config-server

чтобы указать ему путь к config-server.

Теперь когда вы запустите ваш gallery-service, он по умолчанию зарегистрируется в Eureka и будет обращаться к config-server, который уже будет направлять его в локальное хранилище с настройками. Далее происходит получение настроек среди всех следующим образом: Он смотрит на {application} - имя сервиса (в нашем примере это gallery-service), и далее на {profile} - профиль по умолчанию у нас default.

Это необходимо сделать не только для gallery-service, но и для всех остальных сервисов, которые используют какие-либо настройки. Чтобы не плодить конфиг в самих сервисах, лучше вынести их в отдельный сервис, который отвечает за это.

Ссылки на github:

config-server github

ms-config-properties github


Ribbon

Ribbon - это балансировщик нагрузки.

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

Представьте, что у вас есть распределенная система, которая имеет много приложений, работающих на разных компьютерах. Но если количество пользователей большое, приложение обычно cоздает разные реплики, каждая реплика работает на отдельном компьютере. В это время появляется “Load Balancer” (Балансировка нагрузки), которая помогает распределять входящий траффик равно между серверами.

Ribbon предоставляет:

  • Отказоустойчивость
  • Load balancing
  • Поддержка нескольких протоколов (HTTP, TCP, UDP) в асинхронной и реактивной модели
  • Кеширование

и т.д.

Есть 2 вида LoadBalancer:

  • Server side Load Balancer

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

  • Client-Side Load Balancer

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

Существуют такие понятия, как:

  • Список серверов​​​​​​​ (List Of Servers): Наш сервис должен получить необходимую информацию, расположенную на других сервисах от разных производителей. У нас есть список этих серверов. Он включает в себя сервера, которые напрямую сконфигурированы в нашем сервисе.
  • Фильтрованный список серверов (Filtered List of Servers): Например некоторые сервера заняты или недоступны сейчас. Наш сервис отбросит эти сервера из списка, и в конце будет список более подходящих серверов (фильтрованнный список).
  • Load Balancer (Ribbon): Есть некоторые стратегии для решения. Но они обычно основываются на “Rule Component” (Компонент правил). По умолчанию Spring Cloud Ribbon использует стратегию ZoneAwareLoadBalancer (Сервера в одной зоне (zone) с нашим сервисом).
  • Ping: Ping — это способ, который использует наш сервис для быстрой проверки работает ли на тот момент сервис или нет? Eureka занимается этим по умолчанию, но Spring Cloud позволяет вам кастомизировать проверку по вашему усмотрению.

Мы сделали movie-service, который представляет обычный CRUD, работающий с базой MongoDB а порту 27018 (в docker).

также мы сделали реплику этого movie-service:

смотри чуть ниже как сделать реплику сервиса.

Запуск MongoDB на порту 27018

docker run mongo --port 27018

В user-service мы добавили

@RibbonClient(name = "movie-service", configuration = RibbonConfiguration.class)

Теперь наш основной класс user-service выглядит следующим образом

@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
@RibbonClient(name = "movie-service", configuration = RibbonConfiguration.class)
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
// Create a bean for restTemplate to call services
@Bean
@LoadBalanced // Load balance between service instances running at different ports.
public RestTemplate restTemplate() {
return new RestTemplate();
}
// for using WebClient
public @Bean
WebClient webClient() {
return WebClient.builder().clientConnector(new ReactorClientHttpConnector()).baseUrl("http://localhost:8081").build();
}
}

чтобы он сам определял, на какой сервис стучаться (на сам movie-service или на его реплику, в зависимости от нагрузки на сам movie-service).

Также в user-service создаем класс RibbonConfiguration. Теперь нам нужно создать еще один класс конфигурации для Ribbon, чтобы упомянуть алгоритм балансировки нагрузки и проверку работоспособности. Теперь мы будем использовать значение по умолчанию, предоставленное Ribbon, но в этом классе мы можем переопределить их и добавить нашу собственную логику.

Я добавил MovieController, в котором при помощи RestTemplate мы получаем данные из сервиса movie-service (как было рассмотрено ранее, только для gallery-service).

В файле application.yml у нас появились дополнительные записи

movie-service:
ribbon:
eureka:
enabled: true
ServerListRefreshInterval: 1000
#movie-service:
# ribbon:
# listOfServers: localhost:8085,localhost:8086
# eureka:
# enabled: true

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

Если мы выключим первую настройку и включим вторую, то если вы запустите новый экземпляр микросервиса на другом порту (например 8057, которого нет в списке), Ribbon не отправит запрос новому экземпляру, пока мы не зарегистрируем его вручную в Ribbon (пока не добавим в этот список).

Делаем jar файл для movie-service и запускаем ее на другом порту 8086 (сам movie-service на 8085 порту).

Как сделать реплику сервиса ?

Делаем jar файл для movie-service

maven install

и запускаем ее на другом порту 8086 (сам movie-service на 8085 порту).

java -jar -Dserver.port=8086 movie-service-0.0.1-SNAPSHOT.jar

Сейчас мы сэмулировали ситуацию, когда наша реплика не обязательно работает на этом же сервере и даже она может работать в другом часовом поясе. Для нас это не имеет никакого значения, ведь у нас для этого есть Eureka Server :)

У нас запущен Eureka Server, user-service, movie-service, реплика movie-service на порту 8086

Теперь когда шлем запрос из user-service на movie-service

localhost:8082/getAllMovies

получаем список всех фильмов.

Если пошлем запрос на получение стартовой страницы movie-service

localhost:8082/get

Мы увидим главную страницу сервиса movie-service, на которой отображается его имя и порт, на котором он запущен. Если мы попробуем несколько раз обновить страницу по этому запросу, то увидим как меняется порт, т.к. Ribbon определяет сам, какому сервису (movie-service либо его реплике запущенной на другом порту необходимо обратиться, в зависимости от нагрузки).

Очень удобно.

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

Вообще Ribbon позволяет производить более гибкую настройку на ваш вкус.

Ссылка на movie-service на github:

Movie-Service github

Спасибо тем, кто дочитал до конца.


Если вы нашли неточности в описании данной статьи, вы можете написать мне на email и я с радостью вам отвечу.

Kirill Sereda

email: kirill.serada@gmail.com

skype: kirill-sereda

linkedin: www.linkedin.com/in/ksereda

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade