Утечка памяти Elasticsearch, вызванная массовым исключением

Java задняя часть Elasticsearch Eclipse

декларация героини

Эксплуатация и обслуживание онлайн ЭСкластерКогда я случайно столкнулся с проблемой утечки памяти, я увидел это при устранении проблемы.статья, четко и понятно, так что поделись со всеми,надеятьсяЧтобы дать вам некоторые идеи для устранения неполадок.

Эта статья воспроизведена из Цзяньшу. В случае проблем с авторскими правами, пожалуйста, свяжитесь с редактором вовремя.

PS: богатые передовые технологии и разнообразные формы выражения — все это в «HULK First-line Technology Talks», обратите внимание!

Введение

Позавчера онлайн-кластер ElasticSearch в отделе отпусков компании подал сигнал тревоги, а использование кучи с узлами данных продолжало превышать порог предупреждения 80%. Получив тревожное письмо по электронной почте, не смейте пренебрегать им и немедленно авторизуйтесь в системе мониторинга, чтобы проверить состояние кластера. К счастью, все узлы работают нормально, но использование кучи двумя узлами очень велико. В это время старый GC постоянно запускался, но память не может быть восстановлена.

Bulk 异常引发的 Elasticsearch 内存泄漏

Устранение неполадок

Размер кучи проблемного узла составляет 30 ГБ, а коэффициент использования 80% составляет около 24 ГБ. но кластерданныеИтого сумма не большая, все 5 узловпоказательОбъединенный диск, занятый файламикосмосЧуть меньше 10 Гб.

Bulk 异常引发的 Elasticsearch 内存泄漏

Просмотр памяти сегментов иcacheВместительность тоже очень маленькая, на уровне МБ.

Bulk 异常引发的 Elasticsearch 内存泄漏

сгруппированныйQPSВсего около 30 или около того, потребление ЦП составляет менее 10%, а активность различных пулов потоковнитьЦифры тоже очень низкие.

Bulk 异常引发的 Elasticsearch 内存泄漏

Очень озадачивает, что занимает более 20 Гб памяти и не освобождается?

Рассматриваемая версия кластера ES — 5.3.2, и стабильность этой версии давно существует в компании.времяТест, как стабильная версия, был широко развернут в сети. Некоторые другие кластеры с очень высокими нагрузками на чтение и запись не сталкивались с подобными ситуациями и, кажется, столкнулись с новыми проблемами.

Глядя в лог проблемной ноды ES, кроме некоторых исключений Bulk, других очевидных ошибок, связанных с ресурсами, нет:

[2017-11-06T16:33:15,668][DEBUG][o.e.a.b.TransportShardBulkAction] [] [suggest-3][0] failed to execute bulk item (update) BulkShardRequest [[suggest-3][0]] containing [44204 ] requests org.elasticsearch.index.engine.DocumentMissingException: [тип] [Á∫≥ʆºÂ∞îÊûúÂæ∑_1198]: документ отсутствует в org.elasticsearch.action.update.UpdateHelper.prepare(UpdateHelper.java:92) ~[elasticsearch-5.3.2.jar:5.3.2] at org.elasticsearch.action.update.UpdateHelper.prepare(UpdateHelper.java:81) ~[elasticsearch-5.3.2.jar:5.3.2]

Причина подтверждения этих исключений с пользователем заключается в том, что писатель получит данные из источника данных, согласно doc_idОбновите данные в ES. Могут быть случаи, когда какой-то doc_id не существует в ES, но это не влияет на бизнес-логику, поэтому исключение отсутствия документа, записанное ES, следует игнорировать.

Пока нет другого пути, кроме какJVMСделайте анализ дампа.

Анализ дампа кучи

Используемый инструмент Eclipсе МАТ, отсюдаскачатьВерсия для Mac: Загрузки. Для использования этого инструмента необходимо выполнить следующие 2 шага:

  1. Получить файл двоичного дампа головы jmap-dump:format=b,file=/tmp/es_heap.bin где pid — ES JAVAпроцессномер процесса.

  2. Загрузите сгенерированный файл дампа на локальныйразвиватьмашина, запусти ВСУ, с ее GUIоткрыть файл.

Следует отметить, что сам MAT также является приложением JAVA, для которого требуется поддержка операционной среды JDK.

Когда MAT попадает в файл дампа в первый раз, ему необходимоРазобрать, создать несколько индексов. Этот процесс потребляет ЦП и память, но после его завершения открытие файла дампа происходит очень быстро и потребляет очень мало. Для такого большого файла больше 20 ГБ процесс парсинга в первый раз будет очень медленным, и есть вероятность переполнения памяти из-за нехватки памяти на машине разработки. Поэтому я нашел память NTUсерверЧтобы выполнить первую работу по разбору:

