scala系统导出大文件问题解决方案

2023-05-07,,

     这周遇到了一个技术难题,我们系统中的导出文件功能虽然早已完成,但是当导出超过8MB的文件时就会提示GC overhead,一开始只把问题定位到缓冲区不够,所以试图找到一条分批查询的方案,经过一定时间的调研就找到了一个看似挺好的方案:

protected void fillHeader(String[] columns, int contentLen, int headNums) {

    tmpwb = new XSSFWorkbook();

    tmpwb = updateDimensionRef(tmpwb, columns.length - 1, contentLen + headNums);

    wb = new SXSSFWorkbook(tmpwb, DEFAULT_ROW_ACCESS_WINDOW_SIZE);

    sh = wb.getSheet(DEFAULT_SHEET_NAME);

    sh.setRandomAccessWindowSize(DEFAULT_ROW_ACCESS_WINDOW_SIZE);

}

    这一方案中,EFAULT_ROW_ACCESS_WINDOW_SIZE指的是缓冲区中存放的行数,超过这个行数就会把文件溢写到磁盘里,这样就起到了动态释放缓冲区的作用,由于原先用的是

XSSFWorkbook,而这一方案需要使用SXSSFWorkbook,而SXSSFWorkbook中没有更新dimension的方法,所以先在XSSFWorkbook中定义好dimension,然后再转换成SXSSFWorkbook.经过这次改动后试验发现,性能确实有所提升,导出15MB左右不成问题.

    本来以为大功告成,可是第二天测试MM再次给我报bug,说导出70MB的大文件仍然报错,而客户需求的最大文件是80MB,我突然意识到问题没有那么简单,革命尚未成功,同志仍需努力.

    我开始研究报的错,Error java.lang.OutOfMemoryError: GC overhead limit exceeded,在StackOverflow上搜了一些帖子,建议调整jvm参数,于是我把-Xmx和-Xms都调成了4g,结果发现这个错误消除了,但又开始报新的错误,504 gateway-timeout,根据上面的提示,结合Google的帖子,我认为问题可能是出在导出数据量太大,导致需要时间长,而我们的超时时间可能设置得太短,觉得有必要调整一下nginx的参数,于是修改了一下nginx.conf中的配置,把proxy_send_timeout和proxy_read_timeout的值都调成了600,延长了nginx后端服务器数据回传时间和等候后端服务器响应时间,由于和系统连接时间无关,因此proxy_connect_timeout 这个参数就不更改了.经过这次更改之后,发现依然会报504 gateway-timeout,这时候意识到可能问题并不是单方面的,开始思考解决方案.

    现在的重点就是要找出导致导出失败的根源,于是在调用这个接口的一系列相关代码,从routes开始,几个关键点都打上log,最后发现在在一个Handler的最后一步还能打印出日志,说明那个Handler已经执行完毕,而从Handler发送消息的那个接收方Actor收到结果后做的第一件事情就是打印一条日志,但是那一条一直没有打印出来,那么问题就定位到了Actor传送消息的过程,一开始只想到可能是Akka消息传递过程的超时,于是延长了系统中每一个接口的超时时间,之后没有再报接口的Ask TimeOut,但是仍然有504 gateway-timeout,由于涉及到akka通信机制,所以我很自然地联想到akka的参数是否可以设置一下,由于我们传输的是全表查询结果,量非常大,因此想到是否把容量类参数的值设置得大一点,maximum-frame-size这个参数表示最大允许传输的数据量,另外send-buffer-size和receive-buffer-size这两个参数也是限制消息传递大小的,于是把这三个参数都调大,基本上问题定位到了消息传递的大小限制,因为我认为超时时间已经设置得很长了,按照传递消息的速度来看,不至于这么久还没传完,因此唯一的可能性是本身就不允许传递大数据量,而这些一般都可以在参数中设置,这么一想,我满以为问题终于快得到解决了.

    令人遗憾的是,问题并没有因为调参而解决了,后来我一度还尝试了把maximum-payload-bytes这个参数的值也调大,发现依然无济于事,似乎又陷入了迷茫当中.但是我依然没有放弃寻找答案,这个时候发现一篇帖子:http://stackoverflow.com/questions/31038115/akka-net-sending-huge-messages-maximum-frame-size ,上面那个程序员也遇到了类似的问题,根据他的描述,我推断出可能akka消息传输是存在一个上限的,大于这个上限的话设置的参数也会是无效的,但是这一点我还没有去证明,如果要证明这些,需要研究一下akka的源码,愿各位大神们有空做个分享,好,就是你啦~~.回到那个问题,那个帖子的楼主这么描述:Also, if the message is being dropped, is there any way to get notified about it? Sometimes it sends a Dissassociated error and sometimes it just sits doing nothing. log-frame-size-exceeding = on and log-buffer-size-exceeding = 50000 settings do not seem to have an effect.

    这个时候,下面有一个无名神僧道出了天机,让我恍然大悟,他说:In general it's a bad idea to push big portions of data over the wire at once. It's a lot better to split them into smaller parts and send one by one (this also makes a retry policy less expensive, if it's necessary). If you want to keep your actor logic unaware of transport details, you may abstract it by defining a specialized pair of actors, whose only job will be to split/join big messages.

    我突然意识到,akka设计者的初衷可能只是想通过actor来传送消息,并没有让你们通过这种传送消息的机制直接传送大批量的数据,这种全表查询结果写入到大文件并且传送的问题还是更适合使用scp来传送,或者用rz,sz也可以实现文件的收发,或者用一个python的插件也可以实现在网页上的秒传,其中scp和那个我记不清名字的python插件(工作文档有记录)是我用过的体验比较好的两个文件传输工具,可以轻松传输hive和HBase等导出的大批量数据,而Akka或许目前的设计只是用来传递短信息而已,就像一条微博只能输入140个字,你要表达一大段内容,只能使用博客和日志等功能,本身面向的用途和使用场景就是不一样的.

    数据切分,用多线程来解决问题,也是一个思路,同事建议我这么做,但我发现多线程存在同步的问题,读取的顺序必须完全保持一致,我们的产品下周就要交付了,同时测试MM如果要再抽出额外时间测大量多线程问题,大家压力都会比较大,而且留给我们的时间也不多了.当然最主要的还是我考虑到目前文件也不算特别大,能传输100MB已经可以满足客户需求,用多线程会消耗更多系统字段,而且创建每个线程,以及对线程的管理也消耗时间,综合考虑,觉得暂时没有必要使用多线程.既然问题卡在Actor传送消息这块,那么我是否可以把处理逻辑都放在一个Handler上,包括全表查询,查询结果写入Excel以及Excel的导出,这一系列的流程都在一个Handler里搞定,最后只给之前的那个Actor发送是否导出成功的消息,同时回传下载的路径,在那个Actor里,只需要做下载这一步就可以了.说干就干,昨天下午把这块代码改成了我想要的样子,改完之后在本地做了测试,目前导出100MB的文件已经不成问题,时间也就3分钟,最多4分钟,如果有一个进度条,用户应该是很容易接受这样的速度的,毕竟平时我们用其他系统导出文件也都需要时间,至此,这个问题算是告一段落了.

    这次解决问题的过程,从找到一个方案,由于导出更大文件还是不行而否定,再到后来定位问题,定位问题后发现这个方案行不通,最后换方案,试验成功,我发现自己还有许多时间可以优化,主要是要缩短试错的成本,要在较短的时间内能够发现一条路走不通,然后换一条路.路漫漫其修远兮,吾将上下而求索.

《scala系统导出大文件问题解决方案.doc》

下载本文的Word格式文档,以方便收藏与打印。