3240 字
约 10 分钟
14
S18-A08 · 微服务拆分的决策:边界、代价与不拆的理由

微服务拆分的讨论经常从"怎么拆"开始,跳过"为什么拆"和"拆了要付什么"。真实决策里,不拆是一个需要论证的正当选项,而拆分的每一份独立性都有对应账单。这篇只谈决策:判断链、代价结构、可量化上限,以及什么时候应该合并回去。


1. 拆分要解决的四个真实问题

真实问题 判据(不拆能否解决) 拆分的对应收益
独立扩缩 单体能否按模块水平复制后仍划算 只给热点模块加资源
独立发布 发布频率是否被高频改动模块拖累 改动节奏解耦,回滚范围缩小
故障隔离 故障是否能靠线程池 / 舱壁隔离住 进程级隔离,故障域收窄
团队自治 团队规模与交付节奏是否已互相阻塞 所有权与边界重合

"为了架构好看""别人都拆了""简历需要"属于伪动机:它们不产生可验收收益,却照样产生全部代价。判断方式是给每个收益写一条验收判据,写不出来的收益不算收益。


2. 限界上下文的识别与四种误判

识别方法有四个维度:业务能力(谁负责什么决策)、数据所有权(谁能写入某个实体)、变更频率(是否经常一起改)、团队边界(谁在维护)。

候选边界打分(每项 0–2 分,任一项为 0 则该切分不成立)
  业务能力内聚   : 同一个业务规则是否集中
  数据唯一写入权 : 是否只有一个模块写这张表
  变更同步率     : 改 A 是否总要同时改 B(高 → 不该切开)
  团队归属清晰   : 是否明确一个 owner
误判 表面现象 后果
按技术分层切 网关服务、业务服务、数据服务 每个业务改动都要改三层,发布反而更耦合
按表切 一张表一个服务 跨服务 join 变跨服务调用,事务碎成片
按团队人数切 有几个人就切几个服务 边界随组织变动频繁重划
按 CRUD 切 用户服务仅做增删改查 业务规则无处安放,散落在调用方

3. 五类代价与量化方式

代价 量化指标 从哪取数
跨服务事务 需要最终一致的业务动作数量、补偿逻辑行数 领域事件清单与对账任务数量
网络不确定性 新增调用次数 × 单次失败率、超时预算占用 链路追踪的扇出与超时分布
调用链复杂度 单次请求跨进程数与深度 追踪中的 span 数与最大深度
数据一致性 不一致窗口时长、对账频率 对账任务周期与差异率
运维成本 服务数 × 每服务固定开销(部署、日志、监控、值班) 服务目录与配置项总数

运维成本常被按"边际成本很低"估算,实际上每增加一个服务都要多一套:部署流水线、健康检查、日志采集、指标看板、告警规则、依赖库升级窗口。服务数翻倍时这部分是线性增长的固定成本,不随流量摊薄。


4. 数据所有权:共享库与共享表

方案 本质 后果
共享数据库(多服务同库,各写各表) 分布式单体 库级变更一起受影响,事务跨服务但库仍耦合
共享表(多服务写同一张表) 更糟 写入语义无主,schema 变更无法独立
私有库 + API 边界清晰 需要跨服务查询与一致性设计
私有库 + 只读副本/视图 折中 查询方便,但复制延迟进入业务语义

共享表的问题不在技术实现,而在于没有人对这张表的语义负责。一旦两个服务都能写同一实体,不变量就无处校验,最终一致性窗口无法界定。


5. 调用链深度与扇出的安全上限

安全上限经验值(按实际 SLO 反向推导,不要照搬)
  同步调用深度  : ≤ 3 层(含网关)
  单请求扇出    : 并行 ≤ 8,串行累积超时 ≤ 主路径预算的 60%
  单次调用超时  : 主路径预算 ÷ 深度,且必须小于上游超时
深度 风险 缓解
1–2 层 低 常规熔断与超时
3 层 中:任一环节抖动叠加放大 每层超时预算显式分配
≥4 层 高:P99 被级联拉长,故障传播快 改为异步、聚合层或本地缓存
宽扇出 + 串行 最危险 并行化 + 部分结果降级

同步链的深度决定了延迟的下界是各层 P99 之和,也决定了可用性是各层可用性之积。深度每加一层,尾巴就多一条。


6. 分布式事务的四条路

方案 一致性级别 实现成本 适用
本地消息表 最终一致 低:同库事务写业务表与消息表 单体拆出后的首选
事务性发件箱 最终一致 中:需 CDC 或轮询投递 与消息中间件解耦
Saga 最终一致,可补偿 中高:每步都要补偿动作 长流程、跨多服务
TCC 最终一致,预留式 高:三接口 + 幂等 + 空回滚处理 资金类、资源预占
-- 本地消息表:业务与消息同库同事务,先落库再投递
CREATE TABLE outbox_msg (
  id          BIGINT PRIMARY KEY AUTO_INCREMENT,
  topic       VARCHAR(64)  NOT NULL,
  payload     TEXT         NOT NULL,
  status      TINYINT      NOT NULL DEFAULT 0,  -- 0 待投递 1 已投递
  retry_count INT          NOT NULL DEFAULT 0,
  created_at  DATETIME     NOT NULL,
  KEY idx_status_created (status, created_at)
);

