PHP热更新本质不是不重启进程,而是通过Swoole/RoadRunner等实现worker平滑重启;opcache刷新仅能加载新代码但无法更新已编译常量或类定义,eval/runkit等方案在生产环境不可用。

PHP 热更新本质是“不重启 PHP 进程”?先认清现实
PHP 本身没有原生热更新机制,php-fpm 或 Apache mod_php 模式下,代码变更后必须等进程重载或重启才能生效——这不是 bug,是设计使然。所谓“热更新”,实际是绕过 PHP 进程生命周期限制的工程妥协方案,核心思路只有两个:用外部服务托管逻辑,或靠进程管理器自动拉起新实例。强行在运行中的 opcache 里“刷新”文件,只会触发缓存失效,但无法保证函数/类定义实时替换(尤其涉及继承、trait、常量时极易出错)。
用 opcache + file_update_protection=false 实现“伪热更”
这是最常见也最容易翻车的操作。开启 opcache.enable=1 后,默认 opcache.validate_timestamps=1 会让 PHP 每次请求都检查文件修改时间;设为 0 能跳过检查,但必须配合手动触发 opcache_invalidate() 或 opcache_reset() 才能加载新代码。
-
opcache.validate_timestamps=0必须配合opcache.revalidate_freq=0,否则仍可能延迟生效 -
opcache_invalidate('<code>path/to/file.php', true) 只对已加载的单个文件有效,不能递归清理依赖链 - 调用
opcache_reset()会清空整个缓存,高并发下可能引发短暂性能抖动,且无法控制只刷新某模块 - 如果文件里定义了
const MY_CONST = 42;,改完值再opcache_reset(),旧进程里已读取的常量不会变——PHP 的常量是编译期绑定的
真正可用的热更:用 Swoole 或 RoadRunner 替换传统 PHP 生命周期
当项目迁移到 Swoole(如 Swoole\Http\Server)或 RoadRunner,PHP 进程不再随请求结束而销毁,这时热更才具备操作基础。关键不是“更新代码”,而是“平滑切换 worker 进程”。
-
Swoole提供server->reload(),它会 fork 新 worker、等待旧 worker 处理完当前请求后退出,但要求所有全局状态可重建(比如 DB 连接需在onWorkerStart里重连) -
RoadRunner的rr serve -d模式支持监听文件变化,检测到.php文件修改后自动 reload worker,但仅限于应用层代码,不包括vendor/下的类(Composer autoload 不会自动重载) - 两者都不支持“运行中 patch 一个正在执行的函数”,所有更新都发生在新 worker 启动后,老请求仍走旧逻辑,这点必须写进业务容错设计里
别碰 eval、runkit、APCu 动态函数注入
有人试图用 eval() 加载新代码字符串,或用 runkit_function_redefine() 替换函数体——这些在生产环境等于埋雷。
立即学习“PHP免费学习笔记(深入)”;
-
eval()会绕过 opcode 编译优化,性能差,且无法 debug、无法被xdebug跟踪,错误堆栈指向eval()行而非真实文件行 -
runkit扩展早已废弃,PHP 7.4+ 官方移除,且它修改的是运行时符号表,可能导致内存泄漏或 GC 异常 - 用
apcu_store('handler', $closure)再apcu_fetch('handler')调用,看似灵活,实则闭包捕获的变量作用域不可控,容易引用已销毁对象
热更新最难的从来不是技术实现,而是确认“哪部分状态必须丢弃、哪部分必须迁移、哪些请求必须等完”。没做清楚这个判断,任何 reload 都只是把问题延后爆发。











