第一次报障

1、执行用例总是提示获取到 dataprovider 获取到的 Object[][] 为空

Object[][]为空.png

2、在本地调试并不能每次复现,重启一下服务器就能好。没有明显的报错日志。

第一次问题定位

1、运行调试,发现总是读取表格文件的这一步

读取表格文件.png

发现这里的错误只捕获了 IOException。于是在抛出错误的时候,将 IOException 调整为 Exception

修改为捕获Exception.png

一开始捕捉到的是一个 NullPointException

于是第一次的解决方案是:加多一个 NullPointException。

加多一个NullPointException.png

由于本地运行正常,所以就上线了并告诉大家修复了问题。

第二次报障

上线后,测试同学小美仍然反馈出执行不了的问题,然后重启之后,原本的文件就能够正常执行了,但是再上传一个新文件执行,同样会报这个错。

第二次问题定位

1、此时发现测试同学小美所使用的表格文件特别大。正常情况下只有 200k 左右的用例文件,执行有问题的文件居然高达 4M。

文件很大.png

考虑可能是 4M 的文件解析带来的问题,

于是在抛出错误的时候,再次调整为 Exception

修改为捕获Exception.png

【重现步骤】

1、先执行一次 4M 的文件的用例

2、执行完成后再上传一个 4M 的用例(上传用例过程在线上执行,直连线上的数据库,然后复制一份用例文件到本地执行)

3、使用新的文件执行用例

此时明确发现,是由于 OOM 内存溢出导致的问题

OOM.png

于是打开 jprofiler 进行分析

1、第一次运行 4m 文件

第一次运行4M.png

再上传一个新的 4m 文件,然后执行用例(上传用例过程在线上执行,直连线上的数据库,然后复制一份用例文件到本地执行)。

直接内存占用高达 1.62G,GC100% 运行,CPU 未出现过载,所以性能瓶颈点主要在内存。判断可能存在内存泄漏。

第二次运行4M.png

GC活动.png

cpu占用.png

另外还发现:超时还可能导致无法连接上 zk,导致后续的测试无法继续执行

[图片上传失败...(image-5f70f9-1548245913902)]

但从上面只能定位到可能是内存泄漏,但未能定位到是什么原因导致,于是我们转向 Live Memory

打开 jprofiler 的 Live Memory,执行如下步骤:

1、先执行两个普通大小用例文件的用例,观察内存中那类占用比较多 -- 此时没发现什么异常(所以没有截图)

2、再执行一个 4M 大小的用例。

发现下图中 AttrXobj、ElementXobj 暴涨。几乎占据了图中 80% 以上的量。

(后续还执行了第二个 4M 文件,但由于直接跑崩了所以没有收集到第二次 4M 文件执行的内存数据)

Live Memory.png

然后搜索 AttrXobj(搜索 AttrXobj),得知该类与 XSSFWorkbook 有关。

搜索结果.png

并且

1、从第一条搜索结果可知(SXSSFWorkbook & XSSFWorkbook 效率比拼)XSSFWorkbook 本身在大数据量下存在性能问题。

image.png

2、在第四条记录上看到了非常类似情况:记一次 FullGC 的排查

image.png

所以基本上定位到问题是处在我们 ExcelUtils 中用到的 XSSFWorkbook 对象身上。

最终解决思路

第一个:像记一次 FullGC 的排查这样限制用户上传文件大小

思考:虽然该方法成本最低。但由于用例文件可能会存放多个 sheet,即使单个 sheet 数据量不超过 100k,整个 excel 文件的总 size 无法预估,且无法解决根本问题,所以先不采取这个方法。

第二个:检查内存泄漏点,解决内存泄漏

从下图中可发现,黄框部分是执行完 GC 之后的内存占用情况,随着用例执行而增多。

内存占用趋势.png

读取表格方法的源码.png

结合两图可以定位到是 XSSFWorkbook 的问题。那我们关注到调用这个类的方法上。

从下图中可以看到,在方法中,有一个 FileInputStream 对象,但是读取了之后没有对应的关闭方法。

excelWBook、excelWsheet 这两个对象是一个静态对象,在执行完操作之后,却没有进行对象的清空。可能会一直存活着。

从代码里也可以看到一个点:excelWBook 这个变量是一个静态变量,第一次读取 4M 文件后,未被释放。然后到了第二次读取的时候,

excelWBook = new XSSFWorkbook(ExcelFile) 

先执行 new XSSFWorkbook(ExcelFile) 再将 new 出来的对象赋值给 excelWBook。所以 此时 excelWBook 占用一波内存,new XSSFWorkbook(ExcelFile) 又要占用一波内存,导致直接内存爆掉了。

修复前内存情况.png

【优化点】

1、将 excelWSheet、excelWBook、cell 从静态变量改为实例变量,释放内存。

2、加一个 try-finally/try-with-resource 关闭 FileInputStream 文件流。

3、使用 SXSSFWorkbook 替换 XSSFWorkbook,降低内存开销。

参考文档

IntelliJ IDEA 集成 JProfiler,入门教程

SXSSFWorkbook & XSSFWorkbook 效率比拼

记一次 FullGC 的排查

修复结果

多次执行 4M 大小的文件响应正常且内存稳定。

修复后内存情况.png

修复后的关键源码

public static Object[][] getTableArray(String FilePath, String SheetName, int totalCols, Boolean isWithTitleRow) throws IOException {
    String[][] tabArray = (String[][])null;
    FileInputStream excelFile = null;

    try {
        excelFile = new FileInputStream(FilePath);
        XSSFWorkbook excelWBook = new XSSFWorkbook(excelFile);
        XSSFSheet excelWSheet = excelWBook.getSheet(SheetName);
        int startRow = 0;
        int startCol = 0;
        int lastRowNum = excelWSheet.getLastRowNum();
        int lastCol = totalCols - 1;
        if (isWithTitleRow) {
            startRow = 1;
        }

        int totalRow = getRealRowNum(excelWSheet, startRow, lastRowNum, startCol, lastCol);
        if (isWithTitleRow) {
            tabArray = new String[totalRow][totalCols];
        } else {
            tabArray = new String[totalRow + 1][totalCols];
        }

        for(int currentRow = startRow; currentRow <= totalRow; ++currentRow) {
            for(int currentCol = startCol; currentCol <= lastCol; ++currentCol) {
                tabArray[currentRow - startRow][currentCol] = getCellData(excelWSheet, currentRow, currentCol);
            }
        }
    } finally {
        if (excelFile != null) {
            excelFile.close();
        }

    }

    return tabArray;
}


↙↙↙阅读原文可查看相关链接,并与作者交流