PHP-FPM 与常驻内存:从 request 模型到 Workerman 的迁移路径

一只冷漠的狐狸
一只冷漠的狐狸 正式会员认证极客 钻石卡会员
发布于 2026-09-22 16:54 ·3 浏览 ·0 回复

照着这篇做完,你能判断自己的项目该不该离开 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「保存即生效」完全相反,开发时最容易犯的错就是改完没重启,然后怀疑人生。

第三步:迁移前必须先清掉的三类东西

在你把任何业务代码搬过去之前,先搜一遍代码里有没有这些:

  1. 超全局与响应函数:`$_GET`、`$_POST`、`$_SERVER`、`header()`、`setcookie()`、`session_start()`。常驻进程里这些不再由运行时自动填充。
  2. 跨请求的静态/单例缓存:FPM 下写 `static $cache = []` 很安全,常驻下它会一直活着,越滚越大。
  3. 阻塞调用:`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,并给数据库连接加心跳、给进程设最大请求数。
本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-561.html
转载请注明出处,版权归原作者所有。

全部回复 0

还没有回复,来抢沙发~