背景:java
有一个项目作一个系统,分客户端和服务端,客户端用c++写的,用来收集信息而后传给服务端(客户端的数量仍是比较多的,正常的有几千个),c++
服务端用Java写的(带管理页面),属于RPC模式,中间的通讯框架使用的是thrift。程序员
thrift不少优势就很少说了,它是facebook的开源的rpc框架,主要是它可以跨语言,序列化速度快,可是他有个不讨喜的地方就是它必须用本身IDL来定义接口浏览器
thrift版本:0.9.2.缓存
问题定位与分析服务器
步骤一.初步分析架构
客户端没法链接服务端,查看服务器的端口开启情况,服务端口并无开启。因而启动服务端,启动几秒后,服务端崩溃,重复启动,服务端依旧在启动几秒后崩溃。框架
步骤二.查看服务端日志分析socket
若是想学习Java工程化、高性能及分布式、深刻浅出。微服务、Spring,MyBatis,Netty源码分析的朋友能够加个人Java高级交流:854630135,群里有阿里大牛直播讲解技术,以及Java大型互联网技术的视频免费分享给你们。tcp
分析得知是由于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> {<br> public long maxReadBufferBytes = 9223372036854775807L; public AbstractNonblockingServerArgs(TNonblockingServerTransport transport) { super(transport); this.transportFactory(new Factory()); } }
>从上面源码能够看出,默认值竟然给到了long的最大值9223372036854775807L。
因此thrift的开发者是以为使用thrift程序员不够以为内存不够用吗,这个换算下来就是1045576TB,这个太夸张了,这等于没有限制啊,因此确定不能用默认值的。
步骤七.通讯数据抓包分析
须要可靠的证据证实一个客户端通讯的数据包的大小。
若是想学习Java工程化、高性能及分布式、深刻浅出。微服务、Spring,MyBatis,Netty源码分析的朋友能够加个人Java高级交流:854630135,群里有阿里大牛直播讲解技术,以及Java大型互联网技术的视频免费分享给你们。
这个是我抓到包最大的长度,最大一个包长度只有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服务没有内存可申请,没法处理请求。
欢迎工做一到八年的Java工程师朋友们加入Java高级交流:854630135
本群提供免费的学习指导 架构资料 以及免费的解答
不懂得问题均可以在本群提出来 以后还会有直播平台和讲师直接交流噢