FrankenPHP常驻模式下需手动清理:显式释放数据库连接、清空ORM缓存、调用gc_collect_cycles();禁止依赖terminate()做关键清理;严控static/$GLOBALS/闭包/输出缓冲/opcache/APCu跨请求残留。

FrankenPHP 常驻模式(即 worker 模式)下,请求处理完后不会自动重置运行时状态,必须手动收尾,否则 static 变量、全局对象、数据库连接等会跨请求残留,轻则逻辑错乱,重则内存泄漏或连接耗尽。
frankenphp_handle_request 回调末尾必须做三件事
FrankenPHP 的 worker 入口必须用 frankenphp_handle_request() 包裹业务逻辑,而它的回调函数执行完毕后,就是你唯一可控的“请求结束点”。这里要立刻做三件事:
- 显式释放数据库连接:例如 Laravel 中调用
DB::disconnect(),或 PDO 实例设为$pdo = null - 清空 ORM 一级缓存:Doctrine 用户需在事务结束后调用
$entityManager->clear();Laravel Eloquent 不自动清,需手动DB::table('xxx')->where(...)->update(...)类操作前避免复用旧实体 - 强制触发垃圾回收:在回调末尾加
gc_collect_cycles(),不能省略;若入口未显式启用 GC,可前置加gc_enable()
不能依赖 terminate() 做关键清理
Laravel 的中间件 terminate() 方法确实在响应发出后执行,但它只适合轻量日志或异步通知,**不适合释放数据库连接或清空缓存**:
-
terminate()是请求级的,但不保证在所有异常路径下都执行(比如未捕获的 Fatal Error 会跳过) - 它无法访问你在
frankenphp_handle_request()外部初始化的连接句柄(如全局$db) - 若框架容器实例被复用(如
$app在循环外创建),terminate()里调用$app->forgetInstance('request')有效,但$app->resetResolvedInstances()必须配对使用,否则服务别名仍指向旧实例
static 和 $GLOBALS 残留比连接泄漏更隐蔽
很多人盯着 PDO 连接数,却忽略 static $counter 或 $GLOBALS['user_context'] 这类污染源——它们不会报错,但会让第 100 个请求拿到第 1 个请求的用户 ID。
立即学习“PHP免费学习笔记(深入)”;
- 检查所有
static $xxx声明:确认是否真需跨请求保留;否则改用函数参数、php.Context或请求上下文容器传参 - 禁止在 preload 阶段写
$GLOBALS['config'] = loadConfig();,应改为每次请求内按需加载 - 快速排查:在请求末尾加
var_dump(array_keys($GLOBALS));,看是否有意外挂载的键 - 闭包监听器(如
function () use ($bigObject) { })会强持有大对象,removeListener()后仍不释放,优先用[$this, 'method']形式注册
ob_start() 和输出缓冲容易漏关
ob_start() 本身不跨请求残留,但如果你在 preload 阶段误调用,或在回调里漏掉 ob_end_flush(),会导致后续请求响应头错乱、内容重复,甚至 Fatal error: ob_start(): Cannot start output buffering。
- 所有
ob_start()必须在请求处理逻辑内调用,且严格配对ob_end_flush()或ob_end_clean() - 入口处检查
ob_get_level():应为 0;若 > 0,说明上一个请求没清理干净 - 不要在中间件
handle()中开启缓冲然后指望terminate()关闭——两者不在同一执行流中,terminate()无法访问该缓冲上下文
最常被忽略的一点是:FrankenPHP 的 worker 模式下,opcache 和 APCu 缓存也跨请求生效。如果你的代码依赖 apcu_fetch('config'),必须确保这个 key 的更新策略与请求隔离一致——不是 FrankenPHP 的责任,是你代码的责任。



















