Я обновляю проект 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
, но с виртуальными потоками все меняется: идея, насколько я понимаю, состоит в том, чтобы исполнитель получал неограниченное количество задач, поэтому ограничений здесь нет.
Это оставляет мне вопрос: как я могу ограничить максимальное количество одновременных подключений к моей службе при использовании виртуальных потоков?
Я думал о реализации семафора, но, похоже, с этим подходом что-то не так, я думал, что его можно будет настроить.
Большое спасибо!
@LouisWasserman под настраиваемым я имею в виду, что Spring Boot сможет сделать это, как это сделал threads.max, через application.properties, WebMvcConfiguration или любым другим способом. По вашему мнению, подойдет ли вам семафор?
Это обычный способ привязать параллелизм к виртуальным потокам.
Если вам нужна настройка, аналогичная threads.max
, вы можете посмотреть собственный исполнитель потоков Tomcat. Но тогда вы будете привязаны к Tomcat, вас это устраивает? Я думаю, что @LouisWasserman имел в виду чисто программное использование класса Semaphore
, не связанное с конфигурацией.
Привет @igor.zh, с привязкой к Tomcat вообще проблем нет. Вы бы выбрали перехватчик/фильтр сервлетов обработчика семафора или установили собственный исполнитель потока Tomcat? Мне трудно принять решение.
Если вам нравится способ настройки Spring Boot/application.properties
, то есть даже способ, не связанный с Tomcat. Это действительно будет зависеть от вашего дизайна и готовности испортить @Async
методы и тому подобное. Я опубликую первоначальное решение через полчаса, надеюсь.
ВЫ можете установить свойство server.tomcat.max-connections
. По умолчанию — 8192 (с Spring Boot). Это количество принимаемых соединений. По сути, это очередь (принудительная с помощью специального LimitLatch
от Tomcat). Вы можете объединить это с server.tomcat.accept-count
, чтобы указать размер очереди (по умолчанию — 100). Вы можете поэкспериментировать с ними. Или создайте собственное расширение VirtualThreadExecutor
из Tomcat, чтобы включить ограничение (с привязкой к server.tomcat.threads.max
.
@M.Deinum, Tomcat Connections и Accept Count лишь косвенно влияют на количество рабочих потоков. Расширить VirtualThreadExecutor
или создать свой собственный не так уж и сложно. Более серьезная проблема заключается в том, что даже если привязка к Tomcat приемлема, как сказал ОП, общий общий лимит рабочих потоков Tomcat может быть слишком строгим и не масштабируемым для решения - могут быть потоки, которые не используют критический ресурс, такой как БД. связей, но они, тем не менее, подпадают под один и тот же предел.
@M.Deinum не будет ли максимальное количество соединений мешать клиентам, использующим функцию поддержания активности? Например: если у меня 50 максимальных соединений и 10 клиентов открывают по 5 соединений каждый, все 50 запросов завершаются за миллисекунды, но 50 соединений остаются активными около 3 минут. Это означает, что эти 10 клиентов продолжат работать, но новые будут заблокированы на время максимального количества подключений. Я прав? Я не эксперт в таких параметрах! Спасибо!
Рекомендуемый способ ограничить количество потоков, обращающихся к ограниченному ресурсу, — Использовать семафоры для ограничения параллелизма Виртуальных потоков , потока переполнения стека В 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 более предпочтительным.
spring.threads.virtual.enable
делает гораздо больше, чем просто Tomcat, если заставляет все приложение использовать виртуальные потоки. В вашем случае вы выполняете только асинхронный метод, игнорируя API асинхронного сервлета. Так это не то же самое, это полуувечная замена ему.
@M.Deinum, я где-то говорил в своем ответе, что свойство, которое вы написали с ошибкой, влияет только на потоки Tomcat? Я только сказал, что при использовании в ответе пользовательского TaskExecutor
/Executor
этот параметр будет переопределен, и асинхронный метод может выполняться либо в виртуальном, либо в платформенном потоке, в зависимости от используемой фабрики потоков. Решение, предложенное в ответе, соответствует требованиям ОП по ограничению и дает ему большую гибкость.
@igor.zh фиксированный пул потоков с фабрикой виртуальных потоков, похоже, на самом деле неплохо справляется со своей задачей. Чего я не могу сделать, так это изменить все мои контроллеры, чтобы разгрузить обработку в другой пул потоков, потому что это слишком много работы, а поскольку вы уже создали собственный пул потоков, просто передать его Tomcat кажется «более правильным». Не могли бы вы показать, как передать этот «фиксированный пул виртуальных потоков» прямо в Tomcat? Спасибо за ваше время.
@Heits, пожалуйста, ознакомьтесь с обновлением, в котором объясняется решение рабочих потоков Tomcat. Дайте мне знать, если требуется небольшая рабочая демонстрация.
Я хочу сказать, что ваше решение работает иначе, чем решение tomcat, когда устанавливается spring.threads.virtual.enabled
. Вы нигде не упомянули свойство, которое указывало бы на то, что предоставляемое вами решение действует так же, как и при установке свойства, которого оно не делает (безусловно, это не так). Лучшим решением было бы создать собственный кот VirtualThreadExecutor
и установить его вместо стандартного VirtualThreadExecutor
. Для этого потребуется изменить только некоторую конфигурацию Tomcat, а не весь сервис и API.
В дополнение к этому также меняется способ выполнения методов и реакция Tomcat на это. Вы сделали приложение асинхронным, чего не делает пул потоков в Tomcat. Он будет выполнять запрос в потоке, включая операцию блокировки, и ждать результата, а ваше решение этого не делает (по крайней мере, без серьезных изменений кода).
@M.Deinum, вы обновили экран и проверили обновление, которое делает то же самое: определяет WebServerFactoryCustomizer
для установки специального Executor
для обработчика протокола Tomcat? Ответ также объясняет, почему это решение менее масштабируемо по сравнению с асинхронными методами. Вы написали: «предлагаемое вами решение действует так же, как и при установке свойства, которого нет». Не могли бы вы как-нибудь перефразировать это утверждение?
Я не писал этого, я написал полностью. Вы нигде не упомянули свойство, которое указывало бы на то, что предоставленное вами решение делает то же самое, что и при установке свойства, которого оно не делает (безусловно, это не так). При цитировании, пожалуйста, цитируйте полное предложение для контекста. Я только что заметил, что вы изменили свой ответ, который учитывает мои опасения и недостаток контекста/информации, до этого я отвечал на ваши комментарии.
@M.Deinum Что касается введения асинхронности там, где она концептуально не требуется, я бы согласился с вашей критикой, но это цена, которую ОП должен заплатить, если он не хочет программировать с Semaphore
, а вместо этого хочет настраивать вещи с помощью Spring .
Tomcat также находится под контролем Spring и настраивается им. Таким образом, добавление настройщика — отличный способ сделать что-то в Spring.
Я не хотел искажать ваше утверждение, формат комментариев SO слишком ограничен по длине. Но я не понимаю ни вашего утверждения, ни какой проблемы вы видите в асинхронных методах. Tomcat находится под контролем, но, как я уже много раз говорил, ограничение рабочих потоков Tomcat с помощью семафора или фиксированного пула является слишком ограничительным и менее масштабируемым.
@igor.zh большое спасибо, сработало и думаю, что намного лучше предыдущего решения. Только одно: я удалил интерфейс Ordered и метод getOrder, и все работало так же.
Что заставляет вас думать, что семафор нельзя настроить?