记一次内存溢出的分析经历——thrift带给我的痛orz

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 说在前面的话 朋友,你经历过部署好的服务突然内存溢出吗? 你经历过没有看过Java虚拟机,来解决内存溢出的痛苦吗? 你经历过一个BUG,百思不得其解,头发一根一根脱落的烦恼吗? 我知道,你有过! 但是我还是要来说说我的故事..................   背景: 有一个项目做一个系统,分客户端和服务端,客户端用c++写的,用来收集信息然后传给服务端(客户端的数量还是比较多的,正常的有几千个), 服务端用Java写的(带管理页面),属于RPC模式,中间的通信框架使用的是thrift。

说在前面的话

朋友,你经历过部署好的服务突然内存溢出吗?

你经历过没有看过Java虚拟机,来解决内存溢出的痛苦吗?

你经历过一个BUG,百思不得其解,头发一根一根脱落的烦恼吗?

我知道,你有过!

但是我还是要来说说我的故事..................

 


背景:

有一个项目做一个系统,分客户端和服务端,客户端用c++写的,用来收集信息然后传给服务端(客户端的数量还是比较多的,正常的有几千个),

服务端用Java写的(带管理页面),属于RPC模式,中间的通信框架使用的是thrift。

thrift很多优点就不多说了,它是facebook的开源的rpc框架,主要是它能够跨语言,序列化速度快,但是他有个不讨喜的地方就是它必须用自己IDL来定义接口

thrift版本:0.9.2.

问题定位与分析

步骤一.初步分析

客户端无法连接服务端,查看服务器的端口开启状况,服务端口并没有开启。于是启动服务端,启动几秒后,服务端崩溃,重复启动,服务端依旧在启动几秒后崩溃。

步骤二.查看服务端日志分析

分析得知是因为java.lang.OutOfMemoryError: Java heap space(堆内存溢出)导致的服务崩溃。

客户端搜集的主机信息,主机策略都是放在缓存中,可能是因为缓存较大造成的,但是通过日志可以看出是因为Thrift服务抛出的堆内存溢出异常与缓存大小无关。

步骤三.再次分析服务端日志

可以发现每次抛出异常的时候都会伴随着几十个客户端在向服务端发送日志,往往在发送几十条日志之后,服务崩溃。可以假设是不是堆内存设置的太小了?

查看启动参数配置,最大堆内存为256MB。修改启动配置,启动的时候分配更多的堆内存,改成java -server -Xms512m -Xmx768m。

结果是,能坚持多一点的时间,依旧会内存溢出服务崩溃。得出结论,一味的扩大内存是没有用的。

**为了证明结论是正确的,做了这样的实验:**
> 内存设置为256MB,在公司服务器上部署了服务端,使用Java VisualVM远程监控服务器堆内存。
>
> 模拟客户现场,注册3000个客户端,使用300个线程同时发送日志。
>
> 结果和想象的一样,没有出现内存溢出的情况,如下图:

 
> 上图是Java VisualVM远程监控,在压力测试的情况下,没有出现内存溢出的情况,256MB的内存肯定够用的。


步骤四.回到thrift源码中,查找关键问题

服务端采用的是Thrift框架中TThreadedSelectorServer这个类,这是一个NIO的服务。下图是thrift处理请求的模型:

 


**说明:**
>一个AcceptThread执行accept客户端请求操作,将accept到的Transport交给SelectorThread线程,
>
>AcceptThread中有个balance均衡器分配到SelectorThread;SelectorThread执行read,write操作,
>
>read到一个FrameBuffer(封装了方法名,参数,参数类型等数据,和读取写入,调用方法的操作)交给WorkerProcess线程池执行方法调用。
>
>**内存溢出就是在read一个FrameBuffer产生的。**


步骤五.细致一点描述thrift处理过程


