FrankenPHP高并发配置需预留资源余量,避免OOM或连接耗尽:num_threads设为预估并发的70%~80%,max_threads= num_threads + 4;须满足num_threads × memory_limit < 可用内存,并实测单请求峰值内存,同时严防句柄、PDO和日志泄漏。

FrankenPHP高并发配置必须预留资源余量,否则压测看似达标,上线后一遇流量尖峰就OOM或连接耗尽——这不是配置没调够,而是没给系统留“喘气空间”。
num_threads 和 max_threads 怎么配才不踩内存墙
FrankenPHP 默认启动 CPU核心数 × 2 个 PHP 线程,但这只是冷启动安全值。真实场景中,必须按 num_threads × memory_limit 小于服务器可用内存来反推上限:
-
memory_limit不能只看 php.ini 里的值,要实测单请求峰值内存(用xdebug_peak_memory_usage()或memory_get_peak_usage(true)在 Worker 入口打点) -
num_threads建议设为预估并发的 70%~80%,比如压测能稳住 120 QPS,先设num_threads 8(非盲目拉到 16) -
max_threads是保底弹性层,设为num_threads + 4即可,别设太高——自动扩容不是无限兜底,它会加剧 GC 压力和上下文切换开销 - 如果应用用了大量 Composer autoload classmap 或 Doctrine Proxy 类,
memory_limit实际占用可能比配置高 30%~50%,这点常被忽略
Worker 模式下数据库连接池必须显式限流
启用 worker 后,PDO 连接不再随请求销毁,max_connections 容易被悄无声息打满:
- MySQL 的
max_connections要 ≥num_threads × 连接池大小,例如num_threads 8、每个 Worker 复用 5 个 PDO,则 MySQL 至少要设max_connections 48 - Doctrine 配置里必须写死
pdo::ATTR_PERSISTENT => true,且禁用doctrine.dbal.auto_close(默认 false,但有些旧版本 bundle 会覆盖) - 用
netstat -an | grep :3306 | wc -l持续观察连接数趋势,缓慢上涨就是泄漏信号,不是连接池没生效,而是代码里 new PDO() 后没 unset - 避免在循环内创建 PDO 实例;ORM 查询构造器(如 Laravel Eloquent)本身不持有连接,但
DB::connection()->getPdo()这类手动取连接的操作必须配对释放
日志与文件句柄泄漏比 CPU 更早拖垮服务
Worker 进程常驻后,file_put_contents('app.log', $msg, FILE_APPEND) 这类操作会快速耗尽 ulimit -n,现象是请求卡在 fopen 系统调用上,错误日志反而写不出来:
立即学习“PHP免费学习笔记(深入)”;
- 必须换用
RotatingFileHandler并设max_files: 7,禁止用StreamHandler - 所有
fopen()、curl_init()、new PDO()等资源型操作,都要有明确的 close / destroy / unset 路径,不能依赖析构函数 - 检查是否在
public/index.php或框架启动阶段执行了ini_set('error_log', '/path/to/php_error.log')—— 这会在 Worker 生命周期内持续打开新句柄 - 用
lsof -p $(pgrep -f frankenphp) | wc -l监控句柄数,超过 800 就该预警
真正难的不是把 num_threads 调到多少,而是确认每个线程在最差情况下(慢 SQL + 缓存穿透 + 日志刷盘阻塞)仍能回收干净。FrankenPHP 不会替你兜底全局变量、未关闭的句柄或残留的 PDO 对象——它只保证线程活着,不保证线程里跑的东西不腐烂。



















