来由

项目需要做一个批量数据导出程序供相关部门做数据分析,数据量在百万级将近千万,解决方案为每次5000条分页取出数据,导出到缓冲区实现持续导出csv文件。部分代码如下。

  1. public function exportBatCsv($pager, $fileName,$getDataM,$page_name,$diyparameters)
  2. {
  3. // 设置响应头
  4. header("Content-Type: application/CSV");
  5. header(
  6. "Content-Disposition: attachment; filename=" . $fileName . ".csv"
  7. );
  8. header("Expires: 0");
  9. $nowPage = 1;
  10. $limit = isset($pager['exportPageLimit']) ? $pager['exportPageLimit'] : 5000;
  11. $pager['pageSize'] = $limit;
  12. $recordCount = $pager['recordCount'];
  13. $totalPager = ceil($recordCount/$limit);
  14. $pager['pageCount'] = $totalPager;
  15. $fp = fopen('php://output', 'a');
  16. $exportFunc = $pager['exportAllFunc'];
  17. // 判断一下表头数组是否有数据
  18. if ($pager["exportColumns"] != null
  19. && count($pager["exportColumns"]) > 0
  20. ) {
  21. // 循环写入表头
  22. foreach ($pager["exportColumns"] as $key => $column) {
  23. $head[$key] = iconv("UTF-8", "GBK",$column["title"] );
  24. }
  25. fputcsv($fp, $head);
  26. // 判断表中是否有数据
  27. if ($recordCount > 0) {
  28. for ($nowPage=1; $nowPage <= $totalPager; $nowPage++) {
  29. $pager['nowPage'] = $nowPage;
  30. $_POST['dtGridPager'] = json_encode($pager);
  31. $dataArr = $getDataM->$exportFunc($pager['diyparameters'],1);
  32. // 循环写入表中数据
  33. foreach ($dataArr['datas'] as $record) {
  34. $rs = array();
  35. foreach ($pager["exportColumns"] as $tkey => $column) {
  36. $content = $record[$column["id"]];
  37. // 如果内容未被处理则进行格式化
  38. if (!$pager["exportDataIsProcessed"]) {
  39. $content = $this::formatContent(
  40. $column, $content
  41. );
  42. }
  43. $rs[$tkey] = iconv("UTF-8", "GBK//TRANSLIT//IGNORE",$content );
  44. }
  45. fputcsv($fp, $rs);
  46. unset($rs);
  47. }
  48. ob_flush(); //释放内存
  49. flush();
  50. }
  51. }
  52. }
  53. }

OK,编写完成后,测试环境测试,没问题,上线!然后开始了一场漫长的错误排查之旅!!!!!

错误详情

先说一下正式环境的环境详情
服务器环境:centos6.9+nginx1.14.1+php7.0.32,使用的数据库为线上库的一个只供查询的从库,且没有设置超时。不存在查询超时问题。项目框架为ThinkPHP5.0
上线后,执行导出,导出过程中导出部分数据后chrome浏览器即报错’下载失败,网络错误’,但是查看接口调用HTTP状态依旧是200,没有抛出错误信息!

排错过程

  1. 首先查看程序日志,程序日志没找到有用错误信息。
  2. 更换多个浏览器后依然是无法导出,导出部分数据后就报错。跟浏览器无关!
  3. 更换多台电脑尝试还是无法导出,跟客户端环境无直接关系!
  4. 尝试增加数据查询条件,只导出少量数据。结果导出成功。也就是说导出错误跟数据量有一定关系!
  5. 对上面程序打断点,屏蔽掉fputcsv后,再执行导出,一段时间后报错502。502?难道是超时问题?
  6. 检查nginx和php相关配置,包括 fastcgi_connect_timeout,fastcgi_send_timeout,fastcgi_read_timeout,request_terminate_timeout 以及php.ini中max_execution_time,均已配置为不超时或者超时时间很大,排除超时问题。
  7. 查看nginx服务日志,发现报错信息recv() failed (104: Connection reset by peer) while reading response header from upstream。意思大概是nginx在接受php返回数据时链接 被重置导致无响应中断。WTF?这是什么鬼?好,有问题百度,然后大部分答案都在说是超时问题,然而超时问题已经在上面检查过,绝无可能。
  8. 继续查看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!