>1.服务端服务启动后,会listen()一直监听客户端的请求,当收到请求accept()后,交给线程池去处理这个请求
>
>2.处理的方式是:首先获取客户端的编码协议getProtocol(),然后根据协议选取指定的工具进行反序列化,接着交给业务类处理process()
>
>3.process的顺序是,**先申请临时缓存读取这个请求数据**,处理请求数据,执行业务代码,写响应数据,**最后清除临时缓存**
>
> **总结:thrift服务端处理请求的时候,会先反序列化数据,接着申请临时缓存读取请求数据,然后执行业务并返回响应数据,最后请求临时缓存。**
>
> 所以压力测试的时候,thrift性能很高,而且内存占用不高,是因为它有自负载调节,使用NIO模式缓存,并使用线程池处理业务,每次处理完请求之后及时清除缓存。


步骤六.研读FrameBuffer的read方法代码

可以排除掉没有及时清除缓存的可能,方向明确,极大的可能是在申请NIO缓存的时候出现了问题,回到thrift框架,查看FrameBuffer的read方法代码:

public boolean read() {

         // try to read the frame size completely  if (this.state_ == AbstractNonblockingServer.FrameBufferState.READING_FRAME_SIZE) { if (!this.internalRead()) { return false; }
         // if the frame size has been read completely, then prepare to read the actual time if (this.buffer_.remaining() != 0) { return true; } int frameSize = this.buffer_.getInt(0); if (frameSize <= 0) { this.LOGGER.error("Read an invalid frame size of " + frameSize + ". Are you using TFramedTransport on the client side?"); return false; }           // if this frame will always be too large for this server, log the error and close the connection. 
                if ((long)frameSize > AbstractNonblockingServer.this.MAX_READ_BUFFER_BYTES) {
                    this.LOGGER.error("Read a frame size of " + frameSize + ", which is bigger than the maximum allowable buffer size for ALL connections.");
                    return false;
                }

                if (AbstractNonblockingServer.this.readBufferBytesAllocated.get() + (long)frameSize > AbstractNonblockingServer.this.MAX_READ_BUFFER_BYTES) {
                    return true;
                }

                AbstractNonblockingServer.this.readBufferBytesAllocated.addAndGet((long)(frameSize + 4));
                this.buffer_ = ByteBuffer.allocate(frameSize + 4);
                this.buffer_.putInt(frameSize);
                this.state_ = AbstractNonblockingServer.FrameBufferState.READING_FRAME;
            }

            if (this.state_ == AbstractNonblockingServer.FrameBufferState.READING_FRAME) {
                if (!this.internalRead()) {
                    return false;
                } else {
                    if (this.buffer_.remaining() == 0) {
                        this.selectionKey_.interestOps(0);
                        this.state_ = AbstractNonblockingServer.FrameBufferState.READ_FRAME_COMPLETE;
                    }

                    return true;
                }
            } else {
                this.LOGGER.error("Read was called but state is invalid (" + this.state_ + ")");
                return false;
            }
        }


**说明:**
>MAX_READ_BUFFER_BYTES这个值即为对读取的包的长度限制,如果超过长度限制,就不会再读了/
>
>这个MAX_READ_BUFFER_BYTES是多少呢,thrift代码中给出了答案:

    public abstract static class AbstractNonblockingServerArgs<T extends AbstractNonblockingServer.AbstractNonblockingServerArgs<T>> extends AbstractServerArgs<T> {
      public long maxReadBufferBytes = 9223372036854775807L; public AbstractNonblockingServerArgs(TNonblockingServerTransport transport) { super(transport); this.transportFactory(new Factory()); } }

 
>从上面源码可以看出,默认值居然给到了long的最大值9223372036854775807L。

所以thrift的开发者是觉得使用thrift程序员不够觉得内存不够用吗,这个换算下来就是1045576TB,这个太夸张了,这等于没有限制啊,所以肯定不能用默认值的。


步骤七.通信数据抓包分析

需要可靠的证据证明一个客户端通信的数据包的大小。

 

这个是我抓到包最大的长度,最大一个包长度只有215B,所以需要限制一下读取大小


