1.2 选型对比:独立部署与共享部署的账
场景:每户一套环境,还是大家挤一套
架构师会议上吵得最久的就是这个问题:每个租户单独一套代码加一套数据库,还是所有租户共用一套。主张独立部署的同事理由很足:隔离干净,一个租户的慢查询拖不垮别人,大客户参观机房时也更有面子;主张共享部署的同事算了一笔账:十个租户十套 MySQL 实例,光内存和备份就能吃掉大半利润,中小客户那点年费根本养不起。
我们最后选了第三条路:混合分层。代码永远只有一套,数据按客户分层——付费高、数据量大的租户用独立库,入门和小租户用共享库加租户号字段,文件和缓存则全部按前缀逻辑隔离。这个决定后来被证明是最关键的架构选择:它让我们的成本曲线随租户数平滑增长,而不是一步登天;也让销售在谈判桌上有了梯度方案,小户谈不下来可以先上共享版,等他们做大再升级,客户生命周期顺理成章。
选型的本质不是找一个完美的方案,而是找一个当下养得起、未来又转得过去的方案。纯独立部署的问题是新租户的边际成本太高,得提前买服务器;纯共享部署的问题是天花板明显,一个大客户就能把整锅汤挤洒。混合分层把两边的优点各取一半,代价是多写一层连接解析和模型基类,我们觉得这代价值。
做这个决定时还有个绕不开的现实:团队只有八个人,一旦选了纯独立部署,光十几个老客户的机器维护就够大家喝一壶,根本没有时间写新产品功能。架构师喜欢谈理想架构,但小团队的选型第一名永远是活下来,第二名才是优雅。
方案:三种形态逐项对比
光吵没用,我们把三种形态逐项打分。对比维度是当时最关心的五项:隔离强度、单户成本、升级难度、运维复杂度、故障爆炸半径。每项不是凭感觉打钩,而是对照过去一年的故障记录和成本表来填。这张表我们填了整整一个下午,最大的收获不是表格本身,而是填表过程中那些被数字逼出来的反思:比如共享部署的运维复杂度看似最低,但一旦把加租户号这件事全压在代码严谨度上,隐患其实比想象中大,所以最后我们给共享库也配了代理层限流和监控兜底。
| 维度 | 独立部署 | 共享部署(共享库加租户号) | 混合分层(我们的选择) |
|---|---|---|---|
| 隔离强度 | 物理级,互相不可见 | 逻辑级,依赖代码严谨 | 核心数据独立,边缘共享 |
| 单户成本 | 高,每户一套资源 | 低,资源摊薄 | 中位,按套餐升降 |
| 升级方式 | 逐户发布,周期长 | 一次升级全部生效 | 代码一次,数据层两档兼容 |
| 运维复杂度 | N 套环境 N 倍工作 | 一套环境 | 少量独立库加一套共享库 |
| 故障爆炸半径 | 一户故障不影响他人 | 一个慢查询影响全体 | 独立库户隔离,共享库有限流兜底 |
混合分层的核心思想是一句近似废话但很有用的话:把贵的隔离能力留给愿意付费的人。共享库里所有表都加 tenant_id 列并强制走索引,查询层用全局作用域自动注入,应用代码感知不到差异;独立库则是租户专属实例,连接按租户解析。对租户而言两种模式在功能上完全一致,只是后台数据落点不同,升级时我们也不需要为独立库户单独发版本,这点对保持统一升级的纪律至关重要。
这套分层还给了我们一个意外好处:谈大客户时,独立库是现成的卖点,尤其是金融和医疗行业的客户,一听数据物理分开就放心一半;共享库的租户续费升级成独立库,只需要跑一次迁移脚本,业务无感知,销售根本不需要找研发排期。顺带一提,建站部署的选型思路在 轻量建站实战 里有更轻量的单机版本,适合刚起步还没分层的阶段参考。
半年后我们做了一次复盘,用真实账单验证当初的选择,数字说话比推断有说服力得多:
| 复盘指标 | 独立库户 | 共享库户 |
|---|---|---|
| 单户月均基础设施成本 | 约两百元 | 约二十元 |
| 故障影响其他租户的次数 | 零次 | 两次(均已限流兜住) |
| 升级兼容工作量 | 无 | 无差异,代码一套 |
| 迁移到另一档的耗时 | 单户约两小时 | 单户约半小时 |
结论是混合分层如预期般把成本压了下来,共享库户的人均成本只有独立库户的十分之一,而因为限流兜底,两次共享库故障都没有扩散到其他租户。如果没有这张复盘表,我们也不会有底气在第二年又把三个大户迁到独立库——数据让我们既敢投入也敢回头。顺带一提,做复盘时别忘了把人工运维的隐性时间也算进去,独立库户虽然贵,但省下来的运维工时折算下来其实颇为可观。
配置:连接解析与共享库查询层
会员等级决定 db_mode,连接解析器在每个请求开始时根据当前租户切换数据库连接:
class TenantConnection
{
// 根据租户返回数据库连接名
public static function resolve(string $tenantId): string
{
$tenant = Tenant::find($tenantId);
if ($tenant->db_mode === 'dedicated') {
return 'tenant_' . $tenantId; // 独立库连接,配置里按户注册
}
return 'shared'; // 共享库,查询层自动补 tenant_id
}
}
共享库这侧,所有业务模型继承一个带全局作用域的基类,查询时自动拼接租户号,写入时自动填充。关键是开关要放在最底层,一次写好处处生效,而不是每个控制器里手工拼条件,手工拼一定会在某天被漏掉。独立库这侧其实更简单,表里没有 tenant_id 列,因为库本身就是租户的整套环境。两套差异被收敛进连接解析和模型基类两个文件,业务代码写起来完全一样,新人甚至不需要知道当前租户是哪一种模式。
套餐变更引发的模式切换是最微妙的一步:从小户升到独立户,我们用在线迁移工具按表分批搬运,先搬历史数据再追增量,最后一秒切换连接并校验行数,全程业务不断。整套流程跑通之后,升降级对客户来说只是后台点个按钮的事。
这里多说一句连接配置的组织方式。独立库户多了之后,配置文件会膨胀成一长串连接定义,既难看也容易配错。我们的做法是连接配置只写一份模板,具体参数(主机、账号、库名)存在配置中心的租户记录里,启动时按需实例化。这样加新独立库户时开发完全不碰配置文件,开通脚本一把梭,也不怕有人手滑改错别人的连接。
踩坑:混合分层的三个坑
第一个坑是共享库的慢查询连坐。最初共享库没做资源隔离,一个新租户导入了百万级商品数据做压测,整个共享库的其他租户首页都在转圈,客服工单瞬间堆满。后来我们在数据库代理层加了按租户号的并发限制,并把大导入任务改成凌晨低峰执行,同时在应用层对批量接口做了排队,杜绝一个租户把整个实例的 IO 吃满。共享库的 tenant_id 还必须是所有联合索引的第一列,否则优化器在某些查询里会忽略隔离条件,这是另一个偷偷越权的入口。额外加一条吧,就是监控里我们给共享库专门加了一个指标:每分钟慢查询的租户归属。一旦某户慢查询超过阈值,它的新增查询会被自动降级到低优先级队列,其他人完全无感。
第二个坑是两套模式的代码分叉。一开始有人在独立库的模型里直接写字段名,绕过了全局作用域,等这个客户因为业务增长迁回共享库时炸了——导出的数据没有 tenant_id 列,导入脚本直接报错,停机时间比预期多了三小时。教训是:即使独立库不需要租户号,模型基类也统一保留注入逻辑,没有特例就没有分叉。多团队协作时这点尤其重要,接口层面的契约管理可以参考 PHP API 实战 的做法。
第三个坑是计费口径被模式带偏。独立库的资源是独占的,共享库的资源是摊销的,最初我们的账单对这两类租户用同一个成本模型,结果小户的报价偏高(吓跑了好几个),而大户其实在亏钱。后来账单里把两类成本分开核算,共享库户按请求次数和存储量计费,独立库户按实例规格计费,定价才合理。想做更细的用量分析,可以看 腾讯云实践 中关于成本分摊的章节。
还有第四个不痛不痒但值得提的坑:共享库户膨胀的速度超预期。最初我们估计共享库能容纳七八十个租户,结果一年还没到,大表行数就让查询计划开始变形,只能提前做分库。教训是共享库的容量规划要按三倍预估,并且提前把分库方案写进路线图,真到那天才想方案一定手忙脚乱。分库落地那天我们运气也不好,赶上一个促销日,只能硬着头皮切,要是提早三个月演练就不会那么狼狈。
小结
独立部署与共享部署不是非此即彼:小客户要的是便宜和省心,大客户要的是隔离和安心,混合分层用一套代码同时满足这两种需求。关键就两条——代码永远一份,差异收敛进连接解析和模型基类;贵的资源只给付费的人,普通租户共享摊薄成本。如果哪天你发现自己在为某个租户写 if 分支,先停下来想想这是不是在破坏统一升级这条地基。最后给一个混合架构的演进建议:不要一上来就追求完美分层。我们的混合比例从最初全员共享库开始,等大户出现才切独立库,等数量到了才引进第二台服务器。每一步都有真实成本压力驱动,每一步都恰好解决当下最疼的问题——架构是长出来的,不是画出来的。下一节我们看机器怎么切:轻量应用服务器上,这些角色各自跑在哪。
