frankenphp 的线程模型不支持长连接复用,每个长连接或 worker 实例独占一个 php 线程,导致线程池迅速耗尽;必须剥离长连接服务或严格限制 worker 数量,并通过 prometheus 监控 frankenphp_worker_instances 和 frankenphp_busy_threads 预警线程饱和。

会,而且挤占得非常直接——FrankenPHP 的 PHP 线程是按请求(或 Worker)独占分配的,长连接不释放线程,就等于持续占用一个 PHP 线程。
FrankenPHP 的线程模型根本不支持“一个线程处理多个长连接”
FrankenPHP 采用的是“每个请求/每个 Worker 实例绑定一个 PHP 线程”的模型,不是基于事件循环(如 Swoole)或协程的复用模型。这意味着:
-
php_server处理普通 HTTP 请求时:每个请求独占一个线程,直到响应完成、线程归还池中 -
Worker脚本(如 WebSocket 或常驻任务)启动后:每个 Worker 实例长期驻留在线程中,该线程不会被回收,直到 Worker 主动退出或崩溃 - 没有类似
socket_select()或stream_socket_accept()非阻塞轮询机制——线程一旦进入while (true)或socket_read()阻塞调用,就卡死在那里
长连接暴涨 → Worker 实例暴涨 → 线程池迅速耗尽
如果你用 FrankenPHP 启动了一个 WebSocket Worker(比如 worker.php),每建立一个客户端连接,就可能触发一个新的 Worker 实例(取决于你是否用 frankenphp\start_worker() 按连接启新实例),而每个实例都绑死一个线程。后果是:
- 线程数上限由
num_threads配置硬限制(默认auto≈ CPU 核心数 × 2),超出即拒绝新请求或 Worker 启动失败 - 未配置
max_threads时,num_threads就是绝对上限;即使设了max_threads,扩容也需时间,无法应对突发连接洪峰 - 线程池满后,新 HTTP 请求会排队(指标
frankenphp_thread_queue_length上升),新 Worker 启动失败(frankenphp_worker_start_failures_total计数增加) - 观察
frankenphp_thread_busy和frankenphp_thread_total的比值接近 1.0,基本可判定线程已被长连接类 Worker 占满
为什么不能像 Swoole 那样撑住 1 万 WebSocket 连接?
根本差异不在 PHP 本身,而在运行时抽象层:
- Swoole 的
WebSocket\Server基于 epoll/kqueue,主线程+多协程,单线程可调度数千并发连接 - FrankenPHP 的 Worker 是标准 PHP 线程(POSIX thread),每个都带完整 Zend VM 上下文、独立内存空间、阻塞 I/O —— 1 个连接 ≈ 1 个线程 ≈ 几 MB 内存 + 调度开销
- PHP 原生 socket 扩展默认就是阻塞模式:
socket_read($client, 4096)会一直挂起线程,无法让出控制权 - 即使你手动设
socket_set_nonblock($client),FrankenPHP 当前也不提供线程内事件循环调度能力,你得自己写select()/poll()轮询逻辑,极易出错且违背设计初衷
真正可行的缓解路径只有两条
别试图在 FrankenPHP Worker 里硬扛长连接规模,它不是为这个设计的:
- 把长连接服务(如 WebSocket、SSE)剥离出去,用 Swoole、RoadRunner 或 Node.js 单独部署,FrankenPHP 只负责无状态 HTTP API
- 如果必须用 FrankenPHP 接管,只允许极少量、有明确生命周期的 Worker(例如每台机器固定 1–2 个管理型 Worker),并通过反向代理(如 Caddy 的
reverse_proxy)把真实长连接流量导到专用服务 - 务必开启 Prometheus 监控,紧盯
frankenphp_worker_instances{worker="xxx"}和frankenphp_thread_busy,这两个指标失衡就是线程被吃光的第一信号
线程不是弹性资源,FrankenPHP 也不会自动把长连接“折叠”进一个线程里——它看到的是 Worker 实例数,不是连接数。这点容易误判,尤其当你的 Worker 脚本里用了 stream_socket_accept() 循环 accept 新连接却没做协程化时,一个 Worker 就可能悄悄拖住整个线程池。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











