FrankenPHP Worker是长期存活的Go进程内调度的PHP线程,非OS进程;每个Worker对应一个libphp执行上下文,共享进程地址空间,复用框架、连接池与opcache,但不共享超全局变量。

FrankenPHP Worker 是 Go 协程调度的 PHP 线程,不是传统意义的 OS 进程
FrankenPHP 的 frankenphp-worker 命令启动的是一个**长期存活的 Go 进程**,它内部用 Goroutine 调度多个 PHP 执行单元;每个“Worker”实际对应一个 PHP 线程(即 libphp 的 php_execute_script 调用上下文),共享同一进程地址空间,不 fork 新 OS 进程。
这和 PHP-FPM 完全不同:FPM 的每个 worker 是独立的 OS 进程,有自己完整的内存镜像;而 FrankenPHP 的 worker 是线程级复用,启动时加载一次框架、连接池、opcache 预加载脚本,后续所有请求都在该线程内复用这些资源。
- 你看到的
ps aux | grep frankenphp通常只显示 1–2 个 Go 进程(主服务 + worker),不是几十个 PHP 进程 -
FRANKENPHP_WORKERS=4表示启动 4 个 PHP 线程,它们由 Go 主进程统一管理,非独立生命周期 - 线程间不共享运行时变量(如
$GLOBALS、static属性),但可安全共享单例对象(如 PDO 实例),前提是手动管理连接状态 - 若未显式配置
num,默认 worker 数 = CPU 核心数 × 2 —— 这是线程数,不是进程数
为什么容易误以为是进程?
因为命令行叫 frankenphp-worker,且文档常提“worker 进程”,但底层实现完全基于 Go 的 runtime.GOMAXPROCS 和 php_execute_script 的线程安全调用。真正的 OS 进程只有 Go 主体,PHP 层只是它调度的轻量执行单元。
常见错误现象:
立即学习“PHP免费学习笔记(深入)”;
- 在
worker.php中用pcntl_fork()会失败:libphp 不支持在嵌入式 SAPI 下 fork -
getmypid()每次返回相同 PID:所有请求跑在同一个 OS 进程里 - 用
memory_get_usage(true)观察到内存缓慢增长而非锯齿状波动:没有进程级内存释放,只有 PHP 引擎的垃圾回收
和 Swoole、RoadRunner 的线程/进程模型对比
Swoole 的 onWorkerStart 是真正跨请求共享运行时的常驻 PHP 进程;RoadRunner 启动多个 PHP worker 进程并通过 RPC 通信;FrankenPHP 则走中间路线:单 Go 进程 + 多 PHP 线程,无进程间通信开销,也避免了 Swoole 那种需手动清理 static 变量的复杂性。
关键区别在于:
- Swoole:1 个 PHP 进程 → N 个协程 → 共享全部堆内存
- RoadRunner:N 个 PHP 进程 → 每个进程处理 1 请求 → 进程隔离强,但内存开销大
- FrankenPHP:1 个 Go 进程 → N 个 PHP 线程 → 类定义、opcache、PDO 连接复用,但超全局变量、
$_SERVER默认沿用上个请求(需frankenphp_clear_globals())
实际部署时最易忽略的一点
FrankenPHP 的 worker 线程数(num)和 Caddy 的 num_threads 是两个独立配置项,但必须协同:前者控制后台任务线程数,后者控制 HTTP 请求线程池大小。两者都受限于 num_threads × memory_limit < available_memory 这一硬约束——线程不等于进程,但内存是实打实按线程分配的。



















