3250 字
约 10 分钟
14
S18-A09 · 服务边界重构:拆与合的时机判断

"按微服务拆开之后,发布反而更麻烦了"——这句话在拆完一年内的团队里出现频率很高。原本一次发布、一次回归,变成三个服务各自发版、接口要对齐、联调要排期。问题不在于"微服务错了",而在于拆分被当成了一次性动作,而不是一笔有前提条件的交易。前提不成立时,拆分只是把单体内部的耦合换成了跨网络的耦合,成本还多了一层。

下面拆两件事:拆之前要验的前提,和拆之后该不该合回去。


1. 拆分收益的四个前提

前提 检验问法 不成立的后果
独立变更频率 这块代码的改动是否自成节奏 每次需求都要跨服务改,发布次数×N
独立扩缩需求 资源消耗是否与别人差一个量级 拆完仍同比例扩容,省不下机器
故障隔离价值 它挂了是否必须不影响别人 隔离无收益,只增加调用链长度
团队自治 是否真有独立团队、独立 backlog 组织没拆,服务先拆,协调成本翻倍

四条缺一不可,原因很直接:前三条决定"拆了有没有收益",第四条决定"拆了谁来承担协调成本"。少了第四条,收益会被同一个团队的沟通开销吃掉;少了第一条,服务边界会随每次需求被反复跨越,最后变成分布式单体。


2. 合并的信号

信号 观测方式 含义
改一个需求要动三个服务 需求单里关联的服务数 边界与业务能力不匹配
联调成为常态 每次发版的联调工时占比 接口契约不稳
共享库版本地狱 升级一个库要协调多个服务 代码边界假拆分
事务被迫跨服务化 出现补偿、对账、人工修数 数据边界不存在
两个服务总是同时发布 发布记录的时间相关性 事实上是一个部署单元

"总是同时发布"是最容易被忽略的合并信号:它说明两个服务的变更节奏完全耦合,独立发布的收益为零,成本却实打实存在。


3. 拆与合不是对称操作

维度 拆 合
数据 需要切分、迁移、双写 需要合并表、去重、冲突裁决
接口 新增契约,可先粗后细 契约要下线,调用方要改
组织 可以先拆团队再拆服务 团队已独立,合并意味着汇报线变化
心理 被视为进步 被视为回退,没人愿意提

拆容易是因为它只增加接口、不减少承诺;合困难是因为它必须撤回承诺——接口一旦被外部依赖,下线就要走废弃流程。加上组织惯性(团队已按服务划分,合并等于承认决策失误),合并的决策成本远高于技术成本。

结论:拆之前要按"能不能合回来"设计,包括数据能否回灌、接口是否有版本、调用方是否可控。


4. 数据边界才是真正的边界

-- 反例:两张表被两个服务写,边界只存在于 PPT 里
-- order-service:  UPDATE orders SET status='paid'
-- pay-service:    UPDATE orders SET status='paid', paid_at=NOW()

-- 正例:一张表一个属主,另一方通过接口或事件获知
-- pay-service 只写 payments,然后发 OrderPaid 事件
判定 说明
单表单写者 真正的数据边界
单表多写者 拆分未完成,只是换了部署形态
共享视图/只读账号 可接受,但要定义读的语义与延迟
跨库 JOIN 边界已破,需在应用层重组

判据简单:能不能在不改代码的情况下,把这张表的属主换给另一个服务。不能,说明耦合在数据层。


5. 共享代码库与共享数据库

共享物 影响 处理
共享工具库(无状态) 升级需协调,但语义安全 语义化版本 + 允许落后一版
共享领域模型库 一个服务的字段变更传染全部 拆成各自模型 + 转换层
共享数据库 无法独立变更 schema 按属主分库,跨库走接口
// 反例:领域对象直接跨服务复用,A 加字段 B 就得跟着发版
import com.acme.order.OrderEntity;   // 来自另一个服务的 jar

// 正例:各自模型 + 显式转换,契约在接口层
OrderView view = OrderView.from(orderDto);

共享代码库不必然坏,坏的是共享可变的状态(模型、常量、连接)。


6. 接口粒度的取舍

粒度 调用次数 变更成本 适用
极细(每字段一接口) 调用风暴,N+1 低,但要改很多处 极少使用
按用例(聚合接口) 一次往返 中,改动要兼容 主流选择
极粗(整页数据) 一次往返,传输大 高,任何字段变动都改契约 前端聚合层
细接口的代价:一次页面渲染 12 次远程调用,P99 = 单次 P99 × 12 的尾部叠加
粗接口的代价:任一字段变化都要改契约,调用方全体回归

判据:按"一次业务决策需要的数据"定粒度,而不是按数据库表定。表的粒度是存储视角,接口的粒度是使用视角。


7. 分布式事务的替代

