电脑故障问答网

 找回密码
 立即注册
查看: 81|回复: 1

Netty笔记:直接内存OOM且进程僵死问题排查

[复制链接]

1

主题

4

帖子

9

积分

新手上路

Rank: 1

积分
9
发表于 2022-12-19 09:08:54 | 显示全部楼层 |阅读模式
Netty笔记:直接内存OOM且进程僵死问题排查
修改于2022-04-10 15:42:29阅读 1.3K0

Netty 是一个异步事件驱动的网络通信层框架,用于快速开发高可用高性能的服务端网络框架与客户端程序,它极大地简化了 TCP 和 UDP 套接字服务器等网络编程。 和别人单独开发一个基于Netty的高性能Server入门netty不同,我深入了解Netty源自 数据透传Server直接内存OOM且进程僵死问题的排查。
一、问题与背景
一天自己接手的一个日志透传模块出现大量直接内存OOM的异常日志告警,且不久进程出现僵死,服务不可用。关键错误日志如下:
io.netty.util.internal.OutOfDirectMemoryError: failed to allocate 16777216 byte(s) of direct memory (used: 2147483648, max: 2147483648)         at io.netty.util.internal.PlatformDependent.incrementMemoryCounter(PlatformDependent.java:775)         at io.netty.util.internal.PlatformDependent.allocateDirectNoCleaner(PlatformDependent.java:730)         at io.netty.buffer.PoolArena$DirectArena.allocateDirect(PoolArena.java:645)         at io.netty.buffer.PoolArena$DirectArena.newChunk(PoolArena.java:621)         at io.netty.buffer.PoolArena.allocateNormal(PoolArena.java:204)         at io.netty.buffer.PoolArena.tcacheAllocateNormal(PoolArena.java:188)         at io.netty.buffer.PoolArena.allocate(PoolArena.java:138)         at io.netty.buffer.PoolArena.allocate(PoolArena.java:128)         at io.netty.buffer.PooledByteBufAllocator.newDirectBuffer(PooledByteBufAllocator.java:378)         at io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:187)         at io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:178)         at io.netty.channel.unix.PreferredDirectByteBufAllocator.ioBuffer(PreferredDirectByteBufAllocator.java:53)         at io.netty.channel.DefaultMaxMessagesRecvByteBufAllocator$MaxMessageHandle.allocate(DefaultMaxMessagesRecvByteBufAllocator.java:114)         at io.netty.channel.epoll.EpollRecvByteAllocatorHandle.allocate(EpollRecvByteAllocatorHandle.java:75)         at io.netty.channel.epoll.EpollDatagramChannel$EpollDatagramChannelUnsafe.epollInReady(EpollDatagramChannel.java:485)         at io.netty.channel.epoll.AbstractEpollChannel$AbstractEpollUnsafe$1.run(AbstractEpollChannel.java:388)         at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java:164)         at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:472)         at io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:387)         at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)         at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)         at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)         at java.lang.Thread.run(Thread.java:748)
复制
问题出现后,第一步就是要进行紧急定位与恢复。经过定位发现,该时刻点业务有瞬时集中上报大量数据,故直接内存OOM与其直接相关,通过JVM参数-XX:MaxDirectMemorySize=4G,翻倍直接内存大小并重启后业务恢复。
二、问题分析
1、现场回顾


  • netty版本:4.1.58.Final
  • jvm版本:1.8.0_242
  • 堆内存:2GB
之前对Netty和直接内存这块了解不是很多,于是对一些基本问题进行了深入理解。
1)直接内存的默认设置
程序在现网运行阶段,其实我们并没有设置-XX:MaxDirectMemorySize,那实际运行的直接内存为啥是2GB?
2)Netty直接内存申请机制
private static void incrementMemoryCounter(int capacity) { if (DIRECT_MEMORY_COUNTER != null) { long newUsedMemory = DIRECT_MEMORY_COUNTER.addAndGet(capacity); if (newUsedMemory > DIRECT_MEMORY_LIMIT) {             DIRECT_MEMORY_COUNTER.addAndGet(-capacity); throw new OutOfDirectMemoryError("failed to allocate " + capacity                     + " byte(s) of direct memory (used: " + (newUsedMemory - capacity) + ", max: " + DIRECT_MEMORY_LIMIT + ')'); } } }
复制
2、是否存在内存泄漏?
虽然直接内存泄漏问题的排查是极其痛苦和繁琐,但千万不要被这堆讨厌的 OOM 日志和内存泄漏问题吓到。直接内存是否够用,我们先打印出相关的指标再做分析。
1)反射打印出堆外内存计数
由上文所知,Netty的PlatformDependent类中,incrementMemoryCounter方法进行直接内存统计判断,所以我参考了美团这篇技术文章的实现方案,使用反射获取到DIRECT_MEMORY_COUNTER
详细实现如下:
// 使用得是spring的ReflectionUtils,spring yyds! Field field = ReflectionUtils.findField(PlatformDependent.class, "DIRECT_MEMORY_COUNTER"); field.setAccessible(true); directMem = (AtomicLong) field.get(PlatformDependent.class);
复制
笔者后面补充:其实可以直接通过 PlatformDependent.usedDirectMemory() 访问获取到DIRECT_MEMORY_COUNTER的值,不用反射机制。




