Комментарии 19
Именно потому что так просто выстрелить себе в ногу, я бы рекомендовал воздержаться от использования CompletableFuture и Mono/Flux в одном проекте.
А разве проблема была именно в этом? Судя по описанию, вы напоролись на классические грабли смешивания синхронного и асинхронного кода, к Mono/Flux это отношения не имеет.
Быть может вы имели в виду то, что 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.
У 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
Просто из за ресурсов переходит на неблокирующую схему с ее подводными камнями как то…
Я с Вами согласен) Просто у нас не было выбора — нужно было сделать так, чтобы необходимую нагрузку держало ограниченное количество инстансов. Естественно, гораздо проще было просто проскалироваться.
надо еще не забывать про ограничение количества файловых дескрипторов (да и диапазона портов) и нагрузки на ядро 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 помог пережить пандемию