記一次悲慘的excel導出事件

背景

話說這個背景挺慘的,京東某系統使用了poi-ooxml-3.5-final做excel導出功能。起初使用該版本的poi的HSSF配合多線程生成excel,沒有任何問題,後來改成了XSSF生成後上線,導出3w條數據時,cpu使用率達到了100%,內存達到了100%,打死了整個服務器!
這裏寫圖片描述

慘絕人寰的場景:
這裏寫圖片描述
這裏寫圖片描述
這裏寫圖片描述
線上環境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,由於在自主化運維平臺導出的結果文件被我刪除了,所以只能用本機的測試,不過結果現象是相同的。

這裏寫圖片描述

可見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上去直接觀察就可以了。
    就是這個傢伙,當然它是需要破解的:

這裏寫圖片描述
idea也是有插件的:
這裏寫圖片描述

好了,挑出線上的導出代碼,寫個單元測試

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<User> users = 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;
    }

}

奔跑吧小代碼!

整體情況:
這裏寫圖片描述
1. 內存打滿
2. gc無法回收掉對象
3. cpu負載非常高

CPU信息:

這裏寫圖片描述
1. 大量cpu佔用在XSSFCell.setCellValue中
2. 生成excel generateExcel就佔據了所有的cpu

而後,gc回收時間過長導致了:

這裏寫圖片描述

堆信息:

這裏寫圖片描述

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

這裏寫圖片描述

這裏還需要注意的是,需要驗證poi-ooxml-3.5-FINAL在多線程情況下是否會出現這個問題,驗證很簡單,把new Thread去掉,直接在主線程導出。這裏直接說明實驗結果,new Thread去了依然內存爆滿!
而且觀察測試代碼可以發現,雖然是主線程new Thread創建了個新線程,形似多線程,但是測試數據並不存在線程共享問題,沒有在主線程和子線程進行資源競爭,不存在鎖互斥問題。所以排除掉了多線程產生的問題。而且在寫入表格字段值的時候poi也進行了加鎖操作。
這裏寫圖片描述

看看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中

這裏寫圖片描述

memory usage optimization in xssf - avoid creating parentless xml beans
在xxsf進行中做了內存優化 - 避免了創建無父類的xml bean對象

所以得出結論,升級poi-oxxml版本到3.6或者更高版本!

當然,我們的線上環境已經進行了升級。

總結

  • 首先我們知道了poi性能不高
  • 其次我們需要知道我們所依賴的每個版本的特性和bug
  • 而這次事故也提醒我們,我們的應用系統並不是高可用的!
  • 面對這樣的問題,我們能否做好壓力測試?在沒上線之前就發現這樣的問題,以及在線上做好搗亂練習和容災演練。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章