Повторное использование потоков tomcat при ожидании "длительного" времени

КОНФИГУРАЦИЯ
Веб-сервер: Nginx
Сервер приложений: Tomcat с конфигурацией по умолчанию из 200 потоков обслуживания запросов
Ожидаемое время отклика для моего сервера: < 30 секунд (существует множество зависимостей сторонних производителей)

СЦЕНАРИЙ
Каждые 10 секунд приложение должно будет генерировать токен для его использования. Ожидаемое время для создания токена составляет около 5 секунд, но поскольку его сторонняя система связана с сетью, это, очевидно, не согласуется и может опуститься до 10 секунд.
Во время процесса создания токена почти 80% входящих запросов в секунду нужно будет ждать.

ЧТО Я ВЕРЮ СЛЕДУЕТ СЛУЧИТЬ
Поскольку запросы, ожидающие генерации маркера, должны ждать "длительное" время, нет причин для повторного использования этого запроса, чтобы обслуживать другие входящие запросы в ожидании завершения процесса генерации токена.
В принципе, было бы разумно, если бы мои 20% продолжали обслуживаться. Если ожидающие потоки не используются для других запросов, будет достигнут предел обслуживания tomcat, и сервер по существу захлестнет, что то, чего я действительно не возбуждаю.

ЧТО Я ТРЕНАЛ
Первоначально я ожидал, что переход на Tomcat NIO-соединитель выполнит эту работу. Но, посмотрев на это сравнение, я действительно не надеялся. Тем не менее, я попытался заставить запросы ждать 10 секунд, и это не сработало.
Теперь я думаю о строках, которые мне нужны, чтобы отложить запрос, пока он ждет, и нужно сообщить, что этот поток можно повторно использовать. Точно так же мне понадобится tomcat, чтобы дать мне поток из его потока, когда запрос готов к перемещению вперед. Но я ослеплен тем, как это сделать или даже если это возможно.

Любое руководство или помощь?

Ответ 1

Вам нужен асинхронный сервлет, но вам также нужны асинхронные HTTP-запросы к внешнему генератору токенов. Вы ничего не получите, передав запросы сервлета в ExecutorService с пулом потоков, если вы все равно создадите один поток где-то на один токен. Вы должны отделить потоки от HTTP-запросов, чтобы один поток мог обрабатывать несколько HTTP-запросов. Это может быть достигнуто с помощью асинхронного HTTP-клиента, такого как Apache Asynch HttpClient или Async Http Клиент.

Сначала вам нужно создать асинхронный сервлет вроде этого

public class ProxyService extends HttpServlet {

    private CloseableHttpAsyncClient httpClient;

    @Override
    public void init() throws ServletException {
        httpClient = HttpAsyncClients.custom().
                setMaxConnTotal(Integer.parseInt(getInitParameter("maxtotalconnections"))).             
                setMaxConnPerRoute(Integer.parseInt(getInitParameter("maxconnectionsperroute"))).
                build();
        httpClient.start();
    }

    @Override
    public void destroy() {
        try {
            httpClient.close();
        } catch (IOException e) { }
    }

    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        AsyncContext asyncCtx = request.startAsync(request, response);
        asyncCtx.setTimeout(ExternalServiceMock.TIMEOUT_SECONDS * ExternalServiceMock.K);       
        ResponseListener listener = new ResponseListener();
        asyncCtx.addListener(listener);
        Future<String> result = httpClient.execute(HttpAsyncMethods.createGet(getInitParameter("serviceurl")), new ResponseConsumer(asyncCtx), null);
    }

}

Этот сервлет выполняет асинхронный HTTP-вызов с использованием Apache Asynch HttpClient. Обратите внимание, что вы можете настроить максимальные соединения для каждого маршрута, так как согласно спецификации RFC 2616 HttpAsyncClient разрешает до двух одновременных соединений с одним и тем же хостом по умолчанию. И есть множество других параметров, которые вы можете настроить, как показано в Конфигурация HttpAsyncClient. HttpAsyncClient является дорогостоящим для создания, поэтому вы не хотите создавать его в каждой операции GET.

Один прослушиватель подключен к AsyncContext, этот прослушиватель используется только в приведенном выше примере для обработки тайм-аутов.

public class ResponseListener implements AsyncListener {

    @Override
    public void onStartAsync(AsyncEvent event) throws IOException {
    }

    @Override
    public void onComplete(AsyncEvent event) throws IOException {
    }

    @Override
    public void onError(AsyncEvent event) throws IOException {
        event.getAsyncContext().getResponse().getWriter().print("error:");
    }

    @Override
    public void onTimeout(AsyncEvent event) throws IOException {
        event.getAsyncContext().getResponse().getWriter().print("timeout:");
    }

}

