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