步骤八:踏破铁鞋无觅处

在论坛中,看到有人用http请求thrift服务端出现了内存溢出的情况,所以我抱着试试看的心态,在浏览器中发起了http请求,

果不其然,出现了内存溢出的错误,和客户现场出现的问题一摸一样。这个读取内存的时候数量过大,超过了256MB。
> 很明显的一个问题,正常的一个HTTP请求不会有256MB的,考虑到thrift在处理请求的时候有反序列化这个操作。
>
> 可以做出假设是不是反序列化的问题,不是thrift IDL定义的不能正常的反序列化?
>
> 验证这个假设,我用Java socket写了一个tcp客户端,向thrift服务端发送请求,果不其然!java.lang.OutOfMemoryError: Java heap space。
> 这个假设是正确的,客户端请求数据不是用thrift IDL定义的话,无法正常序列化,序列化出来的数据会异常的大!大到超过1个G的都有。


步骤九. 找到原因

某些客户端没有正常的序列化消息,导致服务端在处理请求的时候,序列化出来的数据特别大,读取该数据的时候出现的内存溢出。

查看维护记录,在别的客户那里也出现过内存溢出导致服务端崩溃的情况,通过重新安装客户端,就不再复现了。

所以可以确定,客户端存在着无法正常序列化消息的情况。考虑到,客户端量比较大,一个一个排除,再重新安装比较困难,工作量很大,所以可以从服务端的角度来解决问题,减少维护工作量。

最后可以确定解决方案了,真的是废了很大的劲,不过也是颇有收获


问题解决方案

非常简单

在构造TThreadedSelectorServer的时候,增加args.maxReadBufferBytes = 1*1024 * 1024L;也就是说修改maxReadBufferBytes的大小,设置为1MB。

客户端与服务端通过thrift通信的数据包,最大十几K,所以设置最大1MB,是足够的。代码部分修改完成,版本不做改变**
修改完毕后,这次进行了异常流测试,发送了http请求,使服务端无法正常序列化。

服务端处理结果如下:

thrift会抛出错误日志,并直接没有读这个消息,返回false,不处理这样的请求,将其视为错误请求。

3.国外有人对thrift一些server做了压力测试,如下图所示:

 

使用thrift中的TThreadedSelectorServer吞吐量达到18000以上
由于高性能,申请内存和清除内存的操作都是非常快的,平均3ms就处理了一个请求。
所以是推荐使用TThreadedSelectorServer


4.修改启动脚本,增大堆内存,分配单独的直接内存。

修改为java -server -Xms512m -Xmx768m -XX:MaxPermSize=256m -XX:NewSize=256m -XX:MaxNewSize=512m -XX:MaxDirectMemorySize=128M。


设置持久代最大值 MaxPermSize:256m


设置年轻代大小 NewSize:256m


年轻代最大值 MaxNewSize:512M


最大堆外内存(直接内存)MaxDirectMemorySize:128M


5.综合论坛中,StackOverflow一些同僚的意见,在使用TThreadedSelectorServer时,将读取内存限制设置为1MB,最为合适,正常流和异常流的情况下不会有内存溢出的风险。

 之前启动脚本给服务端分配的堆内存过小,考虑到是NIO,所以在启动服务端的时候,有必要单独分配一个直接内存供NIO使用.修改启动参数。

增加堆内存大小直接内存,防止因为服务端缓存太大,导致thrift服务没有内存可申请,无法处理请求。


总结:

真的是一次非常酸爽的过程,特此发个博客记录一下,如果有说的不对的对方,欢迎批评斧正!如果觉得写的不错,欢迎给我点个推荐,您的一个推荐是我莫大的动力!

 

