温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

tomcat和Netty怎么解决内存泄漏问题

发布时间:2022-01-06 15:37:35 阅读:184 作者:iii 栏目:服务器
开发者测试专用服务器限时活动,0元免费领,库存有限,领完即止! 点击查看>>

这篇文章主要讲解了“tomcat和Netty怎么解决内存泄漏问题”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“tomcat和Netty怎么解决内存泄漏问题”吧!

排查过程如下:

第一步,要日志

分部给到的异常日志大概是这样(鉴于公司规定禁止截图禁止拍照禁止外传任何信息,下面是我网上找到一张类似的报错):

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用来防攻击的。

第四步,初步怀疑

快速扫一下各个类的代码,在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;             logger.debug("-Dio.netty.allocator.type: {}", allocType);         } else if ("pooled".equals(allocType)) {             alloc = PooledByteBufAllocator.DEFAULT;             logger.debug("-Dio.netty.allocator.type: {}", allocType);         } else {             alloc = PooledByteBufAllocator.DEFAULT;             logger.debug("-Dio.netty.allocator.type: pooled (unknown: {})", allocType);         }          DEFAULT_ALLOCATOR = alloc;     }  }

可以看到,是通过io.netty.allocator.type这个参数控制的。

OK,在JVM启动参数中添加上这个参数,并把它赋值为unpooled。

tomcat和Netty怎么解决内存泄漏问题

关闭了池化功能之后,还要能够实时地观测到内存是不是真的有泄漏,这要怎么做呢?

其实,这个也很简单,Netty的PlatformDependent这个类会统计所有直接内存的使用。

最近一直在研究Netty的源码,所以,我对Netty的各种细节了解地很清楚,本文来源于工纵耗彤哥读源码,最近还在准备,等后面弄完了,开始Netty专栏的创作。

所以,我们只需要写一个定时器,定时地把这个统计信息打印出来就可以了,这里,我就直接给出代码了:

@Component public class Metrics {      @PostConstruct     public void init() {         ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();         scheduledExecutorService.scheduleAtFixedRate(()->{             System.out.println("used direct memory: " + PlatformDependent.usedDirectMemory());         }, 11, TimeUnit.SECONDS);     } }

把它扔到跟启动类同级或下级的目录就可以了。

到这里,池化及监控都弄好了,下面就是调试了。

第六步,初步调试

直接运行启动类,观察日志。

used direct memory0 used direct memory0 used direct memory0 used direct memory0 used direct memory0 used direct memory0 used direct memory0

一开始,直接内存都很正常,一直是0。

随便发送一个请求,报404了,而且观察直接内存并没有变化,还是0,说明,随便模拟一个请求还不行,这直接被spring给拦截了,还没到Netty。

第七步,修改配置

随便一个请求不行, 那只能模拟正常的请求转发了,我快速启动了一个SpringBoot项目,并在里面定义了一个请求,修改gateway的配置,让它可以转发过去:

spring:   cloud:     gateway:       routes:       - idtest         uri: http://localhost:8899/test         predicates:         - Path=/test

第八步,再次调试

修改完配置,同时启动两个项目,一个gateway,一个springboot,请求发送,观察直接内存的使用情况:

used direct memory0 used direct memory0 used direct memory0 used direct memory1031 used direct memory1031 used direct memory1031

果然,内存没有释放。

第九步,删除XssFilter

为了验证前面初步怀疑的XssFilter,把它删掉,再次启动项目,发送请求,观察直接内存的使用。

used direct memory0 used direct memory0 used direct memory0 used direct memory1031 used direct memory1031 used direct memory1031

问题依然存在,而且,还是跟前面泄漏的一样大小。

这个是这样的,Netty是靠猜(guess)来决定每次内存分配的大小的,这个猜的初始值是1024。

@Override public ByteBuf allocate(ByteBufAllocator alloc) {     return alloc.ioBuffer(guess()); }

是不是没想到Netty还有这么可爱的一面^^,咳咳,跑题了,强行拉回!

然后,这里还有个7B存储的是换行符回车符啥的,这7B是不会释放的,加到一起就是1031。

