故事背景
嘀嘀嘀~,生产事故,内存泄漏!
昨天下午,突然收到运维的消息,分部某系统生产环境内存泄漏了,帮忙排查一下。
排查过程
第一步,要日志
分部给到的异常日志大概是这样(鉴于公司规定禁止截图禁止拍照禁止外传任何信息,下面是我网上找到一张类似的报错):
LEAK: ByteBuf.release() was not called before it's garbage-collected. See http://netty.io/wiki/reference-counted-objects.html for more information.
Recent access records:
#1:
io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:273)
io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:253)
io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1434)
io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:965)
io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:163)
io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:646)
io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:581)
io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:498)
io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:460)
io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:884)
java.lang.Thread.run(Thread.java:748)
这一看,不得了了,ByteBuf没有释放,导致内存泄漏了。
第二步,看内存指标
既然知道了是内存泄漏,赶紧让运维看下内存使用情况,特别是堆外内存使用情况(因为用了Netty),根据运维反馈,堆内内存使用正常,堆外内存居高不下。
OK,到这里已经可以很明确地断言:堆外内存泄漏了。
此时,分两步走,一步是把gateway换成zuul压测观察,一步是内存泄漏问题排查。
第三步,要代码
让分部这个项目的负责人把代码给到我,我打开一看,傻眼了,就一个简单的Spring Cloud Gateway
项目,里面还包含了两个类,一个是AuthFilter用来做权限校验的,一个是XssFilter用来防攻击的。
Spring Cloud Gateway使用的是Netty,zuul 1.x使用的是Tomcat,本文来源于工纵耗彤哥读源码。
第四步,初步怀疑
快速扫一下各个类的代码,在XssFilter里面看到了跟ByteBuf相关的代码,但是,没有明显地ByteBuf没有释放的信息,很简单,先把这个类屏蔽掉,看看还有没有内存泄漏。
但是,怎么检测有没有内存泄漏呢?总不能把这个类删掉,在生产上跑吧。
第五步,参数及监控改造
其实,很简单,看过Netty源码的同学,应该比较清楚,Netty默认使用的是池化的直接内存
实现的ByteBuf,即PooledDirectByteBuf,所以,为了调试,首先,要把池化这个功能关闭。
直接内存,即堆外内存。
为什么要关闭池化功能?
因为池化是对内存的一种缓存,它一次分配16M内存且不会立即释放,开启池化后不便观察,除非慢慢调试。
那么,怎么关闭池化功能呢?
在Netty中,所有的ByteBuf都是通过一个叫作ByteBufAllocator
来创建的,在接口ByteBufAllocator中有一个默认的分配器,找到这个默认的分配器,再找到它创建的地方,就可以看到相关的代码了。
public interface ByteBufAllocator {
ByteBufAllocator DEFAULT = ByteBufUtil.DEFAULT_ALLOCATOR;
}
public final class ByteBufUtil {
static final ByteBufAllocator DEFAULT_ALLOCATOR;
static {
// 本文来源于工纵耗彤哥读源码
String allocType = SystemPropertyUtil.get(
"io.netty.allocator.type", PlatformDependent.isAndroid() ? "unpooled" : "pooled");
allocType = allocType.toLowerCase(Locale.US).trim();
ByteBufAllocator alloc;
if ("unpooled".equals(allocType)) {
alloc = UnpooledByteBufAllocator.DEFAULT