Анализ и решение проблемы утечки памяти пула соединений с базой данных

Java

гитхаб-адрес

GitHub.com/Я бы хотел 123/Java…

1. Описание проблемы

В прошлую пятницу вечером были отключены некоторые устройства в основном бизнесе.После проверки лога было установлено, что это было вызвано длительным gc системы кеша. Особенности журнала gc здесь:

  • 1. Время gc составляет более 2 с, а некоторые узлы даже имеют сверхдлинный gc 12 с.
  • 2. Временной интервал между тем же узлом и последним сборщиком мусора обычно составляет от 13 до 15 дней.

Затем срочно делаем дамп памяти оставшейся ноды без gc, открываем его матовым инструментом и обнаруживаем, что объект com.mysql.jdbc.NonRegisteringDriver занимает большую часть памяти кучи.

Проверьте количество объектов и найдите com.mysql.jdbc.NonRegisteringDriver$ConnectionPhantomReference.Этот объект имеет 10140 сложенных.

Первоначальное суждение о долгосрочной проблеме gc должно бытьИз-за большого скопления объектов com.mysql.jdbc.NonRegisteringDriver$ConnectionPhantomReference.

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

Текущие зависимости, связанные с базой данных в официальной среде, следующие:

полагаться Версия
mysql 5.1.47
hikari 2.7.9
Sharding-jdbc 3.1.0

Основываясь на приведенном выше описании, задайте следующие вопросы:

  • 1. Что за объект com.mysql.jdbc.NonRegisteringDriver$ConnectionPhantomReference?
  • 2. Почему такого рода объекты накапливаются в больших количествах и не могут быть переработаны JVM?

Что за объект NonRegisteringDriver$ConnectionPhantomReference?

Проще говоря, класс NonRegisteringDriver имеет виртуальную коллекцию ссылок connectionPhantomRefs для хранения всех подключений к базе данных, а метод NonRegisteringDriver.trackConnection отвечает за помещение только что созданного подключения в коллекцию connectionPhantomRefs. Исходный код выглядит следующим образом:

1.public class NonRegisteringDriver implements java.sql.Driver {  
2.	   protected static final ConcurrentHashMap<ConnectionPhantomReference, ConnectionPhantomReference> connectionPhantomRefs = new ConcurrentHashMap<ConnectionPhantomReference, ConnectionPhantomReference>();  
3.	   protected static final ReferenceQueue<ConnectionImpl> refQueue = new ReferenceQueue<ConnectionImpl>();
4.	  
5.	    ....  
6.	  
7.	   protected static void trackConnection(Connection newConn) {  
8.	  
9.	       ConnectionPhantomReference phantomRef = new ConnectionPhantomReference((ConnectionImpl) newConn, refQueue);  
10.	        connectionPhantomRefs.put(phantomRef, phantomRef);  
11.	   }  
12.	    ....  
13.	}  

Мы проследили исходный код процесса создания подключения к базе данных и обнаружили, что он вызывает конструктор com.mysql.jdbc.ConnectionImpl, который вызывает метод createNewIO для создания нового объекта MysqlIO подключения к базе данных, а затем вызывает метод NonRegisteringDriver. .trackConnection, о котором мы упоминали выше, помещаем объект в коллекцию NonRegisteringDriver.connectionPhantomRefs. Исходный код выглядит следующим образом:

1.public class ConnectionImpl extends ConnectionPropertiesImpl implements MySQLConnection {  
2.	  
3.	   public ConnectionImpl(String hostToConnectTo, int portToConnectTo, Properties info, String databaseToConnectTo, String url) throws SQLException {  
4.	        ...  
5.	       createNewIO(false);  
6.	        ...  
7.	       NonRegisteringDriver.trackConnection(this);  
8.	        ...  
9.	   }  
10.} 

connectionPhantomRefs — это коллекция виртуальных ссылок, что такое виртуальная ссылка? Почему он разработан как виртуальная эталонная очередь

  • Фантомная очередь ссылок, также известная как «фантомная ссылка», представляет собой самый слабый вид связи ссылок.
  • Если объект содержит только фантомные ссылки, он может быть удален сборщиком мусора в любое время, как если бы у него вообще не было ссылок.
  • установить виртуальный объект Единственной целью ссылочной ассоциации является получение системного уведомления, когда объект истребуется сборщиком.
  • Когда сборщик мусора собирается переработать объект, если он обнаруживает, что у него все еще есть виртуальная ссылка, он добавит виртуальную ссылку в очередь ссылок после сборки мусора и не будет полностью уничтожать объект до того, как связанная с ним виртуальная ссылка будет удалена из очереди. . Таким образом, вы можете судить о том, был ли объект переработан, проверив, есть ли соответствующая виртуальная ссылка в очереди ссылок.