private static final byte[] ZERO_CRLF_CRLF = { '0', CR, LF, CR, LF }; // 2B private static final ByteBuf CRLF_BUF = unreleasableBuffer(directBuffer(2).writeByte(CR).writeByte(LF)); // 5B private static final ByteBuf ZERO_CRLF_CRLF_BUF = unreleasableBuffer(directBuffer(ZERO_CRLF_CRLF.length)         .writeBytes(ZERO_CRLF_CRLF));

嗯,有点意思,既然不是XssFilter的问题,那么,会不会是AuthFilter的问题呢?

第十步,干掉AuthFilter

说干就干,干掉AuthFiler,重启项目,发送请求,观察直接内存:

used direct memory0 used direct memory0 used direct memory0 used direct memory1031 used direct memory1031 used direct memory1031

问题还是存在,还是熟悉的内存大小。

此时,我的思路已经不顺畅了,下面是跑偏之路。

第十一步,思考

在把XssFilter和AuthFilter相继删除之后,已经只剩下一个启动类了,当然,还有一个新加的监控类。

难道是Spring Cloud Gateway本身有问题,咦,我好像发现了新大陆,这要是发现Spring Cloud Gateway有问题,以后又能吹嘘一番了(内心YY)。

既然,内存分配没有释放,那我们就找到内存分配的地方,打个断点。

通过前面的分析,我们已经知道使用的内存分配器是UnpooledByteBufAllocator了,那就在它的newDirectBuffer()方法中打一个断点,因为我们这里是直接内存泄漏了。

第十二步,一步一步调试

按照第十一步的思路,在UnpooledByteBufAllocator的newDirectBuffer()方法中打一个断点,一步一步调试,最后,来到了这个方法:

// io.netty.handler.codec.ByteToMessageDecoder.channelRead @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {     if (msg instanceof ByteBuf) {         CodecOutputList out = CodecOutputList.newInstance();         try {             first = cumulation == null;             // 1. 返回的是msg本身,msg是一个ByteBuf             cumulation = cumulator.cumulate(ctx.alloc(),                     first ? Unpooled.EMPTY_BUFFER : cumulation, (ByteBuf) msg);             // 2. 解码,本文来源于工纵耗彤哥读源码             callDecode(ctx, cumulation, out);         } catch (DecoderException e) {             throw e;         } catch (Exception e) {             throw new DecoderException(e);         } finally {             if (cumulation != null && !cumulation.isReadable()) {                 numReads = 0;                 // 3. 释放内存                 cumulation.release();                 cumulation = null;             } else if (++ numReads >= discardAfterReads) {                 // We did enough reads already try to discard some bytes so we not risk to see a OOME.                 // See https://github.com/netty/netty/issues/4275                 numReads = 0;                 discardSomeReadBytes();             }              int size = out.size();             firedChannelRead |= out.insertSinceRecycled();             // 4. 读取完out中剩余的值             fireChannelRead(ctx, out, size);             // 5. 回收out             out.recycle();         }     } else {         ctx.fireChannelRead(msg);     } }

这中间花了好几个小时,特别是ChannelPipeLine里面一不小心就跳过去了,又得重新来过,真的是只能一步一步来。

这个方法主要是用来把ByteBuf转换成Message,Message就是消息,可以理解为简单的Java对象,主要逻辑在上面的代码中都标示出来了。

可以看到,这里有个cumulation.release();,它就是释放内存的地方,但是,并没有释放掉,在调用这行代码之前,msg(=cumulation)的引用计数是4,释放之后是2,所以,还有计数,无法回收。

走完下面的4、5两步,out都回收了,msg还是没有被回收,问题肯定是出在这一块。

一直在这里纠结,包括decode里面的代码都反反复复看了好多遍,这里没有释放的msg里面的内容转换之后的对象是DefaultHttpContent,它表示的是Http请求的body,不过这里是Http请求返回值的body。

这也是让我很迷惑的一点,我试了,Http请求的body好像没有走到这块逻辑,又反反复复地找Http请求的Body,搞了好久,一直没有进展。

到晚上9点多的时候,办公室已经没什么人了,灯也关了(疫情期间,每个部门每天只能去几个人),我也收拾下回家了。

第十三步,打车回家

