Как стать автором
Обновить

Комментарии 19

Мне кажется, то что вы описали называется API Gateway.
Да, всё верно. Единственное что обычно API Gateway просто пробрасывает апи наверх апи доменного сервиса без бизнес-логики и по этому можно использовать коробочные решения. У нас тоже такое есть)
Именно потому что так просто выстрелить себе в ногу, я бы рекомендовал воздержаться от использования CompletableFuture и Mono/Flux в одном проекте.

А разве проблема была именно в этом? Судя по описанию, вы напоролись на классические грабли смешивания синхронного и асинхронного кода, к Mono/Flux это отношения не имеет.

Я бы сказал, что мы напоролись на грабли смешивания асинхронного и реактивного кода. И CompletableFuture и WebClient позволяют писать в асинхронном стиле, одно возвращает Future, второе Mono/Flux. Эти конструкции похожи, но в случае с CompletableFuture код выполняется в треде, который больше ничем не должен заниматься и вернётся в пул когда всё сделает. В случае с WebClient — ещё как должен и в пул он не вернётся никогда.

Быть может вы имели в виду то, что Mono/Flux можно использовать и без реакта, например, как имплементацию паттерна publisher-subscriber?

Реактивный код — разновидность асинхронного.


И CompletableFuture и WebClient позволяют писать в асинхронном стиле, одно возвращает Future, второе Mono/Flux.

CompletableFuture ничего не "возвращает", это уже возвращаемое значение.


но в случае с CompletableFuture код выполняется в треде, который больше ничем не должен заниматься и вернётся в пул когда всё сделает. В случае с WebClient — ещё как должен и в пул он не вернётся никогда.

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

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

В случае reactor-http-epoll-1 поток никогда не перейдёт в статус ожидания by design. Это очень хорошо видно, если запустить VisualVm:
картинка

Вот и получается, что если потоки CompletableFuture заблокировать, ничего страшного не произойдёт, а вот если заблокировать потоки WebClient — у приложения будут проблемы.
Когда используется CompletableFuture, из отдельного тред пула берётся поток, который выполняет необходимые операции. После того, как его работа будет закончена, поток вернётся в тред пул и будет ждать пока он снова кому-то не потребуется.

Ну вот таким "отдельным тредпулом" в вашем случае и стал reactor-http-epoll-1. И ваша ошибка — именно в непонимании того, что эта ситуация абсолютно нормальна для CompletableFuture.

Реактивный код — разновидность асинхронного.

Really?
У Mono/Flux есть метод toFuture(), который возвращает как раз CompletableFuture.

Может возникнуть соблазн ни о чём не задумываясь просто использовать этот метод. Так мы и сделали. И уже через несколько секунд нам пришлось откатываться, т.к. наш прод в прямом смысле этого слова встал — все запросы к нему просто тупо начали висеть.

Не очень понял. Вы вообще не тестируете дистрибутив перед деплоем?
Тестируем конечно. Но этот кейс тесты тогда отловить не смогли. Там оказался очень специфический запрос, при котором случалась блокировка и происходило это не каждый раз — только когда треду «повезло» попасть самому на себя.

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

А подскажите, pls.
Вы сократили пул потоков CUSTOM c 1000 до 4. Да, это уменьшило потребляемые ресурсы JVM (к слову 1000 не активных нитей не так уж и много. Только пул NIO обычно на проме у нас минимум 500). Но ресурсы — это не время отклика. Их не настолько жалко.
Просто из за ресурсов переходит на неблокирующую схему с ее подводными камнями как то…

За счет чего выросла производительность и уменьшилось время отклика сервера?
Этот момент я не нашел в статье.

Нить NIO сервера все одно ждет пока все данные не придут. За счет чего конкретно уменьшается время работы прикладной части нити NIO?

Может от платформы (OS) зависит. У Вас не указано на чем работает (Lunux, *nix типа Solaris или Win)
За счет чего выросла производительность и уменьшилось время отклика сервера?

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

