fastcgi_finish_request(不是注册回调,但经常和 shutdown 配套)

提前断开 HTTP 连接,页面返回后后台继续跑代码,属于后置异步执行,常搭配 register_shutdown_function 做双重兜底。

register_shutdown_function

触发时机:脚本无论正常结束、exit/die、致命错误,请求销毁前统一执行
特点:可注册多个,按注册顺序执行;能捕获致命错误(配合 error_get_last())
典型场景:全局日志、报错兜底、后置统计、资源释放

register_shutdown_function 完整避坑指南

一、执行时机与顺序大坑

1. 它是脚本最后执行的逻辑,晚于对象析构 __destruct

执行顺序: 未捕获异常回调 → 错误处理器 → 对象 __destruct()shutdown 注册函数 坑:

  • 在 shutdown 里操作数据库/Redis,如果你在类析构里已经关闭了连接,会报连接失效;
  • 不要依赖业务对象实例,很可能已经被销毁。

2. exit / die / 致命错误 / 正常脚本结束 都会触发

  1. register_shutdown_function(fn() => echo "执行了");
  2. exit; // 会执行
  3. die(); // 会执行
  4. $a->test(); // 调用不存在方法致命错误,会执行
  5. echo 123; // 正常跑完,也会执行

唯一例外:kill -9 强制杀进程、OOM内核杀死进程,不会触发。

3. 可注册多个,按注册顺序依次执行

  1. register_shutdown_function(fn() => echo "1");
  2. register_shutdown_function(fn() => echo "2");
  3. // 输出 1 2

如果其中一个里面调用 exit()后面注册的回调不再执行

二、输出缓冲区、HTTP响应经典坑(FPM环境高频)

坑1:shutdown 内 echo/print 浏览器看不到

PHP-FPM 正常流程:页面输出 → 发送给客户端 → 执行shutdown回调 哪怕你在shutdown里输出内容,响应早已发送完毕,前端接收不到。 解决: 需要返回前端的信息不能写在shutdown里; shutdown只做日志、埋点、推送、清理等后台任务。

坑2:搭配 fastcgi_finish_request 顺序问题

错误写法:

  1. register_shutdown_function(function(){
  2. sleep(3);
  3. file_put_contents('log.txt','任务');
  4. });
  5. fastcgi_finish_request(); // 先断开连接,再执行shutdown,没问题

颠倒就废:

  1. fastcgi_finish_request();
  2. register_shutdown_function(fn()=>{});
  3. // 已经断开响应后注册的回调依旧会执行,但逻辑不推荐,可读性差

坑3:headers_sent() 无法再设置响应头

shutdown阶段HTTP头部早已发送,header()setcookie() 全部失效,报警告。

三、错误捕获的致命大坑(最容易踩)

坑1:无法直接捕获致命错误 E_ERROR、E_PARSE、内存溢出

