Невозможно ограничить параллелизм запросов сервлетов с помощью виртуальных потоков Spring Boot с Tomcat

Я обновляю проект Kotlin (v1.9.25) Spring Boot (v3.3.1) с Java 17 до Java 21, чтобы включить виртуальные потоки.

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

spring.threads.virtual.enabled: true
spring.datasource.hikari.maximum-pool-size: 50
server.tomcat.threads.max: 4 # used to be 45 before virtual threads 

До сих пор мы контролируем максимальное количество одновременных запросов с помощью server.tomcat.threads.max, но с виртуальными потоками все меняется: идея, насколько я понимаю, состоит в том, чтобы исполнитель получал неограниченное количество задач, поэтому ограничений здесь нет.

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

Я думал о реализации семафора, но, похоже, с этим подходом что-то не так, я думал, что его можно будет настроить.

Большое спасибо!

Что заставляет вас думать, что семафор нельзя настроить?

Louis Wasserman 05.08.2024 21:38

@LouisWasserman под настраиваемым я имею в виду, что Spring Boot сможет сделать это, как это сделал threads.max, через application.properties, WebMvcConfiguration или любым другим способом. По вашему мнению, подойдет ли вам семафор?

Heits 05.08.2024 21:49

Это обычный способ привязать параллелизм к виртуальным потокам.

Louis Wasserman 05.08.2024 21:56

Если вам нужна настройка, аналогичная threads.max, вы можете посмотреть собственный исполнитель потоков Tomcat. Но тогда вы будете привязаны к Tomcat, вас это устраивает? Я думаю, что @LouisWasserman имел в виду чисто программное использование класса Semaphore, не связанное с конфигурацией.

igor.zh 05.08.2024 21:56

Привет @igor.zh, с привязкой к Tomcat вообще проблем нет. Вы бы выбрали перехватчик/фильтр сервлетов обработчика семафора или установили собственный исполнитель потока Tomcat? Мне трудно принять решение.

Heits 06.08.2024 00:58

Если вам нравится способ настройки Spring Boot/application.properties, то есть даже способ, не связанный с Tomcat. Это действительно будет зависеть от вашего дизайна и готовности испортить @Async методы и тому подобное. Я опубликую первоначальное решение через полчаса, надеюсь.

igor.zh 06.08.2024 01:05

