Помните процесс устранения неполадок OOM

Java

На прошлой неделе появилась онлайн-программа обратной связи по эксплуатации и техническому обслуживанию OOM, и вывод в журнале программы был следующим:

Exception in thread "http-nio-8080-exec-1027" java.lang.OutOfMemoryError: Java heap space
Exception in thread "http-nio-8080-exec-1031" java.lang.OutOfMemoryError: Java heap space

Имя потока должно быть рабочим потоком nio tomcat. Когда поток обрабатывает программу, возникает OOM, потому что он не может выделить больше памяти в куче. К счастью, параметр запуска JVM настроен с -XX: + HeapDumpOnOutOfMemoryError и использует MAT открыть полученный файл hprof для анализа.

Первый шаг — открыть гистограмму, чтобы увидеть, какие объекты занимают больше всего памяти:

Видно, что байтовый массив занимает размер, близкий к максимальному размеру кучи конфигурации JVM, который составляет 8 ГБ, что, очевидно, и является причиной OOM. Второй шаг — посмотреть, какие байтовые массивы и каково содержимое массивов:
Видно, что это явно связано с HTTP-запросами, а размер массива около 10M. Третий шаг — посмотреть, кто владеет ссылкой на массив, посмотрев на корень GC:
Это согласуется с предыдущим предположением о том, что поток tomcat выделил буфер размером 10 МБ в куче во время обработки. В этот момент сразу можно подумать о каких-то неразумных настройках параметров, вызвавших эту ситуацию, вообще говоря, tomcat не может выделять такой большой буфер под каждый запрос. Четвертый шаг — проверить, есть ли в коде конфигурация, связанная с tomcat или сервером, и убедиться, что такая конфигурация есть:

max-http-header-size: 10000000

До сих пор в основном было установлено, что проблема вызвана этим необоснованным максимальным параметром заголовка HTTP-запроса. Тут еще 3 вопроса:

  1. Даже если запрос выделяет 10 МБ памяти, а куча имеет 8 ГБ, так ли много параллелизма в это время? 800 потоков кота?
  2. Параметр устанавливает только максимальный заголовок запроса 10 М. Почему tomcat выделяет такой большой буфер за один раз?
  3. Почему так много потоков tomcat? Я чувствую, что программа не так много параллельна.

Давайте сначала рассмотрим вопрос 1, который можно продолжить, чтобы найти ответ в дампе через МАТ. Вы можете открыть представление потоков и найти рабочие потоки tomcat и обнаружить, что количество потоков действительно равно 401, но это только половина от 800:

Возвращаясь к списку этих больших массивов, отсортированных по размеру выделения кучи, посмотрите вниз:
Можно обнаружить, что кроме массива в 10008192 байта есть еще массив в 10000000 байт.Посмотрев на эталонный путь, можно увидеть, что этот массив ровно в 10M является выходным буфером, который отличается от входного буфера. видел раньше:
Ну правильно, поток выделяет два буфера для ввода и вывода, занимая 20М памяти, всего 401 поток, занимая 8ГБ, так что ООМ. Это также приводит к вопросу, почему существует так много рабочих потоков,

Давайте посмотрим на вопрос 2, который требует от вас найти исходный код. Прежде всего, max-http-header-size — это параметр, определенный Springboot. Глядя на код Springboot, вы можете видеть, что этот параметр — MaxHttpHeaderSize, установленный для tomcat :

Затем взгляните на исходный код tomcat:
Присмотритесь к входному буферу:

Размер буфера равен MaxHttpHeaderSize+ReadBuffer size, который по умолчанию равен 8192 байтам:

   <attribute name="socket.appReadBufSize" required="false">
        <p>(int)Each connection that is opened up in Tomcat get associated with
        a read ByteBuffer. This attribute controls the size of this buffer. By
        default this read buffer is sized at <code>8192</code> bytes. For lower
        concurrency, you can increase this to buffer more data. For an extreme
        amount of keep alive connections, decrease this number or increase your
        heap size.</p>
      </attribute>

Вот почему раньше я видел большое количество буферов размером 10008192 байта. Очевидно, что есть еще пакет пустых буферов размером 10000000 байт, которые должны быть буферами вывода.Давайте посмотрим на исходный код:

Ну, это буфер заголовков, поэтому он ровно 10000000 байт.

Что касается вопроса 3, очевидно, что наше приложение настроено с самым большим потоком (проверив конфигурацию, мы обнаружили, что мы настроили 2000, что немного велико), иначе не будет 401 рабочего потока (по умолчанию 150), если concurrent в то время Если он не большой, это возможно. Запрос очень медленный. Хотя параллелизм невелик, требуется больше потоков, потому что выполнение запроса медленное. Например, если TPS составляет 100, но в среднем RT — 4 с, это 400 потоков. Ответ на этот вопрос все еще можно найти через MAT.Если вы посмотрите на несколько потоков, вы можете обнаружить, что многие потоки ждут возврата внешнего сервиса, а это означает, что внешний сервис работает относительно медленно. журнал программы в то время, вы можете обнаружить, что есть много «feign.RetryableException»: чтение журнала выполнения истекло по тайм-ауту». . . . Преследуй и убивай вниз по течению! Притормози, тайм-аут нашего симуляции тоже нужно выставить заново, так что не тяни насмерть внешние сервисы.