在互联网大厂的技术宣讲和架构分享中,“单元化(SET 架构)”几乎是高可用、异地多活、无限水平扩展的代名词。PPT 里的架构图总是优雅美观:流量在最前端通过 GSLB 和网关,按照路由键(Routing Key)精准分流到不同的 SET(单元)中,各单元闭环运行,数据库互不干扰,灾备时一键切流。
然而,PPT 表现得多完美,落地时的痛点就有多刻骨铭心。
真正带队冲锋过单元化改造的架构师都知道,这是一场对业务研发、基础架构、数据一致性乃至运维体系的“全体系拉力赛”。本文不谈那些人人皆知的单元化概念,只聊在实际落地、拆分、运行过程中,那些最容易让人踩空、甚至导致灾难性后果的“隐形深坑”。
一、 路由键(Routing Key)的“降维打击”:多维度业务实体的冲突
单元化的核心前提是**“分流”**。要分流,就必须选定一个路由键(比如 user_id、tenant_id 或 merchant_id)。理论上,只要按照这个 Key 将流量和数据打散到不同的单元,就能实现闭环。
但真实的业务绝非单一维度。
1. 业务天然的多维度特征
以电商为例,如果以 user_id 作为路由键:
- 买家流量可以完美闭环在 A 单元。
- 但是,**卖家(商户)**需要管理自己的商品和订单。一个大商户的订单,是由散落在全国各个单元的买家下单产生的。如果商户后台想查询“我今天的所有订单”,由于这些订单数据分散在不同的单元,这就变成了一个典型的 Scatter-Gather(广播汇聚) 问题。
- 如果以
merchant_id作为路由键,那买家下单时,就必须跨单元去读写卖家单元的数据,单元封闭性瞬间瓦解。
2. 隐形坑:跨单元 Join 与全局索引的膨胀
当无法避免多维度查询时,团队通常会采用以下妥协方案,但它们各带剧毒:
- 数据双写/同步:把买家维度的订单,通过 MQ 异步同步一份到商户维度的“全局库”或 ES 中。坑在于:延迟怎么控制?在秒杀场景下,商户后台看到的库存和订单状态与买家端不一致,客诉直接打爆。
- 影子表与全局索引:为了支持按非路由键查询,建立全局路由索引表。这个索引表本身怎么高可用?它一旦挂了,全网非路由键查询全部瘫痪,单元化直接退化为单点系统。
二、 强一致性幻觉:跨单元分布式事务的“真面目”
在单体或普通微服务架构下,我们习以为常的“本地事务”或“强一致性保障”,在单元化下会变成极其昂贵的奢侈品。
1. 无法避免的跨单元调用
即使路由键设计得再好,也总有无法闭环的场景。例如:用户 A(单元 1)给用户 B(单元 2)转账,或者买家(单元 1)使用了全局共享的优惠券模板(主单元)。
此时,RPC 调用必须跨越物理距离。北京到上海的单程网络时延大约是 30ms。一旦发生跨单元调用:
- RT(响应时间)飙升:原本几毫秒的接口,因为几次跨单元的 RPC 和 DB 操作,RT 轻松突破 150ms。
- 连接池枯竭:由于 RT 变长,上游服务的线程和连接被长时间占用,极易引发雪崩。
2. 柔性事务(Saga/TCC)的隐藏代价
为了解决跨单元一致性,大家开始上 Saga、TCC 等分布式事务。
- 业务侵入极深:研发人员需要为每个业务写 Try-Confirm-Cancel,开发成本直接翻倍。
- “幽灵复活”与幂等漏洞:在网络抖动、切流重试的极端情况下,Confirm 阶段失败,后续的 Cancel 或重试由于幂等没做好,导致数据重复扣减。这种资金类的资损事故,是大厂最无法容忍的底线。
三、 全局共享数据的“精神分裂”:延迟与脑裂
并非所有数据都能被“单元化”。配置信息、商品属性、营销规则、系统开关等,属于全局共享数据。
通常的做法是:单点写入,全局广播。在一处修改,然后通过 Canal/DTS 异步同步到所有单元的只读库中。
[ 主单元 (中心写入) ] ──(DTS/MQ 异步同步)──> [ 单元 A (只读) ]
└──> [ 单元 B (只读) ]
1. 别低估“同步延迟”对业务的杀伤力
在平时,网络延迟可能只有几毫秒。但在大促、机房网络拥堵或光纤被挖断时,同步延迟可能达到数秒甚至数分钟。
- 典型惨案:商家在后台将一件商品下架(写入中心单元),但由于延迟,单元 A 的消费者依然能看到并下单。等数据同步过来时,已经超卖了数百件。
- 降级开关失效:核心服务挂了,架构师在中心端按下“降级开关”,结果因为网络抖动,降级指令在某些单元延迟到达,导致该单元直接被大流量冲垮。
2. 双活/多活切换时的“脑裂”与数据覆盖
当发生机房级故障,需要将流量从 A 单元切到 B 单元时,最怕的是 A 单元其实“没死透”(比如只是单向网络中断)。
A 单元还在往本地数据库写数据,而 B 单元已经被接管并开始写入。当网络恢复、双向同步重新拉起时,两边由于产生了相同主键或冲突的更新,DTS(数据传输服务)会陷入疯狂报错,或者直接发生数据覆盖(Data Overlay),事后订正数据能让整个 DB 团队掉几层皮。
四、 爆炸半径“反向扩大”:全局路由表的致命单点
引入单元化,初衷是为了**“隔离故障”**,降低爆炸半径——A 单元挂了,只影响 20% 的用户,其余 80% 正常。
但现实中,往往因为全局路由表的管理不当,导致爆炸半径反而扩大到 100%。
1. 路由配置的“核按钮”
路由表(Mapping Table)决定了哪个用户去哪个单元。这个表通常缓存在网关、甚至客户端本地。
- 配置推送事故:某大厂曾发生过一次事故,运维在更新路由规则时,脚本逻辑出错,将所有用户的路由全部指向了容量最小的容灾单元。瞬间,该单元被瞬间压垮,进而引发雪崩,导致全网不可用。
- 路由抖动(Route Flapping):当网络出现轻微抖动时,路由健康检查机制过于敏感,误判 A 单元挂了,自动将流量切往 B 单元;几秒后又切回来。这种高频的路由切换,会导致数据库连接暴增、缓存击穿,直接把本来没事的机房活活憋死。
五、 研发与运维的“认知过载”:日常协作的无形损耗
很多公司在推行单元化时,只算技术账,不算人效账。单元化落地后,日常的研发和运维成本会呈指数级上升。
1. 研发套路的彻底颠覆
以前新来一个开发,写个 Spring Boot 项目,本地起个 MySQL 就能跑通测试。
单元化后:
- 本地怎么模拟跨单元调用?
- 怎么测试数据同步延迟下的业务表现?
- 为什么我本地跑得好好的代码,上预发环境就因为“非路由键关联查询”被网关拦截报错?
研发人员不仅要懂业务逻辑,还要时刻在脑子里画出一张“单元拓扑图”,极大地消耗了心智。
2. CI/CD 与 DDL 变成高危操作
在多单元环境下,数据库 Schema 的变更(DDL)是一场灾难。
- 几十个单元的数据库,如何保证 DDL 的原子性执行?
- 执行到第 5 个单元时报错了,前 4 个已经生效的怎么回滚?
- 在灰度发布(Canary)期间,新版本的代码往旧版本的数据库写数据,或者旧版本的代码读新版本的字段,兼容性测试稍有疏忽,就是线上 P0 级事故。
结语:如何避坑?给准备上单元化的团队几点建议
单元化不是银弹,它是架构师在规模逼近物理极限时,不得不做出的妥协与置换——用极高的复杂度、运维成本和开发心智,去置换空间上的无限扩展能力。
如果你的业务还没到非上不可的地步,请尽量推迟这个决定。如果必须要上,请牢记以下几条带血的经验:
- 业务边界能切多干,决定了单元化能走多远。如果在拆分前,业务服务之间充斥着剪不断理还乱的强耦合,强行上单元化只会得到一个“分布式单体怪兽”。
- 永远不要信任分布式事务。在单元化架构下,要尽可能通过业务妥协(如最终一致性、允许短时间内数据不一致、事后补偿机制)来规避跨单元强一致要求。
- 路由规则越简单越好。尽量使用无状态的算法路由(如 Hash 取模),避免使用重度依赖动态数据库查询的配置路由。
- 重视可观测性(Observability)。跨单元的 Trace 追踪、数据同步的时延监控,必须在单元化改造的第一天就建设好,否则线上出问题时,你连数据在哪丢的都找不到。