来由
项目需要做一个批量数据导出程序供相关部门做数据分析,数据量在百万级将近千万,解决方案为每次5000条分页取出数据,导出到缓冲区实现持续导出csv文件。部分代码如下。
public function exportBatCsv($pager, $fileName,$getDataM,$page_name,$diyparameters){// 设置响应头header("Content-Type: application/CSV");header("Content-Disposition: attachment; filename=" . $fileName . ".csv");header("Expires: 0");$nowPage = 1;$limit = isset($pager['exportPageLimit']) ? $pager['exportPageLimit'] : 5000;$pager['pageSize'] = $limit;$recordCount = $pager['recordCount'];$totalPager = ceil($recordCount/$limit);$pager['pageCount'] = $totalPager;$fp = fopen('php://output', 'a');$exportFunc = $pager['exportAllFunc'];// 判断一下表头数组是否有数据if ($pager["exportColumns"] != null&& count($pager["exportColumns"]) > 0) {// 循环写入表头foreach ($pager["exportColumns"] as $key => $column) {$head[$key] = iconv("UTF-8", "GBK",$column["title"] );}fputcsv($fp, $head);// 判断表中是否有数据if ($recordCount > 0) {for ($nowPage=1; $nowPage <= $totalPager; $nowPage++) {$pager['nowPage'] = $nowPage;$_POST['dtGridPager'] = json_encode($pager);$dataArr = $getDataM->$exportFunc($pager['diyparameters'],1);// 循环写入表中数据foreach ($dataArr['datas'] as $record) {$rs = array();foreach ($pager["exportColumns"] as $tkey => $column) {$content = $record[$column["id"]];// 如果内容未被处理则进行格式化if (!$pager["exportDataIsProcessed"]) {$content = $this::formatContent($column, $content);}$rs[$tkey] = iconv("UTF-8", "GBK//TRANSLIT//IGNORE",$content );}fputcsv($fp, $rs);unset($rs);}ob_flush(); //释放内存flush();}}}}
OK,编写完成后,测试环境测试,没问题,上线!然后开始了一场漫长的错误排查之旅!!!!!
错误详情
先说一下正式环境的环境详情
服务器环境:centos6.9+nginx1.14.1+php7.0.32,使用的数据库为线上库的一个只供查询的从库,且没有设置超时。不存在查询超时问题。项目框架为ThinkPHP5.0
上线后,执行导出,导出过程中导出部分数据后chrome浏览器即报错’下载失败,网络错误’,但是查看接口调用HTTP状态依旧是200,没有抛出错误信息!
排错过程
- 首先查看程序日志,程序日志没找到有用错误信息。
- 更换多个浏览器后依然是无法导出,导出部分数据后就报错。跟浏览器无关!
- 更换多台电脑尝试还是无法导出,跟客户端环境无直接关系!
- 尝试增加数据查询条件,只导出少量数据。结果导出成功。也就是说导出错误跟数据量有一定关系!
- 对上面程序打断点,屏蔽掉fputcsv后,再执行导出,一段时间后报错502。502?难道是超时问题?
- 检查nginx和php相关配置,包括 fastcgi_connect_timeout,fastcgi_send_timeout,fastcgi_read_timeout,request_terminate_timeout 以及php.ini中max_execution_time,均已配置为不超时或者超时时间很大,排除超时问题。
- 查看nginx服务日志,发现报错信息recv() failed (104: Connection reset by peer) while reading response header from upstream。意思大概是nginx在接受php返回数据时链接 被重置导致无响应中断。WTF?这是什么鬼?好,有问题百度,然后大部分答案都在说是超时问题,然而超时问题已经在上面检查过,绝无可能。
- 继续查看php-fpm日志,发现php-fpm报错 exited on signal 11 (SIGSEGV) 内核抛出中断信号。查找PHP官方对该信号的解释如下
SIGSEGV —- Segment Fault. The possible cases of your encountering this error are:
1.buffer overflow —- usually caused by a pointer reference out of range.
2.stack overflow —- please keep in mind that the default stack size is 8192K.
3.illegal file access —- file operations are forbidden on our judge system.
意思是无非有三种可能:
<1>.缓冲区溢出
<2>.堆溢出
<3>.内存溢出或者操作不合法内存
结合nginx错误信息,错误原因应该就是PHP-FPM发生以上错误。导致PHP-FPM主进程重启,而Nginx对应的进 程获取不到PHP-FPM的返回信息,抛出recv() failed错误!
还是去检查程序,因为数据量大,在查询以及输出方面都有注意释放内存。并且php这种语言几乎 不直接操作内 存,如果有堆溢出,那问题可能就不在程序或者nginx本身,而在php。会不会是使 用的PHP版本内核存在bug?
9 .尝试将正式环境做停机维护,将PHP版本更新为PHP7.1。执行导出,正常!!!!
错误总结
在较大数据量的操作过程中,PHP7.0.32版本PHP在一些堆栈或者操作系统缓冲区上面的bug,会导致一些溢出问题!但是PHP官方的更新日志和问题记录中并没有提及此问题。我也只是将PHP版本更新到7.1,该问题就不存在了!也只能暂时认为是PHP7.0.32的一个未知BUG!
