性能优化-dubbo消费者OOM问题排查解决

3,743 阅读5分钟

1.问题描述

        设备网关突然出现大量延时,容器不断重启,查看日志出现了 "java heap space"内存溢出错误,查看了apm监控数据,发现full gc频繁,响应rt非常慢。加上了

-XX:+HeapDumpOnOutOfMemoryError

-XX:HeapDumpPath=/opt/oom/heapdump

的JVM参数,并将容器文件外挂到主机文件上,很轻易就抓到了dump文件。还要介绍下当前的部署架构如下图所示,网关和领域服务通过dubbo框架进行通信,由于信息发布服务是一个中台架构,除了云班的设备网关使用,还需要给其他系统平台提供服务,调用量比较高,所以实例个数在15个左右,而设备网关只有6个实例。

2.问题分析

2.1 内存溢出错误分析

    一般常见内存溢出错误有以下几种: 原生内存已用完(unable to create new native thread)/永久代或元空间不足(PermGenSpace)/堆内存不足(Java heap space)/超过GC的开销限制(GC overhead limit exceeded),还有很多不同的原因也会导致抛出OutOfMemoryError,对于永久代和普通的堆,内存泄漏是出现OutOfMemoryError的常见原因,堆分析工具可以帮助我们找到泄漏的根源。

       用JProfiler分析dump文件后,发现较大的有两个11MB的字段串,看字符串的内容是信发领域的文本内容,初步判断是Dubbo调用底层的信发微服务后返回的结果内容撑满了内存。再查看当时的信息发布微服务的apm日志情况,发现有很多mysql事务超时,都是获取资源内容的sql语句,进一步验证了推测。

2.2消费者调用过程分析

        看了上面的堆栈信息,大家都很奇怪,为啥会是这样的堆栈分布? 这个跟Dubbo框架有啥关系?应用进程使用Dubbo框架与远端服务提供者进行通信,使用的IO模型如下图。当应用进程向socket写入数据时候,首先需要在应用程序内申请一个写buffer,然后把数据写入到写buffer,然后应用程序的执行会用用户态切换到核心态,核心态程序把应用程序写buffer里面的数据拷贝到操作系统层面的写缓存里面。当应用进程读取数据时候是需要把操作系统层面的读buffer里面的数据拷贝到应用程序层面的读buffer里面。一般情况下应用程序层面的buffer都是从堆空间里面申请的,这就需要在用户态和核心态之间数据传输时候进行一次数据copy。这是因为核心态是不能直接应用程序堆内存的,必须转换为直接内存。大家可以有这么一个基本认识,如果网络中传输了大量的数据需要让应用进程处理,很容易把应用进程给整垮(OOM)。

                         

       我们再来分析Dubbo消费端一个完整的RPC请求过程如下图所示,业务发送RPC 请求,然后通过NettyChannel,写入Socket通道中,当服务提供者处理完请求后会将结果写入已建立好的Socket通道中, Netty网络库会监听到这些 网络IO 事件,并通知应用进程来读取通道中的通信数据,进行解码和反序列化,最后返回结果到上层。

        其实这里很好奇的为什么Hessian2Input对象会占用这么多的内存?首先要跟大家讲下Binary-Rpc框架的协议原理,发送方通常会通过序列化工具如Hessian将对象转换为二进制数据流,接收方接收到数据流后使用序列化工具再转换成对象再使用。从下面的例子看到Hessian 的简单使用案例,主要使用的Hessian2Oput/Hessian2Input来实现。 在Dubbo框架中,这可是很复杂的,这关系到异步网络IO里面的数据流解析,有兴趣的可以去看下ObjectInput/ObjectOutput的解析过程。 

//返回结果类定义
public class Resource {
     Integer type;
     String content;
     .......
}

//Hessian序列化与反序列化调用
ByteArrayOutputStream os = new ByteArrayOutputStream();
Hessian2Output output = new Hessian2Output(os);
output.writeObject(new Resource("test", "test"));
output.close();

ByteArrayInputStream in = new ByteArrayInputStream(os.toByteArray());
Hessian2Input input = new Hessian2Input(in);
System.out.println(input.readObject());

       具体的Hessian编码解析标志位定义在HessianConstants中有定义,在Hessian解码过程中,会先去读取到返回结果类型定义,然后逐一去解析每个字段类型和字段值

当解析String类型字段时,会使用到Hessian2Input#readString方法,其中的一个逻辑就是将网络流中读取到的字符然后全部添加到Hessian2Input中的StringBuilder中对象中,相当于StringBuilder作为接收字符串的缓冲区,所以就导致了会出现两个很大的StringBuilder对象。

3.问题解决

        其实这是很简单的原理,服务提供者返回了过大的对象或者数据导致了消费者OOM了。跟相关同事沟通后,知道这些返回内容是一些文章信息发布内容,由于历史原因是直接存储在数据库的,就导致了获取发布内容时返回大量的文本内容,当时紧急将这几个大文本内容转换成文件并上传到存储云,设备端得到资源url后再去下载后显示,解决了这个消费端OOM问题。

4.总结

       本篇是 Dubbo踩坑的一个线上案例,使用内存分析工具分析了生产环境OOM后的文件,结合Dubbo框架的请求过程的完整分析根因以及原理。其中也对平常遇到的内存OOM问题的产生原因与解决方案做了总结,方便以后快速定位问题,并对Java IO模型使用的内存有了更深的理解。通过对Dubbo框架源码的分析,以前一直停留在Registry/Proxy/Cluster的功能扩展使用的认知水平,这次问题分析排查后,自己也对Exchange/Transpot/Serialize关于网络通信,编解码的逻辑有了理解。

参考资料

zhuanlan.zhihu.com/p/49916939

juejin.cn/post/684490…

github.com/cytle/blog/…

www.cnblogs.com/liandy001/p…

blog.csdn.net/hacker_zxf/…