set_error_handler 抓不到致命错误,只能靠 shutdown + error_get_last() 判断:

  1. register_shutdown_function(function () {
  2. $err = error_get_last();
  3. // 只有存在错误且是致命类型才记录
  4. if ($err && in_array($err['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
  5. // 记录崩溃日志、告警
  6. }
  7. });

坑2:shutdown 回调内部报错不会二次触发自身

如果注册的回调函数内部抛出异常、致命错误,不会再次进入shutdown钩子,错误会直接丢失,日志无记录。 解决:回调内部全部加 try-catch 兜底:

  1. register_shutdown_function(function () {
  2. try {
  3. // 业务收尾逻辑
  4. } catch (Throwable $e) {
  5. // 兜底记录回调内部异常
  6. }
  7. });

四、Session、文件锁、数据库事务坑

坑1:session 已自动关闭,无法读写 $_SESSION

PHP 脚本生命周期内,脚本主体执行完毕后会自动执行 session_write_close(),再走 shutdown。 在 shutdown 里读取/修改 $_SESSION 是空,写入无效。 解决方案:

  1. 主体代码提前手动关闭会话:session_write_close();
  2. 需要在收尾操作session数据,提前存到普通变量里。

坑2:事务已自动回滚/提交

PDO/Mysql 在脚本结束会自动结束事务,shutdown 里无法再执行回滚/提交,只能提前处理事务逻辑。

坑3:文件句柄、连接可能已被回收

脚本结束时PHP会自动回收资源,数据库连接、文件句柄、Redis连接可能已关闭。

  • 方案1:提前保存连接实例(不要依赖全局单例自动回收)
  • 方案2:shutdown内操作前重连

五、变量、对象、作用域坑

坑1:闭包引用变量生命周期问题

  1. $obj = new Test();
  2. register_shutdown_function(function() use ($obj) {
  3. $obj->log();
  4. });

use 会拷贝/持有引用,不会提前销毁对象,可以正常使用; 但如果是全局静态对象,可能已经被析构。

坑2:不能使用超全局变量的动态更新值

$_GET/$_POST/$_SERVER 在shutdown阶段数据还在,但不建议修改; 请求上下文已经结束,修改无意义。

六、CLI / FPM 环境差异坑

  1. FPM:单个请求结束触发shutdown,进程复用,全局静态变量残留;
  2. CLI:整个程序终止才触发;长循环CLI脚本,每次循环结束不会自动触发,只有整体退出才执行;
  3. 长驻Swoole/Workerman:普通 register_shutdown_function 无效! Swoole 有自己的生命周期回调(onWorkerStop),原生shutdown钩子不会在协程/worker退出时触发。

七、超时与进程阻塞坑

坑1:shutdown 仍受 max_execution_time 限制

哪怕你写了 set_time_limit(0),脚本主体超时后进入shutdown,剩余执行时间很短,大量耗时逻辑会被强制终止。 解决:耗时后置任务放到 fastcgi_finish_request 前面执行,或丢队列异步。

坑2:大量阻塞IO(sleep、curl)容易被运维杀掉

FPM进程池数量有限,如果shutdown里大量sleep、远程请求,会占满进程,导致服务无响应。 禁止在shutdown中做重、慢任务,仅做轻量日志记录。

八、卸载与重复注册坑

  1. PHP没有 unregister_shutdown_function,一旦注册无法单独删除;
  2. 多次注册同一个匿名函数,会执行多次;
  3. 框架多次注册会叠加执行,容易出现重复写日志、重复推送。

九、内存与GC坑

shutdown执行阶段GC垃圾回收已开始,循环引用的对象可能提前被销毁,调用对象方法会报: Call to a member function xxx() on null 规避:use 强制持有对象引用,不在回调中依赖临时创建的局部对象。

十、生产环境标准安全模板(避坑完整版)

  1. <?php
  2. // 提前关闭会话,shutdown无法操作session
  3. session_write_close();
  4. register_shutdown_function(function () {
  5. // 内部全部捕获异常,防止回调报错丢失日志
  6. try {
  7. $err = error_get_last();
  8. // 判断是否致命崩溃
  9. if ($err && in_array($err['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
  10. // 记录崩溃日志、告警推送
  11. file_put_contents('crash.log', json_encode($err, JSON_UNESCAPED_UNICODE) . PHP_EOL, FILE_APPEND);
  12. }
  13. // 仅轻量收尾:统计、简短日志,禁止sleep、curl、大查询
  14. } catch (Throwable $e) {
  15. // 兜底记录回调自身异常
  16. file_put_contents('shutdown_err.log', $e->getMessage() . PHP_EOL, FILE_APPEND);
  17. }
  18. });

十一、总结红线(绝对不要做)

  1. 不在 shutdown 输出内容、设置 cookie、header;
  2. 不在 shutdown 读写 session、操作未持久化数据库事务;
  3. 不放置 sleep、远程curl、大批量耗时任务;
  4. 不依赖可能被析构的全局业务对象;
  5. Swoole/Workerman 不要依赖此函数,使用框架原生生命周期事件;
  6. 回调内部不加 try-catch,丢失错误日志;
  7. 多个回调内随意调用 exit(),截断后续收尾逻辑。