FrankenPHP 的 workers.count 控制 PHP 线程池规模,是底层执行单元上限;Octane 的 --workers 是应用层常驻子进程数,必须 ≤ workers.count 且需显式指定 --server=frankenphp 才生效。

FrankenPHP 的 workers.count 控制的是 PHP 线程池规模,不是 Laravel Octane 的 worker 进程数
很多人一看到 workers.count 和 Octane 的 --workers 就默认是“同级配置”,其实它们作用在完全不同的层级。FrankenPHP 的 workers.count 是 Caddy 启动时预分配的 PHP 线程总数(即“能同时处理多少个请求的底层执行单元”),而 Octane 的 --workers 是 Laravel 应用层启动的常驻子进程数,它只在启用 php_server + octane 模式时生效,且必须小于等于 FrankenPHP 提供的线程数。
常见错误现象:Octane 启动报错 "failed to spawn worker: resource temporarily unavailable" 或者压测时 frankenphp_busy_threads 一直卡在低值、frankenphp_queue_depth 持续上涨——这通常说明 FrankenPHP 线程池太小,Octane 想拉更多 worker,但底层没线程可分。
-
workers.count建议设为 CPU 核心数的 1–2 倍(例如 4 核机器设 6–8),这是硬上限 - Octane 的
--workers推荐从min(4, workers.count)起步,再根据frankenphp_busy_threads / frankenphp_total_threads比值动态调优 - 如果
frankenphp_busy_threads长期接近frankenphp_total_threads,但 QPS 上不去,优先加workers.count;如果比值长期低于 0.5,说明 Octane 的--workers可能设高了,浪费内存
Octane 启动命令必须显式传入 --server=frankenphp 才能接入 FrankenPHP 线程池
默认情况下,php artisan octane:start 会走 Swoole 或 RoadRunner 的原生 server,完全绕过 FrankenPHP。不加这个参数,Octane 就是独立进程,FrankenPHP 的 workers.count 对它毫无约束力,两者形同陌路。
正确做法是在 Caddyfile 里用 php_server 指令,并确保 Laravel 的启动逻辑被 FrankenPHP 的 PHP 线程接管。此时 Octane 必须以 “worker mode” 运行,而不是 standalone server mode。
立即学习“PHP免费学习笔记(深入)”;
- Octane 启动命令应为:
php artisan octane:start --server=frankenphp --workers=4 - Caddyfile 中不能写
reverse_proxy到 Octane 的 HTTP 端口,必须用php_server直接执行index.php - 若项目根目录有
octane-server.php,需确认它没有强制绑定端口或 fork 子进程——FrankenPHP 要求 PHP 代码运行在受控线程内,不能自行 daemonize
混用 php_server 和 Octane 时,try_files 规则要避开 Octane 的入口文件
FrankenPHP 的 php_server 默认会把所有匹配路径的请求都交给 PHP 处理,包括 storage/logs/、vendor/ 这类本不该暴露的路径。而 Octane 本身也依赖 index.php 入口做路由分发,如果 try_files 写成 try_files {path} /index.php,就可能让静态资源请求误入 Octane,导致 404 或意外解析。
真实场景中,你既想让 /api/* 走 Octane,又想让 /storage/app/public/* 直接由 FrankenPHP 返回静态文件,就得靠 Caddy 的路由优先级来切流。
- 推荐写法:先
handle_path /storage/*直接 serve 文件,再handle /api/*用php_server,最后 fallback 到 Laravel 的index.php - Octane 的
public/下静态资源(如 JS/CSS)仍由 FrankenPHP 的root和内置静态文件服务托管,不需要 Octane 处理 - 务必验证
curl -I http://localhost/storage/app/image.jpg返回 200 且无 PHP 解析痕迹,否则说明php_server拦截范围过大
监控指标里只有 frankenphp_busy_threads 和 frankenphp_queue_depth 是关键判断依据
Octane 自己的 octane_requests_handled_total 或 octane_memory_usage_bytes 在 FrankenPHP 下不可信——因为 Octane 此时只是“运行在 FrankenPHP 线程里的一个 PHP 脚本”,它的指标不会注册到 Prometheus,也不反映线程争抢情况。
真正该盯住的是 FrankenPHP 暴露的三个基础 gauge:
-
frankenphp_total_threads:当前线程池大小(由workers.count决定) -
frankenphp_busy_threads:正在跑 Octane 请求的线程数(注意:一个活跃 Octane worker 永远占一个线程) -
frankenphp_queue_depth:非 worker 请求排队数(如果这个值高,说明普通 PHP 请求在等线程,Octane 可能抢走了太多线程)
容易被忽略的一点:FrankenPHP 的线程是全局复用的。Octane worker、普通 index.php 请求、甚至 artisan tinker 的 HTTP 请求(如果开了)都会竞争同一组线程。所以线上不能只看 Octane 的吞吐,得看整体线程利用率是否健康。



















