Micronaut: фреймворк на JVM, который вы определённо полюбите

Обложка поста

Рассказывает Роман Иванов

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

Основные требования к микросервисной архитектуре можно свести к двум — скорость и надёжность. Поэтому главная задача, которую старались решить создатели Micronaut, — сделать его более легковесным, чем Spring Boot, и тем самым более быстрым.

Разберёмся подробнее, что из себя представляет данный фреймворк, какие у него преимущества и как создавать на нём микросервисы.

Для эффективного создания микросервисов в фреймворк уже включён HTTP-сервер и контейнер зависимостей, создающийся во время компиляции.

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

Был поставлен под сомнение основной инструмент бизнес-фреймворков — рефлексия, которая используется для инъекции зависимостей и генерации различных прокси-классов на лету. Подобные технологии требуют большого количества ресурсов от виртуальной Java-машины, поэтому они влекут за собой проблемы с производительностью — чрезмерно большое количество действий во время старта и использование памяти в рантайме сильно влияет на запуск.

Создатели фреймворка Micronaut решили пойти другим путём: вобрав в своё творение лучшие практики уже существующих решений, они добавили новый контейнер DI/AOP, который внедряет зависимости во время компиляции, а не во время выполнения. Тем самым львиная доля зависимостей разрешается ещё до запуска приложения. То есть ещё до запуска приложения фреймворк Micronaut просканирует весь код на наличие аннотаций, попытается сгенерировать дополнительные классы, необходимые для правильного исполнения кода, и наполнит ими свой контекст.

Так как в Micronaut всё это произойдёт ещё до старта приложения, время запуска и потребление памяти не находятся в прямой зависимости от количества кода, в отличие от рантайм-фреймворка Spring Boot, который повсеместно использует рефлексию.

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

Сами разработчики считают основополагающими следующие принципы разработки фреймворка:

  • использовать рефлексию только в самых «безвыходных» случаях;
  • избегать прокси;
  • максимально оптимизировать время старта;
  • максимально уменьшать занимаемую память.

Так как Micronaut задумывался изначально как фреймворк для облачных приложений, вместе с ним идёт «джентльменский» набор нативных облачных решений для создания полноценной инфраструктуры: поддержка паттернов Service discovery и Circuit Breaker, трассировки запросов, распределённого логирования, балансировки нагрузки, асинхронных очередей — Kafka и RabitMQ, реактивного HTTP и стандартов JWT и OAuth2.

В этом материале будет показано, как быстро реализовать всю эту мощь всего в пару строк.

Для начала нужно создать проект. По ссылке это можно сделать быстро, подключив все нужные зависимости.

HTTP-сервер

В Micronaut включён быстрый, гибкий и высокопроизводительный HTTP-сервер Netty. Достаточно пары строк, чтобы запустить приложение и начать обрабатывать запросы.

Пример программы «Hello World» ниже:

import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;

@Controller("/hello")
public class HelloController {

   @Get("/{name}")
   public String hello(String name) {
       return "Hello, " + name;
   }
}

Всё выглядит точь в точь, как если бы программа была написана с использованием Spring.

Создать клиентский запрос так же просто:

import io.micronaut.http.annotation.Get;
import io.micronaut.http.client.annotation.Client;

@Client("/hello")
public interface HelloClient {

   @Get("/{name}")
   public String hello(String name);
}

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

Но Netty реактивный, поэтому создатели рекомендуют писать контроллер и клиент следующим образом:

import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.reactivex.*;

@Controller("/hello")
public class RxHelloController {

   @Get("/{name}")
   public Single<String> hello(String name) {
       return Single.just("Hello, " + name);
   }
}

И для клиента:

import io.micronaut.http.annotation.Get;
import io.micronaut.http.client.annotation.Client;
import io.reactivex.*;

@Client("/hello")
public interface RxHelloClient {

   @Get("/{name}")
   public Single<String> hello(String name);
}

Service Discovery

Фреймворк уже поддерживает популярные решения Eureka и Consul.

Чтобы добавить свой сервис в Eureka, даже не нужно добавлять аннотации в предыдущий пример, достаточно подключить зависимость runtime: "io.micronaut:micronaut-discovery-client"

