版权声明:本文为博主原创文章,未经博主允许不得转载。
记一次压测时Java内存泄漏问题的发现过程(2017-08-14)
【前篇】
①20170811进行A系统与B系统之间的会话功能进行压测,加上脚本准备期间的聊天消息,预计累计聊天30w+条消息;
②20170814原计划加大量对会话功能进行压测,情况如下;
【应用表现】
①B系统前台打开报错“504”;
②查看后台应用CPU情况,CPU利用率高达700+%(8核);
③查看后台内存情况,持续FullGC,且一次FullGC的时长在9s左右,从这里可以粗略定位CPU高的原因是内存GC问题导致;
【查看应用JVM配置】
①请教B开发团队,loader提到应该不是JVM配置引起的问题;
【尝试进行分析】
①尝试使用jvisualvm进行“堆 dump”,但是因为没有内存了所以jvisualvm连接后卡死(之前测试可以正确连接并显示JVM情况);
②使用jmap命令“jmap -dump:format=b,file=heap.hprof pid”进行dump,dump文件有16G(无奈使用mat打不开);
③尝试shutdown应用后重启,无法shutdown;最后使用“kill -9 pid”暴力解决无法shutdown的情况,后重启应用;
【重启后情况】
①使用jvisualvm查看堆内存使用情况,表现为“堆内存持续上升”;
②重启1小时后,dump文件进行分析,其中“java.util.concurrent.LinkedBlockingQueue$Node”占用内存高达1G,基本可以判断存在“内存泄漏”;
“com.best.oasis.B.common.entity.messageTransship.MessageTransship”对象151MB,且有160w个MessageTransship对象;
③B开发Review代码:原因所在:线程池中等待执行的任务队列存在内存泄漏的问题;
正常情况:
A应用服务器发送消息给B服务器后,B服务器接收消息后将该消息存于中间表B_messagetransship中,同时将该消息转发给B客服端,B客服端接收消息并对该条消息进行ack,ack成功后删除B_messagetransship中的该条消息。为了防止消息丢失,B有一个定时重发job,用于每隔5s将B_messagetransship表中的消息再次推送一次;
异常情况:
1.A服务器发送给B服务器的消息存在于B_messagetransship表中后(此时状态为“PENDING_SEND”),因为网络/B客户主动退出等问题,致使B客户端并未收到来自B服务器的该消息,则该消息的状态被置为“SEND_FAILED”存在表B_messagetransship中;
2.A服务器发送给B服务器的消息,B客服端正确收到,但是B客服端发送的ACK请求返回失败,则该消息的状态被置为“PENDING_ACK”存在表B_messagetransship中;
失败消息定时重发实现逻辑:
每隔5s从B_messagetransship中逐个取出失败的消息记录,以链式队列的形式链接在等待执行的任务队列中,若5s内该消息被线程处理且推送状态为成功,则删除数据库表中该消息记录;若5s内该消息被线程处理但推送状态为失败,则数据库表中的该条消息记录保持不变;若5s内该消息并未来得及被线程处理,下一次定时重发任务触发时,该消息会保留第二个拷贝在待处理任务队列中,以此类推;
bug发现的诱因:
B_messagetransship表失败推送的消息量比较大,B_messagetransship表11w+条数据,失败消息量大的原因:
①11907条“再见”,状态为:SEND_FAILED
产生原因:B客户端对话完毕未接收到再见语,就发起了“{"type":"close","sid":"${sid}"}”的请求,该现象在实际中也可能产生;
②17120条“很高兴为您服务”,状态为:PENDING_ACK
产生原因:压测脚本未对“很高兴为您服务”消息进行ack;
③剩余的8w+条,为A发送给B的对话消息,推测是在脚本准备期间产生的数据;
开发下期优化思路:
①为B_messagetransship表中的消息增加生存时长,若超时则直接删除;
②限制待执行任务队列中messageTransship对象的数量,达到一定个数则不再从B_messagetransship中获取;
测试脚本修改:
①增加对“开始语”与“结束语”消息的ack;