Почему объект, такой как connectionPhantomRefs, накапливается так много, что JVM не может его переработать?

Вот комбинация конфигурации данных hikaricp и официальной документации в проекте ~

Давайте сначала проверим пул данных Hikaricp.Адрес официального сайта, посмотрите на некоторые свойства, описанные ниже:

maximumPoolSize

This property controls the maximum size that the pool is allowed to reach, including both idle and in-use connections. Basically this value will determine the maximum number of actual connections to the database backend. A reasonable value for this is best determined by your execution environment. When the pool reaches this size, and no idle connections are available, calls to getConnection() will block for up to connectionTimeout milliseconds before timing out. Please read about pool sizing. Default: 10

maxPoolSize контролирует максимальное количество подключений, по умолчанию 10

minimumIdle

This property controls the minimum number of idle connections that HikariCP tries to maintain in the pool. If the idle connections dip below this value and total connections in the pool are less than maximumPoolSize, HikariCP will make a best effort to add additional connections quickly and efficiently. However, for maximum performance and responsiveness to spike demands, we recommend not setting this value and instead allowing HikariCP to act as a fixed size connection pool. Default: same as maximumPoolSize

MinimumIdle контролирует минимальное количество подключений, по умолчанию оно равно maxPoolSize, 10.

⌚idleTimeout

This property controls the maximum amount of time that a connection is allowed to sit idle in the pool. This setting only applies when minimumIdle is defined to be less than maximumPoolSize. Idle connections will not be retired once the pool reaches minimumIdle connections. Whether a connection is retired as idle or not is subject to a maximum variation of +30 seconds, and average variation of +15 seconds. A connection will never be retired as idle before this timeout. A value of 0 means that idle connections are never removed from the pool. The minimum allowed value is 10000ms (10 seconds). Default: 600000 (10 minutes)

После того, как время простоя соединения превысит idleTimeout (по умолчанию 10 минут), соединение будет прервано.

⌚maxLifetime

This property controls the maximum lifetime of a connection in the pool. An in-use connection will never be retired, only when it is closed will it then be removed. On a connection-by-connection basis, minor negative attenuation is applied to avoid mass-extinction in the pool. We strongly recommend setting this value, and it should be several seconds shorter than any database or infrastructure imposed connection time limit. A value of 0 indicates no maximum lifetime (infinite lifetime), subject of course to the idleTimeout setting. Default: 1800000 (30 minutes)

После того, как время жизни соединения превысит maxLifetime (по умолчанию 30 минут), соединение будет сброшено.

Давайте оглянемсь назадхикари конфигурация проекта:

  • Настроено MinimumIdle = 10, maxPoolSize = 50, не настроены idleTimeout и maxLifetime. Так что эти двое будут использовать значения по умолчанию idleTimeout = 10 минут, maxLifetime = 30 минут.
  • То есть, если пул соединений с базой данных заполнен и имеется 50 соединений, если система простаивает, 40 соединений будут отброшены через 10 минут (больше, чем idleTimeout); если система всегда занята, будет создано 50 соединений. через 30 минут (более maxLifetime ) отбрасываются.

Угадайте источник проблемы:

Каждый раз, когда создается новое подключение к базе данных, оно помещается в коллекцию connectionPhantomRefs. Подключение к данным будет сброшено, когда время простоя превысит idleTimeout или время жизни превысит maxLifetime, и будет ожидать перезапуска в коллекции connectionPhantomRefs. Поскольку ресурсы соединений обычно сохраняются в течение длительного времени, после нескольких молодых GC они обычно могут сохраняться до старости. Если сам объект подключения к базе данных устарел, элементы в connectionPhantomRefs будут накапливаться до следующего полного gc. Если в коллекции connectionPhantomRefs слишком много элементов, когда полная сборка мусора завершена, полная сборка мусора займет очень много времени.

Итак, как решить эту проблему? Вы можете рассмотреть возможность оптимизации таких параметров, как MinimumIdle, MaximumPoolSize, idleTimeout и maxLifetime. Мы проанализируем волну в следующем разделе.