Затем вам нужен клиент для HTTP-клиента. Этот пользователь информирует AsyncContext, вызывая complete(), когда buildResult() выполняется внутри HttpClient как шаг для возврата Future<String> к вызывающему ProxyService сервлету.

public class ResponseConsumer extends AsyncCharConsumer<String> {

    private int responseCode;
    private StringBuilder responseBuffer;
    private AsyncContext asyncCtx;

    public ResponseConsumer(AsyncContext asyncCtx) {
        this.responseBuffer = new StringBuilder();
        this.asyncCtx = asyncCtx;
    }

    @Override
    protected void releaseResources() { }

    @Override
    protected String buildResult(final HttpContext context) {
        try {
            PrintWriter responseWriter = asyncCtx.getResponse().getWriter();
            switch (responseCode) {
                case javax.servlet.http.HttpServletResponse.SC_OK:
                    responseWriter.print("success:" + responseBuffer.toString());
                    break;
                default:
                    responseWriter.print("error:" + responseBuffer.toString());
                }
        } catch (IOException e) { }
        asyncCtx.complete();        
        return responseBuffer.toString();
    }

    @Override
    protected void onCharReceived(CharBuffer buffer, IOControl ioc) throws IOException {
        while (buffer.hasRemaining())
            responseBuffer.append(buffer.get());
    }

    @Override
    protected void onResponseReceived(HttpResponse response) throws HttpException, IOException {        
        responseCode = response.getStatusLine().getStatusCode();
    }

}

Конфигурация web.xml для сервлета ProxyService может быть похожа на

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         id="WebApp_ID" version="3.0" metadata-complete="true">
  <display-name>asyncservlet-demo</display-name>

  <servlet>
    <servlet-name>External Service Mock</servlet-name>
    <servlet-class>ExternalServiceMock</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>

  <servlet>
    <servlet-name>Proxy Service</servlet-name>
    <servlet-class>ProxyService</servlet-class>
    <load-on-startup>1</load-on-startup>
    <async-supported>true</async-supported>
    <init-param>
      <param-name>maxtotalconnections</param-name>
      <param-value>200</param-value>
    </init-param>
    <init-param>
      <param-name>maxconnectionsperroute</param-name>
      <param-value>4</param-value>
    </init-param>
    <init-param>
      <param-name>serviceurl</param-name>
      <param-value>http://127.0.0.1:8080/asyncservlet/externalservicemock</param-value>
    </init-param>
  </servlet>

  <servlet-mapping>
    <servlet-name>External Service Mock</servlet-name>
    <url-pattern>/externalservicemock</url-pattern>
  </servlet-mapping>

  <servlet-mapping>
    <servlet-name>Proxy Service</servlet-name>
    <url-pattern>/proxyservice</url-pattern>
  </servlet-mapping>

</web-app>

И макет сервлета для генератора токенов с задержкой в ​​секундах может быть:

public class ExternalServiceMock extends HttpServlet{

    public static final int TIMEOUT_SECONDS = 13;
    public static final long K = 1000l;

    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        Random rnd = new Random();
        try {
            Thread.sleep(rnd.nextInt(TIMEOUT_SECONDS) * K);
        } catch (InterruptedException e) { }
        final byte[] token = String.format("%10d", Math.abs(rnd.nextLong())).getBytes(ISO_8859_1);
        response.setContentType("text/plain");
        response.setCharacterEncoding(ISO_8859_1.name());
        response.setContentLength(token.length);
        response.getOutputStream().write(token);
    }

}

Вы можете получить полностью рабочий пример в GitHub.

Ответ 2

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

Это не проблема, которая может быть решена путем настройки или замены соединителя tomcat.
Вам в основном нужно удалить все блокирующие вызовы ввода-вывода, заменив их неблокирующим IO, скорее всего, потребует перезаписи больших частей приложения.
Ваш HTTP-сервер должен быть неблокирующим, вам нужно использовать неблокирующий API для сервера (например, сервлет 3.1), и ваши вызовы стороннему API должны быть неблокирующими.
Библиотеки, такие как Vert.x и RxJava, предоставляют инструменты, помогающие в этом.

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

Не зная больше о вашем приложении, вы можете предложить советы по конкретному подходу.

Ответ 3

Использование асинхронных запросов сервлетов или реактивных библиотек (как упоминалось в других ответах) может помочь, но потребует значительных архитектурных изменений.

Другим вариантом является разделение обновлений токенов от использования токенов.

Вот наивная реализация:

public class TokenHolder {
    public static volatile Token token = null;
    private static Timer timer = new Timer(true);
    static {
        // set the token 1st time
        TokenHolder.token = getNewToken();

        // schedule updates periodically
        timer.schedule(new TimerTask(){
            public void run() {
                TokenHolder.token = getNewToken();
            }
        }, 10000, 10000);
    }
}

Теперь ваши запросы могут просто использовать TokenHolder.token для доступа к службе.

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