ВЫ можете установить свойство server.tomcat.max-connections. По умолчанию — 8192 (с Spring Boot). Это количество принимаемых соединений. По сути, это очередь (принудительная с помощью специального LimitLatch от Tomcat). Вы можете объединить это с server.tomcat.accept-count, чтобы указать размер очереди (по умолчанию — 100). Вы можете поэкспериментировать с ними. Или создайте собственное расширение VirtualThreadExecutor из Tomcat, чтобы включить ограничение (с привязкой к server.tomcat.threads.max.

M. Deinum 06.08.2024 08:20

@M.Deinum, Tomcat Connections и Accept Count лишь косвенно влияют на количество рабочих потоков. Расширить VirtualThreadExecutor или создать свой собственный не так уж и сложно. Более серьезная проблема заключается в том, что даже если привязка к Tomcat приемлема, как сказал ОП, общий общий лимит рабочих потоков Tomcat может быть слишком строгим и не масштабируемым для решения - могут быть потоки, которые не используют критический ресурс, такой как БД. связей, но они, тем не менее, подпадают под один и тот же предел.

igor.zh 06.08.2024 15:45

@M.Deinum не будет ли максимальное количество соединений мешать клиентам, использующим функцию поддержания активности? Например: если у меня 50 максимальных соединений и 10 клиентов открывают по 5 соединений каждый, все 50 запросов завершаются за миллисекунды, но 50 соединений остаются активными около 3 минут. Это означает, что эти 10 клиентов продолжат работать, но новые будут заблокированы на время максимального количества подключений. Я прав? Я не эксперт в таких параметрах! Спасибо!

Heits 07.08.2024 00:00
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
2
9
81
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Рекомендуемый способ ограничить количество потоков, обращающихся к ограниченному ресурсу, — Использовать семафоры для ограничения параллелизма Виртуальных потоков , потока переполнения стека В Java Как перейти с Executors.newFixedThreadPool(MAX_THREAD_COUNT()) на виртуальный Поток обсуждает альтернативный способ ограничения — фиксированный пул потоков и утверждает, что он эквивалентен Semaphore с точки зрения функциональности и эффективности.

Кроме того, последний способ позволяет получить больше «конфигурируемых» решений Spring.

Чтобы ограничить количество рабочих потоков Tomcat, как того требует OP, мы могли бы установить собственный обработчик протокола Tomcat Executor:

@Configuration
public class TomcatFixedThreadsCustomizer
    implements WebServerFactoryCustomizer<ConfigurableTomcatWebServerFactory>, Ordered {

    @Value("${max.thread.count}")
    private int maxThreadCount;

    @Override
    public void customize(ConfigurableTomcatWebServerFactory factory) {
        factory.addProtocolHandlerCustomizers((protocolHandler) -> 
            protocolHandler.setExecutor(Executors.newFixedThreadPool(maxThreadCount, Thread.ofVirtual().factory())));
    }

    @Override
    public int getOrder() {
        return 2;// need to be executed after TomcatWebServerFactoryCustomizer;
   }

}

Однако помните, что в этом случае все рабочие потоки Tomcat будут находиться под ограничением maxThreadCount, даже те, которые не используют критически важные ресурсы, такие как DB Connections в случае OP.

Более гибкий, но и гораздо более трудоемкий подход — асинхронный вызов методов, которые обращаются к критически важным ресурсам, в отдельном потоке. Во-первых, давайте настроим TaskExecutor на основе фиксированного пула потоков:

@Configuration
@EnableAsync
public class AppConfig {

    @Value("${max.thread.count}")
    private int maxThreadCount;

    @Bean
    public AsyncTaskExecutor fixedTaskExecutor() {
        return new TaskExecutorAdapter(
            Executors.newFixedThreadPool(maxThreadCount, Thread.ofVirtual().factory()));
    }
}

Затем определите асинхронный метод:

@Service
public class AsyncService {

    @Async("fixedTaskExecutor")
    public void doStuff() {
        ...
    }
}

и, наконец, в контроллере вызовите этот метод

@RestController
@RequestMapping
public class Controller {

    @Autowired
    private AsyncService asyncService;

    public String handleRequest() {
        asyncService.doStuff;
        return "OK"; 
    }
}

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

Обратите внимание, что для обоих подходов линия

Executors.newFixedThreadPool(maxThreadCount, Thread.ofVirtual().factory())

фактически переопределяет настройку spring.threads.virtual.enabled для этого пользовательского исполнителя — рабочие потоки Tomcat в любом случае будут виртуальными, как и потоки, в которых выполняются асинхронные методы. С другой стороны, если вы не хотите, чтобы рабочие потоки Tomcat были виртуальными или выполняли асинхронные методы в виртуальных потоках, вам не обязательно использовать для этого фиксированный пул по умолчанию:

Executors.newFixedThreadPool(maxThreadCount)

Как отметил @M.Deinum, spring.threads.virtual.enabled продолжит влиять на другие части приложения. Кроме того, это решение не касается асинхронной обработки запросов.

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

igor.zh 06.08.2024 01:52
spring.threads.virtual.enable делает гораздо больше, чем просто Tomcat, если заставляет все приложение использовать виртуальные потоки. В вашем случае вы выполняете только асинхронный метод, игнорируя API асинхронного сервлета. Так это не то же самое, это полуувечная замена ему.
M. Deinum 06.08.2024 07:58

@M.Deinum, я где-то говорил в своем ответе, что свойство, которое вы написали с ошибкой, влияет только на потоки Tomcat? Я только сказал, что при использовании в ответе пользовательского TaskExecutor/Executor этот параметр будет переопределен, и асинхронный метод может выполняться либо в виртуальном, либо в платформенном потоке, в зависимости от используемой фабрики потоков. Решение, предложенное в ответе, соответствует требованиям ОП по ограничению и дает ему большую гибкость.

igor.zh 06.08.2024 15:26

@igor.zh фиксированный пул потоков с фабрикой виртуальных потоков, похоже, на самом деле неплохо справляется со своей задачей. Чего я не могу сделать, так это изменить все мои контроллеры, чтобы разгрузить обработку в другой пул потоков, потому что это слишком много работы, а поскольку вы уже создали собственный пул потоков, просто передать его Tomcat кажется «более правильным». Не могли бы вы показать, как передать этот «фиксированный пул виртуальных потоков» прямо в Tomcat? Спасибо за ваше время.

Heits 06.08.2024 23:55

@Heits, пожалуйста, ознакомьтесь с обновлением, в котором объясняется решение рабочих потоков Tomcat. Дайте мне знать, если требуется небольшая рабочая демонстрация.

igor.zh 07.08.2024 05:59

Я хочу сказать, что ваше решение работает иначе, чем решение tomcat, когда устанавливается spring.threads.virtual.enabled. Вы нигде не упомянули свойство, которое указывало бы на то, что предоставляемое вами решение действует так же, как и при установке свойства, которого оно не делает (безусловно, это не так). Лучшим решением было бы создать собственный кот VirtualThreadExecutor и установить его вместо стандартного VirtualThreadExecutor. Для этого потребуется изменить только некоторую конфигурацию Tomcat, а не весь сервис и API.

M. Deinum 07.08.2024 08:11

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

M. Deinum 07.08.2024 08:30

@M.Deinum, вы обновили экран и проверили обновление, которое делает то же самое: определяет WebServerFactoryCustomizer для установки специального Executor для обработчика протокола Tomcat? Ответ также объясняет, почему это решение менее масштабируемо по сравнению с асинхронными методами. Вы написали: «предлагаемое вами решение действует так же, как и при установке свойства, которого нет». Не могли бы вы как-нибудь перефразировать это утверждение?

igor.zh 07.08.2024 08:34

Я не писал этого, я написал полностью. Вы нигде не упомянули свойство, которое указывало бы на то, что предоставленное вами решение делает то же самое, что и при установке свойства, которого оно не делает (безусловно, это не так). При цитировании, пожалуйста, цитируйте полное предложение для контекста. Я только что заметил, что вы изменили свой ответ, который учитывает мои опасения и недостаток контекста/информации, до этого я отвечал на ваши комментарии.

M. Deinum 07.08.2024 08:36

@M.Deinum Что касается введения асинхронности там, где она концептуально не требуется, я бы согласился с вашей критикой, но это цена, которую ОП должен заплатить, если он не хочет программировать с Semaphore, а вместо этого хочет настраивать вещи с помощью Spring .

igor.zh 07.08.2024 08:44

Tomcat также находится под контролем Spring и настраивается им. Таким образом, добавление настройщика — отличный способ сделать что-то в Spring.

M. Deinum 07.08.2024 08:47

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

igor.zh 07.08.2024 08:52

@igor.zh большое спасибо, сработало и думаю, что намного лучше предыдущего решения. Только одно: я удалил интерфейс Ordered и метод getOrder, и все работало так же.

Heits 07.08.2024 17:06

Другие вопросы по теме