来由
项目需要做一个批量数据导出程序供相关部门做数据分析,数据量在百万级将近千万,解决方案为每次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!