Java服务端netty堆外内存泄漏问题
内存泄漏往往是比较少见,但是出现了又非常麻烦的问题。这里通过2次线上事故经验总结,希望给后来人一些经验分享。
先说结论:
不要手撸netty
,遇事先怀疑是不是手撸了netty
两次生产环境事故
- 上传文件导致的泄漏
由于平台对性能要求比较高,压测需要达到1w以上的并发,普通spring boot tomcat
容器无法达到这种效果,于是我们在spring boot
套了一层netty
(新版已经支持netty
),用来提高网络连接层并发度。
Object result = ReflectionUtils.invokeMethod(method, bean, paramObjs);
复制代码
这样普通http可以正常请求了,但是有个问题,上传文件需要netty特殊处理,于是手撸了个:
HttpPostRequestDecoder decoder = new HttpPostRequestDecoder(factory, request);
try {
Map<String, String> attrs = Maps.newHashMap();
while (decoder.hasNext()) {
InterfaceHttpData data = decoder.next();
try {
switch (data.getHttpDataType()) {
case FileUpload:
FileUpload fileUpload = (FileUpload) data;
if (fileUpload.isCompleted()) {
File file = new File("somedir", fileUpload.getFilename());
fileUpload.renameTo(file);
files.add(file);
}
break;
case Attribute:
Attribute attribute = (Attribute) data;
attrs.put(attribute.getName(), attribute.getValue());
break;
default:
break;
}
} catch (IOException e) {
e.printStackTrace();
} finally {
decoder.removeHttpDataFromClean(data);
data.release();
}
}
} finally {
decoder.cleanFiles();
decoder.destroy();
}
复制代码
jmeter压力测试结果如下,轻松支持到28000并发。
于是上线到生产环境,过了一段时间以后发现内存只升不降,于是各种内存分析工具轮番上阵,发现堆栈内存都正常,无果。
后来有用户反映上传图片的时候会失败,最后排查日志,发现很多时候指向堆外内存不足的问题,于是调大内存,问题依旧。最后怀疑是上传图片问题,于是将上传文件功能从应用层移除,直接改成nginx upload
模块,再上线观察,堆外内存问题解决。
spring-cloud-gateway
内存泄漏
这是另外一个平台,统一采用spring-cloud
技术架构,上线后长时间运行后也出现了内存缓慢增长的问题。
根据以前经验先怀疑是开发人员改写了netty
相关逻辑,于是审查代码,发现只用到了org.springframework.cloud.gateway.filter.GlobalFilter
,并没有直接对netty
的底层相关操作。再通过jvm分析,发现堆栈内存正常,主要是堆外内存占用过大。打开内存泄漏监控:
-Dio.netty.leakDetection.level=advanced
复制代码
进行压测跟踪,发现错误日志:
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:
#2:
io.netty.buffer.AdvancedLeakAwareByteBuf.nioBuffer(AdvancedLeakAwareByteBuf.java:712)
org.springframework.cloud.gateway.filter.NettyWriteResponseFilter.wrap(NettyWriteResponseFilter.java:115)
org.springframework.cloud.gateway.filter.NettyWriteResponseFilter.lambda$null$1(NettyWriteResponseFilter.java:87)
reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:100)
org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onNext(ScopePassingSpanSubscriber.java:90)
reactor.core.publisher.FluxPeek$PeekSubscriber.onNext(FluxPeek.java:192)
org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onNext(ScopePassingSpanSubscriber.java:90)
reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:114)
reactor.netty.channel.FluxReceive.drainReceiver(FluxReceive.java:256)
reactor.netty.channel.FluxReceive.lambda$request$1(FluxReceive.java:135)
io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java:164)
io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:472)
io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:384)
io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)
io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
java.base/java.lang.Thread.run(Thread.java:834)
复制代码
逐一查找,发现spring-cloud-gateway
官方issue里面果然有一条指向NettyWriteResponseFilter
问题
‘NettyWriteResponseFilter.wrap never releases the original pooled buffer in case of DefaultDataBufferFactory.‘
打开源代码,发现spring cloud
Hoxton.SR8
版本果然是包含netty
操作bug问题的代码,找到依赖包:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway-core</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
复制代码
为了不影响其他模块,大版本不动,只升级spring-cloud-gateway-core
版本,由2.2.5.RELEASE
升级为2.2.6.RELEASE
,查看源代码,已经修复问题。最后压测,堆外内存问题解决。
事故总结
从上面2次事故可以发现一个问题,就是其实问题是可以在测试阶段就可以发现的,只要好好设计下压测流程,测试足够长的时间,所以本质上还是管理流程上的问题。
事故副产品
凡是经历,必有收获,从排查事故可以看到堆外内存和堆栈内存问题排查方式是完全不一样的,dump
快照几乎不能发现问题。
传统堆内存结构:
但是堆外内存几乎与这个图没有任何关系,
堆外内存排查工具:
- 开启
NativeMemoryTracking
监控
#开启参数
-XX:NativeMemoryTracking=[off | summary | detail]
# 打开监控
jcmd 1 VM.native_memory baseline
# 查看实时内存
jcmd 1 VM.native_memory summary.diff scale=MB
复制代码
- 开启
io.netty.leakDetection.level
监控
#开启参数
-Dio.netty.leakDetection.level=paranoid
复制代码
JXRay
这个第三方工具可以跟踪堆外内存
- 底层内存调试工具
gperftools
,btrace
,jemalloc
,pmap
等。这类工具从操作系统层面分析内存问题,所以需要了解一些c语言相关知识。
以上是个人经验总结,欢迎大家交流指正。