1.4 技术栈对齐:PHP 库与云产品地图
场景:库选型满天飞,先对齐再动手
机器到位后,团队又为框架和库吵了一轮。一派要用重型全功能框架,图现成的路由与配置组件;一派要裸 PHP,图性能与可控。我们的结论是走中间路线:网关是 IO 密集型的薄层,重型框架的 ORM、模板引擎、控制台全是累赘,启动开销还高;但完全裸写 PSR-7 请求处理既容易出错又费时间。折中方案是——HTTP 层用标准库(psr7 加 guzzle),Redis 用 predis,配置用自建契约文件,其余能省则省。先对齐地图,再开工写码,避免做到一半为换库返工。
云产品侧同样需要一张地图,让每个模块知道依赖谁、出问题找谁。这张地图贴在团队墙上整整一个迭代周期,新人入职看一眼就知道网关的五脏六腑。
方案:一张总图与两张分表
整个网关的数据流只有一个方向:请求从 Nginx 进 PHP 入口,经中间件链逐个处理,转发后端拿到响应,日志异步推 CLS,监控数据被云监控采集。库存着所有状态的是 Redis,文档入口分列各产品:
| 网关模块 | 自研实现 | 依赖云产品 | 文档入口 |
|---|---|---|---|
| 路由与灰度 | PHP 路由表 | 无 | — |
| 鉴权与签名 | PHP 中间件 | 无 | — |
| 限流令牌桶 | Lua 脚本 | 云数据库 Redis | cloud.tencent.com/document/product/239 |
| 响应缓存 | 字符串缓存 | 云数据库 Redis | 同上 |
| 调用日志 | 异步批量投递 | 日志服务 CLS | cloud.tencent.com/document/product/614 |
| 监控告警 | 自定义指标上报 | 云监控 | cloud.tencent.com/document/product/248 |
| 文档站加速 | 静态站点 | CDN | cloud.tencent.com/document/product/228 |
网关本身部署在 CVM 上,文档入口 cloud.tencent.com/document/product/213。PHP 库的选择遵循三条原则:标准优先(认准 PSR 接口)、社区活跃(issue 有人回)、依赖少(composer 列表越短越好)。最终的依赖清单很克制:
| 用途 | 选择的库 | 放弃的方案及原因 |
|---|---|---|
| HTTP 消息 | guzzlehttp/psr7 | 自写协议解析,易错 |
| 后端转发 | guzzlehttp/guzzle | curl 直写,连接复用差 |
| Redis 客户端 | predis/predis | phpredis 扩展,需编扩展维护 |
| 配置 | 自建 JSON 契约文件 | 框架配置组件,太重 |
| 日志 | monolog | 自写 file_put_contents,无异步 |
配置:极简依赖清单
composer.json 只需要五个依赖,装机到能跑不超过两分钟。这就是轻量路线的底气——依赖越少,升级越容易,供应链越安全:
{
"require": {
"php": ">=8.2",
"guzzlehttp/psr7": "^2.0",
"guzzlehttp/guzzle": "^7.0",
"predis/predis": "^2.0",
"monolog/monolog": "^3.0"
}
}
云产品的对接同样不复杂。Redis 走内网连接字符串,CLS 用 REST 接口异步投递(详见第 4.1 节),云监控走自定义指标上报,CDN 只用在接口文档站上——文档是给客户看的静态站点,放 CDN 后海外客户打开速度快了三倍,工单里问文档为什么打不开的投诉也消失了。CLI 运维脚本约定只调用云 SDK 做上报和查询,绝不出现在网关请求路径里,保持转发链路的纯净。
PHP 8.2 的两个特性我们倚仗甚多:枚举(enum)用来表达调用方分级与限流档位,只取约定值杜绝字符串拼错;readonly 属性用在契约对象上,配置加载后不可变,避免运行中被意外篡改。下面是枚举的用法示例:
<?php
// 调用方分级:契约之外的第一道类型保险
enum Level: string
{
case Paid = 'paid';
case Free = 'free';
case Internal = 'internal';
public function defaultRate(): int
{
return match ($this) {
self::Paid => 500,
self::Free => 20,
self::Internal => 5000,
};
}
}
有了枚举后,限流中间件里写 Level::from($level) 即可,传错值直接抛异常而不是静默给个错档位,少了一整类配置事故。
踩坑:一个扩展引发的血案
我们曾经在压测时临时把 predis 换成 phpredis 扩展图性能,结果在灰度环境出现偶发段错误,定位花了一天,最后只能回退。教训是:扩展的收益(约百分之十的 QPS)远不如稳定性重要,predis 纯 PHP 实现出问题至少还能看堆栈、改代码、热回滚。另一次是 monolog 默认同步写文件,高 QPS 下磁盘 IO 成瓶颈,后来全部改成异步推 CLS 才解决,本地文件只保留 fatal 级别。
选型上还有一个反复犯的错:过度设计。限流模块曾经被设计成可切换六种算法的通用框架,配置项多到没人会配,code review 时谁也说不清线上跑的是哪个算法,最后砍到只剩令牌桶一种——简单的东西才不出错,出错了也好查。灰度模块也走过同样的弯路,从支持按地域、按设备、按城市放量砍到只按调用方白名单放量,代码量少了一半,反而够用。
还有一条不成文的约定:任何依赖升级,先在联调环境跑一天,再灰度百分之十流量观察半小时,全量后盯十分钟错误率。这条流水线把依赖升级事故降到了零,重点是坚持,不是流程多复杂。
地图上还标出了每个模块的失效后果,这张表决定了降级策略的优先级。网关挂掉与 Redis 挂掉完全不是一个量级的事故,提前想清楚才能出事不慌:
| 失效模块 | 影响 | 降级策略 |
|---|---|---|
| 网关进程 | 全站不可用 | Nginx 直连后端兜底 |
| 云数据库 Redis | 限流失效 | 降级为本地计数令牌桶 |
| 日志投递 CLS | 审计缺失 | 本地落盘待补投 |
| 云监控 | 告警延迟 | CLS 告警查询兜底 |
我们把这四个降级开关做成了一行命令的脚本,放在运维手册第一页,新来的同事半小时就能学会操作。技术栈对齐不只是选库,还包括把失效场景想清楚并写进手册,后者往往比前者更保命。
小结
技术栈对齐的核心是克制:五个 composer 依赖加五个云产品,覆盖路由、鉴权、限流、缓存、日志、监控、文档加速全部场景。库选型的三条原则是标准优先、社区活跃、依赖少。网关请求路径上绝不放任何新鲜的、没压测过的东西。云产品负责稳基建(CVM、Redis、CLS、云监控、CDN),自研代码负责贴业务(路由、鉴权、灰度、配额),这个分工贯穿全书。
最后给一份自查清单,开工前逐项确认,能省掉后续大量返工:
| 检查项 | 达标标准 |
|---|---|
| 依赖数量 | composer require 不超过 6 个 |
| 请求路径外部调用 | 仅 Redis 与 CLS 异步投递 |
| PSR 接口覆盖 | 请求响应全部走 PSR-7 |
| 扩展依赖 | 纯 PHP 库优先,扩展需评审 |
| 配置来源 | 单一契约文件,禁止散落 |
| 升级流程 | 联调一天加灰度半小时已落档 |
这份清单后来被我们钉在每个新项目的 README 里,是踩坑换来的护身符。
一个有意思的插曲:对齐技术栈后我们做了一次代码统计,网关核心代码(不含日志和监控旁路)不到 1800 行 PHP。这个数字让团队很自豪——它证明了薄网关的可行性,也说明这一整套东西任何一个 PHP 开发者都能在一周内读懂、改对、接手。比起那些动辄几万行的开源网关,自研轻量路线最大的优势不是性能,而是全组都能修。
补一句关于“谁来维护”的提醒。技术栈选型的最大隐藏成本是知识集中:如果这套库只有一个人熟,那么这个人休假就是系统的单点故障。我们强制要求中间件链的每个模块至少有两个人能独立改,通过结对 review 和轮换值班兑现。选型决策不仅选技术,也在选未来的团队结构。
再补一句关于版本纪律:所有 composer 依赖锁死在 composer.lock,CI 中检查 lock 与 composer.json 是否一致,不一致直接失败构建。网关是长期运行的系统,依赖版本漂移是那种平时看不见、某天升级就翻车的慢性病。
