糟了,线上服务出现 OOM 了
前言
前一段时间,公司同事的一个线上服务 OOM 的问题,我觉得挺有意思的,在这里跟大家一起分享一下。
我当时其实也参与了一部分问题的定位。
1 案发现场
他们有个 mq 消费者服务,在某一天下午,出现 OOM 了,导致服务直接挂掉。
当时我们收到了很多内存的报警邮件。
发现问题之后,运维第一时间,帮他们 dump 了当时的内存快照,以便于开发人员好定位问题。
之后,运维重启了该服务,系统暂时恢复了正常。
大家都知道,如果出现了线上 OOM 问题,为了不影响用户的正常使用,最快的解决办法就是重启服务。
但重启服务治标不治本,只能临时解决一下问题,如果不找到真正的原因,难免下次在某个不经意的时间点,又会出现 OOM 问题。
所以,有必要定位一下具体原因。
2 初步定位问题
当时运维 dump 下来的内存快照文件有 3G 多,太大了,由于公司内网限制,没办法及时给到开发这边。
没办法,只能先从日志文件下手了。
在查日志之前,我们先查看了 prometheus 上的服务监控。查到了当时那个 mq 消费者服务的内存使用情况,该服务的内存使用率一直都比较平稳,从 2022-09-26 14:16:29 开始,出现了一个明显的内存飙升情况。
根据以往经验总结出来的,在追查日志时,时间点是一个非常重要的过滤条件。
所以,我们当时重点排查了 2022-09-26 14:16:29 前后 5 秒钟的日志。
由于这个服务,并发量不大,在那段时间的日志量并不多。
所以,我们很快就锁定了 excel 文件导入导出功能。
该功能的流程图如下:
用户通过浏览器上传 excel,调用文件上传接口。
该接口会上传 excel 到文件服务器。然后将文件 url,通过 mq 消息,发送到 mq 服务器。
mq 消费者消费 mq 消息,从文件服务器中获取 excel 数据,做业务处理,然后把结果写入新的 excel 中。
mq 消费者将新 excel 文件上传到文件服务器,然后发 websocket 消息通知用户。
用户收到通知结果,然后可以下载新的 excel。
经过日志分析,时间点刚好吻合,从 excel 文件导入之后,mq 消费者服务的内存使用率一下子飙升。
3. 打不开 dump 文件
从上面分析我们得出初步的结论,线上 mq 消费者服务的 OOM 问题,是由于 excel 导入导出导致的。
于是,我们查看了相关 excel 文件导入导出代码,并没有发现明显的异常。
为了找到根本原因,我们不得不把内存快照解析出来。
此时,运维把内存快照已经想办法发给了相关的开发人员(我的同事)。
那位同事用电脑上安装的内存分析工具:MAT(Memory Analyzer Tool),准备打开那个内存快照文件。但由于该文件太大,占了 3G 多的内存,直接打开失败了。
MemoryAnalyzer.ini 文件默认支持打开的内存文件是 1G,后来它将参数-xmx 修改为 4096m。
修改之后,文件可以打开了,但打开的内容却有问题。
猛然发现,原来是 JDK 版本不匹配导致的。
他用的 MAT 工具是基于 SunJDK,而我们生成环境用的 OpenJDK,二者有些差异。
SunJDK 采用 JRL 协议发布,而 OpenJDK 则采用 GPL V2 协议发布。两个协议虽然都是开放源代码的,但是在使用上的不同,GPL V2 允许在商业上使用,而 JRL 只允许个人研究使用。
所以需要下载一个基于 OpenJDK 版本的 MAT 内存分析工具。
4. 进一步分析
刚好,另一个同事的电脑上下载过 OpenJDK 版本的 MAT 内存分析工具。把文件发给他帮忙分析了一下。
最后发现 org.apache.poi.xssf.usermodel.XSSFSheet 类的对象占用的内存是最多的。
目前 excel 的导入导出功能,大部分是基于 apache 的 POI 技术,而 POI 给我们提供了 WorkBook 接口。常用的 WorkBook 接口实现有三种:
HSSFWorkbook:它是早期使用最多的工具,支持 Excel2003 以前的版本,Excel 的扩展名是.xls。只能导出 65535 条数据,如果超过最大记录条数会报错,但不会出现内存溢出。
XSSFWorkbook:它可以操作 Excel2003-Excel2007 之间的版本,Excel 的扩展名是.xlsx。最多可以导出 104w 条数据,会创建大量的对象存放到内存中,可能会导致内存溢出。
SXSSFWorkbook:它可以操作 Excel2007 之后的所有版本,Excel 的扩展名是.xlsx。SXSSFWorkbook 是 streaming 版本的 XSSFWorkbook,它只会保存最新的 rows 在内存里供查看,以前的 rows 都会被写入到硬盘里。用磁盘空间换内存空间,不会导致内存溢出。
看到了这个类,可以验证之前我们通过日志分析问题,得出 excel 导入导出功能引起 OOM 的结论,是正确的。那个引起 OOM 问题的功能,刚好使用了 XSSFWorkbook 处理 excel,一次性创建了大量的对象。关键代码如下:
我们通过 MAT 内存分析工具,已经确定 OOM 问题的原因了。接下来,最关键的一点是:如何解决这个问题呢?
5. 如何解决问题?
根据我们上面的分析,既然 XSSFWorkbook 在导入导出大 excel 文件时,会导致内存溢出。那么,我们改成 SXSSFWorkbook 不就行了?关键代码改动如下:
使用 SXSSFWorkbook 将 XSSFWorkbook 封装了一层,其中 100 表示 excel 一次读入内存的最大记录条数,excel 中其余的数据将会生成临时文件保存到磁盘上。这个参数,可以根据实际需要调整。还有一点非常重要:
需要在程序的结尾处加上上面的这段代码,不然生成的临时文件是空的。这样调整之后,问题被暂时解决了。
此外,顺便说一句,在使用 WorkBook 接口的相关实现类时,用完之后,要记得调用 close 方法及时关闭喔,不然也可能会出现 OOM 问题。
6. 后续思考
其实,当时我建议过使用阿里开源的 EasyExcel 解决 OOM 的问题。但同事说,excel 中有很多样式,在导出的新 excel 中要保留之前的样式,同时增加一列,返回导入的结果。
如果使用 EasyExcel 不太好处理,使用原始的 Workbook 更好处理一些。
但是使用 mq 异步导入 excel 文件这套方案,如果并发量大的话,任然可能会出现 OOM 问题,有安全隐患。因此,有必要调整一下 mq 消费者。
后来,mq 消费者的线程池,设置成 4 个线程消费,避免消费者同时处理过多的消息,读取大量的 excel,导致内存占用过多的问题。当然线程个数参数,可以根据实际情况调整。
此外,使用阿里的 arthas 也可以定位线上 OOM 问题,后面会有专门的文章介绍,感兴趣的小伙伴可以关注一下。
评论