四条路的共同前提是消费端幂等。没有幂等键,任何"最终一致"方案都只是把重复从边缘问题变成常态问题。


7. 拆分的正确顺序

① 单体内部模块化:包结构 + 依赖方向约束 + 模块间只走接口
② 数据所有权先行:每张表标注唯一写入模块
③ 旁路新功能:新逻辑走独立模块,旧路径不动
④ 抽独立进程:只把已验证边界、且扩缩需求不同的模块进程化
⑤ 替换调用:调用方切到服务接口,保留回退开关
⑥ 验收:能否独立发布、独立回滚、独立扩容

顺序错了就会得到分布式单体:进程分开了,数据与事务还缠在一起,复杂度翻倍而收益为零。


8. 单体模块化作为前置步骤

// 模块边界用编译期约束,而不是口头约定
// 订单模块只依赖 user-spi,不依赖 user-impl
implementation project(':user-spi')
// 禁止跨模块访问内部包:用 ArchUnit 之类的规则校验
// noClasses().that().resideInAPackage("..order..")
//   .should().dependOnClassesThat().resideInAPackage("..user.internal..")
约束手段 强度 成本
包名约定 弱 几乎为零
代码评审守则 中 持续人工成本
架构测试(ArchUnit 等) 强,可进 CI 一次性 + 维护规则
多模块构建 强,编译期阻断 构建配置复杂度上升

模块化能在单体里验证边界是否成立、数据所有权是否清晰、变更频率是否真的分离——这些验证在拆成进程之后成本高一个量级。


9. 什么时候不该拆

情形 理由
事务密集、强一致要求高 拆开后要做 Saga / TCC,收益抵不过一致性成本
团队规模小(够不上按服务分工) 没有独立的 owner,服务只是把一个仓库拆成十个仓库
调用频繁且延迟敏感 进程内调用变网络调用,延迟与失败率同时上升
数据高度交叉 大量跨表查询,拆开后只能靠冗余或聚合服务
还在找产品方向 边界会随需求频繁变化,拆分成本无法摊销

10. 合并回去的信号

信号 表现 操作
服务间调用占比过高 单请求跨 4 个以上服务,纯转发 合并为同一进程内的模块
每次改动都要同时发多个服务 发布编排成为瓶颈 边界选错了,重新划
数据一致性靠对账硬扛 对账任务成为常态 收回到同一事务边界
服务数增长但部署频率未提升 自治收益为负 合并低频服务
团队维护成本超过收益 值班与升级窗口吃满 按领域收敛到少量服务

合并是正常决策,不是失败。判断标准只有一个:拆分的四项收益里,实际拿到了几项。


11. 演进路线与验收判据

阶段 1  模块化单体:边界、数据所有权、依赖方向
阶段 2  抽 1–2 个边界最清晰的模块(通常是扩缩差异最大的)
阶段 3  建立基础:链路追踪、幂等、消息可靠性、统一配置
阶段 4  按季度评审:收益项 / 代价项的实测对比
# 验收判据写成可检查的清单,而不是会议结论
split_review:
  independent_deploy: true      # 该服务能否单独发布而不动其他服务
  independent_scale: true       # 能否单独扩容且成本可见
  rollback_isolated: true       # 回滚是否只影响自身
  data_owner_unique: true       # 每张表是否只有唯一写入方
  p99_budget_ok: true           # 新增一跳后的延迟预算是否仍满足
  ops_cost_counted: true        # 新增服务的固定运维成本是否入账

拆完如果不能独立发布,这次拆分就没有交付它的核心收益——这是最硬的验收条件,也最容易被跳过。


12. 排错

现象 常见原因 处理
拆分后延迟整体上升 同步调用深度增加 减少跳数、加聚合层或本地缓存
故障传播更快 缺超时与熔断,扇出被一起拖死 逐层超时预算 + 舱壁隔离
对账差异长期存在 缺幂等键、消息重复或乱序未处理 补幂等键与顺序键
发布仍需一起发 边界按表而非按能力划分 重新识别上下文,合并或重划
服务数量增长后交付变慢 运维固定成本超过自治收益 合并低频服务

13. 边界

场景 建议
团队 < 2 个 pizza 规模 模块化单体
强事务业务(账务核心记账) 谨慎拆,优先在单体内做模块边界
超高 QPS 且延迟敏感的主链路 少拆,靠水平扩展与缓存
组织已按业务线分工 边界与团队对齐,拆分收益最高
一次性项目 / 短期交付 不拆

14. 自测

  1. 拆分要解决的四个真实问题中,哪一项最容易被伪动机替代?
  2. 共享表为什么比共享数据库更糟?
  3. 同步调用深度增加一层,延迟与可用性分别如何变化?
  4. 本地消息表与事务性发件箱的差别在哪里?
  5. 出现什么信号时应该考虑把服务合并回去?
  6. 拆分验收里最硬的那个条件是什么,为什么容易被跳过?
S18-A08 · 微服务拆分的决策:边界、代价与不拆的理由
https://www.clxhxhhr.top/posts/6057/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。