1. будетlinuxСкопируйте версию MAT, распакуйте ее и измените.настроитьФайл MemoryAnalyzer.ini, установите память около 20 ГБ:

Bulk 异常引发的 Elasticsearch 内存泄漏

Это гарантирует, что в процессе синтаксического анализа не произойдет переполнения памяти.

2. Скопируйте файл дампа и выполните следующие команды для создания индекса и 3 аналитических отчетов:

mat/ParseHeapDump.sh es_heap.bin org.eclipse.mat.api:suspects

mat/ParseHeapDump.sh es_heap.bin org.eclipse.mat.api:overview

mat/ParseHeapDump.sh es_heap.bin org.eclipse.mat.api:top_components

После успешного анализа создается набор индексных файлов (.index) и аналитических отчетов (.zip)

Bulk 异常引发的 Elasticsearch 内存泄漏

Упакуйте и загрузите эти файлы на локальный компьютер, откройте их с помощью графического интерфейса MAT и проанализируйте их.

Откройте файл дампа в MAT вовремя, вы можете выбрать открытие, сгенерировавшее хороший отчет, например, подозрения на утечку:

Bulk 异常引发的 Elasticsearch 内存泄漏

Через Leak Suspects с первого взгляда видно, что эти более 20 ГБ памяти в основном используются кучей объемных потоков.примерЗанято, каждый экземпляр занимает почти 1,5 ГБ памяти.

Bulk 异常引发的 Elasticsearch 内存泄漏

введите "сделатьminator_tree», отсортированные по «Retained Heap», вы можете видеть, что использование памяти несколькими массовыми потоками очень велико.

Bulk 异常引发的 Elasticsearch 内存泄漏

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

Bulk 异常引发的 Elasticsearch 内存泄漏

Эта ссылочная связь интерпретируется следующим образом:

  1. Mul log4j сохраняется в локальной карте потока этого массового потока.tabобъект leLogEvent.

  2. Объект MutablelogEvent ссылается на объект ParameterizedMessage журнала log4j.

  3. ParameterizedMessage относится к объекту bulkShardRequest.

  4. bulkShardRequest ссылается более чем на 40 000 объектов BulkitemRequest.

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

Подумайте о журнале ES, есть некоторые массовые исключения с отсутствующим документом, угадайте, есть ли утечка при записи этих исключений.

Повторение проблемы

Чтобы проверить догадку, я запустил один узел 5.3.2 на локальной машине разработки.контрольная работаКластер, используйте массовый API для выполнения пакетных обновлений и намеренно установите несуществующий doc_id для одного из запросов на обновление.

Для облегчения тестирования я добавил в конфигурационный файл ES elasticsearch.yml пункт обработчиков: 1. Этот элемент конфигурации влияет на конфигурацию кластерного thread_pool.Размер массового пула потоков будет уменьшен до 1, чтобы различные проверки можно было выполнять быстрее и удобнее.

Запускаем кластер, делаем дамп сразу после отправки массового запроса, повторяем предыдущий процесс анализа, и проблема воспроизводится.

В настоящее время думаете, что другие исключения BULK также вызовут ту же проблему, например, запись данных и сопоставление не совпадают? Тестирование, проблема все еще генерируется. Затем используйте другой объемный размер, чтобы проверить этот объем памяти, который не может быть переработан, в зависимости от последнего выброшенного размера массового объема. На этом этапе в принципе можно определить, как утечки памяти связаны с аномальными сообщениями записи журнала 4J.

Для того, чтобы узнать, является ли эта проблема уникальной для 5.3.2, и исправлена ​​ли она в последующих версиях, такой же тест был проделан на последней 5.6.3, проблема все та же, так что это должно быть глубокое ошибка, которая еще не найдена.

читатьисходный кодПроверьте основную причину

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

В строке 209 TransportShardBulkAction найдите исключение в журнале ES.кодФрагмент.

if (isConflictException(failure)) {

logger.trace((Supplier<?>) () -> new ParameterizedMessage("{} failed to execute bulk item ({}) {}",

request.shardId(), docWriteRequest.opType().getLowercase(), request), failure);

} else {

logger.debug((Supplier<?>) () -> new ParameterizedMessage("{} failed to execute bulk item ({}) {}",

request.shardId(), docWriteRequest.opType().getLowercase(), request), failure);

}

