Контекст

Недавно мы провели исследование того, какую модель программирования следует использовать для одного из наших новых проектов Spring Boot. Два кандидата, которые мы сравнили, — стиль императивный (запрос на поток) и стиль реактивного асинхронного/управляемого событиями.

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

Для нагрузочного теста мы использовали siege — терминал с открытым исходным кодом, основанный на простом инструменте нагрузочного тестирования.

Задний план

Давайте иметь некоторое общее представление об императивном и реактивном стиле реализации.

Императивная модель

В этой модели каждый запрос к конечной точке обрабатывается одним блокирующим потоком. Например, если у нас есть 10 запросов, Tomcat может выделить 10 потоков для обслуживания этого запроса. Обратите внимание, что по умолчанию у tomcat размер пула потоков равен 200. Таким образом, если мы ограничим размер пула потоков до 200, когда количество одновременных запросов увеличится более чем на 200, некоторые запросы могут даже истечь по тайм-ауту.

Реактивная модель

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

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

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

Испытательный стенд

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

  1. вызов GET для этого URL-адреса external_service http://‹external_service›/1/0/customer/customers/%s/users/%s .
  2. Простое преобразование DTO
  3. Нет проверки подлинности, авторизации или проверки

Реактивная реализация:

Императивная реализация:

Обратите внимание, что конечная точка «…/test» предназначена для реактивной реализации логики, а «…/testSync» — для императивной реализации.

Сравнение производительности

Мы используем инструмент siege следующим образом:

1 siege -b -c 250 -r 5 http://localhost:9556/rest/1/0/users/testSync

где -b для включения теста, -c означает количество одновременных пользователей, -r для количества повторений для каждого пользователя.

Время отклика и пропускная способность для реактивного стиля

Ось X по номеру запроса от 10 до 250. Ось Y — среднее время ответа на запрос и пропускная способность, МБ/с.

Время отклика и пропускная способность для императивного стиля

Сравнение времени отклика для обоих стилей

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

Когда бэкэнд делает один единственный запрос

Когда бэкенд делает два запроса

Вывод

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

Обратите внимание, что в нашей реализации размер реактивного пула потоков по умолчанию равен максимальному (4, # CpuCore). Существуют разные стратегии планирования для реактивной реализации (например, немедленное, однократное, параллельное и т. д., подробности см. в см.), которые мы не изменили в нашей реализации. Несмотря на то, что реактивная конфигурация по умолчанию работает лучше, чем императивный стиль, мы можем дополнительно изучить типы планировщика в зависимости от типа нагрузки на сервер.