⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 http://blog.csdn.net/kid_2412/article/details/76633525 「kid_2412」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

背景

话说这个背景挺惨的,京东某系统使用了poi-ooxml-3.5-final做excel导出功能。起初使用该版本的poi的HSSF配合多线程生成excel,没有任何问题,后来改成了XSSF生成后上线,导出3w条数据时,cpu使用率达到了100%,内存达到了100%,打死了整个服务器!

惨绝人寰的场景:

img

img

img

线上环境docker单机配置如下:

  • 内存:8G

  • cpu:2核

  • jvm:

    • -Xmx:4G
    • -Xms:4G
    • -MaxPerm:256M ​ - -Xss:256K ​ - OGC:Parallel Old ​ - YGC:Parallel Scavenge

由于cpu使用率打爆,内存打爆,整个服务器处于拒绝服务状态,而呈现到前端则是应用系统大部分卡死。于是业务方不断反复点击导出按钮,状况不断扩大到集群内其他机器上,导致集群出现雪崩现象。监控系统频繁报警,同时惨遭业务方屠杀。。。

当然我们起初只是升级了版本,同时以为是多线程导致的,改为了单线程生成。当时也没有分析出问题具体出现在哪里,上线后没有出现cpu和内存打爆现象。但是,问题总要找到根源的,于是我们对这次事故做了回溯。

分析过程

由于服务器已经被打死,内存那么高,根本无法dump线上堆内存,甚至连jstack查看线程栈都无法使用。不过在自主运维平台中导出了gc信息,发现eden空间和old空间都被打满,同时yong gc和full gc都非常频繁,也就是说频繁gc没有回收掉任何对象。

下图为我本机测试的 jstat -gcutil 7068 1000 10,由于在自主化运维平台导出的结果文件被我删除了,所以只能用本机的测试,不过结果现象是相同的。

img

可见eden空间的s0和s1已经无法交换了,eden空间已经完全打满,old空间也一样打满,yong gc和full gc都非常频繁,cpu自然使用率高了,不过不足以打满整个cpu!现在目前定位到了fullgc没有回收垃圾,那么需要找到内存打满和为啥没回收的原因。要想找到内存打满的原因肯定需要分析heap空间对象。

那么既然线上已经无法导出heap信息了,是不是可以尝试在本地做这件事?那么俩个问题需要明确:

如何做?

由于问题出现在导出报表,并且已知升级了版本并且改成了单线程导出就解决了,同时之前使用HSSF的时候并没有出现问题,也证明了业务代码没有问题,问题出现在XSSF的版本和多线程上。所以本地可以模拟poi-ooxml-3.5-FINAL的XSSF进行大量数据的导出实验,同时需要进行多线程导出。

由于不是业务代码和业务数据产生的问题,在本地mock数据可以使用简单的大量对象构成的结构进行导出,线上30个列导出,本地测试5个列,线上是本地的6倍,线上的每一行的数据量必然要比本地的数据量大很多。同时怀疑是poi-ooxml-3.5-FINAL内存泄露或内存管理出现的问题,那么其实不需要4g内存,在2g的内存下压榨到死看看heap中大量的对象是不是poi相关的就可以了。然后再升级下版本,继续压榨一下看看会不会压死即可。

如何分析?

其实分析很简单,以往使用线上jmap dump后用mat查看内存泄露,现在由于在本地测试了,可以直接用jprofiler attach上去直接观察就可以了。

就是这个家伙,当然它是需要破解的:

img

idea也是有插件的:

img

好了,挑出线上的导出代码,写个单元测试

