Эволюция оркестратора микросервисов. Как переход на WebClient помог пережить пандемию

    Практически во всех проектах есть такой сервис как оркестратор. Называться он может по разному, но суть одна: всё что от него требуется, это сходить в N сервисов, забрать у них необходимую информацию, как-то агрегировать, и отдать наверх клиенту ответ. И у нас, в Леруа Мерлен, он тоже есть. На данный момент наш оркестратор - это рабочая лошадка, которую используют все кому не лень, например, фронт сайта получает через него данные по товарам для того чтобы показать их пользователю. На данный момент это один из самых высоконагруженных сервисов в нашем контуре.

    Как правило, подобные сервисы не особо кого волнуют ровно до того момента, пока они не становятся узким горлышком на пути к добру, которое несут сервисы ниже уровнем. Когда же ваши доменные сервисы держат больше 2к rps, а оркестратор, который агрегирует из них информацию - всего 200, у бизнеса начинает дёргаться правый глаз, а у разработчика - чесаться руки провести рефакторинг. Давайте разбираться каким образом мы можем это сделать.

    Типовая архитектура оркестратора

    Классическая схема оркестратора для ритейла выглядит примерно так:

    Фронт запрашивает id одного или нескольких товаров, которые хочет отобразить + id маски. Маска - это alias для списка атрибутов, необходимых для отображения. На разных страницах сайта необходимо показывать разный набор атрибутов, информация об этом лежит в отдельном микросервисе. Далее, имея список товаров и необходимых атрибутов, оркестратор идёт в микросервисы, которые информацией по этим атрибутам обладают. Плюс, надо зайти в рекомендательную систему, которая вернёт список id товаров которые мы можем порекомендовать купить вместе с основным (заменители основного и ему сопутствующие). Этот список тоже нужно обогатить атрибутами по маске, которую запросит фронт.

    В общем, логика не бог весть какая, но она есть.

    Варианты оптимизации

    Первое, что можно тут заметить: оркестратор совершает очень много запросов. По этому, первое, о чём нужно подумать когда стоит задача разогнать сервис - это уменьшить их количество в единицу времени. Сделать это можно несколькими способами:

    Прикрутить кэш

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

    Распараллелить запросы

    Как показывает практика, большинство времени подобного рода сервисы просто ждут ответа от их доменных коллег, так что если они могут "ждать" эти ответы не по очереди, а одновременно, то хорошая идея это организовать. Если мы берём стандартный сервер tomcat или undertow, то можно сделать так, чтобы каждый запрос, принимаемый сервером порождал N потоков, каждый из которых ходил бы за своим атрибутом, а поток-родитель потом это всё бы собирал и выплёвывал наверх.

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

    Если до первых двух пунктов мы догадались в самой первой итерации сервиса, то до возможности соединять несколько запросов в один мы дошли не сразу.

    Что имеется в виду: в сервис приходит массив основных продуктов и их атрибутов, по которым нужно собрать информацию. Помимо этого, к продуктам нужно подобрать рекомендации. Сервис рекомендаций, естественно, возвращает только список продуктовых id, которые также нужно обогатить своим набором атрибутов. Атрибуты эти, отличаются от атрибутов основного продукта и тоже приходят как параметр запроса нашего оркестратора.
    Фактически, в оркестратор приходит список основных продуктов и 2 маски: одна для основных запрашиваемых продуктов, и вторая для продуктов, которые мы к ним рекомендуем:

    {
      "products": ["product1", "product2", ..., "productN"],
      "mask" : "product_detail_page_attributes",
      "recommendation_mask": "name_and_photo_only"
    }

    Проблема в том, что список id основных продуктов мы знаем сразу, а за списком id продуктов-рекомендаций ещё нужно сходить. Возникает несколько вариантов того, как это сделать:

    Вариант 1:

    1. Идём с известными основными продуктами и их атрибутами по микросервисам собирать о них информацию;

    2. Параллельно с этим, идём в сервис рекомендаций, который возвращает нам список id рекомендуемых продуктов;

    3. После того, как сервис рекомендаций вернул список товаров, аналогично первому пункту, идём с уже другим набором атрибутов по сервисам и получаем всю необходимую информацию.

    Тут 1 и 2 выполняются параллельно, 3 же придётся подождать первых двух. Как видно, число последовательных запросов: 2, что не так много, но, как показывает практика, проблема может заключаться в их количестве. Это то, что у нас было с самого начала, после этого мы сделали следующую оптимизацию:

    Вариант 2:

    1. Сначала идём в сервис рекомендаций и получаем id продуктов-рекомендаций;

    2. После этого добавляем к основным продуктам рекомендованные товары, и к атрибутам основных товаров атрибуты товаров-рекомендаций, и уже со всем этим добром идём к доменным сервисам.

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

    В этом варианте, как и в пером, число последовательных запросов равно 2-м. Но, число запросов совершаемых сервисом, уменьшается вдвое. С другой стороны, делая 1 жирный запрос по всем продуктам и атрибутам мы по сети гоняем информацию, которая нам не нужна - нас же не интересуют атрибуты основных товаров у рекомендаций. Этого можно избежать, если спроектировать грамотное API, но, скорее всего, в доменных сервисах запрос в базу уйдёт всё равно всё со всем.

    Тем не менее, второй вариант всё же по моему мнению лучше, т.к.
    меньше запросов => меньше потоков => сервис больше таких запросов сможет обработать. Также, доп запрос - это доп затраты на маршаллинг и демаршаллинг, которые могут быть значительными.

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

    Выводы:

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

    Едем дальше

    Итак, мы провели некие оптимизации, и да, сервис действительно стал держать больше нагрузки, но что делать, если этого всё ещё не достаточно?

    Во-первых, сразу нужно сказать об ограничениях, которые у нас были.

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

    • Не всегда возможно из-за ограничений инфраструктуры. Это сейчас у нас кубер и со скалированием проблем нет, но перед пандемией у нас был всего десяток серверов для приложений и на одном сервере мы мы могли поднять только 1 инстанс оркестратора. Т.е. максимальное количество инстансов, которые мы могли себе позволить - 10;

    • Как показывает практика, это не оптимальный подход. Да, прирост производительности есть, но до для того, чтобы получить необходимые 60ms на 95-й персентиле нужно было этими инстансами прямо таки обмазаться, чего мы себе позволить не могли.

      Время ответа при 5-ти и 8-ми инстансах оркестратора
      Время ответа при 5-ти и 8-ми инстансах оркестратора

    Анализ проблемы

    На самом деле проблему "как разогнать сервис до определённого rps?" можно переформулировать в вопрос "почему на нужном rps сервис не держит?". Для того, чтобы понять в чём проблема, даём нарастающую нагрузку, и, в момент когда сервису плохеет смотрим на состояние.
    К сожалению, идея написать статью появилась после того, как мы решили проблему, по этому графики не сохранились, но, если в двух словах, мы увидели, что количество потоков постоянно росло и в какой-то момент garbage collector просто сходит с ума. Причём, ни увеличение возможного количества потоков, ни добавление памяти картину не меняло.

    Чтобы разобраться почему так, давайте взглянем на то, что происходит с приложением при запросе:

    рис 3. Путь запроса в undertow
    рис 3. Путь запроса в undertow
    1. Сервер принимает входящее http соединение. Занимаются этим потоки пула WORKER_IO_THREADS, их обычно по количеству ядер (в нашем примере 4). Допустим, наш запрос попал на XNIO-1 I/O 2. Он установил соединение, и отправил запрос дальше, освобождаясь для принятия новых запросов.

    2. От потока из пула WORKER_IO_THREADS запрос попадает уже на непосредственный обработчик - в нашем случае это XNIO-1 task-2 из пула WORKER_TASK_CORE_THREADS
      Это тот поток, который будет выполнять всё, что мы напишем внутри нашего контроллера.

    3. Как уже говорилось выше, зная список атрибутов, XNIO-1 task-2 создаёт n потоков, каждый из которых параллельно должен добыть какую-то часть информации о продуктах. Потоки берутся из кастомного пула (пулы WORKER_IO_THREADS и WORKER_TASK_CORE_THREADS - это сущности undertow)

    4. Каждый кастомный поток, идёт к клиенту доменного сервиса и делает запрос. Т.к. под капотом используется RestTemplate, поток блокируется и ждёт пока доменный сервис не ответит.

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

    Переход на неблокирующий подход

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

    Чтобы понять в чём суть, мне бы хотелось привести аналогию с фастфудом

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

    Неблокирующий подход:
    Вы всё так же хотите бургер.
    Вы подходите к свободному электронному терминалу и начинаете выбирать себе еду.
    Вы оплачиваете заказ, берёте чек с номером и локатор.
    Вы садитесь за свободный стол, поставив на него локатор.
    Пока вы ждёте заказ вы можете заниматься чем угодно полезным (или не очень) - и это основное преимущество неблокирующего подхода.
    Когда ваш заказ готов, вам его принесут и вы приступаете к трапезе.

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

    Вернёмся к нашему серверу

    Если используется блокирующий RestTemplate, то поток, который получает информацию от доменного сервиса делает следующее:
    Он стучится к сервису, говорит ему "здрасьте", я такое-то приложение, мне нужна такая-то информация, я буду ждать от вас ответ на таком-то порту, и начинает ждать, что на этот порт от сервиса придёт ответ. Пока ответ не придёт, или пока не наступит таймаут поток ничем полезным заниматься не будет.

    Вместо того, чтобы иметь кучу потоков, каждый из которых ничем полезным не занимается, ожидая ответа от сервиса, предлагается создать пул потоков, который будет состоять всего из 4-х (по количеству CPU) потоков, но которые постоянно будут заниматься полезной работой.

    Потоки из этого нового пула будут принимать "заказы" на общение с сервисами моментально возвращать управление вместе с каким-то аналогом Future и избавляя от необходимости заказчика ждать.

    Они поделят между собой диапазон портов, и когда к одному из них придут с "заказом", он выберет свободный порт из диапазона, на котором он будет получать ответ, и, отправит запрос в доменный сервис. После того, как запрос отправлен, этому потоку в отличие от блокирующего подхода будет чем заняться. У него ранее "заказывали" другую информацию, и она уже могла прийти на другой порт, а значит надо сходить проверить - не ответил ли один из доменных сервисов, которых он опрашивал ранее. Если ответил, то нужно нотифицировать поток заказчика, что его заказ выполнен.

    Таким образом, наши потоки либо принимают заказы и отправляют запросы, либо в бесконечном цикле обходят порты, на которых ожидается ответ. Таким образом, они всегда при деле.

    Теория - это классно, как применить-то?

    Со Spring 5 доступен WebClient, который реализует описанный выше неблокирующий подход. Им мы можем заменить используемый ранее RestTemplate.
    WebClient - часть spring-webflux, по этому как результат запроса в доменный сервис возвращает Mono/Flux. Наш основной же код приложения работает с CompletableFuture.

    Возникает вопрос: можно ли использовать WebClient как замену RestTemplate, получив преимущества неблокирующего приложения, но при этом не переписывая с CompletableFuture на Mono/Flux весь проект?

    Ответ: можно, но нужно очень хорошо понимать что делаешь.

    Как положить прод, если не любишь читать документацию.

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

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

    Потоки WebClient(по умолчанию они зовутся reactor-http-epoll-число), которые должны были постоянно работать оббегая каждый свой список портов, заблокированы.
    Всё дело оказалось в методе CompletableFuture thenApply(work) который встретился у нас выше по стеку.
    Этот метод, "докидывает" работы в текущий CompletableFuture на котором вызывается. И всё бы ничего, но у нас так получилось, что эта "работа" оказалась блокирующей.

    Получилось следующее: через thenApplyпотоку reactor-http-epoll-1 из WebClient было "накинуто" задание сходить в ещё один сервис. Пока он этого не сделает и не вернёт управление наверх он не может выполнять свою основную работу - смотреть не ответили ли доменные сервисы. И причём, в конце этого задания у стояла блокировка, т.е. нужно было обязательно дождаться от этого сервиса ответа. И так исторически сложилось, что библиотека WebClient, распределяющая работу типа "сходить в ещё один сервис" выбрала именно reactor-http-epoll-1 для организации взаимодействия в этом запросе. То есть получилась ситуация, когда поток заблокировался, потому что ждал ответа от сервиса, в то время как он же и должен был этот ответ в получить. Но он не мог бегать и смотреть что ему пришло, т.к. был заблокирован. Вот такой вот deadlock.

    Решение в данном случае: использовать thenApplyAsync, чтобы дополнительная работа выполнялась не в потоке WebClient .

    Мораль же следующая:

    Никогда не позволяйте потокам WebClient блокироваться - они постоянно должны работать.

    Именно потому что так просто выстрелить себе в ногу, я бы рекомендовал воздержаться от использования CompletableFuture и Mono/Fluxв одном проекте. Если используете Mono/Flux , то лучше перепишите всё на Netty, благо у CompletableFuture и Mono/Fluxочень похожее API. Вот тут в документации написано почему использовать метод toFuture() конвертирующий одно в другое - не очень хорошая практика.

    Хэппи-энд

    Тем не менее, после правок, всё завелось. После деплоя видим следующую картину:

    Где-то на 17:26 произошёл деплой новой версии. Из графика видно что:

    • количество потоков сократилось больше чем в 4 раза (это сократилось количество потоков в CUSTOM_THREAD_POOL с 1000+ до 4-х);

    • потоки в статусе timed-waiting исчезли как класс;

    • количество потоков в статусе waiting не изменилось, но это понятно - нас всё ещё есть нативные воркеры undertow. Можно использовать Netty, тогда не было бы и их - у приложения тогда бы вообще было бы всего 4 универсальных потока, которые занимались бы всем.

    Результаты по перфомансу превзошли все ожидания: 0 ошибок и 80 ms 75-й персентиль, при всего 4-х инстансах приложения.

    Необходимая производительность в 60 ms на 95 персентиле достигается путём поднятия единственного дополнительного инстанса.

    Выводы

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

    Общие рекомендации по проектированию оркестраторов могут быть следующие:
    Для начала следует задуматься о:

    • кэшировании;

    • параллельном выполнении запросов;

    • объединении запросов.

    Именно эти шаги могут уменьшить время ответа запроса.

    Далее, использование неблокирующего клиента в микросервисах типа "оркестратор" позволяет повысить пропускную способность сервиса, экономя на тредах.
    Именно повысить пропускную способность, т.е. те же результаты, но меньшими ресурсами. Однако, если у вас куча серверов, возможно, вам проще будет поднять побольше инстансов.
    В конце концов, Вы же наверняка рассчитывали, что это решит ваши проблемы с нагрузкой, не так ли?)

    Не советую использовать CompletableFuture и Mono/Flux в одном проекте. Анализ описанных в данной статье проблем при их появлении не тривиален. Как минимум потому, что их не так просто воспроизвести на тестовых стендах. А если используете, то используйте с умом.

    Не допускайте блокировки потоков WebClient . Как только кто-то из них заблокируется, это будет означать, что он уже не сможет выполнять свою основную функцию.

    В чём минусы подхода?

    Обсуждая неблокирующее программирование, мы должны понимать, что идея подобной организации потоков не нова. Может быть, она нова в Java, но в Node.js с самого начала всё так и работало. И тем не менее, когда меня пригласили в Леруа Мерлен распиливать монолит, у которого были проблемы с производительностью, то, что мы распиливали, было как раз Node.js приложением. Это говорит о том, что данный подход не является панацеей. Он хорошо работает, когда потоки не нагружены лишней работой и плохо, когда есть много тяжелых вычислений. Но это уже следующий шаг после внедрения неблокирующего клиента).

    Люди строили highload на Java и до jdk11/Spring 5, неблокирующее программирование - это всего лишь один из подходов к организации, который хорошо бы иметь в своём распоряжении и применять когда в нём возникнет необходимость.

    Спасибо за внимание!

    Леруа Мерлен
    Ритейлер — технологическая компания платформа.

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

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

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

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

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

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


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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                У нас Linux

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

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

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

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

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

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

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

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

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


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

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


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

                  0

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

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

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


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

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

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

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                    Самое читаемое