В этой статье рассмотрим пример с двумя микросервисами. Обнаруживать друг друга они будут с помощью Eureka. Кроме того, рассмотрим, как запускать микросервисы в нескольких экземплярах и балансировать нагрузку на микросервис (со стороны клиента).
Пример с микросервисами
Итак, пусть у нас имеется два микросервиса (два Spring Boot приложения, предоставляющих REST API). Один — для внешнего пользователя, второй — для того, чтобы к нему обращался первый.
Микросервис Zoo
Сюда внешний пользователь приходит, чтобы посмотреть животных. Он заходит на адрес:
localhost:8081/animals/any
и видит там одно случайно выбранное животное. Например, dog:
Вот контроллер для отображения:
@RestController public class ZooController { @Autowired private RandomAnimalClient randomAnimalClient; @GetMapping("/animals/any") ResponseEntity<Animal> seeAnyAnimal(){ return randomAnimalClient.random(); } }
RandomAnimalClient внедрен потому, что животные на самом деле берутся из второго микросервиса Random Animal. RandomAnimalClient как раз и делает запрос к нему.
Микросервис Random Animal — выдает случайное животное
Второй микросервис — тоже отдельное Spring Boot приложение. Вот его контроллер:
@RestController public class RandomAnimalController { private final AnimalDao animalDao; public NameController(AnimalDao animalDao){ this.animalDao=animalDao; } @GetMapping("/random") public Animal randomAnimal(){ return animalDao.random(); } }
Этот микросервис запущен в двух экземплярах на портах 8082 и 8083. То есть случайное животное можно получить по любому из адресов:
localhost:8082/random localhost:8083/random
AnimalDao, внедренный в RandomAnimalController, берет случайное животное из списка:
@Component public class AnimalDao { private List<Animal> list = Arrays.asList(new Animal("cat"), new Animal("dog"), new Animal("fox")); public Animal random(){ Random rand = new Random(); return list.get(rand.nextInt(list.size())); } }
Решение без Spring Cloud и возникающие проблемы
Если решать задачу без использования замечательных возможностей Spring Cloud, то со временем возникают проблемы:
- Придется вести учет url и портов. У нас задача простая — два микросервиса на трех портах, и это не сложно. Но если микросервисов много, и адреса динамически меняются (запускаются новые экземпляры микросервисов на новых портах, какие-то экземпляры падают)? Хотелось бы, чтобы при запуске и отключении очередного экземпляра микросервиса другие микросервисы были автоматически информированы о появлении и пропаже, и всё продолжало бы работать без исправления кода.
- Надо как-то выбирать, на какой из запущенных экземпляров микросервиса обратиться. Мы запускаем два экземпляра Random Animal. И первый микросервис должен долбить не один и тот же экземпляр, а выбирать их (примерно) по очереди.
Есть еще проблемы, но о них в следующих статьях, а пока про первые две.
- Первая проблема решается с помощью сервера Eureka. Мы запускаем отдельное приложение Eureka, которое ведет учет микросервисов и их адресов. Eureka по умолчанию запускается на порту 8761 — клиенты-микросервисы знают номер и уведомляют о себе при старте. Также сама Eureka периодически проверяет, жив ли клиент-микросервис. Чтобы сделать приложение клиентом сервера Eureka, мы добавляем в него Maven-зависимость, и всё. После этого он зарегистрируется в Eureka при запуске автоматически.
- Нагрузка балансируется автоматически, если для обращения к экземплярам использовать не просто RestTemplate, а @LoadBalanced RestTemplate. (Можно еще использовать WebClient — он поддерживает реактивность — но о нем не в этой статье).
Решение со Spring Cloud
Итак, сначала о сервере Eureka, которая ведет реестр микросервисов.
Обнаружение сервисов с помощью Eureka Server
Чтобы создать приложение Eureka Server, в POM-файл нужно добавить зависимость:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency>
А главный класс нужно аннотировать @EnableEurekaServer:
@SpringBootApplication @EnableEurekaServer public class EurekaApplication { public static void main(String[] args) { SpringApplication.run(EurekaApplication.class, args); } }
Eureka Server готов. Осталось добавить настройки.
В application.properties пропишем:
eureka.client.registerWithEureka=false eureka.client.fetchRegistry=false spring.application.name=eureka-server spring.cloud.loadbalancer.ribbon.enabled=false
Теоретически сервер может выступать и клиентом. Первые две настройки отменяют эту возможность: не дают серверу регистрировать самого себя в качестве клиента.
Третья настройка задает имя микросервиса, а четвертая отменяет использование Ribbon в качестве балансировщика нагрузки по умолчанию. Эти настройки будут в всех микросервисов. Но о балансировке ниже.
Пока займемся регистрацией микросервисов в качестве клиентов Eureka.
Сами микросервисы — Eureka Clients
Чтобы сделать микросервисы Zoo и Random Animal клиентами Эврики, в них необходимо добавить зависимости:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
Для каждого микросервиса необходимо задать имя, порт и отключить балансировщик нагрузки Ribbon (потому что будем использовать другой):
spring.application.name=zoo spring.cloud.loadbalancer.ribbon.enabled=false server.port=8081
Все вместе — запуск сервера Eureka и его клиентов
Далее нужно запустить сначала сервер Eureka, а затем клиенты-микросервисы.
Eureka по умолчанию за пускается на порту 8761, и если открыть
http://localhost:8761/
то увидим список запущенных микросервисов с их именами:
Random-animal запущен дважды — на портах 8082 и 8083.
Как запустить экземпляр на другом порту в IDEA
Чтобы указать порт для второго экземпляра Random-animal, в IntelliJ IDEA в Run->Edit Configurations в поле VM Options прописываем:
-Dserver.port=8083
Эта настройка перезаписывает порт, заданный в application.properties.
Балансировка нагрузки с помощью @Loadbalanced RestTemplate
Для обращения одного микросервиса к другому нам понадобятся имена, заданные в настройках (spring.application.name).
Итак, из Zoo к Random Animal мы обращались с помощью RandomAnimalClient, вот его код:
@Component public class RandomAnimalClient { private static final Logger LOGGER = LoggerFactory .getLogger(RandomAnimalClient.class); private final RestTemplate restTemplate; RandomAnimalClient(RestTemplate restTemplate) { this.restTemplate = restTemplate; } //spring.application.name=random-animal есть в настройках Random Animal public ResponseEntity<Animal> random() { LOGGER.debug("Sending request for animal {}"); return restTemplate.getForEntity("http://random-animal/random", Animal.class); } }
Но RestTemplate тут не простой, а сбалансированный. Именно поэтому обращение
http://random-animal/random
идет на соседний микросервис с прописанным в настройках именем random-animal, а не в интернет.
Чтобы сделать RestTemplate сбалансированным, просто аннотируем его @LoadBalanced:
@Configuration public class RestTemplateConfig { @Bean @LoadBalanced RestTemplate restTemplate() { return new RestTemplate(); } }
Для использования @LoadBalanced RestTemplate в POM никакой зависимости добавлять не нужно.
Давайте добавим в RandomAnimalController вывод животного в консоль. Снова всё запустим и будем обновлять главную страницу. В консолях двух запущенных экземпляров Random Animal будет по очереди выводиться животное — то в одной консоли, то в другой. Видно, что балансировка нагрузки происходит:
Рассмотренная балансировка называется client-side load balancing, потому что именно клиент (тот, кто обращается к REST API) решает, к какому именно экземпляру сервиса обратиться. И делает он это с помощью @Loadbalanced RestTemplate.
DiscoveryClient — альтернатива @Loadbalanced RestTemplate
Кстати, код с @Loadbalanced RestTemplate можно переписать на более понятный. Можно взять обычный RestTemplate, внедрить DiscoveryClient в RandomAnimalClient и переписать метод random() так:
@Component public class RandomAnimalClient { private static final Logger LOGGER = LoggerFactory .getLogger(RandomAnimalClient.class); private final RestTemplate restTemplate; private final DiscoveryClient discoveryClient; RandomAnimalClient(RestTemplate restTemplate, DiscoveryClient discoveryClient) { this.restTemplate = restTemplate; this.discoveryClient = discoveryClient; } public ResponseEntity<Animal> random() { ServiceInstance instance = discoveryClient.getInstances("random-animal") .stream().findAny() .orElseThrow(() -> new IllegalStateException("Random-animal service unavailable")); UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder .fromHttpUrl(instance.getUri().toString() + "/random"); return restTemplate.getForEntity(uriComponentsBuilder.toUriString(), Animal.class); } }
DiscoveryClient получает список экземпляров микросервиса random-animal по его имени. Мы выбираем случайный экземпляр и обращаемся к нему.
@Loadbalanced RestTemplate делает примерно то же самое, но алгоритм выбора экземпляра лучше, так что рекомендуется использовать его.
С 2015 по умолчанию в Spring Cloud был включен балансировщик Ribbon от Netflix. Но сейчас есть новый Spring Cloud Load balancer, именно поэтому в настройках мы отключаем старый, как уже было показано выше:
spring.cloud.loadbalancer.ribbon.enabled=false
Итоги
Исходный код доступен на GitHub. В следующей статье рассмотрим API Gateway — балансировку на стороне сервера.
Есть также о Circuit Breaker и Config-сервере.
Самый адекватный ресурс по Sping Framework «по-русски»!
Спасибо большое автору за проделанную работу
Здравствуйте.
«В консолях двух запущенных экземпляров Random Animal будет по очереди выводиться животное – то в одной консоли, то в другой. Видно, что балансировка нагрузки происходит:», а ниже 2 одинаковых скриншота консоли. Видно, что 8082 порт двух на скриншотах. Это скорее всего не корректно. Меня просто сильно озадачил одинаковый вывод, даже если смотреть по времени.
Да, конечно, это ошибка. Там должны быть разные консоли на 8081 и 8082, спасибо.
точнее, на портах 8082 и 8083. Скриншот обновлен.
Спасибо большое за статьи и за то что исправляете ошибки. Поправьте и тут тогда пж.
Этот микросервис запущен в двух экземплярах на портах 8082 и 8083. То есть случайное животное можно получить по любому из адресов:
localhost:8081/random
localhost:8082/random
ок, исправлено.
Спасибо за проделанную работу!
Большое спасибо за ресурс! Не останавливайтесь! 😉