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 / 致命错误 / 正常脚本结束 都会触发
register_shutdown_function(fn() => echo "执行了");exit; // 会执行die(); // 会执行$a->test(); // 调用不存在方法致命错误,会执行echo 123; // 正常跑完,也会执行
唯一例外:kill -9 强制杀进程、OOM内核杀死进程,不会触发。
3. 可注册多个,按注册顺序依次执行
register_shutdown_function(fn() => echo "1");register_shutdown_function(fn() => echo "2");// 输出 1 2
如果其中一个里面调用 exit(),后面注册的回调不再执行。
二、输出缓冲区、HTTP响应经典坑(FPM环境高频)
坑1:shutdown 内 echo/print 浏览器看不到
PHP-FPM 正常流程:页面输出 → 发送给客户端 → 执行shutdown回调 哪怕你在shutdown里输出内容,响应早已发送完毕,前端接收不到。 解决: 需要返回前端的信息不能写在shutdown里; shutdown只做日志、埋点、推送、清理等后台任务。
坑2:搭配 fastcgi_finish_request 顺序问题
错误写法:
register_shutdown_function(function(){sleep(3);file_put_contents('log.txt','任务');});fastcgi_finish_request(); // 先断开连接,再执行shutdown,没问题
颠倒就废:
fastcgi_finish_request();register_shutdown_function(fn()=>{});// 已经断开响应后注册的回调依旧会执行,但逻辑不推荐,可读性差
坑3:headers_sent() 无法再设置响应头
shutdown阶段HTTP头部早已发送,header()、setcookie() 全部失效,报警告。
三、错误捕获的致命大坑(最容易踩)
坑1:无法直接捕获致命错误 E_ERROR、E_PARSE、内存溢出
set_error_handler 抓不到致命错误,只能靠 shutdown + error_get_last() 判断:
register_shutdown_function(function () {$err = error_get_last();// 只有存在错误且是致命类型才记录if ($err && in_array($err['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {// 记录崩溃日志、告警}});
坑2:shutdown 回调内部报错不会二次触发自身
如果注册的回调函数内部抛出异常、致命错误,不会再次进入shutdown钩子,错误会直接丢失,日志无记录。 解决:回调内部全部加 try-catch 兜底:
register_shutdown_function(function () {try {// 业务收尾逻辑} catch (Throwable $e) {// 兜底记录回调内部异常}});
四、Session、文件锁、数据库事务坑
坑1:session 已自动关闭,无法读写 $_SESSION
PHP 脚本生命周期内,脚本主体执行完毕后会自动执行 session_write_close(),再走 shutdown。
在 shutdown 里读取/修改 $_SESSION 是空,写入无效。
解决方案:
- 主体代码提前手动关闭会话:
session_write_close(); - 需要在收尾操作session数据,提前存到普通变量里。
坑2:事务已自动回滚/提交
PDO/Mysql 在脚本结束会自动结束事务,shutdown 里无法再执行回滚/提交,只能提前处理事务逻辑。
坑3:文件句柄、连接可能已被回收
脚本结束时PHP会自动回收资源,数据库连接、文件句柄、Redis连接可能已关闭。
- 方案1:提前保存连接实例(不要依赖全局单例自动回收)
- 方案2:shutdown内操作前重连
五、变量、对象、作用域坑
坑1:闭包引用变量生命周期问题
$obj = new Test();register_shutdown_function(function() use ($obj) {$obj->log();});
use 会拷贝/持有引用,不会提前销毁对象,可以正常使用; 但如果是全局静态对象,可能已经被析构。
坑2:不能使用超全局变量的动态更新值
$_GET/$_POST/$_SERVER 在shutdown阶段数据还在,但不建议修改;
请求上下文已经结束,修改无意义。
六、CLI / FPM 环境差异坑
- FPM:单个请求结束触发shutdown,进程复用,全局静态变量残留;
- CLI:整个程序终止才触发;长循环CLI脚本,每次循环结束不会自动触发,只有整体退出才执行;
- 长驻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中做重、慢任务,仅做轻量日志记录。
八、卸载与重复注册坑
- PHP没有 unregister_shutdown_function,一旦注册无法单独删除;
- 多次注册同一个匿名函数,会执行多次;
- 框架多次注册会叠加执行,容易出现重复写日志、重复推送。
九、内存与GC坑
shutdown执行阶段GC垃圾回收已开始,循环引用的对象可能提前被销毁,调用对象方法会报:
Call to a member function xxx() on null
规避:use 强制持有对象引用,不在回调中依赖临时创建的局部对象。
十、生产环境标准安全模板(避坑完整版)
<?php// 提前关闭会话,shutdown无法操作sessionsession_write_close();register_shutdown_function(function () {// 内部全部捕获异常,防止回调报错丢失日志try {$err = error_get_last();// 判断是否致命崩溃if ($err && in_array($err['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {// 记录崩溃日志、告警推送file_put_contents('crash.log', json_encode($err, JSON_UNESCAPED_UNICODE) . PHP_EOL, FILE_APPEND);}// 仅轻量收尾:统计、简短日志,禁止sleep、curl、大查询} catch (Throwable $e) {// 兜底记录回调自身异常file_put_contents('shutdown_err.log', $e->getMessage() . PHP_EOL, FILE_APPEND);}});
十一、总结红线(绝对不要做)
- 不在 shutdown 输出内容、设置 cookie、header;
- 不在 shutdown 读写 session、操作未持久化数据库事务;
- 不放置 sleep、远程curl、大批量耗时任务;
- 不依赖可能被析构的全局业务对象;
- Swoole/Workerman 不要依赖此函数,使用框架原生生命周期事件;
- 回调内部不加 try-catch,丢失错误日志;
- 多个回调内随意调用 exit(),截断后续收尾逻辑。
