webman 架构解析:常驻内存进程模型与 PHP-FPM 的本质差异
webman 是基于 Workerman 构建的高性能 PHP 框架,官方基准 QPS 常态化压到十几万。但很多人把 webman 当"换个写法的 Laravel"来用,结果内存泄漏、全局状态污染这些经典问题轮番上演。要真正用好 webman,必须先理解它与 PHP-FPM 在架构上的本质差异——这是所有后续实践的地基。
一、两种执行模型:请求级生命周期 vs 常驻内存
PHP-FPM:每次请求都是新生
浏览器 → Nginx → PHP-FPM(worker) → 加载框架 → 初始化 → 处理请求 → 返回 → 销毁一切FPM 模式下,每个请求结束都会释放全部内存、销毁所有变量。代价是每次请求都要重新加载框架(几十毫秒的 bootstrap),但好处是状态绝对隔离——你再怎么写全局变量,下一个请求都是白纸。
webman:一次加载,永久服务
浏览器 → webman(worker 进程) → 直接路由分发 → 处理请求 → 返回(框架和对象留在内存)webman 启动时把框架、路由表、配置全部加载进内存,之后每个请求只是"触发一次回调"。QPS 高的根源就在这:省掉了每请求一次的框架 bootstrap 开销。
这个差异带来两个直接推论:
- 性能提升的本质是"摊销":初始化成本只付一次,请求越多收益越大
- 内存不自动释放:请求之间共享进程内存,全局状态会"存活"——这是福也是祸
二、进程模型:master / worker 的分工
master 进程(不处理业务)
├── 监听端口、管理 worker
├── fork 出 N 个 worker 进程
└── 收到 reload 信号时逐个平滑重启 worker
│
├── worker-1 ← 每个进程独立事件循环,可同时 hold 住数千连接
├── worker-2
├── ...
└── worker-N(默认 = CPU 核数)三个关键认知:
1. worker 数不是越多越好。 webman 是多进程 + 每进程事件循环(非阻塞 IO)模型,worker 数建议等于 CPU 核数。如果你的业务有阻塞调用(同步 HTTP 客户端、同步 RPC),连接会"卡住"事件循环,这时才需要加进程数来对冲——但更好的解法是消灭阻塞调用(后面协程篇细讲)。
2. 进程间不共享内存。 你在 worker-1 里写的静态变量,worker-2 看不见。跨进程共享状态要用 Redis 或进程间通信,别指望 static 属性全站生效。
3. request 对象请求间隔离,但对象池里的组件不是。 框架层面的组件(DB 连接、日志实例)跨请求复用,这是性能来源;你自己的类如果注册成了单例,它同样跨请求存活。
三、启动流程:从 public/index.php 说起
// public/index.php(简化)
require_once __DIR__ . '/../vendor/autoload.php';
support\App::run();App::run() 内部做了什么:
1. 加载 .env 与 config/*.php(此后修改配置需重启)
2. 扫描 app/controller 生成路由表(注解路由另算)
3. 初始化容器(webman 基于 PHP-DI,支持构造器注入)
4. 加载 bootstrap/*.php(进程启动时执行一次的钩子)
5. 初始化数据库/Redis 连接(懒加载,首次使用时建立)
6. master fork workers → 每个 worker 进入事件循环等待请求bootstrap 目录是理解 webman 的钥匙:放在这里的代码每个进程只执行一次。数据库连接的建立、连接池的初始化、Swoole 协程运行时的开启,都在这个阶段。官方的 webman/database、webman/redis 等插件都是通过 bootstrap 文件挂载的。
四、常驻内存的四宗罪与解法
罪一:静态属性 / 全局变量跨请求污染
// ❌ 经典事故:上一单用户的数据泄漏给下一单
class OrderService
{
public static ?array $currentUser = null;
}
// 请求A设置了 currentUser,请求B进来时它还活着!解法:请求级数据一律走 Request 对象传递,或用 $request->xxx 属性挂载。静态属性只允许存"进程级不变量"(配置、编译好的正则、容器实例)。
罪二:内存泄漏
每请求增长的数组、未释放的大结果集、监听器越挂越多……FPM 下这些泄漏随请求结束被清零,webman 下会累积到 memory_limit 进程崩溃。
排查手段:
// 在中间件里记录内存水位
public function process(Request $request, callable $handler): Response
{
$before = memory_get_usage();
$response = $handler($request);
$this->logger->info('mem delta', [
'delta' => memory_get_usage() - $before,
'peak' => memory_get_peak_usage(),
]);
return $response;
}内存水位持续爬升就是泄漏信号。常见元凶:往静态数组里 push 不清理、查询返回百万行、日志 handler 无限累积。
罪三:数据库连接断开(MySQL gone away)
worker 空闲超过 wait_timeout(默认 8 小时)后,MySQL 单方面断开,而 webman 还握着死连接。
解法:配置连接心跳 + 断线重连。用 ORM 插件时开启:
// think-orm 配置
'break_reconnect' => true,更彻底的方案是连接池(数据库篇细讲),池化组件自带健康检查。
罪四:代码改了不生效
FPM 的肌肉记忆是"保存即生效",webman 下路由、配置、类定义都在内存里,必须 restart 或 reload。开发环境用 php start.php restart 太重,webman 有监视文件变自动 reload 的方案(workerman/webman-framework 配合 monitor 进程,dev 模式自动开启)——但注意生产环境要关掉。
五、平滑重启的原理与边界
php start.php reload # 平滑重启:逐个重启 worker
php start.php restart # 硬重启:master 也重启
php start.php stop # 停止
php start.php status # 查看进程状态reload 的机制:master 收到信号后,先通知一个 worker 停止接收新请求,等它处理完当前请求再杀掉重建,依次轮转。这意味着:
- reload 只更新"启动时加载"的代码:controller 文件是每次请求 include 的吗?——webman 中 controller 默认支持 reload 更新(优化级别
support/App的 reloadable 路径),但config/、bootstrap/、路由定义的改动必须 restart - reload 期间正在执行的长任务会被等待,但超过
$maxWaitTime(可配)会被强杀,注意保护长任务
六、与 FPM 共存:渐进式迁移策略
存量 Laravel/TP 项目不必推倒重来。实战迁移路径:
- 新接口用 webman 写,旧接口留在 FPM,Nginx 按路径分流
- 复用原有 Model 层:webman 支持 think-orm 和 Laravel Eloquent(
laravel/database插件),业务模型代码几乎平移 - 逐步搬迁高频接口:把 QPS 最高、逻辑最薄的接口先迁过去,验证稳定性后再迁复杂业务
分流配置示例:
location /api/v2/ {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://127.0.0.1:8787; # webman
}
location /api/ {
try_files $uri /index.php?$query_string; # 旧 FPM 应用
}写在最后
webman 不是"更快的 Laravel",而是一种不同的运行时模型。把进程模型、启动流程、常驻内存的边界刻进脑子,后面的中间件、连接池、协程实践才有意义。下一篇我们进入请求处理的核心链路:中间件洋葱模型与依赖注入容器的实战。
评论 (0)