Существует множество способов реализации REST-API. Большой популярностью пользуется Spring MVC на основе блокирующих вызовов, но все чаще попадаются проекты, использующие WebFlux на неблокирующих вызовах. Меня зовут Альберт Фатхудинов. Я Java-разработчик Технократии. В этой статье буду разбираться, какой из этих двух фреймворков работает лучше.
Технологии, которые использовали
Дисклеймер, в котором перечислим технологии, которые применялись в эксперименте:
Apache JMeter — для нагрузочного тестирования
VisualVm — для профилирования
MongoDb — NoSql База данных
PostgreSql — реляционная база данных
Netty — серверная среда неблокирующего ввода/вывода для разработки сетевых приложений
Apache Tomcat — контейнер сервлетов
Spring MVC — Фреймворк, обеспечивает архитектуру паттерна MVC при помощи слабо связанных готовых компонентов
Spring WebFlux — фреймворк, реализующий парадигму реактивного программирования, добавлен в Spring 5+
На этом дисклеймер закончился. Поехали!
Архитектура тестового приложения
![Схема сервиса для уведомления пользователя о штрафах, налогах и коммунальных платежах (Notification Service) Схема сервиса для уведомления пользователя о штрафах, налогах и коммунальных платежах (Notification Service)](https://habrastorage.org/getpro/habr/upload_files/c24/3c4/a84/c243c4a84b76953466953cc53ca9557d.png)
Представим абстрактного пингвина, которого зовут Шкипер. Он хочет узнать о своих последних штрафах, налогах и коммунальных платежах. Для этого он использует NotificationService. Но есть несколько проблем:
Время ответа БД PostgreSql и MongoDb — от 30 мс до 200 мс (специально занижал производительность БД неправильными индексами и большой вложенностью Json. Также добавил по 1 млн записей в каждую БД)
Время ответа удаленного сервиса — 1 - 3 сек
Шкипер будет ждать ответа от сервиса уведомления по самому медленному источнику т.е минимальное время ответа составить 1 - 3 сек
Внутреннюю реализацию сервиса(MVC Tomcat) можно посмотреть здесь.
Тестовые запросы
Время ответа от сервиса входит в диапазон от 1 до 3 секунд.
![](https://habrastorage.org/getpro/habr/upload_files/d1a/ed4/670/d1aed4670ad6dd3e3ca964d214896e0c.png)
![](https://habrastorage.org/getpro/habr/upload_files/65b/822/313/65b822313e1d41714e851b29af903898.png)
Такое время ответа не устраивает как пингвинов, так и нас. Нужно разобраться в чем же проблема.
Блокирующие вызовы
Без нагрузки Tomcat, который по умолчанию используется в starter-web Спринга, создаёт 10 потоков http exec. При нагрузке же он может масштабироваться до 200 потоков. В моем примере томкат масштабировался примерно до 170 потоков при единовременной нагрузке в 1000 пользователей.
![Без нагрузки Без нагрузки](https://habrastorage.org/getpro/habr/upload_files/31b/c96/034/31bc9603443c7d0fa85f4251529b8f0a.png)
![Под нагрузкой. Огромное количество потоков крадут друг у друга процессорное время. Нецелесообразное использование мощностей процессора Под нагрузкой. Огромное количество потоков крадут друг у друга процессорное время. Нецелесообразное использование мощностей процессора](https://habrastorage.org/getpro/habr/upload_files/cd2/e65/dba/cd2e65dba4825719b77bc388c715878b.png)
Нагрузочное тестирование показало, что 1000 одновременных запросов сервис обработал за 2 минуты 38 сек, количество ошибок составило 23.1 % от общего количества запросов при пропускной способности в 6.3 запроса в секунду.
![Сводная таблица по нагрузочному тестированию Сводная таблица по нагрузочному тестированию](https://habrastorage.org/getpro/habr/upload_files/1c2/984/ebd/1c2984ebda8c1887af47032f08ea7741.png)
Модель Tomcat основана на блокирующих вызовах. Когда поток обращается к БД или удаленному сервису, он блокируется и, пока не будет получен ответ, так и будет находится в заблокированном состоянии.
![Как ведет себя поток в стандартном MVC приложении на Tomcat Как ведет себя поток в стандартном MVC приложении на Tomcat](https://habrastorage.org/getpro/habr/upload_files/cdb/e8d/eba/cdbe8deba68e466213253f380f747903.png)
WEBFLUX Неблокирующие вызовы
Перейдем к другой реализации. Что за такой зверь WebFlux? Это микрофреймворк, который представляет полностью асинхронный и неблокирующий веб-стек, который позволяет обрабатывать большее количество одновременных запросов по сравнению с стандартным MVC.
![](https://habrastorage.org/getpro/habr/upload_files/dda/1b7/74d/dda1b774db25773a0ae6a4a3d33a3b06.png)
В основе WebFlux лежит Project reactor - project reactor это библиотека java 8, которая реализует модель reactive streams и предоставляет реактивные типы. По умолчанию WebFlux использует Netty. Возникает логичный вопрос: «Это все очень интересно, тут у нас и reactive streams, и реактивные типы, и еще Netty. Но что же это такое?» как раз на этот вопрос ответим чуть ниже.
Reactive streams
![](https://habrastorage.org/getpro/habr/upload_files/81a/cfc/34a/81acfc34afd55110594abce194426bd5.png)
Это стандартный способ асинхронной обработки в потоковом стиле. В него входят следующие интерфейсы: subscriber, publisher, subscription и processor.
Принцип работы reactive streams:
Subscriber подписывается на publisher(subscribe()), но общаться с publisher будет через subscription.
Subscription получает данные от publisher и отгружает их в подписчика (onNext(data)).
C помощью методов onError() и onComplete() Subscription принимает от Subscriber информацию о том, сколько данных он хочет получить от publisher через метод request(n). Старый добрый паттерн Наблюдатель, в лучшей реализации.
Реактивный сервер?
Да! Мы преобразуем наш сервис уведомления о штрафах в реактивное приложение. Для начала представим реактивную архитектуру:
![Архитектура реактивного приложения Архитектура реактивного приложения](https://habrastorage.org/getpro/habr/upload_files/577/971/73f/57797173f383da07cdeb0fa2992d39f5.png)
Приложение состоит из 5 составляющих:
HTTP Server. В нашем случае Netty, так как WebFlux по умолчанию предоставляет данный сервер.
Реактивный адаптер. Интересно и зачем же здесь адаптер? Все очень просто. Netty и WebFlux не совместимы, поэтому здесь и нужен адаптер.
Spring WebFlux.
Контроллер.
Репозиторий для коннекта с БД.
И последнее, все элементы архитектуры начинают общаться с помощью реактивного типа FLUX
Теперь подробнее про каждую часть.
NETTY
NETTY — асинхронная среда сетевых приложений, управляемая событиями. На входе у Netty в бесконечном цикле крутится поток. За счет каналов и селекторов он перенаправляет входящие запросы во входящие буферы и делегирует обработку запросов выделенному пулу асинхронных потоков.
![](https://habrastorage.org/getpro/habr/upload_files/659/549/ed2/659549ed270b3910f349fe92bea4a23b.png)
Есть очередь событий и event loop, который их обрабатывает и делегирует пулу асинхронных потоков. В то же время происходит регистрация Callback-а. Он вызывается для отгрузки данных, после завершения обработки асинхронным пулом потоков.
![](https://habrastorage.org/getpro/habr/upload_files/4ab/a74/6f3/4aba746f33dae2124ff2c4eb9b94d480.png)
Как же это выглядит в приложении? А вот как:
![](https://habrastorage.org/getpro/habr/upload_files/3d8/158/e41/3d8158e41fd45b7812f922e1dcdbb1b6.png)
Поток подписывается на определенное событие (выгрузка данных из БД), получает callback и идет работать дальше. После того, как данные будут готовы, поток вернется чтоб их забрать.
Reactive Adapter
![](https://habrastorage.org/getpro/habr/upload_files/028/7e3/b5b/0287e3b5b5cde58d7c8a5d065680595d.png)
Вернемся к реактивному адаптеру. Я упомянул, что Netty и WebFlux несовместимы. Вот тут и появляется Reactor IPC.
Это расширение, позволяющее интегрироваться с различными платформами и системами. Когда запрос поступает на Netty, он обрабатывается ChannelOpertaions, затем вызывается цепочка вызовов, которая достигает Dispatcher handler, а затем запрос достигает контроллера.
Затем на основе publisher выстраивается поток, достигающий ChannelOpertaions. Следом в ChannelOpertaions вызывается метод subscribe. Только в этот момент поток начинает свою работу.
Реактивные типы
![](https://habrastorage.org/getpro/habr/upload_files/1dc/4fd/98b/1dc4fd98ba536138ad7c3a35a8eef425.png)
К реактивным типам относятся Mono и Flux. Они имплементируют интерфейс publisher, т.е являются источниками данных.
Если нам нужно отгружать пользователю от 0 до N объектов используем FLUX
Если же нужно отгружать от 0 до 1 элементов используем MONO
Если мы ничего не хотим отгружать используем MONO<Void>
Publisher делят на два вида HOT и COLD.
Приведу аналогию: фильм, запущенный на Netflix с самого начала — это cold publisher, а стрим на twitch, в который мы ворвались на середине, — HOT. Cold publisher начинает отгружать данные, когда на него подписываются с самого начала, а Hot publisher отгружает данные тем, кто подписался с момента остановки отгрузки данных.
Элемент Processor импелементирует интерфейс subscriber и publisher, используется для обработки данных Mono или Flux для того, чтобы не обрывать стрим.
Теперь переведем сервис уведомления о штрафах из стандартного MVC в реактивный. Шкиперу должно понравиться.
Реактивное приложение
Начнем переводить наше приложение со стандартного MVC на WebFlux.
Чтобы перейти на WebFlux, нам нужно поменять зависимость с web на webflux. Мы получим готовый к использованию реактивный сервис.
![Было Было](https://habrastorage.org/getpro/habr/upload_files/3cf/2f0/7cc/3cf2f07ccdbe1b738179801fa2d775a0.png)
![Стало Стало](https://habrastorage.org/getpro/habr/upload_files/064/3ef/e8b/0643efe8b5b4f845ef65dac5e9d7b1d3.png)
Изменим контроллер так, чтобы он возвращал реактивный тип:
![](https://habrastorage.org/getpro/habr/upload_files/ae8/932/e2e/ae8932e2edf723968e97b5018eedac19.png)
И проведем нагрузочное тестирование(1000 единовременных пользователей):
![Без нагрузки
Без нагрузки](https://habrastorage.org/getpro/habr/upload_files/194/cf6/a91/194cf6a91026568a88bb61afa7bd5535.png)
![Под нагрузкой Под нагрузкой](https://habrastorage.org/getpro/habr/upload_files/c48/8b3/63c/c488b363c5d6ef25c5b1eac549d546c5.png)
Без нагрузки создался один поток reactor-http-nio-1. Он работает постоянно. Под нагрузкой NETTY масштабировал количество потоков до 12. Мой ноутбук 6-ядерный, работает в 12 потоках, поэтому NETTY масштабировал на количество потоков процессора. Они также все время работают и не простаивают.
![1. Таблица MVC 2. таблица WebFlux 1. Таблица MVC 2. таблица WebFlux](https://habrastorage.org/getpro/habr/upload_files/875/4ea/154/8754ea15402fde3d2d7368ff02d736d8.png)
Нагрузочное тестирование показало: обработали быстрее, но получили большее количество ошибок. Непорядок.
![](https://habrastorage.org/getpro/habr/upload_files/f4b/5c0/98c/f4b5c098cb7a9f9cacd21f9261ad211e.png)
«Что и требовалось доказать, еще не много и нас взорвут» — говорит Шкипер.
Нужно разобраться, в чем причина такого количества ошибок.
![Основная проблема — блокирующие вызовы к БД и удаленному сервису. Основная проблема — блокирующие вызовы к БД и удаленному сервису.](https://habrastorage.org/getpro/habr/upload_files/8c6/5ba/428/8c65ba428f2a7f6a8dca6eb0c7924196.png)
![Netty делегирует обработку событий асинхронным потокам. Этих потоков критически мало по сравнению с Tomcat: 200 против 12
Netty делегирует обработку событий асинхронным потокам. Этих потоков критически мало по сравнению с Tomcat: 200 против 12](https://habrastorage.org/getpro/habr/upload_files/e35/602/082/e3560208200e761a18f0478ca7c3be44.png)
Если заблокируются все потоки в AsyncThreadPool, придется откидывать запросы, пока не освободится AsyncThreadPool.
Сперва сделаем БД реактивными. Для этого подключим реактивные драйверы на MongoDb и PostgreSql. Начнем с MongoDb.
Reactive Mongo Driver
MongoDb предоставляет свою реализацию reactive streams. Чтобы ее использовать, нам нужно поменять зависимости.
![было было](https://habrastorage.org/getpro/habr/upload_files/f6b/7b2/2cc/f6b7b22cca35f2f864474c033fbc0679.png)
![стало стало](https://habrastorage.org/getpro/habr/upload_files/56b/0da/391/56b0da3916be219dbc161106f760a1f9.png)
Теперь поменяем имплементацию репозитория и сменим ее на реактивную.
![Было Было](https://habrastorage.org/getpro/habr/upload_files/ec8/ae7/1f9/ec8ae71f9daa804f1b4ed71120b15735.png)
![Стало Стало](https://habrastorage.org/getpro/habr/upload_files/ad9/65c/536/ad965c536798e200e54a7bb3d956c1fd.png)
Теперь наш репозиторий реактивный. Вместо List возвращаем реактивный тип Flux. Он возвращает нам от 0 до N элементов. Реактивный драйвер MongoDb использует под капотом Netty, а в прошлых версиях asynchronous socket channel. Если хотите разобраться в этом глубже, переходите по ссылке.
Reactive Driver PostgreSql
Наступило время превратить нашу реляционная базу данных в реактивную.
Компания Pivotal релизнула spring-data-r2dbc (Декабрь 2019), что позволяет легко перейти на реактивный драйвер.
Заменим зависимости:
![Было Было](https://habrastorage.org/getpro/habr/upload_files/67b/0f5/470/67b0f54709765d6d5bbfaffc524629bb.png)
![Стало Стало](https://habrastorage.org/getpro/habr/upload_files/673/ce7/b7c/673ce7b7c14b7ce857eb833a972e69fb.png)
Изменения незначительные, репозиторий имплементирует r2dbc репозиторий и возвращает Flux.
![Было Было](https://habrastorage.org/getpro/habr/upload_files/0ef/d51/b70/0efd51b701e861ed9caaea5040efa82d.png)
![Стало Стало](https://habrastorage.org/getpro/habr/upload_files/03a/996/9de/03a9969dea98f4051f42cb4783a350e9.png)
Из интересных фактов: под капотом свою работу выполняет Netty. Для разбора закрепляю ссылку на r2dbc-driver.
WebClient
Перейдем к самому интересному — интеграции с удаленном сервисом. Вместо RestTemplate будем использовать более удобный и крутой WebClient.
![Было Было](https://habrastorage.org/getpro/habr/upload_files/d6b/e55/aa6/d6be55aa6db05bb33ee8f837072cf0c6.png)
![Стало Стало](https://habrastorage.org/getpro/habr/upload_files/606/f00/172/606f001726bff833c74bfb7f1b08f126.png)
Client возвращает реактивный тип Mono.
Reactive Service
Теперь приступим к изменению сервиса и контроллера, так как они должны возвращать тоже реактивные типы.
![Было Было](https://habrastorage.org/getpro/habr/upload_files/0aa/cb0/694/0aacb0694c8fd71321181044681bd14b.png)
![Стало Стало](https://habrastorage.org/getpro/habr/upload_files/3b4/d1a/0e2/3b4d1a0e25923f661b2623af301ea01b.png)
Операцией flatMapIterable преобразуем Mono<List<CommunalFineDTO>> > Flux<CommunalFineDTO>. Операцией Flux.merge соединяем несколько Flux в один общий Flux.
Reactive Controller
Также меняем контроллер.
![Было Было](https://habrastorage.org/getpro/habr/upload_files/790/55c/de3/79055cde36a5d72d37dbb39b48f5eb70.png)
![Стало Стало](https://habrastorage.org/getpro/habr/upload_files/d53/25b/b59/d5325bb5980fa10d880d91a4e72470e1.png)
MediaType.APPLICATION_STREAM_JSON_VALUE дает понять контроллеру, что мы будем стримить данные пользователю пачками. То есть, как будет готов объект NotificationDTO, он сразу отгрузится пользователю, не дожидаясь остальных.
Как это выглядит в браузере:
![Данные приходят не массивом, а пачками, по готовности Данные приходят не массивом, а пачками, по готовности](https://habrastorage.org/getpro/habr/upload_files/766/e1b/915/766e1b915636057c3696e8b0234850b2.png)
![transfer-encoding: chunked > позволяет надежно доставлять данные от сервера без необходимости заранее знать точный размер всего HTTP-сообщения. Как доказательство не присутствует Content-Length.
transfer-encoding: chunked > позволяет надежно доставлять данные от сервера без необходимости заранее знать точный размер всего HTTP-сообщения. Как доказательство не присутствует Content-Length.](https://habrastorage.org/getpro/habr/upload_files/921/ddb/7b8/921ddb7b8f00cbd4b91e3e0075bfafa2.png)
Теперь с пользователем общаемся с помощью событий. Каждое уведомление(NotificationDTO) и есть событие, которое нужно обработать. Пользователь не нужно ждать всего ответа.
Осталось поменять application.yaml.
![Было. Обратите внимание на Jdbc(блокирующий драйвер). Было. Обратите внимание на Jdbc(блокирующий драйвер).](https://habrastorage.org/getpro/habr/upload_files/01e/ccf/6b1/01eccf6b1608ef997aa54040309a8281.png)
![Стало. Поменял jdbc > на r2dbc. Для MongoDB настройки остались такими же Стало. Поменял jdbc > на r2dbc. Для MongoDB настройки остались такими же](https://habrastorage.org/getpro/habr/upload_files/6a3/367/6db/6a33676db762a35734ddc7e01fee56d2.png)
Нагрузочное тестирование реактивного приложения
![Нагрузочное тестирование MVC
Нагрузочное тестирование MVC](https://habrastorage.org/getpro/habr/upload_files/805/de7/83b/805de783b1f24496457c9e4204d27535.png)
![WebFlux с блокирующим БД и RestTemplate
WebFlux с блокирующим БД и RestTemplate](https://habrastorage.org/getpro/habr/upload_files/6f9/cea/e82/6f9ceae8216bdf1e0e18452e301269fd.png)
![WebFlux c реактивными драйверами и WebClien WebFlux c реактивными драйверами и WebClien](https://habrastorage.org/getpro/habr/upload_files/064/30c/b81/06430cb81036d9f768620a0dde7773bf.png)
Из нагрузочного тестирования видно, что мы смогли обработать 1000 одновременных запросов за 2:35, что быстрее MVC на 3 сек. Количество ошибок меньше на 9%. Реактивный сервис обработал большее количество запросов при меньшем количестве потоков. Заглянем в профилировщик.
![Без нагрузки. Общее количество потоков составило 30(Netty). У MVC 34(Tomcat)
Без нагрузки. Общее количество потоков составило 30(Netty). У MVC 34(Tomcat)](https://habrastorage.org/getpro/habr/upload_files/6ec/958/373/6ec95837371e6f3a132e5af523b85e2c.png)
![Под нагрузкой. Общее количество потоков составило 77(Netty). У MVC 221(Tomcat). Потоки работают на максимуме, используя всю мощность процессора.
Под нагрузкой. Общее количество потоков составило 77(Netty). У MVC 221(Tomcat). Потоки работают на максимуме, используя всю мощность процессора.](https://habrastorage.org/getpro/habr/upload_files/9f8/140/6fc/9f81406fcf3c0712a900c2b1257ba609.png)
Не напрягая зрение видно, что, помимо потоков приложения(reactor-htp-nio), появились еще два вида потоков:
reactor-tcp-nio — потоки для обслуживания r2dbc драйвера
nioEventLoopGroup — потоки реактивного драйвера MongoDB
Модернизированный сервис уведомления о штрафах
![](https://habrastorage.org/getpro/habr/upload_files/b5b/483/90b/b5b48390ba83e4be8653401a2ac40d76.png)
Новая архитектура позволяет делать выводы, что сервис отвечает требованиям Reactive Manifesto:
![](https://habrastorage.org/getpro/habr/upload_files/988/12d/c95/98812dc955fb06a45015586c57f246b7.png)
Responsive(отзывчивость) — с пользователем общаемся с помощью событий. Он получает все частями и работает с ними, даже если в процессе возникнет ошибка, пользователь получит часть данных.
Elastic(Эластичность) — приложение использует минимальное количество потоков, они не простаивают и работают на максимум. Также применимо вертикальное масштабирование. Мощнее процессор, больше потоков.
Message Driven — сервисы должны общаться с помощью событий. Реактивный сервис с помощью реактивных драйверов и WebClient общаются между собой именно так. То же и с пользователем: за счет MediaType.APPLICATION_STREAM_JSON_VALUE стримим событиями.
Resilient(Устойчивость) — сервис должен адекватно реагировать на возникновение ошибок. В модернизированном приложении не делал никаких действий по устойчивости для чистоты эксперимента.
Ну что, скажешь на это, Шкипер?
![](https://habrastorage.org/getpro/habr/upload_files/4a6/44e/bcd/4a644ebcd6705feeca842d87aba633f5.png)
Вопрос закрыт. Пингвины не справились с потоком штрафов, заказали судно и решили скрыться из страны. Миссия выполнена.
Вывод
WebFlux не про скорость обработки запроса, а про одновременное обслуживание большого количество запросов.
Нужно правильно продумать архитектуру приложения, чтобы не было блокирующих соединений. Система должна отвечать требованиям Reactive Manifesto.
Чтобы избавиться от блокировки бд и интеграции с медленными сервисами, используем реактивные драйверы и WebClient(R2dbc и Reactive MongoDb driver)
WebFlux лучше всего использовать для большого количества одновременных запросов(Высоконагруженные системы)
Если есть большое количество блокирующих соединений и малое количество одновременных запросов, лучше посмотреть в сторону стандартной реализаций MVC на tomcat
welovelain
Моя большая проблема в понимании, когда какие шедулеры и потоки использовать, чтобы избежать истощения пула потоков и блокировок. Если кто-то мог бы подсказать, что почитать на тему, было бы здорово.
nehaev
Вот, например.