package cn.geapi.service;
import cn.geapi.User;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.xssf.usermodel.XSSFCell;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.junit.Test;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
* Created by kid on 2017/1/9.
*/
public class UserServiceTest {


@Test
public void testLogin() {
int size = 500000;
​ List<Userusers = new ArrayList<(size);
​ User user;
for (int i = 0; i < size; i++) {
​ user = new User();
​ user.setId(Integer.toUnsignedLong(i));
​ user.setAge(i + 10);
​ user.setName("user" + i);
​ user.setRemark(System.currentTimeMillis() + "");
​ user.setSex("男");
​ users.add(user);
​ }

new Thread(() -{
​ String[] columnName = {"用户id", "姓名", "年龄", "性别", "备注"};
​ Object[][] data = new Object[size][5];
int index = 0;
for (User u : users) {
​ data[index][0] = u.getId();
​ data[index][1] = u.getName();
​ data[index][2] = u.getAge();
​ data[index][3] = u.getSex();
​ data[index][4] = u.getRemark();
​ index++;
​ }
​ XSSFWorkbook xssfWorkbook = generateExcel("test", "test", columnName, data);
​ }
​ ).start();

try {
​ Thread.currentThread().join();//等待子线程结束
​ } catch (InterruptedException e) {
​ e.printStackTrace();
​ }
​ }

private static XSSFWorkbook generateExcel(String sheetName, String title, String[] columnName, Object[][] data) {

​ XSSFWorkbook workBook = new XSSFWorkbook();

// 在workbook中添加一个sheet,对应Excel文件中的sheet

// 如果没有给定sheet名,则默认使用Sheet1
​ XSSFSheet sheet;
if (StringUtils.isNotBlank(sheetName)) {
​ sheet = workBook.createSheet(sheetName);
​ } else {
​ sheet = workBook.createSheet();
​ }

// 构建大标题,可以没有
​ XSSFRow headRow = sheet.createRow(0);
​ XSSFCell cell = null;
​ cell = headRow.createCell(0);
​ cell.setCellValue(title);

//大标题行的偏移
int offset = 0;
if (StringUtils.isNotBlank(title)) {
​ offset = 1;
​ }

// 构建列标题,不能为空
​ headRow = sheet.createRow(offset);
for (int i = 0; i < columnName.length; i++) {
​ cell = headRow.createCell(i);
​ cell.setCellValue(columnName[i]);
​ }

// 构建表体数据(二维数组),不能为空
for (int i = 0; i < data.length; i++) {
​ headRow = sheet.createRow(++offset);
for (int j = 0; j < data[0].length; j++) {
​ cell = headRow.createCell(j);
if (data[i][j] instanceof BigDecimal)
​ cell.setCellValue(((BigDecimal) data[i][j]).doubleValue());
else if (data[i][j] instanceof Double)
​ cell.setCellValue((Double) data[i][j]);
else if (data[i][j] instanceof Long)
​ cell.setCellValue((Long) data[i][j]);
else if (data[i][j] instanceof Integer)
​ cell.setCellValue((Integer) data[i][j]);
else if (data[i][j] instanceof Boolean)
​ cell.setCellValue((Boolean) data[i][j]);
else if (data[i][j] instanceof Date)
​ cell.setCellValue((Date) data[i][j]);
else
​ cell.setCellValue((String) data[i][j]);
​ }
​ }
return workBook;
​ }

}

奔跑吧小代码!

整体情况:

img

  1. 内存打满

  2. gc无法回收掉对象

  3. cpu负载非常高

CPU信息:

img

  1. 大量cpu占用在XSSFCell.setCellValue中

  2. 生成excel generateExcel就占据了所有的cpu

而后,gc回收时间过长导致了:

img

堆信息:

img

他喵的全是poi的对象!!!

这里还需要注意的是,需要验证poi-ooxml-3.5-FINAL在多线程情况下是否会出现这个问题,验证很简单,把new Thread去掉,直接在主线程导出。这里直接说明实验结果,new Thread去了依然内存爆满!

而且观察测试代码可以发现,虽然是主线程new Thread创建了个新线程,形似多线程,但是测试数据并不存在线程共享问题,没有在主线程和子线程进行资源竞争,不存在锁互斥问题。所以排除掉了多线程产生的问题。而且在写入表格字段值的时候poi也进行了加锁操作。

img

看看XSSF和HSSF的区别

The supplied data appears to be in the Office 2007+ XML. You are calling the part of POI that deals with OLE2 Office Documents. You need to call a different part of POI to process this data (eg XSSF instead of HSSF)

其实区别就是XSSF支持excel 2007以后的导出,HSSF只支持以前的。excel 2007以后能导出更多的数据了。

解决方案

查看poi官网的change log http://poi.apache.org/changes.html ,既然3.5-FINAL的XSSF有问题,向上查找3.5-FINAL之后的XSSF相关字样的信息,会发现在3.6中

img

memory usage optimization in xssf - avoid creating parentless xml beans

在xxsf进行中做了内存优化 - 避免了创建无父类的xml bean对象

所以得出结论,升级poi-oxxml版本到3.6或者更高版本!

当然,我们的线上环境已经进行了升级。

总结

  • 首先我们知道了poi性能不高
  • 其次我们需要知道我们所依赖的每个版本的特性和bug
  • 而这次事故也提醒我们,我们的应用系统并不是高可用的!
  • 面对这样的问题,我们能否做好压力测试?在没上线之前就发现这样的问题,以及在线上做好捣乱练习和容灾演练。
文章目录
  1. 1. 背景
  2. 2. 分析过程
  3. 3. 看看XSSF和HSSF的区别
  4. 4. 解决方案
  5. 5. 总结