单体里,复杂度藏在代码、数据库和发布队列;拆开后,它沿网络、超时、版本与团队边界重新出现。你需要的不是更多服务名,而是一份能证明边界值得存在的账单。
创业第 4 年 8 个月,「秒抢」累计用户约 2000 万,公司 260 人,研发 60 人,其中 30 人的日常改动集中在订单单体主仓。拆分前,发布队列最长排三天;拆成十多个服务半年后,一次「已付款却迟迟不出票」的跨服务故障已经查了两周。
你把十段调用链摊开。没有哪个服务彻底宕机,每个团队都能拿出自己那段成功率;但用户要的是整条链路完成,不是十份局部健康证明。
抽样 trace 终于露出一条细缝:订单服务调用库存预占平均 90ms,P99 却偶尔超过 1.2s;订单超时后返回失败,库存其实已经成功。补偿任务随后释放库存,而一条迟到的支付成功消息又继续推进出票。每个局部动作都能解释,组合起来却把一个订单撕成两条时间线。
老陈把半年前的发布报表放到旁边:单体时代,30 人改同一个仓库,冲突、全量回归和发布窗口让小改动排三天。拆分确实解开了发布队列,却没有让业务复杂度消失;它把原来编译器和事务能看见的关系,搬到了网络和异步消息里。
A是对的,理由只有一条:把局部证词串成一次用户旅程。用订单业务键连接 HTTP、消息和补偿,才能看见「库存已成功—订单超时—补偿释放—支付迟到」这条跨边界时序。先获取端到端证据,再决定谁修。
B会重复过去两周。局部错误率最高不等于根因;本次每个服务都在阈值内,故障恰好藏在它们之间。让团队继续各看各的大盘,只会得到更多无法拼接的正常截图。
C可能把故障拖得更长。统一长超时会占住线程与连接,把快速失败变成排队;而库存已成功、上游未知的问题并不会因多等几秒自动获得幂等语义。超时必须从端到端预算向下分配。
D方向有价值,但证据仍不完整。冻结发布能缩小变量集,适合作为并行动作;它无法重建已经断掉的调用链,也不能解释历史受影响订单。先补关联标识和关键采样。
「拆分不降低复杂度,它把复杂度从代码里搬到了网络上。」单体里的函数调用失败,通常会抛异常并共享同一事务上下文;跨服务调用则多出超时、重试、部分成功、版本兼容、序列化、认证和可观测性。复杂度的总量未必减少,只是归属改变了。
假设单个服务可用率都是 99.9%,一条用户请求必须串联 10 个服务,并且其中任意一个失败都会让整条请求失败:
精确值 0.999^10 ≈ 0.99004,也就是约 99.0%。一年约 8,760 小时,单服务 0.1% 不可用对应约 8.76 小时;整链约 1% 不可用,对应约 87.6 小时。这个估算假设故障相互独立、十个服务都是强依赖;共享基础设施会让故障相关,降级和缓存则可能让部分依赖变成可选,因此真实值要用端到端 SLI 验证。
公式的价值不是劝你拒绝拆分,而是提醒你:每个新同步依赖都在消耗整条链路的可用性预算。如果推荐服务失败不该阻止下单,就不要把它放进强依赖乘积;超时后返回空推荐,整体路径仍可用。
| 单体里的问题 | 拆分后的形态 | 需要的新护栏 | 漏交账单的后果 |
|---|---|---|---|
| 函数调用与共享类型 | 网络调用与接口版本 | 超时预算、契约测试、向后兼容 | 调用方升级节奏不一致,运行时才暴露破坏 |
| 本地事务 | 跨服务部分成功 | 幂等、状态机、补偿与对账 | 订单失败但库存成功,业务状态分叉 |
| 进程内日志和调试器 | 跨主机、跨队列调用链 | 统一 trace id、结构化日志、端到端 SLI | 每个服务都正常,用户路径仍失败 |
| 一次部署与回滚 | 多版本并存、独立发布 | 兼容窗口、灰度、服务目录 | 旧调用方遇到新响应,回滚顺序互相锁死 |
| 一个团队内协调 | 跨团队依赖与排期 | 边界归属权、SLO、升级协议 | 组织问题伪装成接口问题 |
第一,业务能力是否内聚。订单服务应拥有订单生命周期,而不是把「创建订单」「查询订单」「取消订单」按技术动作拆成三个互相聊天的服务。高频一起变化的规则应该留在同一边界内。
第二,数据归属权是否清楚。一个服务写自己的数据,其他服务通过契约读取或订阅事件;若三个服务都直连同一组表,发布虽然分开了,数据耦合仍留在地下。
第三,故障能否被隔离。拆出推荐服务的价值,是推荐挂了仍能下单;如果订单同步等待推荐成功,边界没有购买可用性,反而多乘了一个失败概率。
第四,团队是否能独立交付。康威定律提醒你,系统接口会映照组织沟通结构。一个 3 人团队维护 12 个服务,不会因为仓库变多就获得 12 份自治;一个边界若每次发布仍要六个团队开会,它在组织上尚未独立。
可以用变更记录验证边界:过去三个月哪些文件总是一起改、哪些表总在同一事务里、哪些故障需要同一批人处理。如果两个候选服务 80% 的发布都必须同步,它们也许仍是一个业务能力;先在单体内模块化,等待边界稳定,往往比立刻上网络更便宜。
「代码很多」不是充分理由。拆分提案应该给出拆前基线和拆后可验收目标,否则半年后只能用服务数量证明工作发生过。至少从五个维度交账:
| 门槛 | 拆前证据 | 拆后目标 | 不满足时的替代动作 |
|---|---|---|---|
| 独立变更 | 模块与主仓发布耦合率、排队时间 | 多数发布不要求其他模块同步 | 先做模块依赖检查与增量测试 |
| 独立扩容 | 该模块资源曲线明显不同 | 热点只扩目标服务,成本可下降 | 先在单体内隔离线程池和缓存 |
| 故障隔离 | 模块故障经共享资源拖垮主链 | 目标服务失败时主流程可降级 | 先加舱壁、超时和功能开关 |
| 数据归属 | 写表与事务边界可以明确圈定 | 其他服务不再绕过契约直写 | 先收敛 DAO 与写入口 |
| 团队归属 | 有长期团队承担需求与值班 | 发布、SLO、成本由同一团队闭环 | 保持模块,不创建无人服务 |
以通知模块为例:它占主仓发布的 18%,峰值资源与订单不一致,失败时可以降级为延迟发送,有独立团队维护,也不需要参与订单事务——五项证据支持拆出。相反,「订单创建」与「订单状态推进」过去三个月 86% 一起修改、共享同一状态表和同一值班人,强行拆开只会把一次事务改成跨服务补偿。
| 阶段 | 动作 | 放行门槛 | 回滚点 |
|---|---|---|---|
| 1 · 模块化 | 在单体内建立 API、依赖方向和表写入边界 | 架构测试抓住越界调用 | 仍是同进程,无远程回滚 |
| 2 · 影子验证 | 新服务读取复制流量,结果只比较不生效 | 关键字段差异率低于约定阈值 | 停止影子流量,不影响用户 |
| 3 · 小流量切读 | 按租户或业务键让 1% 查询走新服务 | P99、错误率、结果一致性均达标 | 路由开关切回单体 |
| 4 · 单写迁移 | 写入口只保留一处,事件同步给旧读模型 | 对账持续闭合,补偿可演练 | 在明确停写窗口恢复旧入口 |
| 5 · 扩大与退役 | 逐级放量,确认无旧调用方后移除旧代码 | 服务目录和依赖扫描无遗留边 | 每一级保留有限时间回切能力 |
最危险的捷径是长期双写:单体与新服务都修改同一业务事实,看似方便回滚,实际制造了两个持有者。更安全的迁移始终保留一个写入裁决点,通过事件构建另一侧读模型;需要回切时先停写、对账,再迁移裁决权。
假设用户请求预算 2 秒,入口调用 5 层,每层都配置 2 秒超时,最坏路径不会神奇地保持 2 秒:上游可能在下游等待耗尽后继续重试,线程和连接被层层占住。更合理的方式是从端到端预算倒推:预留网络与响应开销,越靠下游的超时越短,并确保重试仍落在剩余预算内。
例如 2 秒预算中,网关留 200ms 返回,订单编排预算 1.8s;库存 400ms、支付 800ms、出票异步受理 300ms,其余留给抖动。支付已耗 750ms 后,订单服务不应再发起一次完整 800ms 重试。预算是一张逐层递减的支票,不是每层重新领一张。
第 7 章的 No.09 级联故障会在微服务里获得更多传播边;第 14 章要解决的,就是怎样用 metrics、traces 和 logs 把这些边重新画回一张可诊断的图。
本章不新增病历卡。最重要的是回收一张已经见过的卡,再预告一张只收录在附录里的隐蔽病:一个负责传播,一个负责伪装健康。
两张卡叠在一起,就是这次两周排障:库存调用偶发长尾,没有触发单服务阈值;订单超时和补偿却把局部灰色故障传播成业务级联。诊断不能只找一块红色面板,要沿用户路径寻找哪一条边先改变了形状。
发布排队三天是真成本,跨服务排障两周也是真成本。架构选择不是「先进」与「落后」,而是决定复杂度由谁、在哪一层支付。每一个都是对的,每一个都有账单。
A · 继续拆:团队可按业务能力独立发布与扩容,热点和故障有机会隔离。账单:网络失败、跨服务一致性、可观测性与平台治理成本全面上升;需要与服务数量匹配的团队归属。适合边界稳定、发布冲突高且平台能力成熟的组织。
B · 暂停拆分:本地事务、调试器和一次回滚仍然便宜,可以集中治理测试与发布。账单:30 人继续共享仓库和数据库,发布协调会随团队增长变重,热点模块也难独立扩容。适合团队较小、边界仍频繁变化或分布式治理尚未就绪的阶段。
C · 模块化单体:先用代码依赖、表归属权和内部契约验证边界,保留进程内调用;拆出时迁移路径更清楚。账单:需要纪律和架构测试,不能只建几个目录就宣布模块化;独立扩容与故障隔离要等真正拆出。适合边界需要验证、又已出现协作摩擦的团队。
「秒抢」的选择:采用 C,停止按名词继续切服务。订单核心状态机先回收成明确模块,推荐、通知等可降级能力保持独立;以三个月变更耦合和故障数据证明边界后,再拆出具备独立团队与 SLO 的模块。康威定律的账单也写进方案:没有长期 owner 的服务不再新增。
正确复盘不是找一个团队背锅,而是重建复杂度如何跨过边界:
补齐业务关联。按订单键连接 HTTP、库存事件、支付回调和补偿记录,完整 trace 覆盖从 12% 提升到 86%,首次看见两条分叉时间线。
止住部分成功。订单超时后先按幂等键查询库存预占结果,不再立即判失败;补偿释放增加版本条件,paid-but-not-issued 从 0.72% 降到 0.09%。
重写超时预算。从 2 秒端到端 SLO 向下分配各段预算,取消中间层重复重试;库存 P99 长尾仍可见,但不再占满上游线程。
收敛边界。冻结两个高耦合服务的继续拆分,把状态机归属权交给一个订单域团队;发布清单加入契约兼容、trace 覆盖和降级演练。
调用链各服务探活正常、平均错误率平稳,只有特定分区请求持续失败,整体面板呈 ▇▇▇▇。最值得优先怀疑什么?
B。「部分坏 + 探活正常 + 特定维度失败」是灰色故障。A 是最诱人的错误:探活只覆盖一个探针路径,不能代表每个用户分区;C 与 D 都有明显的时间形状,不会保持这种局部、持续的伪健康。
10 个强依赖服务串联,每个可用率 99.9%,故障相互独立。整体可用率最接近哪个值?
B。0.999^10 ≈ 0.99004,约 99.0%,年不可用量级约 88 小时。A 忽略了十个必经依赖的乘积;C 把 0.1% 错当成每个服务损失 1 个百分点后直接相减。
两个候选服务 80% 的发布都要同步、共享同一批表,并由同一个 4 人团队维护。下一步更稳妥的是?
B。高变更耦合、共享数据和同一团队说明业务边界尚未独立。A 与 D 会增加网络和发布对象,却不改变协作事实;C 只换通信工具,也没有解决归属权。先在进程内证明边界更便宜。
端到端请求预算 2 秒,五层调用各自都配置 2 秒超时并允许重试。主要风险是什么?
B。端到端预算应向下递减分配,重试还要消耗剩余时间。A 把每层超时当成同一张支票,实际却可能串行等待;C 忽略资源占用和用户截止时间,长超时会把故障传播得更远。