在车上的时候,一直在想这个问题,回忆整个过程,会不会是我的方向错了呢?

Spring Cloud Gateway出来也挺久了,没听说有内存泄漏的问题啊,此时,我开始自我怀疑了。

不行,我回家得自己写一个项目,使用Spring Cloud Gateway跑一下试试。

第十四步,写一个使用Spring Cloud Gateway的项目

到家了,赶紧打开电脑,动手写了一个使用Spring Cloud Gateway的项目和一个SpringBoot的项目,把监控打开,把池化功能去掉,启动项目,发送请求,观察直接内存。

used direct memory0 used direct memory0 used direct memory0 used direct memory0 used direct memory0 used direct memory0

纳尼,阿西巴,到这里,已经很明确了,不是Spring Cloud Gateway的问题,那是什么问题呢?

肯定是使用的姿势不对,不过公司那个项目,也没有别的什么东西了,类都被我删完了,只剩下启动类了。

哎不对,pom文件。

打开跳板机,登录到公司电脑,查看pom.xml,发现里面都是SpringBoot或者SpringCloud本身的一些引用。

嗯,不对,有个common包,分部自己写的common包,点进去,里面引用了三个jar包,其中,有一个特别扎眼,tomcat!!!!

哎哟我次奥,此时,我真的想骂娘,这都什么事儿~~

其实,我在删除AuthFilter的时候就应该想到pom的问题的,当时,只顾着YY Spring Cloud Gateway 可能有bug的问题了,一头就扎进去了。

我们知道,Spring Cloud Gateway使用的是Netty做为服务端接收请求,然后再转发给下游系统,这里引用tomcat会怎样呢?还真是一件有趣的事呢。

第十五步,干掉tomcat

在pom文件中,把tomcat的jar包排除掉,重启项目,发送请求,观察直接内存:

used direct memory0 used direct memory0 used direct memory0 used direct memory0 used direct memory0 used direct memory0

哦了,没有问题了,就是tomcat捣的鬼。

那么,tomcat是怎么捣鬼的呢?加了tomcat也能正常的响应请求,请求也能正常的转发,返回给客户端,而且,更可怕的是,内部也确实是使用了Netty进行请求的读写响应,真的有点神奇。

第十六步,发现新大陆

为了验证这个问题,我们还是先退出跳板机,回到我自己的电脑,在pom中加入tomcat,启动项目,咦,确实能起得来,好好玩儿~~

难道是tomcat和Netty同时监听了同一个端口,两者都起来了?

观察一下项目启动日志:

Connected to the target VM, address: '127.0.0.1:52162', transport: 'socket'    .   ____          _            __ _ _  /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \  \\/  ___)| |_)| | | | | || (_| |  ) ) ) )   '  |____| .__|_| |_|_| |_\__, | / / / /  =========|_|==============|___/=/_/_/_/  :: Spring Boot ::        (v2.2.6.RELEASE)  2020-05-19 08:50:04.448  INFO 7896 --- [           maincom.alan.test.Application                : No active profile set, falling back to default profiles: default 2020-05-19 08:50:04.829  INFO 7896 --- [           maino.s.cloud.context.scope.GenericScope     : BeanFactory id=082e67ca-d4c7-3a8c-b051-e806722fd225 2020-05-19 08:50:04.998  INFO 7896 --- [           maino.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http) 2020-05-19 08:50:05.006  INFO 7896 --- [           maino.apache.catalina.core.StandardService   : Starting service [Tomcat2020-05-19 08:50:05.006  INFO 7896 --- [           mainorg.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.332020-05-19 08:50:05.859  INFO 7896 --- [           maino.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [After2020-05-19 08:50:05.860  INFO 7896 --- [           maino.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [Before2020-05-19 08:50:05.860  INFO 7896 --- [           maino.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [Between2020-05-19 08:50:05.860  INFO 7896 --- [           maino.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [Cookie2020-05-19 08:50:05.860  INFO 7896 --- [           maino.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [Header2020-05-19 08:50:05.860  INFO 7896 --- [           maino.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [Host2020-05-19 08:50:05.860  INFO 7896 --- [           maino.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [Method2020-05-19 08:50:05.860  INFO 7896 --- [           maino.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [Path2020-05-19 08:50:05.860  INFO 7896 --- [           maino.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [Query2020-05-19 08:50:05.860  INFO 7896 --- [           maino.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [ReadBodyPredicateFactory2020-05-19 08:50:05.860  INFO 7896 --- [           maino.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [RemoteAddr2020-05-19 08:50:05.860  INFO 7896 --- [           maino.s.c.g.r.RouteDefinitionRouteLocator    : 本文来源于工纵耗彤哥读源码 2020-05-19 08:50:05.860  INFO 7896 --- [           maino.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [CloudFoundryRouteService2020-05-19 08:50:06.731  INFO 7896 --- [           maino.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path '' 2020-05-19 08:50:07.304  INFO 7896 --- [           maincom.alan.test.Application                : Started Application in 4.271 seconds (JVM running for 5.0)

