先说个实际场景:你们团队的 PHP 项目准备上 Swoole,或者已经上了,但发版还是走“低峰期、全量重启、出问题赶紧回滚”的老流程。这个流程在流量小的时候还好,一旦日活上来,一次有问题的发版可能就会让一小部分用户直接看到 502,而你再想保留现场排查都没机会。我在这个阶段踩了不少坑之后,决定把灰度发布和 A/B 测试的路由统一放到 Swoole 网关层来做,也就是用一个常驻的 Swoole HTTP Server 作为请求入口,在路由转发之前先把流量分组,再根据规则分流到不同版本的服务。整个方案围绕 Swoole 的共享内存 Table 和协程客户端展开,规则支持热更新,不重启进程就能调整放量比例。这篇博文就是这套方案的完整复盘,适合正在做 PHP 架构升级、准备引入 Swoole,或者被灰度工具链折腾得够呛的团队参考。
1. 项目背景与整体设计思路
很多人一听“灰度发布”和“A/B 测试”,下意识会觉得这是大数据团队或者运维平台的事,跟普通 PHP 开发者关系不大。但实际做下来你会发现,如果团队没有专门的基础设施,灰度发布最终还是会落到应用层代码上。既然要落到应用层,那不如选一个最合适的落点。Swoole 常驻内存的特性,让它成为 PHP 技术栈里做流量路由的最佳载体之一。
1.1 为什么选择 Swoole 来做这套路由
传统 PHP-FPM 模式下,每个请求进来都要重新走一遍框架初始化、加载配置、编译代码的过程。想实现灰度发布,常规做法是在 Nginx 里按 IP 段或者 Cookie 转发到不同 upstream,或者干脆在业务代码里写 if/else 判断用户版本。这两种方案都有明显短板:Nginx 层的规则太死板,改一次配置要 reload,复杂条件很难表达,而且 reload 期间如果处理不当会有闪断;业务代码里的判断则会让灰度逻辑散落在各个项目里,A 项目写一套,B 项目又写一套,时间一长根本维护不动。
Swoole 出现之后,情况就不一样了。它的 HTTP Server 常驻内存,进程启动时加载的类、配置可以一直保留在内存里;再加上 Swoole\Table 这种共享内存表,所有 worker 进程都能实时读取同一份规则。这相当于把“路由决策”这件事从 Nginx 和业务代码中抽出来,放到一个独立的、可以热更新的网关层。我在实际项目中验证过,这套路线的维护成本比在 Nginx 里写 lua 低得多,也比在业务代码里埋点干净。
我可以打个比方:传统 PHP-FPM 是一次性餐具,每个请求来都得重新拆一套;Swoole 是反复使用的桌布和碗筷,桌子摆好了一次,后面只需要往桌上换菜。灰度规则就是那道“菜”,你不用重新摆桌子,直接换菜就行了。
1.2 整体架构:路由层和业务层怎么划分
在设计这套方案时,其实有三个候选位置可以放分流逻辑:Nginx 层、业务代码层、Swoole 网关层。我列了一张对比表,方便你看清楚各自的边界:
| 方案 | 优点 | 缺点 |
|---|---|---|
| Nginx 层配置 | 性能好、部署简单、对业务完全透明 | 规则表达能力弱,复杂条件难写,改配置要 reload,灰度规则直观性差 |
| 业务代码层 | 灵活,能直接读业务数据,容易定制 | 侵入性强,多项目重复实现,灰度逻辑容易漏写,测试成本高 |
| Swoole 网关层 | 常驻内存、集中管理、规则可热更新、业务无感知 | 需要额外维护一个网关服务,转发增加一层网络开销 |
我最终选择 Swoole 网关层,不是因为它最先进,而是因为它的成本和收益最平衡。网关层只负责一件事:拿到请求,根据规则决定往哪个目标服务转发,同时把分组结果写进响应头和日志。业务服务完全不知道灰度逻辑的存在,它们还是按普通接口服务来开发。这样灰度规则要是写错了,影响的只是分流结果,不会污染业务代码。而且网关层可以顺带做请求日志、限流、监控上报,基础设施收拢到一个点。
需要注意,这里的“路由”是应用层的流量分发路由,和网络设备里常说的静态路由、策略路由不是一回事,和前端框架里的 vue-router 之类的路由也不一样。很多同事听到“路由”第一反应是路由表、网关 IP,其实这里指的是“这个 HTTP 请求由哪个后端服务来处理”的决策过程。后面我提到的所有路由,都是这个含义。
1.3 路由规则设计:灰度规则和 A/B 规则是两套东西
设计规则时,我一开始把灰度规则和 A/B 实验规则混在一个 JSON 里,结果发现字段互相干扰,灰度要一组“放量比例+白名单”,A/B 要一组“实验组+权重+对照组”,凑在一起非常别扭。后来我把它们拆成两套互不影响的规则结构,共用同一个“规则管理器”,但各自解析各自的字段。
灰度规则的典型结构长这样:
json复制{
"key": "gray_v2",
"type": "gray",
"rule_name": "用户中心v2灰度",
"gray_percent": 10,
"white_list": ["uid:10001", "uid:10002"],
"conditions": {
"ua_contains": ["WeChat"],
"path_prefix": ["/api/user/"]
},
"targets": {
"gray
