Когда клиент обращается к 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.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Md Sajedul Karim: “Writing Asynchronous Non-Blocking Rest API using JAVA”

Предыдущая статьяПростое объяснение интерфейсов на Golang
Следующая статьяTypeScript: основы