记录这次IBM MQ消息队列接收变慢的发现和处理过程,告诫自己:码字需谨慎
先介绍我们遇到的问题:因为涉及公司业务,所以简单描述。就是利用springBoot开发一个接受IBM MQ消息队列的服务。消息对象主要是XML格式的报文,该服务主要就是从IBM MQ消息队列中获取XML格式字符型,然后获取XML报文中的某一个节点信息,作为文件名落地在一个固定的文件。
我们的SpringBoot项目是打包成jar,在Windows通过Java命令启动:
java -jar jar包名
一开始我们的接收速度还行,能接受,大概50毫秒接收一个IBMMQ消息落地成XML格式的报文。但是后来接收速度变慢,分析大概接收一个报文大概需要2000毫秒,我们的本地消息队列堵的消息数量越来越多,我们平台每天大概7万回执报文,队列很快堵到30万。接收回执报文速度慢,影响了我们对于报文内容的解析,影响业务运营。
根据运营同事描述,突然服务变慢了。因为我们的生产环境使用IBM MQ免费版,有三个月的试用期,每3个月都需要卸载IBM MQ软件重新安装,还好IBM MQ卸载会保留配置信息(消息队列、通道等等的配置信息),也还算简单。
因为中途我们重新安装过IBM MQ,所以运营同事给了我们第一次的误导:自从重新安装IBM MQ以后,接收报文越来越慢。我们的注意力全被转移到IMB MQ配置上面,猜测是不是因为IBM MQ试用版的原因。所有我们一直在纠结IBM MQ配置问题。
我观察Windows服务器的性能参数,发现服务已启动的时候内存爆满,CPU还行。我开始优化代码,减少String对象的时候,多用StringBuild和StringBuffer对象。后来,发现内存下降了一点。但是接受报文速度还是比较慢。
由于处理报文落地的代码不复杂,我们采取了最简单的办法:打印程序运行时间+二分查找法,通过两段代码运行时间对比,不断缩小范围,找到程序最耗时的代码范围:
打印程序运行时间
long startTime=System.currentTimeMillis(); //获取开始时间
程序代码
long endTime=System.currentTimeMillis(); //获取结束时间
System.out.println("程序运行时间: "+(end-start)+"ms");
最后终于定位到了耗时最长的一行代码,执行时间大概需要 1900毫秒
耗时代码:
//新建文件
if(!saveFile.exists()){
saveFile.createNewFile();//创建文件耗时慢的一行代码
}
看到这一行代码,都惊呆了,为什么创建文件的代码会耗时这么长,一行代码。后来再仔细分析,终于找到原因了,超级坑爹,这是XML解析,获取节点信息时临时创建的一个文件,每次用完没有删除,导致临时文件里面的文件数量越来越多,大概180多万个文件存在,所以导致在文件夹下创建文件速度特别慢。
终于找到原因了,自己掉进了自己设置的陷阱进去,还是因为自己代码不够严谨。找到问题过程却让人有所收获。
代码优化
思考,解析XML的时候为什么需要创建这个临时文件呢,为什么不直接将接受字符进行解析呢?将字符串落地成临时报文需要读写磁盘,也是一个耗时过长,完全不必要的。于是修改了解析方法XML方法
原来:
/**
* 从传输报文中获取数据 BASE64解码
*
* @param dataMap
* @param file
*/
public static Map<String,String> getXmlDate(String xml){
Map<String,String> retMap=new HashMap<String,String>();
Document document=null;
try{
//得到document
document=DocumentHelper.parseText(xml);
//得到第一个根节点
Element root = document.getRootElement();
Element msgIdEl = root.element("DxpMsg").element("Data");
String data=msgIdEl.getText();
//base64解密
String ret= Encodes.decodeBase64String(data);
retMap.put("Data", ret);
Element msgTypeEl = root.element("DxpMsg").element("TransInfo").element("MsgType");
String msgType=msgTypeEl.getText();
retMap.put("MsgType", msgType);
}catch(Exception e){
log.error("从传输报文获取数据异常:"+e);
}
return retMap;
}
改写后:
/**
* 从传输报文中获取数据 BASE64解码
*
* @param dataMap
* @param file
*/
public static Map<String,String> getXmlDate(File file){
Map<String,String> retMap=new HashMap<String,String>();
try{
//得到document
Document document =getDocument(file.getAbsolutePath());
//得到第一个根节点
Element root = document.getRootElement();
Element msgIdEl = root.element("DxpMsg").element("Data");
String data=msgIdEl.getText();
//base64解密
String ret= Encodes.decodeBase64String(data);
retMap.put("Data", ret);
Element msgTypeEl = root.element("DxpMsg").element("TransInfo").element("MsgType");
String msgType=msgTypeEl.getText();
retMap.put("MsgType", msgType);
}catch(Exception e){
log.error("从传输报文获取数据异常:"+e);
}
return retMap;
}
两个方法区别就是参数不一样,一个接受文件,一个直接传入字符串。这样就不需要临时创建临时文件,直接解析报文内容,减少了不必要的读写磁盘时间。
主要是记录这次解决问题的方式:
1、二分排除法+打印代码执行时间=定位耗时的代码位置。
2、Windows系统一个文件夹下文件数量有上限,达到一定数量,创建新文件的耗时就会增加。