На самом деле, абсолютно то же самое происходит, если хорошо нагрузить приложение с WebFlux — у любого решения будет своё пиковое prs. Просто из нашего опыта чтобы такое увидеть у WebFlux нужно гораздо больше нагрузки, нежели чем используя RestTemplate. Кстати, мы так и не смогли дойти до этой пиковой нагрузки — у нас начинали тормозить уже доменные сервисы, а оркестратору было норм)

Может от платформы (OS) зависит.

У нас Linux

Просто из за ресурсов переходит на неблокирующую схему с ее подводными камнями как то…

Я с Вами согласен) Просто у нас не было выбора — нужно было сделать так, чтобы необходимую нагрузку держало ограниченное количество инстансов. Естественно, гораздо проще было просто проскалироваться.
С учетом того, что custom thread предназначены, в данном случае, для запроса внешнего сервера (установки TCP/IP и относительно длительного удержания его), то
надо еще не забывать про ограничение количества файловых дескрипторов (да и диапазона портов) и нагрузки на ядро OS.

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

Такое, как у Вас, переписывание кода дало эффект в определенных условиях (*), но еще одном повышении нагрузки все упрется в ограничение OS.

* — Накладные расходы ресурсов на нити в JVM это не так и много. На другом оборудовании, с другими ресурсами и при той же нагрузке разницы возможно бы и не было заметно в основных параметрах «время отклика и количество отказов». Видимомо, тут было было все на грани для данного железа/выделенных на инстанты ресурсах.
IMHO

Хотя опыт интересный. Спасибо за статью и ответ.

Надо будет скидать тест простой и проверить разницу на чистом эксперименте.
Будет ли в Java разница в блокированном чтении из порта отдельными нитями или циклическом опросе открытых дескрипторов портов (если спускаться на нижний уровень) в одной ните.
На уровне функций OS и нативных нитей OS без сомнения опрос (неблокированный режим) лучше. Что в nginx и сделано. Но для JVM ответ не так однозначен.

Накладные расходы ресурсов на нити в JVM это не так и много. [...] На уровне функций OS и нативных нитей OS без сомнения опрос (неблокированный режим) лучше. Что в nginx и сделано. Но для JVM ответ не так однозначен.

Насколько я знаю, Project Loom ещё не доделан, а значит любой поток JVM — это поток OS. А потоку OS нужна, как минимум, память под стек.


надо еще не забывать про ограничение количества файловых дескрипторов (да и диапазона портов) и нагрузки на ядро OS.

Но это же решаемые проблемы.


Порты, кстати, вообще не проблема если переиспользовать исходящие HTTP соединения.

Если коротко: 1000 неактивных нитей, может быть, и не так много — вот только под нагрузкой далеко не факт что этих 1000 нитей окажется достаточно.

А вы точно сделали то что надо? Сокращение тредов это вообще не показатель оптимизации, тут нужно смотреть время потраченное на запрос.
Согласен. Влияние на время при комфортной нагрузке оказывает только кэширование, параллелизация/объединение запросов. Тем не менее, нашей основной задачей было сделать так, чтобы сервис держал нагрузку при заданных показателях задержки. Но, как написано в этом комментарии проблемы начинаются под нагрузкой, если нет возможности поднять очень много инстансов приложения. Так как у нас этой возможности не было, мы сделали так, чтобы нагрузка держалась на тех инстансах, которые есть. Из этих соображений: да, я считаю, что мы сделали то, что надо)

Я извиняюсь, но правды ради:


C 11-й Java доступен httpClient, который реализует описанный выше неблокирующий подход. А с Spring 5 есть его обёртка — WebClient, которым можно заменить RestTemplate.

Вот нет, не обёртка над ним. Я сам, если честно, их не использую, но имея желание нагуглить подтверждение вашему утверждению, сделать этого, увы, не смог.

Да, Вы знаете, я посмотрел исходники WebClient — там действительно используется класс HttpClient, только у него пакет не java.net.http, а reactor.netty.http.client. Видимо, я пропустил этот момент в процессе подготовки, спасибо за замечание, исправлю.
Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.