背景
話說這個背景挺慘的,京東某系統使用了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
- 而這次事故也提醒我們,我們的應用系統並不是高可用的!
- 面對這樣的問題,我們能否做好壓力測試?在沒上線之前就發現這樣的問題,以及在線上做好搗亂練習和容災演練。