Когда клиент обращается к API, сервер создает поток и назначает его поступившему запросу. Этот поток ожидает, пока упомянутому клиенту не будет отправлен ответ.
Каждый сервер может открыть только ограниченное количество потоков, и если это число превышено, новый запрос клиента сохраняется в очереди запросов сервера.
Для сервера Tomcat максимальное количество потоков по умолчанию (maxThreads
) равно 200, что соответствует максимальному количеству одновременных потоков, разрешенных для запуска в любой момент времени.
Существует и другой показатель, maxConnections
, представляющий общее количество одновременных подключений, которые сервер примет и обработает. Любые дополнительные входящие соединения будут помещаться в очередь до того времени, пока поток не станет доступным. Значение по умолчанию для режима NIO/NIO2
равно 10000, а для APR/Native
— 8 192.
Еще один показатель — acceptCount
. Он представляет максимальное количество TCP-запросов, которые могут ожидать в очереди на уровне операционной системы, когда рабочие потоки недоступны. Значение по умолчанию равно 100.
Запрос, который проходит слишком долго или обслуживание которого занимает много времени, создает огромную проблему для приложения с высоким трафиком. Чтобы справиться с этой проблемой и эффективнее управлять потоками, можно обратиться к асинхронному программированию.
Согласно visualstudiomagazine.com
, определение асинхронного программирования таково:
Асинхронное программирование — это способ параллельного программирования, в котором единица работы выполняется отдельно от основного потока приложения и уведомляет вызывающий поток о своем завершении, сбое или прогрессе.
Как синхронные приложения создают проблемы?
Давайте разберемся. Вот как протекает запрос:
- Запрос поступает на сервер Tomcat через слушающий порт. Сервер Tomcat имеет свою внутреннюю конфигурацию —
maxThreads
,maxConnections
и т.д. Если сервер в данный момент превышает максимальное значение потока, то запрос ожидает в очереди. - Далее сервер передает запрос в приложение, что означает: сервер назначает новый поток для этого запроса и затем осуществляет передачу. Этот поток ожидает, пока запрос не будет передан клиенту.
- Если отработка API занимает много времени, то поток ждет ответа. У Tomcat значение для потока по умолчанию равно 200, чтобы обслуживать одновременно не более 200 запросов. В этом случае клиенты столкнутся с существенными задержками во время вызовов API.
Сегодня мы обсудим, как написать асинхронный REST API с помощью CompletableFuture
, чтобы освободить Tomcat и решить проблему переполнения потока.
CompletableFuture для асинхронного программирования
Для преодоления описанной выше проблемы полезен будет следующий код:
@RequestMapping(path = "/asyncCompletable", method = RequestMethod.GET)
public CompletableFuture<String> getValueAsyncUsingCompletableFuture() {
logger.info("Request received");
CompletableFuture<String> completableFuture
= CompletableFuture.supplyAsync(this::processRequest);
logger.info("Servlet thread released");
return completableFuture;
}
private String processRequest() {
Long delayTime = 10000L;
logger.info("Start processing request");
try {
Thread.sleep(delayTime);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
logger.info("Completed processing request");
return "Processing done after " + delayTime;
}
Здесь происходит вот что:
- Это простые конечные точки, обращение к которым происходит методом GET, и его возвращаемый тип —
CompletableFuture<String>
. - Во время обработки запроса вызывается метод
ProcessRequest
, в котором есть толькоThread.sleep
в течение десяти секунд. - Оба метода содержат журнал входа и выхода.
После вызова конечной точки через браузер/Postman через десять секунд отправляется ответ. Журнал этого вызова API приведен ниже.
Здесь:
- Первая и вторая строчки журнала отображаются в потоке
nio-8080-exec-8
. Там также показаны полученные запросы и освобожденный поток сервлетов. Этот поток исходит из неблокирующего ввода-вывода от встроенного сервера. - Из первых двух строчек журнала становится понятно, что освобождение потока происходит перед выполнением части метода
processRequest
, которая находится в частиCompletableFuture
. - Последние две строчки журнала предназначены для
CompletableFuture
. Обработка идет в потокеonPool-worker-2
. Это означает, что он отделен от потока tomcat, а этот поток от Spring,ForkJoinPool
.
Итог
- Сервер Tomcat передает запросы в приложения после получения вызовов от клиентов.
- Приложение назначает эту задачу в другой поток с помощью
CompletableFuture
. Этот поток отвечает за выполнение назначенной задачи и отвечает клиентам через подключения Tomcat. - Приложение немедленно освобождает назначенный поток Tomcat. Этот поток вернется в пул потоков Tomcat и будет готов к обслуживанию нового запроса API.
- С помощью этого механизма Tomcat сможет обрабатывать большой трафик.
- Возможно принимать больше запросов, изменив файл
server.xml
.
Можно тонко настроить файл server.xml
, чтобы он мог обрабатывать большое количество входящего трафика. Эта настройка должна выполняться на основе ресурсов нашего сервера, таких как мощность процессора, количество ядер, оперативная память и т.д.
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
maxThreads="250"
redirectPort="8443" />
Чтобы сократить время ожидания потока сервера, можно воспользоваться асинхронным API. Альтернативный способ — DeferredResult или Spring Web Flux.
Читайте также:
- Топ - 9 фреймворков Java в 2020 году
- Осваиваем реактивное программирование на Java
- Асинхронность в Java
Читайте нас в Telegram, VK и Дзен
Перевод статьи Md Sajedul Karim: “Writing Asynchronous Non-Blocking Rest API using JAVA”