Независимо от того, следуете ли вы или нет, веб -приложения Java используют пулы потоков для обработки запросов в большей или меньшей степени. Детали реализации пулов потоков могут быть проигнорированы, но необходимо понимать рано или поздно при использовании и настройке пулов потоков. В этой статье в основном представлены использование пула потоков Java и как правильно настроить пул потоков.
Одиночная резьба
Давайте начнем с оснований. Независимо от того, какой сервер приложений или фреймворк (например, Tomcat, пристани и т. Д.) Используются аналогичные базовые реализации. Основой веб -службы является сокет, который отвечает за прослушивание порта, ожидание соединения TCP и принятие соединения TCP. После того, как соединение TCP будет принято, данные могут быть прочитаны и отправлены из недавно созданного соединения TCP.
Чтобы понять вышеуказанный процесс, мы не используем какой -либо сервер приложений напрямую, но создаем простую веб -службу с нуля. Эта служба представляет собой микрокосм большинства серверов приложений. Простая веб-сервис с однопоточным, выглядит так:
Serversocket alluster = new Serversocket (8080); try {while (true) {socket socket = слушатель.accept (); try {handleRequest (сокет); } catch (ioException e) {e.printstackTrace (); }}} наконец {alinger.close ();}Приведенный выше код создает сокет сервера (Serversocket) , прослушивает порт 8080, а затем петли, чтобы проверить сокет, чтобы увидеть, есть ли новое соединение. Как только новое соединение будет принято, розетка будет передана в метод HandleRequest. Этот метод анализирует поток данных в HTTP -запрос, отвечает и записывает данные ответа. В этом простом примере метод HandleRequest просто реализует чтение в потоке данных и возвращает простые данные ответа. В целом этот метод будет намного сложнее, например, чтение данных из базы данных и т. Д.
Окончательная статическая строка ответа = "http/1.0 200 ok/r/n" + "контент-тип: text/rain/r/n" + "/r/n" + "hello world/r/n"; public static void handleRequest (сокет сокета) бросает ioException {// Читать поток ввода и вернуть "200 ok" try {bufferedReader in = new BufferedReader (новый inputStreamRead (socket.getInputStream ())); log.info (in.readline ()); OutputStream out = socket.getOutputStream (); out.write (response.getbytes (standardcharsets.utf_8)); } наконец {socket.close (); }}Поскольку существует только один поток для обработки запроса, каждый запрос должен ждать обработки предыдущего запроса, прежде чем его можно будет ответить. Предполагая, что время ответа на запрос составляет 100 миллисекунд, количество ответов в секунду (TPS) этого сервера составляет всего 10.
Многопоточный
Хотя метод HandleRequest может блокировать в iO, процессор все еще может обрабатывать больше запросов. Но в одном резервом случае это не может быть сделано. Следовательно, способность параллельной обработки сервера может быть улучшена путем создания методов мультипотчика.
Public Static Class HandlereQuestrunnable Reflsements Runnable {Final Socket Socket; public handleRequestrunnable (сокет сокета) {this.socket = ocket; } public void run () {try {handleRequest (socket); } catch (ioException e) {e.printstackTrace (); }}} Serversocket usinger = new Serversocket (8080); try {while (true) {socket socket = sluster.accept (); Новый поток (новый HandleRequestrunnable (сокет)). start (); }} наконец {alinger.close ();}Здесь метод Accept () все еще вызывается в основном потоке, но как только соединение TCP будет установлено, будет создан новый поток для обработки нового запроса, который должен выполнить метод HandleRequest в предыдущем тексте в новом потоке.
Создавая новый поток, основной поток может продолжать принимать новые соединения TCP, и эти запросы могут быть обработаны параллельно. Этот метод называется «один поток на запрос». Конечно, существуют и другие способы повышения производительности обработки, такие как асинхронная модель, управляемая событиями, используемая Nginx и Node.js, но они не используют пулы потоков и, следовательно, не покрываются этой статьей.
В каждом запросе в одном реализации потока создание потока (и последующего разрушения) накладных расходов очень дорого, потому что как JVM, так и операционная система должны выделять ресурсы. Кроме того, вышеуказанная реализация также имеет проблему, то есть количество созданных потоков неконтролируемо, что может привести к быстрому истощению системных ресурсов.
Истощенные ресурсы
Каждый поток требует определенного количества пространства памяти стека. В последнем 64-битной JVM размер стека по умолчанию составляет 1024 КБ. Если сервер получает большое количество запросов, или метод HandLeRequest выполняется медленно, сервер может сбой из -за создания большого количества потоков. Например, существует 1000 параллельных запросов, а 1000 созданных потоков необходимо использовать 1 ГБ памяти JVM в качестве места стека потоков. Кроме того, объекты, созданные во время выполнения кода каждого потока, также могут быть созданы в куче. Если эта ситуация ухудшится, она превысит память о куче JVM и генерирует большое количество операций сбора мусора, что в конечном итоге вызовет переполнение памяти (OutofmemoryErrors).
Эти потоки не только потребляют память, они также используют другие ограниченные ресурсы, такие как ручки файлов, подключения к базе данных и т. Д. Неконтролируемые потоки создания могут также вызывать другие типы ошибок и сбоев. Следовательно, важный способ избежать истощения ресурсов - избежать неконтролируемых структур данных.
Кстати, из -за проблем с памятью, вызванных размером стека потока, размер стека можно отрегулировать через переключатель -xss. После уменьшения размера стека в потоке накладные расходы на поток можно уменьшить, но можно поднять переполнение стека (StackoverFlowerRors). Для общих приложений по умолчанию 1024 КБ является слишком богатым, и может быть более целесообразным уменьшить его до 256 КБ или 512 КБ. Минимальное допустимое значение в Java составляет 160 КБ.
БИЛЕР НИДЕЙ
Чтобы избежать непрерывного создания новых потоков, вы можете ограничить верхний предел пула потоков, используя простой пул потоков. Пул ниток управляет всеми потоками. Если количество потоков не достигло верхнего предела, пул резьбов создает потоки до верхнего предела и как можно больше использует бесплатные резьбы.
Serversocket alluster = new Serversocket (8080); executorservice executor = executors.newfixedthreadpool (4); try {while (true) {socket socket = slieder.accept (); Executor.submit (new HandlereQuestrunnable (сокет)); }} наконец {alinger.close ();}В этом примере вместо того, чтобы напрямую создавать поток, используется исполнительница. Он передает задачи, которые необходимо выполнить (необходимо реализовать интерфейс Runnables) в пул потоков и выполняет код, используя потоки в пуле потоков. В примере пул потоков фиксированного размера с несколькими потоками 4 используется для обработки всех запросов. Это ограничивает количество потоков, которые обрабатывают запросы, а также ограничивают использование ресурсов.
В дополнение к созданию пула потоков фиксированного размера через метод NewFixedThreadpool, класс Executors также предоставляет метод NewCachedThreadpool. Повторное использование пула потоков может привести к неконтролируемому количеству потоков, но он будет использовать потоки холостого хода, которые были созданы как можно больше. Обычно этот тип пула потоков подходит для коротких задач, которые не заблокированы внешними ресурсами.
Работа в очереди
После использования пула потоков фиксированного размера, если все потоки заняты, что произойдет, если появится другой запрос? ThreadPoolexeCutor использует очередь для хранения запросов, а пулы потоков фиксированного размера по умолчанию используют неограниченные связанные списки. Обратите внимание, что это, в свою очередь, может вызвать проблемы истощения ресурсов, но это не произойдет до тех пор, пока скорость обработки потока больше, чем скорость роста очереди. Затем в предыдущем примере каждый запрос в очереди будет содержать гнездо, который в некоторых операционных системах будет потреблять дескриптор файла. Поскольку операционная система ограничивает количество ручек файлов, открываемых процессом, лучше всего ограничить размер очереди работы.
Public Static ExecutorService newboundFixedThreadpool (int nthreads, int емкость) {return new ThreadPoolexeCutor (nthreads, nThreads, 0l, TimeUnit.milliseconds, new LinkedBlockingque <runnable> (емкость), New Threadpoolexecutor.discardpolicy (); BADEDTHREADPOOLSERVERSOCKETOCKEST () бросает ioException {serversocket alluster = new Serversocket (8080); Executorservice executor = newboundedfixedthreadpool (4, 16); try {while (true) {socket socket = sluster.accept (); Executor.submit (new HandlereQuestrunnable (сокет)); }} наконец {alinger.close (); }}Здесь, вместо того, чтобы непосредственно использовать метод executors.newfixedThreadpool для создания пула потоков, мы сами создали объект ThreadPoolexeCutor и ограничили длину очереди работы до 16 элементов.
Если все потоки заняты, новая задача будет заполнена в очередь. Поскольку очередь ограничивает размер до 16 элементов, если этот предел превышен, с ним необходимо обрабатывать последним параметром при построении объекта ThreadPoolexeCutor. В примере используется сбросполитика, то есть когда очередь достигает верхнего предела, новая задача будет отброшена. В дополнение к впервые существует также политика прерывания (Abortpolicy) и политика выполнения вызывающего абонента (Callerrunspolicy). Первый бросит исключение, в то время как последний выполнит задачу в потоке вызывающего абонента.
Для веб -приложений оптимальная политика по умолчанию должна заключаться в том, чтобы отказаться от политики или отмены политики и вернуть ошибку клиенту (например, ошибка HTTP 503). Конечно, также возможно избегать отказов запросов клиентов, увеличивая длину рабочей очереди, но запросы пользователей, как правило, не хотят ждать долгое время, и это будет потреблять больше ресурсов сервера. Целью работы работы является не отвечать на запросы клиентов без ограничений, а сглаживать и разорвать запросы. Обычно очередь работы должна быть пустой.
Настройка подсчета потоков
В предыдущем примере показано, как создавать и использовать пул потоков, но проблема с использованием пула потоков состоит в том, сколько потоков следует использовать. Во -первых, мы должны убедиться, что когда ограничение потока достигнут, ресурс не будет исчерпан. Ресурсы здесь включают в себя память (куча и стек), количество ручек открытых файлов, количество подключений TCP, количество удаленных подключений к базе данных и других ограниченных ресурсов. В частности, если задачи с резьбой являются вычислительными интенсивными, количество ядер ЦП также является одним из ограничений ресурса. Вообще говоря, количество потоков не должно превышать количество ядер ЦП.
Поскольку выбор подсчета потоков зависит от типа применения, он может потребоваться много тестирования производительности, прежде чем можно получить оптимальные результаты. Конечно, вы также можете улучшить производительность вашего приложения, увеличив количество ресурсов. Например, изменить размер памяти JVM или изменить верхний предел дескриптора файла операционной системы и т. Д. Затем эти корректировки в конечном итоге достигнут теоретического верхнего предела.
Закон Литтл
Закон Литтл описывает взаимосвязь между тремя переменными в стабильной системе.
Если L представляет среднее количество запросов, λ представляет частоту запросов, а W представляет среднее время для ответа на запрос. Например, если количество запросов в секунду составляет 10, а каждое время обработки запросов составляет 1 секунду, то в любой момент обрабатывается 10 запросов. Вернемся к нашей теме, для обработки требуется 10 потоков. Если время обработки одного запроса удваивается, количество обработанных потоков также удвоится, станет 20.
После понимания влияния времени обработки на эффективность обработки запросов мы обнаружим, что теоретический верхний предел может не быть оптимальным значением для размера пула потоков. Верхний предел пула потоков также требует времени обработки справочной задачи.
Предполагая, что JVM может обрабатывать 1000 задач параллельно, если каждое время обработки запроса не превышает 30 секунд, то в худшем случае можно обработать не более 33,3 запросов в секунду. Однако, если каждый запрос занимает всего 500 миллисекунд, приложение может обрабатывать 2000 запросов в секунду.
Распределите бассейн
В микросервисах или сервис-ориентированных архитектурах (SOA) обычно требуется доступ к нескольким бэкэнд-службам. Если одна из служб выполняет деградированную, это может привести к тому, что пул потоков не закончится, что влияет на запросы на другие услуги.
Эффективный способ справиться с отказом от бэкэнд обслуживания - это изолировать пул резьбов, используемый каждой службой. В этом режиме все еще существует отправленный пул потоков, который отправляет задачи в различные пулы потоков запроса бэкэнд. Этот пул потоков может не иметь нагрузки из -за медленного бэкэнда и перенести бремя в пул резьбов, который запрашивает медленный бэкэнд.
Кроме того, многопоточный режим объединения также должен избегать проблем с тупиком. Если каждый поток блокирует в ожидании результата необработанного запроса, возникает тупик. Следовательно, в режиме многопоточного бассейна необходимо понять задачи, выполняемые каждым пулом потоков, и зависимости между ними, чтобы как можно больше избежать проблем с тупиком.
Суммировать
Даже если пулы потоков не используются непосредственно в приложении, они, скорее всего, будут косвенно использоваться сервером приложений или структурой в приложении. Такие фреймворки, как Tomcat, JBoss, Infertow, Dropwizard и т. Д. Все предоставляют параметры для настройки пулов потоков (пулы потоков, используемые в соответствии с выполнением сервлета).
Я надеюсь, что эта статья может улучшить ваше понимание пула потоков и помочь вам учиться.