И прописать в конфигурации параметры сервера:

ureka:
 client:
   registration:
     enabled: true
   defaultZone: "${EUREKA_HOST:localhost}:${EUREKA_PORT:8761/eureka}"

Если установлена переменная окружения (EUREKA_HOST), будет использована она, в противном случае соединение будет устанавливаться с локальной машиной.

Балансировка нагрузки

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

Micronaut также предоставляет нативную поддержку Netflix Ribbon. Для запуска балансировщика на стороне приложения необходимо выполнить пару простых действий.

Добавить зависимость compile "io.micronaut.configuration:micronaut-netflix-ribbon"

И указать настройку для Ribbon:

ribbon:
 VipAddress: test
 ServerListRefreshInterval: 2000

Circuit Breaker

При взаимодействии с другими службами в распределённой системе неизбежно наступит момент, когда удалённый сервис временно перестанет работать или будет отвечать ошибкой на любой запрос. Micronaut предлагает ряд инструментов помогающих восстановить связь. Например, любой метод в Micronaut может быть аннотирован с помощью @Retryable, чтобы применить настраиваемую политику повторов к методу. По умолчанию будет выполнено три запроса к другому сервису с перерывом в одну секунду. Количество попыток соединения и время ожидания между попытками можно настроить:

@Retryable( attempts = "5", delay = "2s" )
@Client("/hello")
public interface HelloClient { /*...*/ }

Но данные параметры лучше не менять, а находить причины задержек.

Более сложной формой @Retryable является аннотация @CircuitBreaker. Она не позволит конкретному сервису начать постоянное «бомбардирование» удалённого сервиса запросами, давая ему время восстановиться. То есть после нескольких неудачных попыток отправки запроса, следующие запросы будут сразу возвращены с ошибкой.

Трассировка запросов

Поддержка Zipkin уже внедрена во фреймворк. Для её добавления не нужно никакого дополнительного кода, всё уже сделано создателями.
Добавляем в файл настроек:

tracing:
 zipkin:
   enabled: true
   http:
     url: http://localhost:9411

И можем начать собирать информацию о наших микросервисах.

Работа с брокерами сообщений

Так как в микросервисном мире очень важен обмен асинхронными сообщениями, фреймворк нативно поддерживает такие решения как Kafka и RabiitMQ. Рассмотрим взаимодействие с первым. Добавляем зависимость в сборку:
compile 'io.micronaut.configuration:micronaut-kafka'

И настроим адрес сервера в YML-конфигурации:

kafka:
 bootstrap:
   servers: localhost:9092

Всё, мы готовы работать с одним из быстрейших асинхронных брокеров сообщений в мире.

Создаём клиент, который будет отправлять сообщения в очереди:

@KafkaClient
public interface SayHello {

   @Topic("test")
   void sendHello(@KafkaKey String key, String msg);
}

Фреймворк создаст класс, который мы можем забрать из контекста и отправить сообщение:

SayHello client = applicationContext.getBean(SayHello.class);
client.sendHello("HelloKey", name);

Принимать сообщения так же просто:

import io.micronaut.configuration.kafka.annotation.*;

@KafkaListener(offsetReset = OffsetReset.LATEST)
public class HelloListener {

   @Topic("test")
   public void receive(@KafkaKey String key, String msg) {
       System.out.println("Got Msg- " + key + " msg: " + msg);
   }
}

Мониторинг активности

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

URIОписание
/beansВозвращает информацию о загруженных бинах.
/healthВозвращает информацию об общем текущем состоянии сервиса.
/loggersВозвращает информацию о доступных логгерах и позволяет изменять настроенный уровень журналирования.

Вывод

Таким образом, Micronaut уже из коробки поддерживает множество инструментов, сильно упрощающих жизнь создателю любого микросервиса, а его отчасти инновационный подход к созданию контейнера бинов и разрешению зависимостей во время компиляции даёт ему неоспоримое преимущество перед главным конкурентом — Spring Boot.

Хинт для программистов: если зарегистрироваться на соревнования Huawei Honor Cup, бесплатно получите доступ к онлайн-школе для участников. Можно прокачаться по разным навыкам и выиграть призы в самом соревновании. Перейти к регистрации.