null

Что если использовать Spring WebFlux вместо аннотации @Async?

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

Существует несколько вариантов её реализации. Например, один из подходов заключается в использовании Spring аннотации @Async. Эта аннотация позволяет разработчикам пометить метод как асинхронный, что означает, что он будет выполняться асинхронно в отдельном потоке. Таким образом, выполнение длительных операций не будет приводить к блокировке основного потока приложения и оно останется отзывчивым. Другой подход заключается в использовании Spring WebFlux, который предоставляет неблокирующую, управляемую событиями модель программирования для создания реактивных веб-приложений.

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

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

В этой статье мы кратко рассмотрим как использование Spring WebFlux вместо аннотации @Async может привнести улучшения в быстродействие приложения.

Рассматривать мы будем на примере веб-приложения, которое обращается к внешнему сервису API для получения информации о запрашиваемом ip адресе в формате JSON. Само приложение, а также результаты тестирования его производительности, взяты из труда "Why You Should Use Spring WebFlux Instead of the @Async Annotation" автора Ganesh Sahu.

И так, в качестве внешнего АПИ будем использовать сервис, расположенный по адресу https://ipinfo.io/161.185.160.93/geo

GET запрос по нему возвращает:

{
  "ip": "161.185.160.93",
  "city": "New York City",
  "region": "New York",
  "country": "US",
  "loc": "40.7143,-74.0060",
  "org": "AS22252 The City of New York",
  "postal": "10004",
  "timezone": "America/New_York",
  "readme": "https://ipinfo.io/missingauth"
}

Мы будем проводить стресс тестирование и зафиксируем показатели производительности, выполняя вызовы к этому API с использованием @Async и Web-flux.

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

@RestController
public class AsyncExternalCall {

    @Autowired
    AsyncService service;

    @GetMapping(value = "/data", produces = MediaType.APPLICATION_JSON_VALUE)
    public CompletableFuture<String> Data() {
        return service.getDataAsync();
    }
}

@Service
public class AsyncService {

    private final RestTemplate restTemplate;

    public AsyncService (RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @Async
    public CompletableFuture<String> getDataAsync() {
        String result = restTemplate.getForObject("https://ipinfo.io/161.185.160.93/geo", String.class);
        return CompletableFuture.completedFuture(result);
    }
}

Web-flux контроллер выглядит так:

@RestController
public class WebFluxExternalAPIController {

    private final WebClient webClient;

    public ExternalAPIController(WebClient.Builder webClientBuilder) {
        this.webClient = webClientBuilder.baseUrl("https://ipinfo.io").build();
    }

    @GetMapping(value = "/data", produces = MediaType.APPLICATION_JSON_VALUE)
    public Mono<String> getData() {
        return webClient.get()
                .uri("/161.185.160.93/geo")
                .retrieve()
                .bodyToMono(String.class);
    }
}

Чтобы смоделировать большое количество запросов и оценить время ответа и пропускную способность каждого из контроллеров (сервисов) при различных условиях, был применен инструмент нагрузочного тестирования - Apache JMeter (генерировалась нагрузка в 100 запросов в секунду, каждый сервис был запущен на своей отдельной виртуальной машине)

Ниже приведена статистика, полученная при использовании JMeter:

Как видно из статистики, сервис на Webflux превзошел сервис на @Async (4021 против 6684 в среднем, 8090 против 11542 в максимуме; чем меньше значение, тем лучше).

Также было проведено изолированное тестирование с помощью Postman, которое показало значительное сокращение времени отклика сервиса Webflux по сравнению с сервисом на @Async.

Подводя итог, хочется также отметить, что помимо более эффективной асинхронной обработки, Spring WebFlux предлагает готовую интеграцию с библиотекой Reactor.

Reactor - это мощный набор инструментов для реактивного программирования, включающий широкий спектр операторов, планировщиков и других функций, которые могут упростить написание отзывчивого кода. С помощью Spring WebFlux и Reactor можно создавать приложения, которые не только очень эффективны, но и гибки и просты в обслуживании. А еще Spring WebFlux можно использовать как с Java, так и с Kotlin. Это делает его отличным выбором для для создания современных высокопроизводительных приложений на удобном для вас языке.