尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

记一次线上OOM排查:从“玄学“到定位的全过程

记一次线上OOM排查:从“玄学“到定位的全过程 周五晚上十点半手机开始疯狂震动。运维群里甩出来一张监控截图——我们那个跑了大半年的订单服务堆内存使用率从40%直接飙到98%GC频率高得像抽风Full GC一轮接一轮每次回收完只能释放掉不到5%的内存。服务没挂但接口响应时间从平时的30ms飙到了3秒以上用户侧已经开始报超时了。我当时的第一反应是谁又上了什么新代码翻了一下最近的发布记录最近一次上线是周三改了一个批量导出Excel的功能。我当时心里就咯噔了一下——批量导出大数据量内存……这几个关键词凑在一起大概率就是它。但光靠猜没用得看证据。第一步先止血线上服务不能一直这么半死不活地挂着。先做了两件事把批量导出的入口临时下掉防止新的请求继续往内存里塞数据对服务做了一次滚动重启先把内存释放掉让服务恢复正常重启之后内存果然降下来了但我知道这只是治标。根因没找到下次上线还可能炸。第二步拿到现场好在我们之前配了JVM启动参数开了OOM时自动dump-XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/data/logs/heapdump/但这次比较尴尬——服务还没真正抛OOM只是GC压力巨大堆还没完全打满。所以没有自动dump文件。只能手动来。先登录机器用jmap打一份堆快照jmap -dump:formatb,file/data/logs/heapdump/manual_dump.hprof pid这里有个坑要提一下jmap在堆很大的时候会触发一次STWStop The World当时服务本来就快挂了这一锤子下去差点直接把它打死。所以生产环境打dump一定要小心最好先把流量切走或者在备节点上操作。dump文件大概1.2Gscp拉到本地用MATEclipse Memory Analyzer打开。第三步分析dumpMAT打开之后第一眼看的是Dominator Tree支配树这个视图能直接告诉你哪些对象占了最多的内存。结果很明显——排名第一的是一个byte[]数组占了将近600MB。再往下看Histogram大量的byte[]和char[]对象加起来占了堆的70%以上。顺着引用链往上追右键 → List Objects → with incoming references最终定位到了一个类public class OrderExportService { public byte[] exportOrders(Date startTime, Date endTime) { ListOrder orders orderMapper.selectByDateRange(startTime, endTime); // 问题就出在这里 ByteArrayOutputStream baos new ByteArrayOutputStream(); Workbook workbook new XSSFWorkbook(); Sheet sheet workbook.createSheet(订单数据); for (int i 0; i orders.size(); i) { Row row sheet.createRow(i); row.createCell(0).setCellValue(orders.get(i).getOrderNo()); row.createCell(1).setCellValue(orders.get(i).getAmount().toString()); // ... 还有十几个字段 } workbook.write(baos); return baos.toByteArray(); } }问题一目了然selectByDateRange没有分页时间范围一大直接查出几十万条数据全塞进内存XSSFWorkbookxlsx格式本身就是把整个文档加载到内存里的不像SXSSFWorkbook支持流式写入最终baos.toByteArray()又复制了一份完整的byte数组也就是说一份导出数据在内存里至少存在了三份拷贝原始查询结果List、Workbook对象、最终的byte数组。如果查出来10万条数据每条数据序列化后大概1KB那光数据就是100MB乘以3就是300MB。再加上GC来不及回收堆直接被打爆。第四步修复改了两个地方1. 查询加分页流式处理public void exportOrders(Date startTime, Date endTime, OutputStream outputStream) { int pageSize 5000; int pageNum 1; SXSSFWorkbook workbook new SXSSFWorkbook(500); // 只保留500行在内存 Sheet sheet workbook.createSheet(订单数据); int rowIndex 0; while (true) { ListOrder orders orderMapper.selectByDateRange( startTime, endTime, (pageNum - 1) * pageSize, pageSize ); if (orders.isEmpty()) break; for (Order order : orders) { Row row sheet.createRow(rowIndex); row.createCell(0).setCellValue(order.getOrderNo()); row.createCell(1).setCellValue(order.getAmount().toString()); } pageNum; } workbook.write(outputStream); workbook.dispose(); // 清理临时文件 }2. 不再把结果存成byte[]返回而是直接写到response的OutputStream里GetMapping(/export) public void exportOrders(RequestParam Date startTime, RequestParam Date endTime, HttpServletResponse response) throws IOException { response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setHeader(Content-Disposition, attachment; filenameorders.xlsx); orderExportService.exportOrders(startTime, endTime, response.getOutputStream()); }这样改完之后不管导出多少数据内存中最多只保留5000条记录 500行的Workbook滑动窗口内存占用基本是恒定的。复盘和反思回过头来看这个问题其实不复杂但有几个点值得注意1. 不要相信数据量不大的假设开发的时候测试环境就那么几百条数据怎么测都不会出问题。但线上用户一选就是三个月的时间范围数据量直接翻了几百倍。做批量操作的时候一定要考虑最坏情况。2. POI的xlsx模式是个内存杀手XSSFWorkbook底层用的是DOM模型整个文档都在内存里。如果要导出大数据量的Excel要么用SXSSFWorkbook流式写入要么考虑换成EasyExcel这类专门优化过大文件导出的库。3. 监控告警要配好这次是运维看到内存飙了才通知的如果告警阈值设得合理比如堆使用率超过80%就报警能更早发现问题。我们后来把告警阈值从90%调到了80%并且加了GC频率的告警。4. 生产环境的dump操作要谨慎jmap打dump会STW堆越大影响越大。更好的做法是用jcmd或者配好自动dump参数或者在预发环境复现问题后再dump。写这篇主要是给自己做个记录顺便也希望能帮到遇到类似问题的朋友。如果你也有过线上OOM的排查经历欢迎在评论区聊聊你的故事尤其是那些排查到最后发现原因很离谱的案例我最喜欢听这种了。
返回列表