跳到正文

微服务治理与架构决策 ​

一句话:架构决策不是选「最先进」的组件,而是在可逆性、故障半径、团队成本之间做有边界的取舍;写清楚「什么信号出现时推翻它」,比结论本身更重要。

适用版本:Spring Boot 3.2.x / Spring Cloud 2023.0.x。与 01 的分工是:01 讲组件职责,本文只给可执行的决策表。

为什么需要 ​

同样的争论会反复出现:同步还是异步、要不要拆服务、用 Nacos 还是 Apollo、上不上 Service Mesh。没有决策记录时,每次换一个人就重新吵一遍,最后往往按「上次谁声音大」决定。

决策表的作用不是给标准答案,而是把判断条件、默认选择、代价和反转信号写下来,让后来的人能复查。

核心机制 ​

决策的四要素 ​

要素要回答的问题
可逆性这个决定多久能撤回?撤回成本多大?
故障半径选错时,影响一个接口、一个服务,还是全站?
团队成本团队是否有人能维护它?值班时能不能排障?
反转信号出现什么指标/事件时,必须重新评估?

优先做「可逆、故障半径小」的决定;不可逆或全站级的决定,先做小范围验证。

决策一:同步调用还是异步消息 ​

判据同步(Feign/Dubbo)异步(MQ)
调用方是否需要立即拿到结果是否
下游失败是否必须让上游失败是否,可补偿
是否能接受最终一致否是
典型场景查库存、扣款、鉴权发短信、写日志、通知、削峰

默认:核心链路同步,非核心旁路异步。反转信号:同步链路 P99 随下游线性上升、下游故障导致上游大面积失败,说明该异步化或加熔断;异步链路出现「消息丢了/重复」且没有对账,说明补偿机制没建好。

反例:下单成功后用 MQ 通知「订单已创建」,用户立刻点支付却查不到订单。这是把「必须同步确认」做成了异步;正确做法是本地事务提交后再发消息,或让支付页容忍短暂延迟。

决策二:API 版本怎么管 ​

方式示例优点代价
URL 版本/api/v1/order直观、网关好路由URL 膨胀
Header 版本X-API-Version: v2URL 稳定排查时容易漏看
参数版本?version=v2网关友好污染业务参数

默认:对外 API 用 URL 版本;内部 RPC 用「新增字段兼容 + 不改变已有字段语义」,尽量不升版本。

兼容规则:

  • 新增可选字段:兼容,直接上线。
  • 改字段含义/类型:不兼容,新建版本。
  • 删除字段:先标记废弃,观察调用量归零,再下线。

反转信号:旧版本调用量长期不降、或调用方无法升级,说明下线计划要考虑合同/外部依赖,不能只按技术判断。

决策三:拆不拆微服务 ​

判据倾向单体/模块化倾向拆分
团队规模< 5 人多个团队按业务域负责
业务边界还在频繁变化边界稳定,能独立演进
发布节奏一起发版可接受需要独立发布
扩容需求整体扩容够用某模块需要独立扩容
一致性大量跨模块强一致跨服务最终一致可接受
运维能力手工部署、监控弱有 CI/CD、可观测性

默认:边界不清时先模块化,不要拆进程。反转信号:一个模块的故障/发布/扩容已经阻塞其他模块,且团队能承担网络与分布式事务成本,再拆。

决策四:Spring Cloud 还是 Service Mesh ​

判据Spring Cloud(SDK)Service Mesh(Sidecar)
语言Java 为主多语言
治理需求注册、配置、限流、熔断细粒度流量、mTLS、多集群
团队熟悉 Spring有 K8s/平台团队
成本框架升级要动所有服务Sidecar 资源与网络跳数

默认:Java/Spring 团队用 Spring Cloud;多语言、强平台能力再评估 Mesh。反转信号:治理逻辑在多个语言重复实现、框架升级无法推动,才考虑下沉。

决策五:自建还是买/用托管 ​

判据自建托管/云服务
团队有中间件运维能力是否
数据合规要求必须自控可接受厂商边界
规模大到摊薄成本中小规模
故障责任自己兜底有 SLA,但受厂商影响

默认:注册/配置/网关这类基础设施,优先用成熟开源 + 托管;只有规模或合规逼到一定程度才自建。

实战场景 ​

一、把“必须同步确认”做成了异步:支付页查不到订单 ​

现象:订单服务改为“本地事务提交后发 MQ,由订单查询服务落库”。上线后客服收到投诉:用户下单成功,立刻点支付却提示“订单不存在”,约 3 分钟后自动恢复。按 MQ 监控看,消费延迟 P99 只有 200ms,看不出问题。

排查:这类问题不在中间件指标里,在业务时序里。

