记一次悲惨的 Excel 导出事件
背景
話說這個背景挺慘的,京東某系統(tǒng)使用了poi-ooxml-3.5-final做excel導出功能。起初使用該版本的poi的HSSF配合多線程生成excel,沒有任何問題,后來改成了XSSF生成后上線,導出3w條數(shù)據(jù)時,cpu使用率達到了100%,內(nèi)存達到了100%,打死了整個服務器!
慘絕人寰的場景:
線上環(huán)境docker單機配置如下:
-
內(nèi)存:8G
-
cpu:2核
-
jvm:
-
-Xmx:4G
-
-Xms:4G
-
-MaxPerm:256M
- -Xss:256K
- OGC:Parallel Old
- YGC:Parallel Scavenge
-
由于cpu使用率打爆,內(nèi)存打爆,整個服務器處于拒絕服務狀態(tài),而呈現(xiàn)到前端則是應用系統(tǒng)大部分卡死。于是業(yè)務方不斷反復點擊導出按鈕,狀況不斷擴大到集群內(nèi)其他機器上,導致集群出現(xiàn)雪崩現(xiàn)象。監(jiān)控系統(tǒng)頻繁報警,同時慘遭業(yè)務方屠殺。。。
當然我們起初只是升級了版本,同時以為是多線程導致的,改為了單線程生成。當時也沒有分析出問題具體出現(xiàn)在哪里,上線后沒有出現(xiàn)cpu和內(nèi)存打爆現(xiàn)象。但是,問題總要找到根源的,于是我們對這次事故做了回溯。
分析過程
由于服務器已經(jīng)被打死,內(nèi)存那么高,根本無法dump線上堆內(nèi)存,甚至連jstack查看線程棧都無法使用。不過在自主運維平臺中導出了gc信息,發(fā)現(xiàn)eden空間和old空間都被打滿,同時young gc和full gc都非常頻繁,也就是說頻繁gc沒有回收掉任何對象。
下圖為我本機測試的 jstat -gcutil 7068 1000 10,由于在自主化運維平臺導出的結果文件被我刪除了,所以只能用本機的測試,不過結果現(xiàn)象是相同的。
可見eden空間的s0和s1已經(jīng)無法交換了,eden空間已經(jīng)完全打滿,old空間也一樣打滿,yong gc和full gc都非常頻繁,cpu自然使用率高了,不過不足以打滿整個cpu!現(xiàn)在目前定位到了fullgc沒有回收垃圾,那么需要找到內(nèi)存打滿和為啥沒回收的原因。要想找到內(nèi)存打滿的原因肯定需要分析heap空間對象。
那么既然線上已經(jīng)無法導出heap信息了,是不是可以嘗試在本地做這件事?那么倆個問題需要明確:
如何做?
由于問題出現(xiàn)在導出報表,并且已知升級了版本并且改成了單線程導出就解決了,同時之前使用HSSF的時候并沒有出現(xiàn)問題,也證明了業(yè)務代碼沒有問題,問題出現(xiàn)在XSSF的版本和多線程上。所以本地可以模擬poi-ooxml-3.5-FINAL的XSSF進行大量數(shù)據(jù)的導出實驗,同時需要進行多線程導出。
由于不是業(yè)務代碼和業(yè)務數(shù)據(jù)產(chǎn)生的問題,在本地mock數(shù)據(jù)可以使用簡單的大量對象構成的結構進行導出,線上30個列導出,本地測試5個列,線上是本地的6倍,線上的每一行的數(shù)據(jù)量必然要比本地的數(shù)據(jù)量大很多。同時懷疑是poi-ooxml-3.5-FINAL內(nèi)存泄露或內(nèi)存管理出現(xiàn)的問題,那么其實不需要4g內(nèi)存,在2g的內(nèi)存下壓榨到死看看heap中大量的對象是不是poi相關的就可以了。然后再升級下版本,繼續(xù)壓榨一下看看會不會壓死即可。
如何分析?
其實分析很簡單,以往使用線上jmap dump后用mat查看內(nèi)存泄露,現(xiàn)在由于在本地測試了,可以直接用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?{@Testpublic?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名,則默認使用Sheet1XSSFSheet?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]);}//?構建表體數(shù)據(jù)(二維數(shù)組),不能為空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]);elsecell.setCellValue((String)?data[i][j]);}}return?workBook;}}奔跑吧小代碼!
整體情況:
內(nèi)存打滿
gc無法回收掉對象
cpu負載非常高
CPU信息:
大量cpu占用在XSSFCell.setCellValue中
生成excel generateExcel就占據(jù)了所有的cpu
而后,gc回收時間過長導致了:
堆信息:
他喵的全是poi的對象!!!
這里還需要注意的是,需要驗證poi-ooxml-3.5-FINAL在多線程情況下是否會出現(xiàn)這個問題,驗證很簡單,把new Thread去掉,直接在主線程導出。這里直接說明實驗結果,new Thread去了依然內(nèi)存爆滿!
而且觀察測試代碼可以發(fā)現(xiàn),雖然是主線程new Thread創(chuàng)建了個新線程,形似多線程,但是測試數(shù)據(jù)并不存在線程共享問題,沒有在主線程和子線程進行資源競爭,不存在鎖互斥問題。所以排除掉了多線程產(chǎn)生的問題。而且在寫入表格字段值的時候poi也進行了加鎖操作。
看看XSSF和HSSF的區(qū)別
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)
其實區(qū)別就是XSSF支持excel 2007以后的導出,HSSF只支持以前的。excel 2007以后能導出更多的數(shù)據(jù)了。
解決方案
查看poi官網(wǎng)的change log http://poi.apache.org/changes.html ,既然3.5-FINAL的XSSF有問題,向上查找3.5-FINAL之后的XSSF相關字樣的信息,會發(fā)現(xiàn)在3.6中
memory usage optimization in xssf - avoid creating parentless xml beans
在xxsf進行中做了內(nèi)存優(yōu)化 - 避免了創(chuàng)建無父類的xml bean對象
所以得出結論,升級poi-oxxml版本到3.6或者更高版本!
當然,我們的線上環(huán)境已經(jīng)進行了升級。
總結
-
首先我們知道了poi性能不高
-
其次我們需要知道我們所依賴的每個版本的特性和bug
-
而這次事故也提醒我們,我們的應用系統(tǒng)并不是高可用的!
-
面對這樣的問題,我們能否做好壓力測試?在沒上線之前就發(fā)現(xiàn)這樣的問題,以及在線上做好搗亂練習和容災演練。
總結
以上是生活随笔為你收集整理的记一次悲惨的 Excel 导出事件的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 拼多多技术事故复盘,程序员应该学到什么?
- 下一篇: Java 线程的 wait 和 noti