记一次悲惨的 Excel 导出事件

>>2020,微服务装逼指南

记一次悲惨的 Excel 导出事件

作者:kid_2412

blog.csdn.net/kid_2412/article/details/76633525

  • 背景

  • 分析过程

  • 看看XSSF和HSSF的区别

  • 解决方案

  • 总结


背景

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

惨绝人寰的场景:

记一次悲惨的 Excel 导出事件
img
记一次悲惨的 Excel 导出事件
img
记一次悲惨的 Excel 导出事件
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,由于在自主化运维平台导出的结果文件被我删除了,所以只能用本机的测试,不过结果现象是相同的。

记一次悲惨的 Excel 导出事件
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上去直接观察就可以了。

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

记一次悲惨的 Excel 导出事件
img

idea也是有插件的:

记一次悲惨的 Excel 导出事件
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;
    }

}

奔跑吧小代码!

整体情况:

记一次悲惨的 Excel 导出事件
img
  1. 内存打满

  2. gc无法回收掉对象

  3. cpu负载非常高

CPU信息:

记一次悲惨的 Excel 导出事件

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

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

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

记一次悲惨的 Excel 导出事件
img

堆信息:

记一次悲惨的 Excel 导出事件
img

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

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

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

记一次悲惨的 Excel 导出事件
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中

记一次悲惨的 Excel 导出事件
img

memory usage optimization in xssf - avoid creating parentless xml beans

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

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

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

总结

  • 首先我们知道了poi性能不高

  • 其次我们需要知道我们所依赖的每个版本的特性和bug

  • 而这次事故也提醒我们,我们的应用系统并不是高可用的!

  • 面对这样的问题,我们能否做好压力测试?在没上线之前就发现这样的问题,以及在线上做好捣乱练习和容灾演练。


Java面试题专栏

【61期】MySQL行锁和表锁的含义及区别(MySQL面试第四弹)
【62期】解释一下MySQL中内连接,外连接等的区别(MySQL面试第五弹)
【63期】谈谈MySQL 索引,B+树原理,以及建索引的几大原则(MySQL面试第六弹)
【64期】MySQL 服务占用cpu 100%,如何排查问题? (MySQL面试第七弹)
【65期】Spring的IOC是啥?有什么好处?
【66期】Java容器面试题:谈谈你对 HashMap 的理解
【67期】谈谈ConcurrentHashMap是如何保证线程安全的?
【68期】面试官:对并发熟悉吗?说说Synchronized及实现原理
【69期】面试官:对并发熟悉吗?谈谈线程间的协作(wait/notify/sleep/yield/join)
【70期】面试官:对并发熟悉吗?谈谈对volatile的使用及其原理


记一次悲惨的 Excel 导出事件


欢迎长按下图关注公众号后端技术精选

记一次悲惨的 Excel 导出事件

原文始发于微信公众号(后端技术精选):记一次悲惨的 Excel 导出事件