Здесь мы видим, что в процессе инстанцирования ParameterizedMessage запрос используется какпараметрвходящий. Запрос здесь представляет собой объект BulkShardRequest, в котором хранится пакет запросов массовых элементов для записи в сегмент. Таким образом, чем больше запросов пишет пакет, тем больше памяти сохраняет объект. Но вопрос в том, почему эта ссылка не освобождается после вызова logger.debug()?

Путем тщательного сравнения с деревом доминаторов на предыдущем MAT мы видим, что ParameterizedMessage не может быть выпущен, потому что на него ссылается MutableLogEvent, и это MutableLogEvent хранится как локальный поток. Поскольку пул потоков ES Bulk имеет фиксированный размер, то есть он создается заранее и не будет уничтожен и создан заново. Затем эти объекты MutableLogEvent являются локальными для потока, пока поток не будет уничтожен, экземпляр потока всегда будет существовать глобально и всегда будет ссылаться на последнее обработанное ParameterizedMessage. Следовательно, в случае относительно большого запроса, такого как массовое исключение, записанное ES, локальная переменная потока строго ссылается на весь объект запроса и не может быть освобожден, что приводит к большому количеству утечек памяти.

Продолжайте копаться в исходном коде log4j и обнаружите, что MutableLogEvent находится в org.apache.logging.log4j.core.impl.ReusableLogEventFactory создается как локальный поток.

public class ReusableLogEventFactory implements LogEventFactory { private static finalThreadNameCachingStrategy THREAD_NAME_CACHING_STRATEGY = ThreadNameCachingStrategy.create(); private static final Clock CLOCK = ClockFactory.getClock(); private static ThreadLocal<MutableLogEvent> mutableLogEventThreadLocal = new ThreadLocal<>();

И org.apache.logging.log4j.core.config.LoggerConfig решает, какую LogEventFactory использовать в соответствии со значением константы ENABLE_THREADLOCALS.

if (LOG_EVENT_FACTORY == null) {

LOG_EVENT_FACTORY = Constants.ENABLE_THREADLOCALS

? new ReusableLogEventFactory()

: new DefaultLogEventFactory();}

Продолжайте копать глубже, смотрите в org.apache.logging.log4j.util.Constants, log4j будет судить, является ли это веб-приложением в соответствии с работающей средой, если нет, то на основе системных параметров.log4j2.enable.threadlocals считывает эту константу, если она не установлена, значение по умолчанию равно true.

public static final boolean ENABLE_THREADLOCALS = !IS_WEB_APP && PropertiesUtil.getProperties().getBooleanProperty( "log4j2.enable.threadlocals", true);

Поскольку ЕС не являетсяwebApplication, log4j выбирает использование ReusableLogEventFactory, поэтому thread_local используется для создания объекта MutableLogEvent, и, наконец, в специальном сценарии массового исключения записи ES возникает очень значительная утечка памяти.

Другой вопрос, почему log4j создает logevent как локальный поток? Я пошел на официальный сайт log4j, чтобы забрать документацию, здесь Garbage-free Steady StateLoggingНашел разумное объяснение. Оказалось, что для того, чтобы уменьшить количество повторно создаваемых объектов в процессе логирования, нужно уменьшить GCдавлениеЧтобы повысить производительность, log4j использует thread_local во многих местах для повторного использования переменных. Но использование локальных полей потока для загрузки классов, отличных от JDK, может привести к утечке памяти, особенно для веб-приложений. Таким образом, работающая среда будет оцениваться при запуске, а переменные локального типа потока будут отключены для веб-приложений.

"

ThreadLocal fields holding non-JDK classes can cause memory leaks in web applications, когда пул потоков сервера приложений продолжает ссылаться на эти поля после того, как веб-приложение не развернуто.Чтобы избежать утечек памяти, Log4j не будет использовать эти ThreadLocals, когда обнаружит, что они используются в веб-приложении (когда javax.servlet.Servlet class is in the classpath, or when system property log4j2.is.webapp is set to "true").

После обращения к приведенному выше документу я также нашел способ избежать этой проблемы для ES: в файле конфигурации JVM jvm.options ES добавьте системную переменную log4j - Dlog4j2.enable.threadlocals=false и отключите локальный поток. После тестирования этот параметр может эффективно избежать этой проблемы с утечкой памяти.

Эта проблема также была отправлена ​​​​на Github, и соответствующая ссылка: Утечка памяти при частичном сбое TransportShardBulkAction.

https://github.com/elastic/elasticsearch/issues/27300

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