歡迎光臨
每天分享高質量文章

記一次悲慘的 Excel 匯出事件

  • 背景
  • 分析過程
  • 看看XSSF和HSSF的區別
  • 解決方案
  • 總結

背景

話說這個背景挺慘的,京東某系統使用了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;
        Listnew ArrayList        User user;
        for (int i = 0; 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             cell = headRow.createCell(i);
            cell.setCellValue(columnName[i]);
        }

        // 構建表體資料(二維陣列),不能為空
        for (int i = 0; i             headRow = sheet.createRow(++offset);
            for (int j = 0; j 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
  • 而這次事故也提醒我們,我們的應用系統並不是高可用的!
  • 面對這樣的問題,我們能否做好壓力測試?在沒上線之前就發現這樣的問題,以及線上上做好搗亂練習和容災演練。
贊(0)

分享創造快樂