发现确实只启动了tomcat,那它是怎么把请求移交给Netty来处理的呢?

第十七步,tomcat -> Netty

学习过NIO相关知识的同学应该知道,NIO将SocketChannel分成了两种,一种是ServerSocketChannel,一种是SocketChannel,其中,ServerSocketChannel是服务启动的时候创建的,用来监听客户端连接的到来,而SocketChannel就表示客户端与服务端之间的连接。

看过NIO源码的同学又知道,SocketChannel是通过ServerSocketChannel创建出来的。

看过Netty源码的同学又知道,Netty根据不同的协议又把这些Channel分成了NioXxxChannel、EpollXxxChannel等等,针对每一种协议的Channel同样分成NioServerSocketChannel、NioSocketChannel等。

而在Windows平台下,默认使用的是NioXxxChannel,而从上可知,NioSocketChannel应该是通过NioServerSocketChannel创建出来的,如果是正常使用Netty,也确实是这样的。

下图是正常使用Netty时NioSocketChannel创建时的线程栈:

tomcat和Netty怎么解决内存泄漏问题

不过,我们现在的场景是 tomcat + Netty,那又是怎样的呢?

此时,在NioSocketChannel的构造方法中打一个断点,发送一个请求,发现断点到了NioSocketChannel的构造方法中,观察线程栈的情况(从下往上看):

tomcat和Netty怎么解决内存泄漏问题

tomcat和Netty怎么解决内存泄漏问题

tomcat和Netty怎么解决内存泄漏问题

tomcat和Netty怎么解决内存泄漏问题

tomcat和Netty怎么解决内存泄漏问题

可以看到,经过tomcat->spring->reactor->reactor-netty->netty,千转百回之后,终于创建了NioSocketChannel。

这里的情况就有点复杂了,后面有时间,我们再详细分析。

第十八步,内存泄漏

从上面可以看出,Tomcat最终把请求的处理交给了Netty,但是为什么会内存泄漏呢?这依然是个问题。

经过我的对比检测,问题还是出在第十二步的代码那里,在使用正常的Netty请求时,在fireChannelRead()的里面会往NioEventLoop中添加一个任务,叫作

MonoSendMany.SendManyInner.AsyncFlush:

final class AsyncFlush implements Runnable {     @Override     public void run() {         if (pending != 0) {             ctx.flush();         }     } }

这是用来把写缓冲区的数据真正写出去的(读完了写出去),同时,也会把写缓冲区的数据清理掉,也就是调用了这个方法客户端才能收到响应的结果,而使用 tomcat + Netty 的时候,并没有执行这个任务,数据就发送给了客户端(猜测可能是通过tomcat的连接而不NioSocketChannel本身发送出去的),这是一个遗留问题,等后面再研究下了,现在脑子有点凌乱。

感谢各位的阅读,以上就是“tomcat和Netty怎么解决内存泄漏问题”的内容了,经过本文的学习后,相信大家对tomcat和Netty怎么解决内存泄漏问题这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是亿速云,小编将为大家推送更多相关知识点的文章,欢迎关注!

亿速云「云服务器」,即开即用、新一代英特尔至强铂金CPU、三副本存储NVMe SSD云盘,价格低至29元/月。点击查看>>

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

原文链接:https://www.toutiao.com/a6828428436210450947/

AI

开发者交流群×