PHP-FPM 与常驻内存:从 request 模型到 Workerman 的迁移路径
照着这篇做完,你能判断自己的项目该不该离开 PHP-FPM,并在不改崩现有站点的前提下,把一部分业务一步步挪到 Workerman 上。
第一步:先接受 FPM 的「一次性」设定
PHP-FPM 的工作方式是:master 进程 fork 出一批 worker,每个 worker 一次只处理一个请求,请求结束后 PHP 会把这个请求期间产生的所有变量、对象、静态属性全部销毁。下一次请求进来时,内存是干净的。
这带来了两个你可能习以为常、但其实是优点的东西:
- 全局变量、静态变量不会残留,不用考虑「上次请求的数据污染这次」
- 某个请求里把内存写爆,只影响那一个请求,worker 重启即可恢复
代价也很明确:每个请求都要重新建立 MySQL 连接、重新 include 文件、重新跑一遍框架初始化。同时在线越高,这部分固定开销占比就越大。
注意:很多「FPM 慢」的抱怨,其实是数据库没加索引或查了 N+1,跟请求模型无关。先看慢日志,再谈换架构。
第二步:搞清 Workerman 换掉了什么
Workerman 启动后是一个常驻进程组,靠事件循环(epoll/select)处理连接。它不销毁内存——这正是它的全部收益来源,也是全部坑的来源。
安装只要一行:
composer require workerman/workerman
一个最小 HTTP 服务:
<?php
require __DIR__ . '/vendor/autoload.php';
$http = new Workerman\Worker('http://0.0.0.0:8080');
$http->count = 4; // 4 个 worker 进程
$http->onMessage = function ($connection, $request) {
$connection->send('hello');
};
Workerman\Worker::runAll();
启动与运维命令:
php start.php start # 前台调试
php start.php start -d # 守护进程
php start.php status # 看进程状态
php start.php reload -g # 平滑重启(改代码后必须执行)
注意:常驻模式下改了 PHP 代码不会自动生效,必须 reload。这一点和 FPM「保存即生效」完全相反,开发时最容易犯的错就是改完没重启,然后怀疑人生。
第三步:迁移前必须先清掉的三类东西
在你把任何业务代码搬过去之前,先搜一遍代码里有没有这些:
- 超全局与响应函数:`$_GET`、`$_POST`、`$_SERVER`、`header()`、`setcookie()`、`session_start()`。常驻进程里这些不再由运行时自动填充。
- 跨请求的静态/单例缓存:FPM 下写 `static $cache = []` 很安全,常驻下它会一直活着,越滚越大。
- 阻塞调用:`sleep()`、同步 `file_get_contents()`、同步 curl。在 FPM 里只是这一个请求慢,在常驻进程里会把该进程的所有连接一起卡住。
正确做法是先做一次分层:把业务逻辑抽成「传参进去、返回数据出来」的纯类,不碰超全局、不直接输出。这一层在 FPM 和 Workerman 下都能跑,是迁移的地基。
第四步:按风险从低到高分批搬
不要一次性重写整个站点。建议顺序:
① 定时任务与队列消费者——收益最大、风险最小。这类代码本来就不依赖 HTTP 上下文,直接挂到 Worker 的 `onWorkerStart` 里跑循环即可。
② 长连接场景——站内通知推送、WebSocket 聊天。这块 FPM 天生做不了,用 Workerman 单独起一个端口:
$ws = new Workerman\Worker('websocket://0.0.0.0:8282');
③ HTTP 主站——最后再动。要么用 Workerman 的 HTTP 协议自己写路由,要么直接上基于它的框架。
数据库连接是这一阶段的重点。常驻进程持有连接不释放,会撞上 MySQL 的 `wait_timeout`(默认 8 小时空闲断开),表现为半夜之后第一批请求全报「MySQL server has gone away」。解决办法二选一:定时心跳 `$pdo->query('SELECT 1')`,或者捕获异常后重连。
第五步:用 Nginx 分流,新旧并存
不用一次切换。让 Nginx 按路径把流量分给两边:
location /ws/ {
proxy_pass http://127.0.0.1:8282;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location / {
# 仍然走原来的 PHP-FPM
}
这样 Workerman 只承担它擅长的部分,主站继续由 FPM 扛。等长连接那部分稳定跑上几周,再考虑把更多路由挪过去。
注意:单个 worker 进程是单线程的,`count` 设成 CPU 核数即可,别盲目调大;进程间不共享内存,任何需要共享的状态(在线列表、计数器)必须外置到 Redis。
第六步:上线后必看的两组指标
- 内存:在 `onWorkerStart` 里注册定时器,每分钟打印 `memory_get_usage(true)`。曲线只涨不落就是泄漏,用二分法注释代码定位。
- 进程重启:给 worker 设最大请求数,做到「跑够 N 个请求自动退出重建」,用重启换安全。类似 FPM 的 `pm.max_requests`。
小结
- FPM 每次请求清零内存,换来的是无状态、好调试;Workerman 常驻内存,换来的是去掉重复初始化开销。
- 迁移的真正工作量不在装 Workerman,而在清理超全局、静态缓存和阻塞调用这三类代码。
- 顺序上先搬定时任务和长连接,HTTP 主站最后动,用 Nginx 按路径分流灰度。
- 常驻进程必须外置共享状态到 Redis,并给数据库连接加心跳、给进程设最大请求数。
转载请注明出处,版权归原作者所有。
正式会员
认证极客
钻石卡会员





