Тактика изучения снижения производительности веб-приложения на основе java с течением времени

Я работаю над корпоративным java-приложением, в котором уже есть много инструментов / фреймворков, таких как Struts, JAX-RS и Spring MVC. Он содержит пользовательские интерфейсы и конечные точки REST, объединенные в файл.war. Проект развивается, и мы избавляемся от старых инструментов, стремясь придерживаться только Spring MVC/Webflux.

Приложение выполняет поиск по миллионам записей XML/JSON, и недавно поисковая система была переключена с Marklogic на Elasticsearch.

Мы заметили, что в производственной среде с не очень интенсивным использованием (до 1,7 тыс. Об / мин на 2–4 узлах приложений) время отклика на некоторых конечных точках со временем увеличивается. Elasticsearch имеет пространство для роста и не показывает никаких признаков огромной нагрузки. Таким образом, в настоящее время мы должны перезапускать / заменять экземпляры prod один раз в неделю или две, когда среднее время ответа превышает 3 секунды вместо обычных 200-300 миллисекунд.

Я попытался получить графики пламени процессора и кучи с помощью async-profiler, но профиль нагрузки меняется при каждом измерении, так как у нас есть множество доступных функций, поэтому я не могу реально сравнить, как графики меняются с течением времени.

Можете ли вы посоветовать мне некоторые тактики / подходы к поиску подходящего места в коде?

1 ответ

Обнаружена проблема. Это связано с пулом потоков.

Мы заметили, что со временем количество активных потоков tomcat росло вместе со временем отклика: на изображении вы также можете видеть, что сервер был перезапущен 9 мая.

Мне удалось получить дамп кучи до перезапуска сервера, и после некоторого рытья я обнаружил интересный повторяющийся фрагмент в дампе потока:

Thread xxx
  at sun.misc.Unsafe.park(ZJ)V (Native Method)
  at java.util.concurrent.locks.LockSupport.park(Ljava/lang/Object;)V (LockSupport.java:175)
  at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await()V (AbstractQueuedSynchronizer.java:2039)
  at org.apache.http.pool.AbstractConnPool.getPoolEntryBlocking(Ljava/lang/Object;Ljava/lang/Object;JLjava/util/concurrent/TimeUnit;Ljava/util/concurrent/Future;)Lorg/apache/http/pool/PoolEntry; (AbstractConnPool.java:377)
  at org.apache.http.pool.AbstractConnPool.access$200(Lorg/apache/http/pool/AbstractConnPool;Ljava/lang/Object;Ljava/lang/Object;JLjava/util/concurrent/TimeUnit;Ljava/util/concurrent/Future;)Lorg/apache/http/pool/PoolEntry; (AbstractConnPool.java:67)
  at org.apache.http.pool.AbstractConnPool$2.get(JLjava/util/concurrent/TimeUnit;)Lorg/apache/http/pool/PoolEntry; (AbstractConnPool.java:243)
  at org.apache.http.pool.AbstractConnPool$2.get(JLjava/util/concurrent/TimeUnit;)Ljava/lang/Object; (AbstractConnPool.java:191)
  at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.leaseConnection(Ljava/util/concurrent/Future;JLjava/util/concurrent/TimeUnit;)Lorg/apache/http/HttpClientConnection; (PoolingHttpClientConnectionManager.java:282)
  at org.apache.http.impl.conn.PoolingHttpClientConnectionManager$1.get(JLjava/util/concurrent/TimeUnit;)Lorg/apache/http/HttpClientConnection; (PoolingHttpClientConnectionManager.java:269)
  at org.apache.http.impl.execchain.MainClientExec.execute(Lorg/apache/http/conn/routing/HttpRoute;Lorg/apache/http/client/methods/HttpRequestWrapper;Lorg/apache/http/client/protocol/HttpClientContext;Lorg/apache/http/client/methods/HttpExecutionAware;)Lorg/apache/http/client/methods/CloseableHttpResponse; (MainClientExec.java:191)
  at org.apache.http.impl.execchain.ProtocolExec.execute(Lorg/apache/http/conn/routing/HttpRoute;Lorg/apache/http/client/methods/HttpRequestWrapper;Lorg/apache/http/client/protocol/HttpClientContext;Lorg/apache/http/client/methods/HttpExecutionAware;)Lorg/apache/http/client/methods/CloseableHttpResponse; (ProtocolExec.java:185)
  at org.apache.http.impl.execchain.RetryExec.execute(Lorg/apache/http/conn/routing/HttpRoute;Lorg/apache/http/client/methods/HttpRequestWrapper;Lorg/apache/http/client/protocol/HttpClientContext;Lorg/apache/http/client/methods/HttpExecutionAware;)Lorg/apache/http/client/methods/CloseableHttpResponse; (RetryExec.java:89)
  at org.apache.http.impl.execchain.RedirectExec.execute(Lorg/apache/http/conn/routing/HttpRoute;Lorg/apache/http/client/methods/HttpRequestWrapper;Lorg/apache/http/client/protocol/HttpClientContext;Lorg/apache/http/client/methods/HttpExecutionAware;)Lorg/apache/http/client/methods/CloseableHttpResponse; (RedirectExec.java:111)
  at org.apache.http.impl.client.InternalHttpClient.doExecute(Lorg/apache/http/HttpHost;Lorg/apache/http/HttpRequest;Lorg/apache/http/protocol/HttpContext;)Lorg/apache/http/client/methods/CloseableHttpResponse; (InternalHttpClient.java:185)
  at org.apache.http.impl.client.CloseableHttpClient.execute(Lorg/apache/http/client/methods/HttpUriRequest;Lorg/apache/http/protocol/HttpContext;)Lorg/apache/http/client/methods/CloseableHttpResponse; (CloseableHttpClient.java:83)
  at org.apache.http.impl.client.CloseableHttpClient.execute(Lorg/apache/http/client/methods/HttpUriRequest;)Lorg/apache/http/client/methods/CloseableHttpResponse; (CloseableHttpClient.java:108)
  at io.searchbox.client.http.JestHttpClient.executeRequest(Lorg/apache/http/client/methods/HttpUriRequest;)Lorg/apache/http/client/methods/CloseableHttpResponse; (JestHttpClient.java:136)
  at io.searchbox.client.http.JestHttpClient.execute(Lio/searchbox/action/Action;Lorg/apache/http/client/config/RequestConfig;)Lio/searchbox/client/JestResult; (JestHttpClient.java:70)
  at io.searchbox.client.http.JestHttpClient.execute(Lio/searchbox/action/Action;)Lio/searchbox/client/JestResult; (JestHttpClient.java:63)
...

В нашем случае мы используем библиотеку Jest для общения с Elasticsearch. Внутри он использует HTTP-клиент Apache и HTTP-асинхронный клиент Apache.

Как видно из дампа потока, ясно, что этот поток ожидал доступного потока в пуле потоков HTTP-клиента. И было больше потоков с точно таким же стеком.

Я также обнаружил, что мы устанавливаем maxTotal (максимальное общее количество подключений) до 20 а также defaultMaxPerRoute (максимальное количество соединений на маршруте) до 2:

По умолчанию пул позволяет всего 20 одновременных подключений и два одновременных подключения на один уникальный маршрут. Ограничение двух подключений связано с требованиями спецификации HTTP. Однако на практике это часто может быть слишком ограничительным.

См. Описание пулов подключений.

Итак, исправление, которое я сделал, увеличило эти значения до 50 а также 40соответственно. Я бы по-прежнему предпочел, чтобы эти параметры были неограниченными и увеличивались по мере использования, но пока придерживайтесь этих значений.

Другие вопросы по тегам