个人博客网站 http://www.janti.cn
相关实践学习
【涂鸦即艺术】基于云应用开发平台CAP部署AI实时生图绘板
【涂鸦即艺术】基于云应用开发平台CAP部署AI实时生图绘板
相关文章
|
Web App开发 监控 JavaScript
监控和分析 JavaScript 内存使用情况
【10月更文挑战第30天】通过使用上述的浏览器开发者工具、性能分析工具和内存泄漏检测工具,可以有效地监控和分析JavaScript内存使用情况,及时发现和解决内存泄漏、过度内存消耗等问题,从而提高JavaScript应用程序的性能和稳定性。在实际开发中,可以根据具体的需求和场景选择合适的工具和方法来进行内存监控和分析。
|
5月前
|
存储 弹性计算 缓存
阿里云服务器ECS经济型、通用算力、计算型、通用和内存型选购指南及使用场景分析
本文详细解析阿里云ECS服务器的经济型、通用算力型、计算型、通用型和内存型实例的区别及适用场景,涵盖性能特点、配置比例与实际应用,助你根据业务需求精准选型,提升资源利用率并降低成本。
433 3
|
1月前
|
设计模式 缓存 Java
【JUC】(4)从JMM内存模型的角度来分析CAS并发性问题
本篇文章将从JMM内存模型的角度来分析CAS并发性问题; 内容包含:介绍JMM、CAS、balking犹豫模式、二次检查锁、指令重排问题
97 1
|
4月前
|
存储 人工智能 自然语言处理
AI代理内存消耗过大?9种优化策略对比分析
在AI代理系统中,多代理协作虽能提升整体准确性,但真正决定性能的关键因素之一是**内存管理**。随着对话深度和长度的增加,内存消耗呈指数级增长,主要源于历史上下文、工具调用记录、数据库查询结果等组件的持续积累。本文深入探讨了从基础到高级的九种内存优化技术,涵盖顺序存储、滑动窗口、摘要型内存、基于检索的系统、内存增强变换器、分层优化、图形化记忆网络、压缩整合策略以及类操作系统内存管理。通过统一框架下的代码实现与性能评估,分析了每种技术的适用场景与局限性,为构建高效、可扩展的AI代理系统提供了系统性的优化路径和技术参考。
241 4
AI代理内存消耗过大?9种优化策略对比分析
|
8月前
|
存储 Java
课时4:对象内存分析
接下来对对象实例化操作展开初步分析。在整个课程学习中,对象使用环节往往是最棘手的问题所在。
|
8月前
|
Java 编译器 Go
go的内存逃逸分析
内存逃逸分析是Go编译器在编译期间根据变量的类型和作用域,确定变量分配在堆上还是栈上的过程。如果变量需要分配在堆上,则称作内存逃逸。Go语言有自动内存管理(GC),开发者无需手动释放内存,但编译器需准确分配内存以优化性能。常见的内存逃逸场景包括返回局部变量的指针、使用`interface{}`动态类型、栈空间不足和闭包等。内存逃逸会影响性能,因为操作堆比栈慢,且增加GC压力。合理使用内存逃逸分析工具(如`-gcflags=-m`)有助于编写高效代码。
167 2
|
12月前
|
JavaScript
如何使用内存快照分析工具来分析Node.js应用的内存问题?
需要注意的是,不同的内存快照分析工具可能具有不同的功能和操作方式,在使用时需要根据具体工具的说明和特点进行灵活运用。
381 62
|
12月前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
400 1
|
开发框架 监控 .NET
【Azure App Service】部署在App Service上的.NET应用内存消耗不能超过2GB的情况分析
x64 dotnet runtime is not installed on the app service by default. Since we had the app service running in x64, it was proxying the request to a 32 bit dotnet process which was throwing an OutOfMemoryException with requests >100MB. It worked on the IaaS servers because we had the x64 runtime install
211 5
|
Web App开发 JavaScript 前端开发
使用 Chrome 浏览器的内存分析工具来检测 JavaScript 中的内存泄漏
【10月更文挑战第25天】利用 Chrome 浏览器的内存分析工具,可以较为准确地检测 JavaScript 中的内存泄漏问题,并帮助我们找出潜在的泄漏点,以便采取相应的解决措施。
1391 9

热门文章

最新文章