FrankenPHP压测应优先使用vegeta而非ab或wrk默认配置,因其支持HTTP/2、响应体校验及结构化延迟输出,能真实反映Laravel应用吞吐与状态完整性。

用 vegeta 模拟真实请求链路,别碰 ab 和 wrk 默认配置
FrankenPHP 是嵌入式 SAPI 运行时,不是传统 FPM,ab 默认走 HTTP/1.1 且不复用连接,wrk 默认不校验响应体——这两个工具测出来的“QPS”基本是 PHP 进程启动开销,不是你 Laravel 应用的真实吞吐。
vegeta 才是目前最匹配的 CLI 工具,它支持 HTTP/2、能捕获完整响应体用于断言、输出结构化延迟分布。关键不是跑多高数字,而是验证「在持续负载下是否维持预期状态」:
-
vegeta attack -targets=targets.txt -rate=50 -duration=60s -http2 -include-body:-rate 控制每秒请求数,-include-body 后可用vegeta report -type=json提取响应体做 JSON 格式校验 - targets.txt 中写
GET http://localhost:8000/api/user/profile,避免只压/这类无业务逻辑路径 - 若出现大量
context deadline exceeded错误,说明 FrankenPHP 的 worker 队列已满,不是网络或 PHP 代码慢,要调frankenphp.server.max_workers或检查系统ulimit -n
必须加 Laravel 应用层埋点,光看 HTTP 状态码会漏掉 terminate() 失效
FrankenPHP 的 worker 生命周期和信号处理机制与 FPM 不同,Laravel 的 Kernel::terminate() 回调、队列监听、日志刷盘等异步行为,在压测中极易被跳过——而这些恰恰是业务完整性关键。
在 App\Http\Kernel.php 的 terminate() 方法里加一行:
立即学习“PHP免费学习笔记(深入)”;
file_put_contents('/tmp/terminate.log', date('H:i:s') . "\n", FILE_APPEND);
压测结束后检查该文件行数是否 ≈ 请求总数。若明显偏少,说明部分请求的 terminate 未执行,常见原因是:
- FrankenPHP 的
graceful_shutdown_timeout设置过短(默认 30s),worker 被强制 kill 前没来得及跑完 terminate - PHP 进程收到 SIGTERM 后,Go 层未正确转发信号给 PHP 运行时
- OPcache 在压测中触发 OOM restart,导致部分请求中断在中间态
curl 并发模拟多用户订阅 Mercure,绕过浏览器连接限制
测试 Mercure 推送能力时,别依赖开 20 个 Chrome 标签页——Chrome 单域名 EventSource 连接上限是 6,超出的请求会排队或失败,这不是 Mercure 问题,是浏览器策略。
用 curl + shell 并发更可控,且能逼近真实后端连接压力:
- 确保 Mercure 订阅端点可访问,例如
https://localhost:8000/.well-known/mercure?topic=https://acme.com/msg - 运行:
for i in $(seq 1 100); do curl -N "https://localhost:8000/.well-known/mercure?topic=https://acme.com/msg" --max-time 15 & done -
-N禁用缓冲,--max-time 15防止挂死;若大量进程卡住,先查系统ulimit -n和 FrankenPHP 的frankenphp.mercure.max_connections - 另起终端用
mercure_publish()或 POST 向该 topic 发布消息,观察各 curl 是否在 100ms 内打印响应——延迟突增说明 Hub 分发队列堆积
Worker 模式下 WebSocket 多用户推送,内存泄漏比性能更致命
如果你没走 Mercure,而是用 FrankenPHP Worker + WebSocket 自建聊天室这类场景,“多用户接收”本质是 PHP 脚本维护一个连接池并轮询广播——这里最容易出问题的从来不是 QPS,而是内存泄漏和连接假死。
Worker 模式下每个 PHP 实例长期存活,static 变量、全局数组、未 unset 的资源句柄都会累积:
- 每次广播前用
gc_collect_cycles()主动触发回收,尤其在大消息循环后 - 用
memory_get_usage(true)在关键节点打点,压测中若内存曲线持续上扬,大概率是连接对象没被销毁 - 不要依赖
__destruct()清理连接,FrankenPHP 的 worker 可能重用 PHP 实例,__destruct不一定被调用;改用显式close()+unset()组合 - 连接假死检测不能只靠心跳包,要结合
stream_select()的超时返回 +feof()双重判断,否则空连接会一直占着 worker
真正难的不是让 100 个用户同时收到消息,而是让这 100 个连接稳定维持 10 分钟以上不泄漏、不假死、不拖垮 worker 内存——这点在 FrankenPHP 的 Go 层抽象下反而更隐蔽,得靠应用层自己兜底。



















