朋友,你经历过部署好的服务突然内存溢出吗?
你经历过没有看过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;
}
}