方案 一致性 复杂度 适用
发件箱(Outbox) 最终一致,不丢事件 中,需中继进程 跨服务状态传播
状态机 + 补偿 最终一致,可见中间态 中高,需定义每个失败分支 长流程、有明确阶段
对账 事后一致 低,需定时任务与差异处理 金额类,必须有兜底
两阶段提交 强一致 高,阻塞与协调者单点 极少,且多在同一系统内
-- 发件箱:业务写与事件写在同一个本地事务里
BEGIN;
INSERT INTO payments(id, order_id, amount, status) VALUES (?, ?, ?, 'SUCCESS');
INSERT INTO outbox(id, topic, payload, created_at) VALUES (?, 'OrderPaid', ?, NOW());
COMMIT;
-- 独立中继进程轮询 outbox 投递,成功后标记,失败重试

关键点:一致性不是靠一个更强的协议换来的,而是靠"可重试 + 幂等 + 对账"三件套兜住的。


8. 逐步重构的路径

阶段 动作 判断是否继续
1 模块化 同一进程内按包/模块划边界,禁止跨模块直接访问 模块内改动是否已能独立
2 绞杀者 新逻辑走新服务,旧路径保留,流量按比例切 新旧一致性是否可控
3 数据切分 新服务拥有自己的库,旧库通过同步过渡 双写差异是否可观测
4 下线旧路径 观察窗口结束后删除 回滚是否还有意义
# 绞杀者:按路径与比例分流,回滚只需改权重
upstream legacy { server 10.0.0.10:8080; }
upstream modern { server 10.0.0.11:8080; }
split_clients "${remote_addr}" $backend {
    10%     modern;
    *       legacy;
}

模块化这一步最常被跳过,但它决定了后面能不能"只搬不改":模块边界清晰时,拆服务是搬目录;边界不清时,拆服务是重写。


9. 回滚设计

// 拆分开关:读旧写双,或读双比对,出问题一键回旧
if (flags.isOn("order.read.new")) return newClient.get(id);
return legacyClient.get(id);
回滚级别 手段 生效时间 前提
流量回退 权重/开关 秒级 旧路径仍在线
数据回灌 反向同步 分钟到小时 双向映射还在
代码回退 回滚发布 分钟级 契约未破坏
结构回退 合并库表 天级 几乎等于重做

判据:拆分方案里若没有"旧路径继续可写"的阶段,就没有真正的回滚。只有读回退而没有写回退,等于把数据留在了新结构里。


10. 康威定律的实际含义

服务边界 ≈ 沟通边界
两个团队要天天开会才能改一个接口 → 这个接口会被拆成两个
一个团队维护三个服务 → 这三个服务会逐渐长成同一个样子(共享库、共享表)

实际含义不是"照着组织图画架构",而是组织变更会在一到两个季度内改写架构。所以拆服务前先问:这个边界对应的团队是否存在、是否有独立的决策权。不存在时,先调整模块与负责人,比先拆服务便宜。


11. 度量

指标 采集方式 健康信号
变更周期 需求提出到上线的时间 拆分后应缩短,未缩短即失败
部署频率 各服务发布次数 独立发布比例上升
跨服务改动比例 需求单关联服务数 / 总需求 持续下降
变更失败率 回滚次数 / 发布次数 不因拆分而上升
联调工时占比 迭代工时统计 下降或持平
# 粗口径:近 90 天各服务的提交分布,看是否真的独立演进
git log --since="90 days ago" --format='%H' --name-only \
  | awk 'NF' | cut -d/ -f1 | sort | uniq -c | sort -rn

度量要在拆之前留基线,否则拆完无法判断收益,只能靠感觉争论。


12. 排错

现象 原因 处理
发布必须一起发 接口无版本,契约同步演进 加版本或加兼容字段
循环调用 边界按"技术层"而非业务能力划 上移或合并成一个服务
数据对不上 双写未覆盖失败路径 加对账任务,补齐差异
延迟突然变高 细接口导致调用叠加 聚合接口或批量接口
回滚后数据错乱 只回滚了读,没回滚写 保留反向同步通道
共享库升级阻塞 领域模型跨服务复用 拆模型,加转换层

13. 边界

场景 建议 理由
团队小、变更频率低 不拆,先模块化 协调成本由同一批人重复支付
单库性能足够 不拆,先加索引与读写分离 拆分不解决慢查询
代码质量差 不靠拆分解决 拆完只是把烂代码分布到网络上
业务边界还在剧烈变化 延迟拆,保持模块化 边界没定型时拆,注定要重拆
已有跨服务事务缠身 先合并再重划 拆合顺序错了,越拆越乱

一句话收尾:拆分是把组织成本换成技术成本,合并是把技术成本换回组织成本。哪边更便宜,取决于团队规模与变更节奏,而不是架构图好不好看。


14. 自测

  1. 拆分收益的四个前提里,为什么"团队自治"缺一不可?
  2. "两个服务总是同时发布"为什么是合并信号?
  3. 拆与合为什么不对称?不对称带来的设计约束是什么?
  4. 如何判断一张表是否已经构成真正的数据边界?
  5. 只有读回退、没有写回退的拆分,回滚会留下什么问题?
  6. 拆分前为什么要先留度量基线?
S18-A09 · 服务边界重构:拆与合的时机判断
https://www.clxhxhhr.top/posts/6122/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。