PHP开发者在工作中经常会遇到,后台系统功能,每开发一个数据查询列表功能,都少不了对应的数据导出功能。PHP的技术栈中,通常会采用PHPExcel这个开源的库来解决数据导出问题。但在实际使用过程中,总是会遇到导出报超时,或超内存的问题,为业务方所诟病。这篇文章,我们分别从超时和超内存两个方面,分析原因,及提出对应的解决方案。
超内存原因
(1)导出内容过多。这是很容易理解的一个原因。导出的列与行越多,文件越大,自然意味着占用更多的内存去存储这些内容。php.ini里设置了memory_limit,限制了内存的使用上限(PHPExcel默认存储的方式为存内存),一旦存储的内存超过这个阈值,程序自然会报错。除了文件大小外,文件内数据存放的格式越丰富,内存也会消耗的越快。
(2)PHPExcel1.7.6官方文档写道
PHPExcel uses an average of about 1k/cell in your worksheets, so large workbooks can quickly use up available memory. Cell caching provides a mechanism that allows PHPExcel to maintain the cell objects in a smaller size of memory, on disk, or in APC, memcache or Wincache, rather than in PHP memory. This allows you to reduce the memory usage for large workbooks, although at a cost of speed to access cell data.PHPExcel平均大概使用1k/单元格的内存,因此大量的单元格内容会很快地耗完可用的内存。从PHPExcel1.7.3版本开始支持cell的缓存方式,也就是说,支持数据存放在缓存中,而不是直接放在有限的内存资源中。可供选择的缓存方式可参考PHPExcel的代码包文件:PHPExcel/CachedObjectStorageFactory.php,比如,我们将数据存储在Memcache中,那么就应该这么写:
$cacheMethod = PHPExcel_CachedObjectStorageFactory::cache_to_memcache;
$cacheSettings = array( 'memcacheServer' => 'localhost',
'memcachePort' => 11211,
'cacheTime' => 600
);
PHPExcel_Settings::setCacheStorageMethod($cacheMethod, $cacheSettings);
$excel = new PHPExcel();注意,这里设置缓存方式的代码要在创建PHPExcel对象之前。如果你也想知道memcache存储数据用的key名称的结构,可以查 PHPExcel/CachedObjectStorage/Memcache.php
(3)优化代码
PHP的内存分配、回收遵循“引用计数 写时复制”的原则,所以在具体代码中,要尽量避免内存浪费,特别是在操作数组变量的时候,可考虑使用备忘录算法,并及时unset释放变量。另外,实际业务代码,一般先获取所有待导出数据,然后赋值给一个大数组,传入PHPExcel。如果这个数组大于可用内存,就会报内存溢出,这种情况下没什么比较好的方法,只能从其他方面做优化改进,比如说引导用户按数据所在时间区间维度分多次导出等等。
超时原因
在使用PHPExcel导出数据的时候,我们以nginx作为web服务器为例,与超时相关的主要影响因子有如下几点:
(1)php.ini中设置的max_execution_time参数。这个参数计算的只是PHP脚本本身执行的时间,不包含sleep、socket交互、数据交互(如:file_get_contents)等的时间。也就是说,如果脚本本身执行时间超过这个参数设定的值,就会报响应超时。但cli sapi强制覆盖了php.ini的max_execution_time设置,最大运行时间被设置为了无限值,也就是说,cli模式下执行php,不会有超时。
(2)php-fpm.conf设置的request_terminate_timeout参数是真正影响cgi sapi模式下php脚本的执行时间。该参数设置为0,代表PHP脚本会一直执行下去。(如果你的导出代码里有file_get_contents函数,可能会导致webserver进程堵塞而无法处理其他请求)
(3)nginx中的fastcgi请求参数:
#指定连接到后端FastCGI 的超时时间
fastcgi_connect_timeout 60;
#向FastCGI 传送请求的超时时间,这个值是指已经完成两次握手后向FastCGI传送请求的超时时间
fastcgi_send_timeout 60;
接收FastCGI 应答的超时时间,这个值是指已经完成两次握手后接收FastCGI应答的超时时间
fastcgi_read_timeout 300;
通过上面的分析可以知道,使用nginx作为webserver的情况下,请求超时的影响因子较多。而同样可以作为web服务器的apache,在httpd.conf也设置了在mod_cgi中等待从CGI脚本输出的时间长度。
综上,我们可以采用缓存方式存储cell数据+异步队列处理导出,解决PHPExcel导出Excel时频繁超时超内存的问题。关于异步导出部分,之前正好看到一篇文章《异步导入导出架构设计》,可供参考。作者用异步队列的方式,做导出,给客户端生成一次导出操作的唯一id,同时根据这个id,客户端用ajax轮询的方式查找生成导出文件的目标地址,这将极大地提升用户体验。在之前的工作经验中,我们也采用了类似做法,为每一次导出行为生成唯一id,客户端用户可以同步拿到这个id,然后去专门的导出列表里按id搜生成的文件,相比,前者的方式在用户体验上有一定程度的优点。在队列设计上,为防止为每一个导出功能创建一个队列,我们可以以系统为维度,每一个系统(比如crm)创建一个队列(比如:命名为crmExportQueue),通过传参识别处理具体业务的导出逻辑(适配器模式),这样就方便了队列的管理,也避免了大量队列闲置(导出功能是一个低频的用户行为)。
参考文献: