FrankenPHP采用“进程+协程”模型,每个worker为独立OS进程,内部通过Go runtime调度成千上万个goroutine;关键配置是max_workers(建议设为逻辑CPU数),无threads参数,性能瓶颈多在I/O阻塞或未协程化的PHP扩展调用。

FrankenPHP 默认不使用线程,它用的是进程 + 协程
FrankenPHP 不是传统多线程模型(比如 Java 或 C# 那种 pthread 或 std::thread),它底层基于 Go 运行时,而 Go 的并发模型是「协程(goroutine)+ M:N 调度器」。你看到的 frankenphp-worker 进程,每个都是独立的 OS 进程,但内部可同时跑成千上万个 goroutine —— 它们不是 OS 线程,也不直接绑定 CPU 核心数。
所以:不存在“FrankenPHP 线程数应设为 CPU 核数”的配置项。你不会在 frankenphp.yaml 里找到 threads、worker_threads 这类字段。
真正影响并发能力的是 max_workers 和 Go 调度器行为
max_workers 控制的是 FrankenPHP 启动多少个独立的 PHP Worker 进程(每个对应一个 Go 进程),这才是和 CPU 核心数有实际关系的参数:
- 每个 Worker 进程默认由 Go 运行时调度,能自动利用多个 OS 线程(
GOMAXPROCS默认 = 逻辑 CPU 数) - 如果你机器有 8 个逻辑 CPU(
lscpu | grep "CPU(s):"输出为 8),Go 默认最多并行执行 8 个 goroutine(在不同 OS 线程上) - 但 goroutine 数量本身无硬上限——它只受限于内存(每个 goroutine 初始栈约 2KB)和任务类型(I/O 密集型可轻松上万)
示例配置(frankenphp.yaml):
立即学习“PHP免费学习笔记(深入)”;
workers: max_workers: 8 # 注意:没有 threads 字段
这个 8 值通常建议设为逻辑 CPU 数(nproc 输出),原因不是“每个 worker 绑定一个核”,而是避免过多进程争抢调度、增加上下文切换开销;太少则无法压满多核。
为什么别强行配“1:1 线程:核心”?
FrankenPHP 的典型瓶颈从来不是 CPU 核数,而是:
- I/O 等待(HTTP 请求、数据库、Redis)——协程天然挂起,不占 OS 线程
- PHP 扩展阻塞调用(如未 hook 的
file_get_contents)——会卡住整个 goroutine 所在的 OS 线程 - 内存压力(尤其大量并发时 PHP 内存泄漏或大对象驻留)
- Go runtime 的
GOMAXPROCS被意外覆盖(比如环境变量设了GOMAXPROCS=1,会让所有 goroutine 强制串行)
常见错误现象:top 显示单个 frankenphp-worker 进程 CPU 占用 100%,其余核空闲——大概率是某个 PHP 扩展没被协程 Hook,或者代码里写了死循环/深度递归,而不是“线程数配少了”。
需要手动干预的只有两个地方
如果你真遇到并发吞吐上不去,优先检查:
-
frankenphp.yaml中的max_workers是否 ≤nproc输出值(超了反而降低性能) - 是否启用了协程 Hook:
swoole.enable_coroutine对 FrankenPHP 无效;它用的是自己的 Hook 机制,需确认你加载的扩展(如curl,pdo_mysql)是否在 官方支持列表 内 - 避免在协程中调用未被 Hook 的阻塞函数,例如直接写
sleep(1);改用co_sleep(1)(如果可用)或依赖事件循环
FrankenPHP 的并发模型本质是「进程级隔离 + 协程级轻量并发」,它的伸缩性藏在 goroutine 数量和 I/O 效率里,而不是靠堆线程数。最容易被忽略的一点:你写的 PHP 代码是否真的“协程友好”——哪怕开了 100 个 worker,一段 while(true) { $i++; } 也能让一个核彻底卡死。



















