由excel导出引起的cpu 100% 和gc 的问题

本文分享了一次由大量数据导出至Excel导致的CPU使用率异常飙升及JVM内存不足的问题排查过程。起初,前端导出功能在处理小规模数据时表现正常,但在面对大规模数据时遭遇瓶颈,表现为页面卡顿和异常响应。通过监测CPU使用率、分析GC日志和线程状态,最终定位到问题根源在于Excel工具类中单元格样式的不当创建,导致内存资源过度消耗。通过对代码进行优化,实现了对单元格样式的复用,有效缓解了内存压力,恢复了系统的稳定运行。

大家好,我是烤鸭:
    记一次 由excel导出 导致的cpu飙升200%,jvm 内存不足。

1.  场景复现


    前端页面导出Excel,之前导出4,5W条数据都没什么问题的。
    今天业务突然反馈说导出不了了,我试着导出了2w数据,发现页面卡住了,
    没有响应了,查日志,报错如下。

java.lang.IllegalStateException: Cannot call sendRedirect() after the response has been committed
	at org.apache.catalina.connector.ResponseFacade.sendRedirect(ResponseFacade.java:488)
	at javax.servlet.http.HttpServletResponseWrapper.sendRedirect(HttpServletResponseWrapper.java:138)

     查看了导出方法,发现如果repsonse在响应过程中抛出异常的话,就会有类似的问题。

     
     正常的话,因为需要把错误信息带到页面,catch之后再return到对应的页面。
     但如果是上图所示的write方法,如果这地方报异常,就会出现
     Cannot call sendRedirect() after the response has been committed 的异常。

     输出文件的代码如下,如果这时候异常。

/**
	 * 输出到客户端
	 * @param fileName 输出文件名
	 */
	public ExportExcel write(HttpServletResponse response, String fileName) throws IOException{
		response.reset();
        response.setContentType("application/octet-stream; charset=utf-8");
        response.setHeader("Content-Disposition", "attachment;fileName=" + new String(fileName.getBytes("GBK"), "iso-8859-1")); //中文文件名处理
        write(response.getOutputStream());
		return this;
	}

    知道这是写法的问题,但是并没有什么好的办法解决。
    因为 异常需要通过addMessage(redirectAttributes, e.getMessage()) 带到重定向的页面。

    但是转念一想,这个问题不应该在这地方出现,因为之前用了很长时间是没有这个问题的。

2.    寻找问题

    再次执行导出的时候,监测了一下cpu,为啥,第六感吧。

top    #查看cpu进程运行
ps -ef|grep tomcat_xxx    #查看tomcat 的 pid
	

  pid 是 19021

  发现 19021 的进程爆表了...如下图

  

jstat -gcutil 19021 5000    #每隔5秒打印一次gc  

 又去看了gc的情况,每隔5秒打印一次gc,各个空间全都满了。而且YGC和FGC的频次和时间在迅速增加。(下图中的时间还没到满的时候,后来确实都100了)

还好,服务器还撑得住,应该是数据量不是特别大,没有报java.lang.OutOfMemoryError。

jmap -heap 19021    #查看堆内存详细信息
jstack 19021    #查看线程问题(是否死锁)

  其实我的问题在这就解决了,想了想最近改动过的代码,基本就锁定了问题,导出时候的问题,
  后来查了excel工具类的代码提交,发现在创建cell单元格的时候,没有判断样式是否存在,
  只要是空单元格,就新创建一个单元格样式。
  改之前:

CellStyle style = wb.createCellStyle();
cell.setCellStyle(style);

  改之后:

    if (val != null){
		CellStyle style = styles.get("data_column_"+column);
		if (style == null){
			style = wb.createCellStyle();
			style.cloneStyleFrom(styles.get("data"+(align>=1&&align<=3?align:"")));
	        style.setDataFormat(wb.createDataFormat().getFormat(cellFormatString));
			styles.put("data_column_" + column, style);
		}
		cell.setCellStyle(style);
	}

    不要小瞧了这个判空,原来的写法会多创建2w行*20列=40w个对象,导致各个空间的满了原因。
    如果还没找到问题的话,就输出gc日志,慢慢查。

jmap -histo 9021>xxx.log    #输出gc日志到文件

    
3.    总结

    如果是gc或者内存问题。

    获取当前进程pid

    ps -ef|grep tomcat_xxx 或者 ps -ef|grep java 或者 jps
    查看cpu利用率 
    top
    监测gc
    jstat -gcutil pid 5000
    查看堆内存详细信息
    jmap -heap pid
    查看是否死锁情况
    jstack pid
    还找不到的话,输出gc的日志慢慢找
    jmap -histo pid>xxx.logs
 