image.png

按秒打印 DIRECT_MEMORY_COUNTER 的值后发现,其大小是会上下波动。
自己的一个极端猜想被实际实验给打破:高qps数据来了之后,DIRECT_MEMORY_COUNTER 会增加到最大值,哪怕后面qps降低了也不会对应调小。
2)使用netty自带的内存泄漏检测工具
Netty使用虚引用跟踪每一个 ByteBuf(涉及到java常见面试题《强应用、软引用、虚引用、虚幻引用的区别》)。




image.png

这类问题排查十分困难,好在netty自带了一个内存泄漏的检测工具:
jvm启动参数增加 -Dio.netty.leakDetectionLevel=[检测级别]


  • disabled 完全关闭内存泄露检测
  • simple 以约1%的抽样率检测是否泄露,默认级别
  • advanced 抽样率同simple,但显示详细的泄露报告
  • paranoid 抽样率为100%,显示报告信息同advanced
注意抽样率越高,Netty性能越低! 不过日志并未显示任何异常的报告!
3)SimpleChannelInboundHandler自动释放是否存在性能瓶颈
通过继承SimpleChannelInboundHandler定义入站消息处理,在该类会保证消息最终被自动release。
参考阅读:https://segmentfault.com/a/1190000021469481
这里我有个猜想:自动释放机制是否存在性能瓶颈。
验证方法:
基于ChannelInboundHandlerAdapter,实现相关逻辑,buf.release();, 对比SimpleChannelInboundHandler 实现的性能。
实验发现,二者都在差不多qps场景下出现oom情况。
3、为何出现进程僵死?
观察程序gc日志我们发现,存在频繁full gc的情况。




image.png

分析原理:
DirectByteBuffer(int cap)构造方法中才会初始化Cleaner对象,方法中检查当前内存是否超过允许的最大堆外内存,如果直接内存分配超出限制后,则会先尝试将不可达的Reference对象加入Reference链表中,依赖Reference的内部守护线程触发可以被回收DirectByteBuffer关联的Cleaner的run()方法
如果内存还是不足, 则执行 System.gc(),触发full gc,来回收堆内存中的DirectByteBuffer对象来触发堆外内存回收,如果还是超过限制,则抛出java.lang.OutOfMemoryError(代码位于java.nio.Bits#reserveMemory()方法)。
所以这样就导致了一个恶性循环,qps高 =》 直接内存满 =》触发full gc =》 jvm stw =》堆内存中的DirectByteBuffer对象释放慢 =》 直接内存满 =》触发full gc  =》 ……。
尝试了增加jvm参数-XX:+DisableExplicitGC,但是没有奏效。
4、初步结论
综上所述,不难发现问题根源还是在于业务瞬时qps过高,击穿了Netty,并导致了一系列恶性循环的后果。
直接解决方案:还是老办法,业务减少瞬间上报、适当加内存。
三、引申思考:引入反压
是否可以通过高水位控制,超过高水位设置Netty不可写,丢弃一部分数据保证服务不被击穿? 具体实现方式待调研!
回复

使用道具 举报

1

主题

5

帖子

10

积分

新手上路

Rank: 1

积分
10
发表于 2025-2-20 12:14:01 | 显示全部楼层
秀起来~
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

云顶设计嘉兴有限公司模板设计.

免责声明:本站上数据均为演示站数据,如购买模板可以上DISCUZ应用中心购买,欢迎惠顾.

云顶官方站点:云顶设计 模板原创设计:云顶模板   Powered by Discuz! X3.4© 2001-2017 Comsenz Inc.

快速回复 返回顶部 返回列表