1.4 技术栈对齐:PHP 框架与云产品地图

场景:框架是自研还是用 Laravel,云产品怎么对接

团队是 PHP 出身,框架没什么可争的。我们选了 Laravel 风格的写法但保持克制:路由、模型、中间件这套骨架沿用社区习惯,招人时新人上手快;但不多引第三方依赖,多租户这层完全自研,因为我们的租户逻辑太核心,交给插件等于把命脉交出去。这个决定在今天看依然正确——后来几次大改隔离方案,全在自己代码里,不需要等插件作者发版,想怎么改就怎么改,发版节奏完全由业务说了算。

团队里也讨论过换 Go 或者 Node 重写,被一句话问回来:现有 PHP 代码沉淀了五年业务逻辑,重写的风险和周期 nobody 背得起,而且 PHP 在 Web 场景的开发效率依然是第一梯队。多租户难不难不在语言,在架构。

云产品对接上吃过亏才知道收敛。最初大家随手在业务里 new 各家 SDK,COS 上传写一份、日志写一份、短信又一份,配置散落在十几个文件。后来花了整整一周做改造,把所有云能力收进一个统一的服务层,业务代码只面向接口,实现细节集中在一个目录里。这一节把这张地图画清楚,后面的章节都会按图索骥。

其实框架与云产品的取舍有一条隐藏的主线:所有选型都要为多租户这两条核心约束服务,即数据隔离与统一升级。框架要灵活到让我们能从请求最深处注入租户上下文,云产品要天然支持按前缀或按实例分区。不符合这两条的组件,再流行也不进我们的技术栈。

方案:一张云产品地图

先用一张表对齐技术栈里每个角色对应的云产品及其文档入口,后面每一节都会反复引用到它们。

技术角色 我们的选择 用途 官方文档
Web 运行环境 轻量应用服务器 站点与后台部署 cloud.tencent.com/document/product/1207
关系型数据 云数据库 MySQL 租户业务数据 cloud.tencent.com/document/product/236
对象存储 对象存储 COS 租户文件与上传 cloud.tencent.com/document/product/436
缓存与会话 云数据库 Redis 缓存隔离与会话 cloud.tencent.com/document/product/239
日志与审计 日志服务 CLS 按租户检索 cloud.tencent.com/document/product/614

为什么这么选:轻量应用服务器对中小团队是最省心的计算载体,自带面板和镜像,比裸 CVM 少一半运维活;MySQL 用托管实例省去了备份和高可用的事情;Redis 托管同样如此;COS 天然适合多租户——一个桶加键名前缀就能分区,权限还能按前缀授权;CLS 则把全集群的日志收编到一处,按租户检索只是查询语法的事。

只有一个地方我们保持了克制:消息队列没用云服务,而是先用 Redis 的列表顶了一阵子,量真正起来之后才单独评估。创业阶段少引一个组件就少一份运维成本,能用成熟方案顶住的需求不急着上新东西。同理,早期也没有引入搜索引擎服务,站内搜索直接走 MySQL 的全文索引,等大租户开始抱怨搜索慢了才上正轨。

这张地图还有一个隐含原则值得单独说:同一类能力只用一款产品。比如缓存与队列都是 Redis,监控与日志都走 CLS,好处是排障时不需要在多个控制台之间跳来跳去,坏处几乎没有。组件越少,认知负担越低,八个人的团队才转得动。

地图刚开始落地时我们也做过一次债务盘点,把暂时绕开的技术选型列出来挂在 wiki 上,谁 jog 经过都能瞄一眼:

暂缓的组件 当时的替代 计划引入时机
消息队列 Redis 列表 消息量超过日均十万条
搜索引擎 MySQL 全文索引 有租户搜索响应超过一秒
配置中心 数据库加缓存 租户数超过五十
CI 流水线 部署脚本加验证 团队超过十人

有意思的是列完之后我们发现,前三项直到第二年年底才真正引入,而引入的理由恰好就是表里写的触发条件。先有替代方案顶着,才有底气在正确的时间而不是焦虑的时间做升级,这张债务表后来也成了技术规划会的开场材料。

配置:统一的云服务注册表

