
说在前面的话朋友你经历过部署好的服务突然内存溢出吗你经历过没有看过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均衡器分配到SelectorThreadSelectorThread执行readwrite操作read到一个FrameBuffer封装了方法名参数参数类型等数据和读取写入调用方法的操作交给WorkerProcess线程池执行方法调用。**内存溢出就是在read一个FrameBuffer产生的。**步骤五.细致一点描述thrift处理过程1.服务端服务启动后会listen()一直监听客户端的请求当收到请求accept()后交给线程池去处理这个请求2.处理的方式是首先获取客户端的编码协议getProtocol然后根据协议选取指定的工具进行反序列化接着交给业务类处理process3.process的顺序是**先申请临时缓存读取这个请求数据**处理请求数据执行业务代码写响应数据,**最后清除临时缓存** **总结thrift服务端处理请求的时候会先反序列化数据接着申请临时缓存读取请求数据然后执行业务并返回响应数据最后请求临时缓存。** 所以压力测试的时候thrift性能很高而且内存占用不高是因为它有自负载调节使用NIO模式缓存并使用线程池处理业务每次处理完请求之后及时清除缓存。步骤六.研读FrameBuffer的read方法代码可以排除掉没有及时清除缓存的可能方向明确极大的可能是在申请NIO缓存的时候出现了问题回到thrift框架查看FrameBuffer的read方法代码12345678910111213141516171819202122232425262728293031323334353637383940414243444546publicbooleanread() {// try to read the frame size completelyif(this.state_ AbstractNonblockingServer.FrameBufferState.READING_FRAME_SIZE) {if(!this.internalRead()) {returnfalse;}// if the frame size has been read completely, then prepare to read the actual timeif(this.buffer_.remaining() !0) {returntrue;}intframeSize 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?);returnfalse;}// 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.);returnfalse;}if(AbstractNonblockingServer.this.readBufferBytesAllocated.get() (long)frameSize AbstractNonblockingServer.this.MAX_READ_BUFFER_BYTES) {returntrue;}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()) {returnfalse;}else{if(this.buffer_.remaining() 0) {this.selectionKey_.interestOps(0);this.state_ AbstractNonblockingServer.FrameBufferState.READ_FRAME_COMPLETE;}returntrue;}}else{this.LOGGER.error(Read was called but state is invalid (this.state_ ));returnfalse;}}**说明**MAX_READ_BUFFER_BYTES这个值即为对读取的包的长度限制如果超过长度限制就不会再读了/这个MAX_READ_BUFFER_BYTES是多少呢thrift代码中给出了答案12345678publicabstractstaticclassAbstractNonblockingServerArgsTextendsAbstractNonblockingServer.AbstractNonblockingServerArgsTextendsAbstractServerArgsT {brpubliclongmaxReadBufferBytes 9223372036854775807L;publicAbstractNonblockingServerArgs(TNonblockingServerTransport transport) {super(transport);this.transportFactory(newFactory());}}从上面源码可以看出默认值居然给到了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的都有。步骤九. 找到原因某些客户端没有正常的序列化消息导致服务端在处理请求的时候序列化出来的数据特别大读取该数据的时候出现的内存溢出。查看维护记录在别的客户那里也出现过内存溢出导致服务端崩溃的情况通过重新安装客户端就不再复现了。所以可以确定客户端存在着无法正常序列化消息的情况。考虑到客户端量比较大一个一个排除再重新安装比较困难工作量很大所以可以从服务端的角度来解决问题减少维护工作量。最后可以确定解决方案了真的是废了很大的劲不过也是颇有收获