定位层取证据判定
谁依赖这条消息列出“下单成功后 1 秒内会读订单”的所有入口(支付页、订单详情、客服)有同步读取依赖 → 该链路不能异步
实际延迟分布消息发送时间戳与查询服务落库时间戳的差值分布,取 P99 与最大出现过分钟级 → 有积压、重投或消费失败
失败是否可见消费端异常是否只写日志、有无重试与死信无对账即可能永久缺单
事务与发送顺序发消息是在本地事务内还是 AFTER_COMMIT事务内发送 → 回滚后消息仍被消费,出现幽灵订单
用户侧兜底支付页是否容忍“订单处理中”不容忍就必须同步确认

根因是把决策表里的“调用方是否需要立即拿到结果”回答错了:需要,但实现选了异步。

对症做法:

java
// 1. 写路径回到同步确认:订单主记录在下单请求内本地事务落库
@Transactional(rollbackFor = Exception.class)
public Order create(CreateReq req) {
    Order o = orderRepo.save(req);            // 同步可见
    outbox.save(new OrderCreatedEvent(o.id()));// 同事务写本地消息表
    return o;
}
// 通知、风控留痕、搜索索引继续异步,由 outbox 投递

// 2. 消息只在事务提交后发出,避免“回滚了但消息已消费”
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
text
3. 如果确实要异步(跨服务写),必须同时具备:
   - 用户可感知的中间态(“订单处理中”,支付页轮询/查询兜底)
   - 对账任务:下单流水 vs 订单表,缺单自动补或告警
   - 死信与人工队列,消费失败不能只 log.warn

ADR 要补上代价那一栏:把“支付页在 3 分钟内查不到订单”写成可接受的代价并获得业务方签字,才是完整决策;没写过代价的决策,出事时没人认。

复盘要点:判据可以标准化——凡“同一次用户操作内的读写”不允许跨异步边界,异步只用于旁路(通知、统计、索引)。这条应成为评审检查项,配一个日级对账作为兜底信号。

二、版本混用与“别人家的选型”:升级半途停在最贵的位置 ​

现象:一个团队照搬兄弟公司的技术栈,把注册中心从 Eureka 换成 Nacos、Gateway 换成 4.x,但 Boot 还停在 2.7、Seata 客户端是 1.4、Sentinel 是 1.7。灰度环境正常,生产首个发布批次里 3 个服务启动失败:NoSuchMethodError、bootstrap 不加载配置、Nacos 客户端连不上 9848。

排查:这类故障的特征是“启动期或第一次调用期暴露”,而不是运行期。

定位层取证据判定
兼容矩阵Boot ↔ Cloud ↔ SCA ↔ 中间件客户端的版本对应关系(以官方兼容表为准)任一项不在同一世代即为混用
实际依赖树mvn -q dependency:tree -Dincludes=org.springframework.cloud,com.alibaba.cloud出现 spring-cloud-starter-netflix-ribbon/hystrix → 迁移未完成
配置加载方式Spring Cloud 2020+ 默认不加载 bootstrap老写法继续用 bootstrap 会静默不生效
网络前置9848/9849 是否放通客户端 2.x 走 gRPC,与 1.x 假设不同
谁在维护值班手册、告警接手人、故障复盘记录里有没有该组件无 owner 的组件等于没有能力

按四要素复盘:这次决策的故障半径是“全站入口”,可逆性差(配置与客户端已改),团队成本被低估(没人排障过 Nacos 2.x)——三项都不满足,却直接做了。

对症做法:

text
1. 先把版本收进一个 BOM,禁止子模块自带中间件客户端版本
   dependencyManagement 引入 Spring Cloud / SCA BOM,CI 里断言依赖树无 Netflix 旧件
2. 拆成可逆步骤:先升 Boot → 再换注册中心 → 再换网关 → 最后换治理组件
   每步都有回滚方案与验证口径(启动成功 + 一条端到端用例)
3. 迁移期间双写:新旧注册中心并行注册,按调用方分批切流,随时能退回
4. 组件必须有 owner 与演练:没有演练过的容错方案不算高可用方案
bash
# CI 门禁:依赖世代一致性检查(示例口径,按团队 BOM 实现)
mvn -q dependency:tree -Dincludes=io.github.openfeign | grep -E "feign-core:12\.|11\." && \
  { echo "feign 版本混用"; exit 1; }

复盘要点:兄弟公司的选型可以借鉴结论,不能跳过他们的团队成本评估。ADR 里“团队成本”要具体到“谁能值班排这个组件的故障”,写不出人名就说明还没具备上线条件。反转信号也要写:例如“同类启动失败再次发生”即触发重新评估是否回退到原方案。

三、反转信号没接监控:决策过期了三年没人复查 ​

现象:一条 2023 年的 ADR 写着“库存与订单用 Seata AT,反转信号是热点行锁冲突”,阈值 0.1%。三年里没人看过这个指标;大促当天锁冲突率实际到 12%,才在故障复盘里发现当初设计的方案早已不再适用,且回滚成本已经很高(6 个服务都依赖全局事务语义)。

