В последней статье был представлен принцип модели памяти Netty.Поскольку неправильное использование Netty приведет к утечкам памяти за пределы кучи, в Интернете относительно мало информации по этому аспекту, поэтому я написал эту статью, чтобы представить точки знаний, связанные к устранению неполадок памяти Netty за пределами кучи. Диагностические инструменты и идеи по устранению неполадок для справки
Феномен
Основное явление утечек памяти вне кучи заключается в том, что объем памяти, занимаемой процессом, относительно велик (вы можете использовать команду top для его просмотра под Linux), но занятость памяти кучи Java невысока (команда jmap для просмотра) , Основываясь на соответствующих интерфейсах в java.nio для применения к памяти вне кучи, вызовов JNI и т. д., нижеследующее посвящено устранению неполадок утечек памяти Netty вне кучи.
Базовая реализация освобождения памяти вне кучи
1 выпуск памяти вне кучи java.nio
Память вне кучи Netty основана на объекте DirectByteBuffer нативного java.nio, поэтому сначала необходимо понять принцип его освобождения.
DirectByteBuffer, предоставляемый java.nio, предоставляет метод clean() класса sun.misc.Cleaner.Когда выполняется системный вызов для освобождения памяти вне кучи, есть две ситуации, в которых запускается метод clean().
- (1) Приложение активно вызывает
ByteBuffer buf = ByteBuffer.allocateDirect(1);
((DirectBuffer) byteBuffer).cleaner().clean();
- (2) Переработка на основе ГХ
Класс Cleaner наследует java.lang.ref.Reference.Поток GC установит внутреннюю переменную Reference (ожидающая переменная является головным узлом связанного списка, а обнаруженная переменная — следующим узлом связанного списка), а недостижимая Справочный объект, который может быть переработан, будет заменен организованным в виде связанного списка.
Внутренний поток демона Reference потребляет данные из заголовка связанного списка.Если потребляемый объект Reference также имеет тип Cleaner, поток вызовет метод clean() (Reference#tryHandlePending())
2 Политика Netty noCleaner
Перед введением стратегии noCleaner нужно понять, что делает DirectByteBuffer с объектом Cleaner при его инициализации:
Объект Cleaner инициализируется только в конструкторе DirectByteBuffer(int cap), который проверяет, превышает ли текущая память максимально допустимую память вне кучи (настраивается параметром -XX:MaxDirectMemorySize).
Если он превышает, он сначала попытается добавить недостижимый объект Reference в список Reference, а внутренний поток демона, который зависит от Reference, запускает метод run() Cleaner, связанный с DirectByteBuffer, который можно переработать.
Если память все еще недостаточна, выполните System.gc (), вызвать полный GC, чтобы вернуть объект DirectbyteBuffer в памяти кучи, чтобы вызвать восстановление памяти Off-кучи, если он все еще превышает предел, бросить Java.lang.OutofmemoryError ( Код находится в Java. NIO.BITS # Бреметипа () Метод)
Netty представила стратегию noCleaner в версии 4.1: создание объекта DirectByteBuffer без Cleaner. Преимущество этого заключается в том, чтобы обойти DirectByteBuffer с помощью Cleaner и выполнить некоторые дополнительные накладные расходы в методе clean() Cleaner. Когда памяти вне кучи недостаточно Когда, System.gc() не будет запускаться, что повышает производительность.
Основные различия между DirectByteBuffer hasCleaner и DirectByteBuffer noCleaner заключаются в следующем:
-
Конструкторы бывают разные: Объект noCleaner: создан путем отражения, вызывающего частный DirectByteBuffer (длинный адрес, кепка int) Объект hasCleaner: создан новым DirectByteBuffer (int cap)
-
Различные способы освобождения памяти объект noCleaner: используйте UnSafe.freeMemory(адрес); Объект hasCleaner: метод clean() с использованием DirectByteBuffer Cleaner
**примечание: **Unsafe — это класс, расположенный в пакете sun.misc, который может предоставлять локальные методы, такие как операции с памятью, операции с объектами, планирование потоков и т. д. Эти методы играют роль в повышении эффективности работы Java и расширении возможность работы с базовыми ресурсами языка Java.Большой эффект, но неправильное использование класса Unsafe повысит вероятность программных ошибок, и программа перестанет быть "безопасной", поэтому она официально объявлена устаревшей и может быть удалена в будущие версии jdk
Netty необходимо оценить, разрешают ли текущая среда и параметры конфигурации среды политику noCleaner (конкретная логика находится в статическом блоке кода PlatformDependent) при ее запуске.Например, при работе под Android класс Unsafe отсутствует, а Политика noCleaner не разрешена. Если не разрешена, используйте стратегию hasCleaner
Заметка:Вы можете вызвать метод PlatformDependent.useDirectBufferNoCleaner(), чтобы узнать, использует ли текущая программа Netty стратегию noCleaner.
Прочитав это, некоторые читатели могут спросить, если Netty запускает Cleaner.clean() через GC на основе стратегии hasCleaner и автоматически освобождает память вне кучи, можно ли игнорировать вызов метода ByteBuf.release(), а утечек памяти не будет?
Конечно, нет. С одной стороны, причина в том, что автоматический запуск не в реальном времени: объект ByteBuffer должен быть возвращен потоком GC для запуска. ждать, пока частота отправки не станет низкой, прежде чем сработает старая сборка мусора.
С другой стороны, Netty необходимо выполнять другие операции на основе метода ByteBuf.release(), такие как освобождение объединенной памяти обратно в пул памяти, иначе объект всегда будет помечен как используемый пулом памяти.
Механизм триггера ByteBuf.release()
В отрасли существует неправильное понимание того, что ByteBuf, выделенный фреймворком Netty, будет выпущен фреймворком автоматически, и бизнесу не нужно его выпускать; ByteBuf, созданный бизнесом, должен быть выпущен сам по себе, а фреймворк Netty не выпустит.
Этому недоразумению есть причина: в некоторых сценариях фреймворк Netty вызывает метод ByteBuf.release():
1 Обработка входящих сообщений
При обработке входящих сообщений Netty создаст ByteBuf для чтения сообщений в канале и инициирует вызов ChannelHandler в конвейере.Определенный приложением ChannelHandler, использующий ByteBuf, должен отвечать за release()
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf buf = (ByteBuf) msg;
try {
...
} finally {
buf.release();
}
}
Если ByteBuf не обрабатывается текущим ChannelHandler, он передается следующему обработчику в конвейере:
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf buf = (ByteBuf) msg;
...
ctx.fireChannelRead(buf);
}
Обычно используется мы будем передавать наследованиеChannelInboundHandlerAdapterОпределить обработчик для обработки входящих сообщений.В этом случае, если обработчики всех программ не вызывают метод release(),Входящее сообщение Netty не будет выпускать() в конце, что вызовет утечку памяти.;
Когда в обработчике конвейера возникает исключение, платформа Netty, наконец, перехватывает исключение и выполняет ByteBuf.release(); Весь процесс находится в AbstractNioByteChannel.NioByteUnsafe#read(), а ключевые фрагменты извлечены ниже:
try {
do {
byteBuf = allocHandle.allocate(allocator);
allocHandle.lastBytesRead(doReadBytes(byteBuf));
// 入站消息已读完
if (allocHandle.lastBytesRead() <= 0) {
// ...
break;
}
// 触发pipline上handler进行处理
pipeline.fireChannelRead(byteBuf);
byteBuf = null;
} while (allocHandle.continueReading());
// ...
} catch (Throwable t) {
// 异常处理中包括调用 byteBuf.release()
handleReadException(pipeline, byteBuf, t, close, allocHandle);
}
Тем не менее, он также обычно используется через наследованиеSimpleChannelInboundHandlerОпределите обработку входящих сообщений, при которой сообщение будет гарантированно в конечном итоге выпущено:
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
boolean release = true;
try {
// 该消息由当前handler处理
if (acceptInboundMessage(msg)) {
I imsg = (I) msg;
channelRead0(ctx, imsg);
} else {
// 不由当前handler处理,传递给pipeline上下一个handler
release = false;
ctx.fireChannelRead(msg);
}
} finally {
// 触发release
if (autoRelease && release) {
ReferenceCountUtil.release(msg);
}
}
}
2 Обработка исходящих сообщений
В отличие от входящих сообщений, которые автоматически создаются инфраструктурой Netty, исходящие сообщения обычно создаются приложением, а затем вызывают метод write() на основе канала или метод writeAndFlush(), который отвечает за вызов метода release() входящего byteBuf. внутренне) метод
Заметка:Метод write() имеет проблему в версиях до netty-4.0.0.CR2 и не вызывает ByteBuf.release().
3 Примечания к выпуску()
- (1) Подсчет ссылок
Существует также распространенное заблуждение, что пока вызывается метод release() класса ByteBuf или метод ReferenceCountUtil.release(), память объекта гарантированно освобождается, но не освобождается.
Поскольку счетчик ссылок Netty ByteBuf управляет жизненным циклом объектов ByteBuf, ByteBuf наследует интерфейс ReferenceCounted и предоставляет методы keep() и release() для увеличения или уменьшения значения счетчика ссылок.Когда вызывается метод release(), значение внутреннего счетчика уменьшается до 0, чтобы инициировать действие освобождения памяти.
- (2) derived ByteBuf
производное, производное значение, в методах ByteBuf.duplicate(), ByteBuf.slice() и ByteBuf.order(ByteOrder) будет создан производный ByteBuf, созданный ByteBuf и исходный ByteBuf совместно используют счетчик ссылок, исходный ByteBuf Release() вызов метода также приведет к восстановлению этих объектов.
Вместо того, чтобы метод ByteBuf.copy() и ByteBuf.readBytes (int) создавать из объектов, не являющихся производными ByteBuf, эти объекты не используются совместно с исходным подсчетом ссылок ByteBuf,Исходный вызов метода release() ByteBuf не приведет к возврату этих объектов.
Параметры управления размером памяти вне кучи
Параметры для настройки размера памяти вне кучи: -XX:MaxDirectMemorySize и -Dio.netty.maxDirectMemory В чем разница между этими двумя параметрами?
- -XX:MaxDirectMemorySizeИспользуется для ограничения NettyDirectByteBuffer для стратегии hasCleanerРазмер памяти вне кучи. Значение по умолчанию — это максимальная память, которую JVM может выделить из операционной системы. Если сама память не ограничена, значение — Long.MAX_VALUE байт (значение по умолчанию возвращает Runtime. getRuntime().maxMemory()), код находится в методе java.nio.Bits#reserveMemory().
Заметка:-XX: MaxDirectMemorySize не может ограничиваться в NettyDirectByteBuffer для стратегии noCleanerразмер памяти вне кучи
- -Dio.netty.maxDirectMemoryиспользуется для ограниченияDirectByteBuffer Netty в соответствии с политикой noCleanerРазмер максимально выделяемой вне кучи памяти, если значение равно 0, используется стратегия hasCleaner, код находится в методе PlatformDependent#incrementMemoryCounter()
Внешний монитор куча памяти
Как я могу использовать стек памяти?
1 инструмент кода
- (1) DirectByteBuffer мониторинг hasCleanerДля DirectByteBuffer стратегии hasCleaner класс java.nio.Bits записывает использование памяти вне кучи, но этот класс имеет права доступа на уровне пакета и не может быть получен напрямую, его можно получить через MXBean
** примечание: ** MXBean, ряд специальных bean-компонентов, предоставляемых Java для мониторинга статистики С помощью различных типов MXBean вы можете получать индикаторы мониторинга, такие как информация о загрузке памяти, потоков и классов процесса JVM.
List<BufferPoolMXBean> bufferPoolMXBeans = ManagementFactoryHelper.getBufferPoolMXBeans();
BufferPoolMXBean directBufferMXBean = bufferPoolMXBeans.get(0);
// hasCleaner的DirectBuffer的数量
long count = directBufferMXBean.getCount();
// hasCleaner的DirectBuffer的堆外内存占用大小,单位字节
long memoryUsed = directBufferMXBean.getMemoryUsed();
Заметка:MappedByteBuffer: это еще один байтовый буфер памяти вне кучи, полученный с помощью отображения памяти mmap (реализация нулевого копирования) на основе FileChannelImpl.map.Память вне кучи можно получить с помощью ManagementFactoryHelper.getBufferPoolMXBeans().get(1). индикаторы
- (2) DirectByteBuffer мониторинг noCleanerМониторинг DirectByteBuffer для noCleaner в Netty относительно прост, и к нему можно получить прямой доступ через PlatformDependent.usedDirectMemory().
2 Netty поставляется с инструментом обнаружения утечек памяти
Netty также поставляется с инструментом обнаружения утечек памяти, который можно использовать для обнаруженияОбъекты ByteBuf освобождаются сборщиком мусора, но память, управляемая ByteBuf, не освобождается.В случае объектов ByteBuf, которые не были восстановлены сборщиком мусора из-за утечек памяти, таких как очередь задач
Чтобы облегчить пользователю обнаружение раскрытия памяти, Netty предоставляет четыре уровня обнаружения:
- disabled полностью отключает обнаружение утечки памяти
- простой обнаруживает утечки с частотой дискретизации около 1%, уровень по умолчанию
- расширенный имеет ту же частоту дискретизации, что и простой, но отображает подробный отчет об утечке
- Параноидальная частота дискретизации составляет 100%, а отображаемая информация отчета такая же, как и в расширенном режиме.
Способ его использования - установить параметр командной строки:
-Dio.netty.leakDetectionLevel=[检测级别]
Пример программы выглядит следующим образом, установив уровень обнаружения на параноидальный:
// -Dio.netty.leakDetectionLevel=paranoid
public static void main(String[] args) {
for (int i = 0; i < 500000; ++i) {
ByteBuf byteBuf = UnpooledByteBufAllocator.DEFAULT.buffer(1024);
byteBuf = null;
}
System.gc();
}
Вы можете увидеть отпуску вывода консоли:
十二月 27, 2019 8:37:04 上午 io.netty.util.ResourceLeakDetector reportTracedLeak
严重: LEAK: ByteBuf.release() was not called before it's garbage-collected. See https://netty.io/wiki/reference-counted-objects.html for more information.
Recent access records:
Created at:
io.netty.buffer.UnpooledByteBufAllocator.newDirectBuffer(UnpooledByteBufAllocator.java:96)
io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:187)
io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:178)
io.netty.buffer.AbstractByteBufAllocator.buffer(AbstractByteBufAllocator.java:115)
org.caison.netty.demo.memory.BufferLeaksDemo.main(BufferLeaksDemo.java:15)
Принцип утечки памяти заключается в использовании слабых ссылок.Слабая ссылка (WeakReference) должна указывать очередь ссылок (refQueue) при создании слабой ссылки, обертывая объект ByteBuf слабой ссылкой (запись кода находится в AbstractByteBufAllocator# метод toLeakAwareBuffer())
Когда происходит GC, если поток GC обнаруживает, что объект ByteBuf связан только с объектом слабой ссылки, он добавит WeakReference в refQueue; Когда память ByteBuf освобождается нормально, будет вызван метод clear() WeakReference для освобождения ссылки на ByteBuf, и последующие потоки GC не будут добавлять WeakReference в refQueue;
Каждый раз, когда Netty создает ByteBuf, основываясь на частоте дискретизации, она будет опрашивать (опрашивать) объект WeakReference в refQueue, когда происходит выборка, а ByteBuf, связанный с ненулевым WeakReference, возвращаемым опросом, представляет собой утечку вне кучи. память (запись кода находится в методе ResourceLeakDetector #track())
3 графических инструмента
На основе кода для получения памяти вне кучи ее можно обнаружить и получить, настроив доступ к некоторым инструментам мониторинга, а также можно рисовать графики, такие как более популярные Prometheus или Zabbix
Его также можно получить через Visualvm, который поставляется с jdk.Необходимо установить подключаемый модуль Buffer Pools.Основной принцип заключается в доступе к индикаторам мониторинга в MXBean, и можно получить только использование DirectByteBuffer hasCleaner.
Кроме того, куча памяти, выделенная для сгенерированных внешних вызовов JNI, может использоваться для мониторинга google-perftools.
Диагностика утечек памяти вне кучи
Существует много конкретных причин утечек памяти вне кучи: сначала вводится мониторинг накопления очереди задач, а затем вводятся общие идеи диагностики утечек памяти вне кучи.
1 Очередь задач накапливается
Очередь задач здесь — это Queue taskQueue в значении NioEventLoop, а сценарии, отправленные в очередь задач, следующие:
- (1) Определяемые пользователем общие задачи
ctx.channel().eventLoop().execute(runnable);
- (2) Написать в канал
channel.write(...)
channel.writeAndFlush(...)
- (3) Пользовательские запланированные задачи
ctx.channel().eventLoop().schedule(runnable, 60, TimeUnit.SECONDS);
Когда в очереди слишком много невыполненных задач, сообщения не могут быть записаны в канал, а затем выпущены, что приведет к утечкам памяти
Идея диагностики состоит в том, чтобы отслеживать количество задач в очереди задач, размер невыполненной работы ByteBuf и информацию о классе задач.Конкретная программа мониторинга выглядит следующим образом (кодовый адрес)GitHub.com/Это ты/Это...):
public void channelActive(ChannelHandlerContext ctx) throws NoSuchFieldException, IllegalAccessException {
monitorPendingTaskCount(ctx);
monitorQueueFirstTask(ctx);
monitorOutboundBufSize(ctx);
}
/** 监控任务队列堆积任务数,任务队列中的任务包括io读写任务,业务程序提交任务 */
public void monitorPendingTaskCount(ChannelHandlerContext ctx) {
int totalPendingSize = 0;
for (EventExecutor eventExecutor : ctx.executor().parent()) {
SingleThreadEventExecutor executor = (SingleThreadEventExecutor) eventExecutor;
// 注意,Netty4.1.29以下版本本pendingTasks()方法存在bug,导致线程阻塞问题
// 参考 https://github.com/netty/netty/issues/8196
totalPendingSize += executor.pendingTasks();
}
System.out.println("任务队列中总任务数 = " + totalPendingSize);
}
/** 监控各个堆积的任务队列中第一个任务的类信息 */
public void monitorQueueFirstTask(ChannelHandlerContext ctx) throws NoSuchFieldException, IllegalAccessException {
Field singleThreadField = SingleThreadEventExecutor.class.getDeclaredField("taskQueue");
singleThreadField.setAccessible(true);
for (EventExecutor eventExecutor : ctx.executor().parent()) {
SingleThreadEventExecutor executor = (SingleThreadEventExecutor) eventExecutor;
Runnable task = ((Queue<Runnable>) singleThreadField.get(executor)).peek();
if (null != task) {
System.out.println("任务队列中第一个任务信息:" + task.getClass().getName());
}
}
}
/** 监控出站消息的队列积压的byteBuf大小 */
public void monitorOutboundBufSize(ChannelHandlerContext ctx) {
long outBoundBufSize = ((NioSocketChannel) ctx.channel()).unsafe().outboundBuffer().totalPendingWriteBytes();
System.out.println("出站消息队列中积压的buf大小" + outBoundBufSize);
}
- note:Вышеупомянутая программа должна быть основана на Netty 4.1.29, по крайней мере, прежде чем ее можно будет использовать, иначе возникнут проблемы с производительностью.
Как следует обрабатывать трудоемкий код бизнес-логики при реальном развитии бизнеса на основе Netty?
Заключение и рекомендацииНастройте новый набор пулов бизнес-потоков и отправляйте трудоемкие задачи в пул бизнес-потоков.
Рабочий поток Netty (NioEventLoop), помимо обработки данных соединения, считываемых как поток NIO, выполняющий логику каналаHandler в конвейере, также потребляет задачи, отправленные в taskQueue, включая операцию записи канала.
Если трудоемкая задача отправляется в очередь задач, это также повлияет на обработку потока NIO и задач в очереди задач, поэтому рекомендуется изолировать обработку в отдельном пуле бизнес-потоков.
2 Общие диагностические идеи
Существуют различные причины утечек памяти за пределами кучи Netty.Например, код пропускает запись для вызова release(); значение счетчика ссылок ByteBuf увеличивается с помощью функции сохранения(), но значение счетчика ссылок не очищается при вызове release(). call; release(); Эталонный объект ByteBuf был предварительно проверен сборщиком мусора, и связанная с ним память вне кучи не была освобождена и т. д. Здесь невозможно перечислить их все, поэтому постарайтесь предоставить набор общих диагностических идей для справки
В первую очередь проблему нужно воспроизвести, чтобы не повлиять на работу онлайн-сервиса, попробуйте смоделировать в тестовой среде или локальной среде. Однако в этих средах обычно не так много параллелизма, как в сети, и вы можете имитировать запросы с помощью инструментов стресс-тестирования.
Для некоторых сценариев, которые невозможно смоделировать, реальный онлайн-трафик можно скопировать в тестовую среду с помощью инструмента копирования трафика Linux, не влияя на онлайн-бизнес.Подобные инструменты включают Gor, tcpreplay, tcpcopy и т. д.
После того, как это может быть воспроизведено, следующим шагом будет обнаружение проблемы.Во-первых, с помощью методов мониторинга и информации журнала, представленных выше, попытайтесь найти проблему напрямую; Если вы не можете найти его, вам нужно найти условия срабатывания утечки памяти вне кучи, однако иногда приложение является относительно большим, и есть много входов внешнего трафика, которые невозможно проверить по одному.
В среде, не подключенной к сети, вы можете закомментировать запись о трафике, каждый раз закомментировать ее половину, а затем запустить проверку, существует ли проблема. Если она существует, продолжайте закомментировать оставшуюся половину. , несколько испытаний могут быстро найти триггер проблемы
После обнаружения условия срабатывания проверьте логику обработки условия срабатывания в программе.Если программа обработки очень сложна и ее нельзя увидеть напрямую, вы можете продолжить закомментировать часть кода и выполнить дихотомию, пока, наконец, не найдете конкретную проблему. кодовый блок.
Суть всей идеи в том, чтоПовторение проблемы, мониторинг и устранение неполадокОн также может быть использован для расследования других проблем, таких как внутренние утечки памяти, CPU 100%, вешающий процесс обслуживания и т. Д.
Суммировать
Вся статья посвящена представлению знаний и теорий, и в ней отсутствуют практические ссылки. Вот несколько качественных статей в блогах:
«Праздник устранения неполадок с утечкой памяти вне кучи netty» как отладить утечку памяти вне кучи из The Flashwoo woo Краткое описание.com/afraid/4 oh 96 страховая сумма коэффициент 37…
«Меры Netty по предотвращению утечек памяти», автор авторитетного руководства Netty, обмен знаниями об утечках памяти Huawei Li Linfeng.Tickets.WeChat.QQ.com/Yes/IU is IV похожа на сказку…
Дело: «Расследование утечки памяти Mystery Track Spring Boot», американская группа поделится технической командой солдат дисциплиныTickets.WeChat.QQ.com/Yes/AY — это IH0TN3…
«Введение и борьба с Netty: имитация системы обмена мгновенными сообщениями WeChat», буклет The Flash's Nuggets (платный), я изучаю эту колонку, чтобы начать работу с Netty.Наггетс Талант/книга/684473…
Более интересно, добро пожаловать, чтобы обратить внимание на общедоступную архитектуру распределенной системы