Анализ проблемы FullGC, вызванной пулом соединений с базой данных

Java

проблемное явление

В какой-то рабочий день я внезапно получил сигнал тревоги онлайн-сервиса, и большое количество запросов было задержано.Когда я проверил онлайн-сервис, я обнаружил, что в основном время соединения с базой данных истекло, и время воздействия составляло всего 3-4 секунды. , и обслуживание возобновилось. нормально. Через несколько минут снова появилось большое количество будильников, но эффект пришел в норму через 3-4 секунды. Поскольку мы являемся службой низкого уровня и на нее полагаются многие службы верхнего уровня, такие частые аномальные колебания серьезно повлияли на использование бизнеса. Начать устранение неполадок

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

Влияние БД?

  1. Когда генерируется первый аварийный сигнал, первая реакция заключается в том, что может быть большое количество интерфейсных вызовов в службе верхнего уровня и задействованы некоторые сложные SQL-запросы, что приводит к недостаточному количеству подключений к базе данных.Очевидных изменений нет, за исключением влияние всплеска трафика
  2. Запросите ситуацию с БД, загрузка хорошая, медленный запрос отсутствует, и влияние, вызванное БД, исключено.

Влияние контейнера или JVM?

После исключения влияния БД проверить влияние контейнера вверх Мы снова посмотрели на аномальные тревоги и обнаружили, что в период каждой волны тревог они в основном генерировались одним и тем же IP-адресом контейнера.В настоящее время вероятность того, что это проблема GC, составляет 80%. Убедитесь, что загрузка ЦП контейнера в период тревоги в норме. Глядя на ситуацию с памятью и GC JVM, можно обнаружить, что вся кривая использования памяти выглядит следующим образом:

Heap

Old Gen

Из приведенного выше рисунка видно, что в памяти есть объекты, на которые ссылались в течение длительного времени и которые не могут быть восстановлены YongGC, а размер объектов увеличивается. Объект не будет восстановлен до тех пор, пока не будет запущена полная сборка мусора после того, как старая сборка будет заполнена, а одна полная сборка мусора занимает очень много времени.

Временные меры

Теперь, когда проблема обнаружена, только 3 экземпляра запустили FullGC, но при рассмотрении использования памяти другими экземплярами обнаруживается, что в основном все экземпляры Old Gen приближаются к критической точке. Таким образом, временное решение состоит в том, чтобы оставить один экземпляр на месте и перезапустить все остальные экземпляры поочередно, чтобы избежать одновременного выполнения FullGC большим количеством экземпляров. В противном случае это может вызвать лавину обслуживания.

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

анализ проблемы

какие объекты не перерабатываются

Что я узнал на данный момент: Память не может быть восстановлена ​​YoungGC и увеличивается бесконечно.Только FullGC может восстановить эту партию объектов, а FullGC занимает много времени.

 jmap -histo:live pid

Во-первых, я просто наблюдал за волной онлайн, второй HashMap$Node выглядит ненормально, но больше деталей я не вижу. Лучший способ — сделать дамп снимка памяти и использовать MAT для анализа волны.

jmap -dump:format=b,file=filename pid 

После использования MAT для открытия вы можете обнаружить очевидные проблемы:

class com.mysql.cj.jdbc.AbandonedConnectionCleanupThread

Этот класс занимает более 80% памяти, так что же делает этот класс? Как вы можете видеть из имени класса, это должен быть поток в драйвере MySQL, используемый для очистки просроченных соединений. Давайте посмотрим на исходный код:

Этот класс представляет собой синглтон, который открывает только один поток для очистки соединений с базой данных, которые не были закрыты явно.

Вы можете видеть, что этот класс поддерживает Set

private static final Set<ConnectionFinalizerPhantomReference> connectionFinalizerPhantomRefs = ConcurrentHashMap.newKeySet();

В соответствии с HashMap$Node со второй по величине скоростью использования памяти, которую мы видели выше, можно в основном определить, что существует высокая вероятность того, что здесь есть утечка памяти. Подтвердите выстрел с помощью list_object на MAT:

Конечно же, виновник найден! Так что же в нем? Почему он продолжает расти и не может быть переработан YoungGC? см. имяConnectionFinalizerPhantomReferenceМы можем предположить, что он должен содержать фантомную ссылку на соединение с базой данных.

чтоphantom reference? Когда объект имеет толькоphantom referenceКогда на него ссылаются, он будет повторно использован во время GC виртуальной машины и будетphantom referenceобъект вreferenceQueueсередина.

Давайте проследим исходный код, чтобы подтвердить

Конечно жеPhantomReference, в котором хранится созданное соединение с MySQL, посмотрите, куда оно было помещено:

Как видите, каждый раз, когда создается новое соединение с базой данных, созданное соединение оборачивается какPhantomReferenceпоставить послеconnectionFinalizerPhantomRefs, то этот поток очистки получит соединение в referenceQueue и закроет его в бесконечном цикле.

только вобъект соединенияДругих ссылок нет, толькоphantom referenceможно GCed и положить вreferenceQueueсередина

Почему Connection растет бесконечно?

Теперь, когда проблема обнаружена, после создания соединения с базой данных она поместитconnectionFinalizerPhantomRefsВо, но по какой-то причине нормальное использование предварительно подключенных, после того, как ряд мелких GC не были восстановлены, продвинуты на старую эру. Но через какое-то время по какой-то причине соединение не удается, в результате чего пул соединений и новые соединения.

В нашем проекте используется пул соединений с базой данных Druid, конфигурация пула соединений следующая:

Видно, что он установленkeepAlive,а такжеminEvictableIdleTimeMillisПараметр — 5 минут, после инициализации соединения, когда количество запросов к БД не колеблется часто, пул соединений должен поддерживать минимум 30 соединений, а операция keepAlive будет выполняться, когда время простоя соединения превышает 5 минут: Теоретически пул соединений не будет часто создавать соединения, если активных соединений мало и есть колебания, а операция keepAlive не вступает в силу.Когда пул соединений выполняет операцию keepAlive, соединение MySQL уже не удалось, тогда оно будет будет отброшено. Это недействительное соединение будет восстановлено в следующий раз.

Чтобы проверить эту гипотезу, нужно проверить следующее: сначала мы проверяем количество активных подключений и обнаруживаем, что в большинстве случаев количество активных подключений к базе данных с одним экземпляром равно 3.Есть около 20 колебаний, и в бизнесе есть запланированные задачи, каждые 30 минут.За 1 час будет много запросов к БД.
Поскольку у Друида пульсация каждые 5 минут, почему не удается установить соединение? Самая большая вероятность - это работа сервера MySQL.Время ожидания по умолчанию для сервера MySQL составляет 8 часов.Есть ли изменения в соответствующей конфигурации?

show global variables like '%timeout%'

Конечно же, время ожидания базы данных было установлено на 5 минут! Тогда проблема очевидна

краткое изложение проблемы

  1. Частая перестройка соединений с БД приводит к сбою повторного использования, что приводит к чрезмерно быстрому увеличению объема памяти.
  2. Во время minorGC, так как соединение все еще активно с сильной ссылкой, что приводит кconnectionFinalizerPhantomRefsБесконечный рост до тех пор, пока не будет опустошен FullGC, так что время FullGC слишком велико.

В заключение

  1. Бездействующие соединения полагаются на временную задачу Druid keepAlive для обнаружения сердцебиения и keepAlive.По умолчанию задача определения времени обнаруживается каждые 60 секунд и только тогда, когда время простоя соединения превышаетminEvictableIdleTimeMillisобнаружение сердцебиения.
  2. из-заminEvictableIdleTimeMillisОн установлен на 5 минут.Теоретически незанятые соединения будут выполнять обнаружение пульса в течение временного интервала 5 минут ± 60 секунд. Однако, поскольку время ожидания сервера MySQL составляет всего 5 минут, существует высокая вероятность того, что соединение прервалось, когда Druid выполняет операцию keepAlive.
  3. Поскольку активные соединения с базой данных колеблются, иmin-idleПараметр равен 30. Когда активное соединение находится на пике, необходимо создать и поддерживать большое количество соединений в пуле соединений. Но когда активность падает до нижней точки, большое количество соединений удаляется из пула соединений из-за сбоев keepAlive. Неделя за неделей.
  4. Каждый раз, когда создается соединение, объект Connection помещается вconnectionFinalizerPhantomRefs, и поскольку соединение активно после его создания, оно не будет восстановлено miniorGC в течение короткого времени, пока оно не будет переведено в старое состояние, в результате чего SET становится все больше и больше, что приводит к длительной паузе FullGC.

решить

Зная причину проблемы, решение простое:

  1. Уменьшите количество недопустимых подключений и уменьшите скорость роста памяти: будетminEvictableIdleTimeMillisУстановите на 3 минуты, чтобы обеспечить действительность Keepalive и избежать повторного подключения все время.
  2. FullGC сокращает время восстановления: Замените сборщик мусора на G1.

Расширенное чтение

1. Фантомная ссылка, слабая ссылка и т. д.

2. Принцип реализации пула соединений Druid

3. Использование инструмента анализа памяти MAT

Анализ проблемы FullGC, вызванной пулом соединений с базой данных (продолжение)

【Важно】Перепечатка, пожалуйста, укажите источник:nuggets.capable/post/684490…