3. Верификация проблемы

среда онлайн-симуляции

Чтобы проверить проблему, нам нужно смоделировать онлайн-среду и настроить такие параметры, как maxLifetime~Идея испытания под давлением заключается в следующем.:

  • 1. Кэш-система имитирует онлайн-конфигурацию и использует систему измерения давления для непрерывного нажатия на кеш-систему в течение определенного периода времени, так что кеш-система создает/отбрасывает большое количество подключений к базе данных за короткий период времени, наблюдает накапливаются ли объекты NonRegisteringDriver в больших количествах, как ожидалось, а затем вручную вызывает System.gc(). Наблюдайте, очищается ли объект NonRegisteringDriver.
  • 2. Отрегулируйте параметр maxLifetime и посмотрите, накапливаются ли объекты NonRegisteringDriver в течение того же времени измерения напряжения.

Вот следующие моменты, на которые следует обратить внимание:

  • 1. Объект NonRegisteringDriver удовлетворяет условиям входа в старое поколение только в том случае, если выполняется условие (время интервала gc * количество выживаний до того, как новое поколение войдет в старое поколение
  • 2. MinimumIdle = 10, maxPoolSize = 50 (minimumIdle и maxPoolSize соответствуют онлайн-конфигурации), idleTimeout установлено на 10 с, а maxLifetime установлено на 100 с (время gc составляет около 20 с, поэтому оно должно быть больше 20 * 3 = 60-х). Таким образом, ожидается, что 10 новых подключений будут генерироваться каждые 30 с при непрерывном стресс-тесте (даже если установлено максимальное значениеPoolSize = 50, 10 подключений для стресс-теста такого рода программы достаточно, чтобы справиться с ним).
  • 3. Выделение памяти проекта меньше, а количество времени выживания до того, как новое поколение войдет в старое поколение, уменьшено, так что объекты NonRegisteringDriver нового поколения могут войти в старое поколение за короткое время, и это удобно наблюдать очевидный рост объекта за короткое время.
  • 4. Следить за выживаемостью соединений пула соединений системных данных кэша, а также за ситуацией в системе gc.

Окончательная конфигурация среды выглядит следующим образом:

Результаты симуляции

  • Включите инструмент jvisualvm для наблюдения за системой кэширования в режиме реального времени.
  • Откройте журнал отладки, связанный с hikari, чтобы наблюдать за ситуацией с пулом соединений.

Установите maxLifetime = 100s, запустите систему кэширования

Подтвердите, что конфигурации hikari и jvm вступили в силу.

Наблюдайте за jvisualvm и обнаружите, что создано 20 объектов NonRegisteringDriver.

Просмотрите журнал hikari и убедитесь, что создано 20 объектов соединения, а также создано 10 полных соединений и 10 незанятых соединений.

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

Запустите программу стресс-теста, стресс-тест на 1000 сек.

Наблюдайте за журналом gc в течение периода, временной интервал gc составляет около 20 с, а gc происходит 5 раз после 100 с.

Просмотрите журнал hikari и убедитесь, что создано 20 объектов подключения.

Обратите внимание, что jvisualvm превращается в 40 объектов NonRegisteringDriver, как и ожидалось.

Непрерывное наблюдение, теоретически 220 объектов будут сгенерированы после 1000 с (20 + 20 * 1000 с / 100 с), см. jvisualvm следующим образом

Было изготовлено 240 объектов, в основном, как и предполагалось.

Анализ результатов

В сочетании с нашими производственными проблемами, предположим, что у нас есть 14-часовой пиковый период (12:00-2:00) каждый день, количество подключений в течение 10-часового низкого пикового периода, количество подключений в течение 10-часового пикового периода, часовой период, а интервал каждого полного GC составляет 14 дней, объекты NonRegisteringDriver, накопленные в следующем полном GC, будут (20 * 14 + 10 * 10) * 2 * 14 = 10640, что в основном согласуется с количеством NonRegisteringDriver объектов в проблемном дампе 10140.

На данный момент первопричина проблемы полностью подтверждена! ! !

В-четвертых, решение проблемы

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

  • 1. Уменьшить создание и накопление заброшенных объектов подключения к данным.
  • 2. Оптимизируйте полное время сборки мусора.

[Настроить параметры хикари]

Мы можем рассмотреть возможность установки большего значения maxLifetime, чтобы продлить жизненный цикл соединения и уменьшить частоту отказа от соединений с базой данных.Объекты соединений с базой данных, которые необходимо очистить при выполнении следующего полного gc, будут значительно уменьшены.

Хикари рекомендует установить для параметра maxLifetime значение от 30 секунд до 1 минуты меньше, чем время ожидания базы данных wait_timeout. Если вы используете базу данных mysql, вы можете использовать глобальные переменные show, такие как '%timeout%'; для просмотра wait_timeout, которое по умолчанию равно 8 часам.

Запустим проверку, установим maxLifetime = 1 час, остальные условия остаются без изменений. Наблюдайте за jvisualvm перед началом стресс-теста, а количество объектов NonRegisteringDriver равно 20.

1000s, обратите внимание, что объект NonRegisteringDriver все еще 20

Объекты NonRegisteringDriver не накапливались, и проблема была решена.

Также обратите внимание: Не устанавливайте слишком большие значения MinimumIdle и MaximumPoolSize.Вообще говоря, вы можете настроить MinimumIdle=10 и MaximumPoolSize=10~20.

【Использование G1 Recycler】

Сборщик G1 является последним достижением текущего сборщика мусора Java. Это отличный сборщик с низкой задержкой и высокой пропускной способностью. Пользователи могут настроить максимальное целевое время паузы. G1 попытается достичь высокой пропускной способности при соблюдении целевого времени паузы сборки мусора. . . .

Начнем проверять работоспособность ресайклера G1.Процесс проверки требует длительного периода наблюдения, и в то же время, с помощью средства отслеживания ссылок. После 10 дней наблюдения результат таков: Использовать сборщик G1, некоторые параметры jvm -Xms3G -Xmx3G -XX:+UseG1GC

Используйте комбинацию сборщиков Parallel GC по умолчанию для Java 8, некоторые параметры jvm -Xms3G -Xmx3G

Четыре содержимого на приведенном выше рисунке слева направо:

  • 1. Память кучи делится на используемую и свободную память.
  • 2. Память области метода, это не должно беспокоить
  • 3. Молодой gc и полный gc time
  • 4. Молодой сборщик мусора и полный сбор мусора после запуска программы

Мы видим, что служба, использующая комбинацию сборщиков Parallel GC, потребляет память быстрее, с 6996 молодыми сборщиками мусора и одним полным сборщиком мусора, а время полного сбора мусора составляет целых 5 с. Другая группа сервисов, использующих сборщик G1, потребляет память с относительно стабильной скоростью: всего 3827 молодых сборщиков мусора и ни одного полного сборщика мусора. Из этого видно, что сборщик G1 действительно может быть использован для решения нашей проблемы накопления объектов соединения с базой данных.

【Создайте систему проверки】

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

  • 1. Создайте Java-программу и периодически вызывайте System.gc() с временными задачами. Недостаток этого метода в том, что даже при ручном вызове System.gc() jvm не обязательно начнет работу по переработке сразу, а может выбрать оптимальное время для начала работы по переработке по собственному алгоритму.
  • 2. Создайте сценарий оболочки для вызова jmap -dump:live,file=dump_001.bin PID, используйте задачу linux crontab для обеспечения регулярного выполнения и удалите dump_001.bin после выполнения. Этот метод может гарантировать, что произойдет полный сбор мусора, но недостатком является то, что функции слишком разрозненны и разбросаны, а централизованное управление непросто.

V. Резюме

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

  • 1. Настройте параметры конфигурации hikari. Например, установите для maxLifetime большее значение (на 30 с меньше, чем wait_timeout базы данных), значения MinimumIdle и MaximumPoolSize не могут быть установлены слишком большими, или можно напрямую использовать значения по умолчанию.
  • 2, используя сборщик мусора G1.
  • 3. Создайте систему проверки, чтобы активно запускать полную сборку мусора в периоды низкой пиковой нагрузки.

Личный публичный аккаунт

  • Если вы хороший ребенок, который любит учиться, вы можете подписаться на мой официальный аккаунт, чтобы вместе учиться и обсуждать.
  • Если вы считаете, что в этой статье есть какие-либо неточности, вы можете прокомментировать или подписаться на мой официальный аккаунт, пообщаться со мной в частном порядке, и все смогут учиться и прогрессировать вместе.
  • адрес гитхаба:GitHub.com/Я бы хотел 123/Java…