所有云客户端的创建集中在服务注册表里,配置只在 .env 中出现一次,业务代码通过接口拿到实例:

// app/Services/CloudRegistry.php
class CloudRegistry
{
    private static array $box = [];

    public static function cos(): CosClient
    {
        return self::$box['cos'] ??= new CosClient([
            'region'      => env('COS_REGION'),
            'credentials' => [
                'secretId'  => env('COS_SECRET_ID'),
                'secretKey' => env('COS_SECRET_KEY'),
            ],
            'bucket' => env('COS_BUCKET'),
        ]);
    }

    public static function redis(): Redis
    {
        return self::$box['redis'] ??= new Redis([
            'host'     => env('REDIS_HOST'),
            'port'     => env('REDIS_PORT'),
            'password' => env('REDIS_PASSWORD'),
        ]);
    }
}

业务里要用 COS 就调 CloudRegistry::cos(),要用 Redis 就调 CloudRegistry::redis()。这个注册表看似简单,收益却很大:换实例、调参数、加埋点都只改一个文件;新人也不用满世界找配置在哪里读的。测试时我们还能整体替换成打桩实现,租户开通、文件上传这些流程都加了自动化覆盖,不用真连云。

顺带一个实战小细节:所有云客户端在创建时都会记录一次接入日志到 CLS,上线后扫一眼就能确认每台机器的服务都正常起来,比登机检查快得多。想看更完整的云上架构组合,可以翻 腾讯云实践 的组件选型部分。

再补一点调试经验:云 SDK 的错误码一定要在服务层统一翻译成业务可读的消息,而不是原样抛给前端。租户上传文件失败时,看到存储拒绝访问和看到一串英文错误码,客服话术是完全两种体验。我们维护了一份错误码映射表,新加一类云调用就补一类,慢慢沉淀成产品的一部分,客服那边也终于不用再来问错误码什么意思了。

踩坑:对接阶段的四个坑

第一个坑是密钥直接写在代码里。早期图省事,SecretId 和 SecretKey 硬编码到一个常量文件,推到内部仓库后不得不轮换全部密钥,还出现有人在测试环境用生产密钥上传文件的险事。后来所有密钥统一走环境变量注入,生产环境权限收到部署系统手里。

第二个坑是 SDK 版本不统一。COS 的 SDK 升级后接口变了,一个同事升了另一个没升,同一份代码在不同机器表现不同。之后我们锁版本号并把所有云 SDK 收进 composer 锁文件,升级必须统一走冒烟流程。PHP 依赖管理上的经验,PHP API 实战 里有更细的版本兼容讨论。

第三个坑是忽略 SDK 的超时与重试默认值。默认超时偏长,某次 COS 网络抖动,上传请求挂住把 PHP-FPM 的 worker 全部占满。后来所有云调用统一包了超时和降级:COS 传失败提示重试、日志写入失败落本地、Redis 不可用直接穿透到 MySQL 并告警,绝不能让依赖把服务拖死。

第四个坑是本地开发环境与云的脱节。开发同学本地连假服务,联调时才发现真实 COS 的跨区域延迟和本地完全不是一个量级,一些分页逻辑因此慢得没法用。后来我们在测试环境也连真实的云服务,只是用独立的桶和实例,保证从代码到云的行为与生产一致,联调再没有翻过车。

小结

技术栈地图的意义是让每个能力都有一个明确的归属:运行环境在轻量应用服务器,数据在云数据库 MySQL 与云数据库 Redis,文件在对象存储 COS,日志在日志服务 CLS。自研只投在最核心的多租户逻辑上,其余能托管的绝不自己造轮子。地图画清楚之后,团队里的每个人——包括新来的同学——都知道遇到问题该去哪查、该去哪改,这本身就是架构最大的价值。新同学入职我们有一个固定考核:三天内能默画出这张地图。听起来苛刻,但一张地图能被新人快速复述,说明它确实简洁;复述不出来的地方,往往就是过度设计藏身的地方。这个考核帮我们砍掉过两次想引入的中间件,都因为画不进地图、也讲不清归属。

地图画清楚之后,从下一节开始我们正式进入租户模型的设计。

延伸阅读