<think> 我们之前讨论的是千万数据Excel导出的优化方法。由于Excel在处理大量数据时存在性能瓶颈(如内存占用高、速度慢),因此需要采用特殊的技术手段。以下是几种优化方案: 1. **分批次处理与导出**:避免一次性加载所有数据到内存。可以采用分页查询或分段读取的方式,每次处理一定数量(例如1万条)的数据,并分批写入Excel文件。这样可以显著减少内存压力。 2. **使用流式写入**:传统的POI库(如XSSF)在写入大数据量时容易内存溢出。可以使用SXSSFWorkbook(POI的流式API),它采用滑动窗口机制,只保留部分数据在内存中,其余写入临时文件,从而控制内存使用。 3. **优化数据格式**: - 避免使用复杂的样式(如合并单元格、过多的字体颜色等),因为样式会占用大量内存。 - 使用数据类型优化,例如将字符串转为数值或日期类型,减少存储空间。 4. **多线程异步导出**:将数据分割成多个部分,利用多线程同时处理多个分段,最后合并。注意:Excel写入操作通常不是线程安全的,因此需要谨慎设计(例如每个线程写入不同的临时文件,最后合并)。 5. **采用CSV格式代替XLSX**:CSV是纯文本格式,没有样式,体积小且读写速度快。如果用户接受CSV,优先使用CSV导出。若必须用Excel,再考虑其他优化。 6. **使用专用工具或数据库导出**:如果数据来自数据库,可以直接用数据库的导出功能(如MySQL的`SELECT ... INTO OUTFILE`)导出为CSV,再由程序转换为Excel(如果需要)。或者使用专业的数据导出工具(如Apache POI的替代品:EasyExcel等)。 7. **压缩导出文件**:导出后对文件进行压缩(如ZIP),减少传输时间。 实际操作中,推荐结合使用分批次处理SXSSFWorkbook。下面是一个使用Java POI SXSSFWorkbook的示例代码: ```java // 使用SXSSFWorkbook,设置滑动窗口大小为100行 try (SXSSFWorkbook workbook = new SXSSFWorkbook(100)) { Sheet sheet = workbook.createSheet("数据"); // 模拟从数据库分页查询 int pageSize = 10000; int page = 0; while (true) { List<Data> dataList = queryDataFromDatabase(page, pageSize); // 自定义查询方法 if (dataList.isEmpty()) break; // 写入当前页数据 for (int i = 0; i < dataList.size(); i++) { Row row = sheet.createRow(page * pageSize + i); // 填充单元格... } page++; } // 写入到文件 try (FileOutputStream out = new FileOutputStream("large_data.xlsx")) { workbook.write(out); } // 清理临时文件(SXSSFWorkbook会在写入时生成临时文件,使用完毕后需要清理) workbook.dispose(); } ``` 注意:实际应用中,还需考虑异常处理、资源释放等。 §§相关问题§§ 1. 除了SXSSFWorkbook,还有哪些Java库可以高效导出Excel? 2. 如何实现Excel大文件的分段下载? 3. 在Web应用中,如何避免导出千万数据时请求超时? </think> ### 千万数据Excel导出优化方案 处理千万级数据导出时需解决**内存溢出**、**性能瓶颈****文件体积**三大问题。以下是已验证的优化方案: #### 一、核心技术优化 1. **流式处理(推荐)** - 使用Apache POI的`SXSSFWorkbook`,仅保留指定行数在内存中: ```java SXSSFWorkbook workbook = new SXSSFWorkbook(1000); // 仅缓存1000行 ``` - 每写入$N$行自动刷新到磁盘临时文件 - 内存消耗稳定在$O(1)$级别[^1] 2. **分批次导出** - 采用分页查询:`SELECT * FROM table LIMIT offset, batch_size` - 推荐批次大小:5~10万行/批 - 公式计算总页数:$total\_batches = \lceil \frac{total\_rows}{batch\_size} \rceil$ 3. **异步导出** ```mermaid graph LR A[用户请求导出] --> B(后台任务队列) B --> C{生成进度监控} C --> D[完成时邮件通知] C --> E[支持中途取消] ``` #### 二、性能提升策略 1. **数据预处理** - 禁用样式引擎:`cell.setCellStyle(null)` - 提前转换数据类型:避免运行时$O(n)$转换 - 压缩文本:将重复值映射为数字ID 2. **格式优化** - 优先使用`.xlsx`而非`.xls`(节省40%空间) - 禁用公式:使用预计算结果 - 移除空行列:减少$file\_size = k \times row\_count \times col\_count$ 3. **内存管理** - 设置JVM参数:`-XX:+UseG1GC -Xmx4g` - 强制GC回收:每10万行调用`System.gc()` #### 三、替代方案(大数据量首选) 1. **CSV分段导出** ```python # Python示例 with open('output.csv', 'w') as f: while has_more_data: batch = fetch_batch(limit=100000) f.write('\n'.join(','.join(row) for row in batch)) ``` - 速度提升$10\times$以上 - 配合`split -l 500000`分割文件 2. **数据库直导** - MySQL:`SELECT ... INTO OUTFILE` - PostgreSQL:`COPY table TO '/path.csv'` #### 四、部署实践 1. **服务器配置** ```ini # Nginx优化 proxy_max_temp_file_size 10240m; proxy_buffers 16 128k; ``` 2. **监控指标** | 指标 | 阈值 | 监控工具 | |---|---|----| | 内存占用 | <70% | Grafana | | 磁盘IO | <100MB/s | Prometheus | | CPU负载 | <80% | ELK | > ⚠️ 注意:单Excel文件超500万行可能损坏,建议自动分割为多文件
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

烤鸭的世界我们不懂

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值