Reactive — это парадигма программирования, использующая асинхронное программирование. Она наследует эффективность параллелизма асинхронной модели с простотой использования декларативного программирования.
Многопоточность позволяет распараллелить работу на нескольких процессорах, но когда выполняется операция ввода-вывода, поток блокируется в ожидании завершения ввода-вывода.
Reactive/Async не распараллеливает работу на нескольких процессорах, но когда выполняется операция ввода-вывода, процессор передается следующей задаче в цикле событий.
Как правило, использование нескольких процессов или потоков лучше для систем с ограниченным процессором, а async/reactive — для систем с ограниченным вводом-выводом.
ПРИМЕЧАНИЕ: Это модифицированный репост теста, проведенного в 2018 году. Фото Andrew Wulf on Unsplash
Более простое асинхронное программирование
Давайте рассмотрим пример метода, который извлекает пользователя из базы данных, выполняет некоторые преобразования, трансформации и затем выводит результаты.
Синхронная версия выглядит следующим образом:
User user = getUserFromDBSync(id);
user = convertUser(user);
user = processResult(user);
displayResults(user);
Довольно просто.
Асинхронная версия с обратными вызовами имеет глубоко вложенный код, по сути, «ад обратных вызовов».
getUserFromDB(id, user -> {
convertUser(user, convertedUser -> {
processResult(convertedUser, processedUser -> {
displayResults(processedUser);
});
});
});
Теперь тот же пример с реактивным подходом. Он гораздо более читабелен и удобен для сопровождения, чем версия с асинхронным/обратным вызовом.
getUserFromDBAsync(id)
.map(user -> convertUser(user))
.map(user -> processResult(user))
.subscribe(user -> displayResults(user));
Улучшенная многозадачность
Что касается параллелизма, я решил провести небольшой тест, чтобы оценить разницу между реактивной и синхронной версией для операций, связанных с IO.
Вы можете получить тестовый проект здесь https://github.com/trincaog/reactivetest.
Тест настроен следующим образом:
- Клиент нагрузочного тестирования (Gatling)
- Тестовый сервис (Spring Boot)
- Внешний бэкенд-сервис (симулированный)
Тестовый бэкенд-сервис
Тестовый бэкенд-сервис имитирует запрос к внешнему сервису (например, базе данных), которому требуется некоторое время, чтобы вернуть список записей. Для упрощения тест не отправляет запрос к реальной базе данных, вместо этого он имитирует задержку ответа.
Настройка синхронной версии:
- Приложение Spring Boot 2.0 (2.0.0.RC1) / фреймворк Spring MVC.
- Встроенный контейнер Tomcat с max threads=10.000 (большое число, чтобы избежать очереди запросов).
- Хостинг на AWS ECS/Fargate с 256 mCPU / 2GB RAM
Установка реактивной версии:
- Приложение Spring Boot 2.0 (2.0.0.RC1) / фреймворк Spring Webflux
- фреймворк Netty
- Размещено на AWS ECS/Fargate с 256 mCPU / 2GB RAM
Клиент нагрузочного тестирования
Использовались следующие компоненты:
- AWS EC2 t2.small 1vCPU / 2GB RAM
- Gatling 2.3.0
- Непрерывный цикл запросов без каких-либо задержек между запросами
- 2 конфигурации внешнего сервиса: одна с временем отклика 500 мс; другая с временем отклика 2.000 мс.
Нагрузочный тест #1: Задержка внешнего сервиса 500 мс
При <=100 одновременных запросах время ответа очень похоже между двумя версиями.
После 200 одновременных пользователей версия synchronous/tomcat начинает ухудшать время отклика, в то время как реактивная версия с Netty держится до 2.000 одновременных пользователей.
Нагрузочный тест №2: Задержка внешней службы 2.000 мс
В этом тесте используется гораздо более медленная служба поддержки (в 4 раза медленнее), и служба справляется с гораздо большей нагрузкой. Это происходит потому, что, хотя количество одновременных пользователей одинаково, количество запросов/сек в 4 раза меньше.
В этом тесте синхронная версия начинает ухудшаться при 4-5-кратном количестве одновременных пользователей, чем в предыдущем тесте с задержкой 500 мс.