排查:决策失效通常是静默的,需要专门检查。

定位层取证据判定
反转信号是否存在ADR 里有没有可度量的阈值与观察周期只写“性能不好时重评”等于没有信号
信号是否接进监控该指标在告警规则里能否搜到搜不到 → 决策不会自动被复查
谁负责看指标 owner 与值班职责是否对应无人负责 → 指标有数也没人看
决策当前形态依赖该决策的服务数、数据量、外部合同依赖越广,撤回成本越高,越需要提前验证
最近一次演练故障演练记录里是否覆盖这条决策的失败场景没演练过 → 高可用假设只是纸面

对症做法:

md
# ADR-0014:热点 SKU 从 Seata AT 改为分桶库存 + 本地消息表

- 状态:已接受(2026-10),取代 ADR-0003
- 触发:AT 全局锁冲突率连续 7 天 > 0.1%(监控项 lock_conflict_ratio,实际值 12%)
- 决策:stock 按 bucket_no 拆 16 行,扣减随机选桶;跨库写改本地消息表 + 对账
- 代价:库存不再是单行强一致,需日级对账兜底;对账差异有人工处理流程
- 回滚:保留 AT 分支代码与 undo_log 配置,按 SKU 灰度切换,可 1 天内回退
- 复查:每季度看 lock_conflict_ratio、对账差异笔数、超卖投诉数
yaml
# 反转信号必须是可执行告警(Prometheus 规则口径示例)
- alert: GlobalLockConflictHigh
  expr: seata_lock_conflict_ratio{service="order-service"} > 0.001
  for: 7d
  labels: { action: "review-adr-0003" }

ADR 的复查节奏与 owner 写在文档头部,季度架构评审只过“已被告警触发的 ADR”,不做全量翻阅。

复盘要点:这份专栏里所有决策类内容的共同纪律是——没有反转信号的决策等于没有决策,接不进监控的反转信号等于没写。把“新 ADR 必须附带告警规则 ID”作为评审通过条件,就能让决策在数据出现变化时自动回到桌面,而不是等故障逼出来。

最小上手 ​

把决策写成 ADR,放进仓库,和代码一起评审:

md
# ADR-0001:订单与库存之间使用同步调用

- 状态:已接受(2026-10)
- 背景:下单必须立即知道库存是否足够;库存服务 P99 80ms。
- 决策:Feign 同步调用,readTimeout=500ms,关闭重试。
- 代价:库存服务故障会直接导致下单失败;需要 Sentinel 熔断 + 降级。
- 反转信号:库存不可用导致下单失败率连续一周 > 0.1%,或 P99 > 500ms。

检查依赖是否混入旧组件:

bash
mvn -q dependency:tree -Dincludes=org.springframework.cloud,com.alibaba.cloud

输出里如果还有 spring-cloud-starter-netflix-ribbon、spring-cloud-starter-netflix-hystrix,说明迁移没完成。

生产动作 ​

  • 每次架构评审留 ADR:背景、决策、代价、反转信号四段,不写「因为先进」。
  • 依赖收敛:用 dependency:tree + BOM 对齐版本;混用不同世代的 Spring Cloud 组件,问题往往在启动时才暴露。
  • 故障演练验证决策:杀掉下游、断开注册中心、压满线程池,看降级/熔断/缓存是否符合预期。
  • 把反转信号接进监控:ADR 里写的阈值要变成告警,否则永远没人复查。
  • 定期复查:业务量、团队、合规变化后,旧决策可能不再成立;ADR 要能更新,不是一次性文档。

边界 ​

  • 决策表不是标准答案:同样的表,不同团队/合规/规模会得出不同结论;表的作用是让争论有共同语言。
  • 不要为了「技术先进性」上 Mesh/Serverless:运维复杂度是真实成本,尤其是值班和故障定位。
  • 不要用微服务解决组织问题:团队协作问题先改组织/流程,拆服务只会把问题变成网络问题。
  • 不要过度设计:能用单体 + 模块化撑两年,就不要提前付分布式事务的成本。

讲清楚 ​

架构决策的核心不是选型,而是把「可逆性、故障半径、团队成本、反转信号」写清楚。同步还是异步,看调用方是否需要立即结果、失败是否必须传导;API 版本优先 URL 版本,字段只加不改;拆不拆微服务,看团队、边界、发布节奏和运维能力,而不是看公司大小;Spring Cloud 和 Service Mesh 是 SDK 与 Sidecar 的取舍,Java 团队默认前者;基础设施优先成熟开源 + 托管。每个决定都写一份 ADR,把代价和「什么信号出现时推翻」写进去,并接到监控上。没有反转信号的决策,等于没有决策。

把一门技术讲透到既懂也会用