前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Netty笔记:直接内存OOM且进程僵死问题排查

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

原创
作者头像
皮皮熊
修改2022-04-10 15:42:29
4.4K0
修改2022-04-10 15:42:29
举报
文章被收录于专栏:大数据与实时计算

Netty 是一个异步事件驱动的网络通信层框架,用于快速开发高可用高性能的服务端网络框架与客户端程序,它极大地简化了 TCP 和 UDP 套接字服务器等网络编程。 和别人单独开发一个基于Netty的高性能Server入门netty不同,我深入了解Netty源自 数据透传Server直接内存OOM且进程僵死问题的排查。

一、问题与背景

一天自己接手的一个日志透传模块出现大量直接内存OOM的异常日志告警,且不久进程出现僵死,服务不可用。关键错误日志如下:

代码语言:java
复制
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直接内存申请机制

代码语言:java
复制
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

详细实现如下:

代码语言:java
复制
// 使用得是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
image.png

按秒打印 DIRECT_MEMORY_COUNTER 的值后发现,其大小是会上下波动。

自己的一个极端猜想被实际实验给打破:高qps数据来了之后,DIRECT_MEMORY_COUNTER 会增加到最大值,哪怕后面qps降低了也不会对应调小。

2)使用netty自带的内存泄漏检测工具

Netty使用虚引用跟踪每一个 ByteBuf(涉及到java常见面试题《强应用、软引用、虚引用、虚幻引用的区别》)。

image.png
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
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不可写,丢弃一部分数据保证服务不被击穿? 具体实现方式待调研!

image.png
image.png

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、问题与背景
  • 二、问题分析
    • 1、现场回顾
      • 1)直接内存的默认设置
      • 2)Netty直接内存申请机制
    • 2、是否存在内存泄漏?
      • 1)反射打印出堆外内存计数
      • 2)使用netty自带的内存泄漏检测工具
      • 3)SimpleChannelInboundHandler自动释放是否存在性能瓶颈
    • 3、为何出现进程僵死?
      • 4、初步结论
      • 三、引申思考:引入反压
      相关产品与服务
      检测工具
      域名服务检测工具(Detection Tools)提供了全面的智能化域名诊断,包括Whois、DNS生效等特性检测,同时提供SSL证书相关特性检测,保障您的域名和网站健康。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档