- 背景
- 分析過程
- 看看XSSF和HSSF的區別
- 解決方案
- 總結
背景
話說這個背景挺慘的,京東某系統使用了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 {
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;
}
}
奔跑吧小程式碼!
整體情況:
-
記憶體打滿
-
gc無法回收掉物件
-
cpu負載非常高
CPU資訊:
-
大量cpu佔用在XSSFCell.setCellValue中
-
生成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
- 而這次事故也提醒我們,我們的應用系統並不是高可用的!
- 面對這樣的問題,我們能否做好壓力測試?在沒上線之前就發現這樣的問題,以及線上上做好搗亂練習和容災演練。