28.4 服务编排与编制
一句话
编排(orchestration)是 “中心指挥”:一个协调者按流程依次调用服务并处理补偿;编制(choreography)是 “各自响应”:每个服务订阅事件、自行决定下一步;前者可观测、可测,后者松耦合、难追因。
对照表
| 维度 | 编排(orchestration) | 编制(choreography) |
|---|---|---|
| 流程定义在哪 | 协调者/工作流引擎里 | 分散在各服务的订阅逻辑里 |
| 可观测性 | 强(一个地方看全流程) | 弱(要跨服务拼链路) |
| 耦合 | 协调者知道所有参与者 | 服务之间只认事件 |
| 变更成本 | 改流程 = 改协调者(集中评审) | 加一步 = 新增订阅(分散、易漏) |
| 单点 | 协调者(需高可用) | 无 |
| 适合 | 步骤多、分支多、要补偿的业务(订单/支付/履约) | 事件多、参与方多、彼此不关心的场景(通知/积分/统计) |
代码:编排用一个 “可重放的工作流”
// Temporal 风格(示意):流程即代码,天然可重放、可补偿
func OrderWorkflow(ctx workflow.Context, in OrderInput) error {
// 1) 预留库存;失败即返回
if err := workflow.ExecuteActivity(ctx, ReserveStock, in).Get(ctx, nil); err != nil {
return err
}
// 2) 扣款;失败则补偿(释放库存)
if err := workflow.ExecuteActivity(ctx, Charge, in).Get(ctx, nil); err != nil {
_ = workflow.ExecuteActivity(ctx, ReleaseStock, in).Get(ctx, nil)
return err
}
// 3) 履约(可异步、可重试)
return workflow.ExecuteActivity(ctx, Fulfill, in).Get(ctx, nil)
}
编制版本则没有 “流程代码”,只有各自的事件处理器:
OrderCreated → 库存服务(预占) → StockReserved → 支付服务(扣款) → PaymentSucceeded → 履约服务
实测:协同漏一个订阅,就少一次补偿
本机模拟 “4 步 Saga,第 4 步 notify 失败,应补偿 ship/pay/reserve”:
| 模式 | 实际执行的补偿 | 是否完整 |
|---|---|---|
| 编排(协调者知道全部补偿) | cancel_ship, refund, release_stock | ✅ |
| 协同(notify 失败无人订阅) | refund, release_stock | ❌ 漏 cancel_ship |
漏掉的 cancel_ship 就是 “包裹已经出库但没人取消”——协同的流程知识分散,漏订阅不会被编译或测试发现,只会变成线上资损。
编排:失败点 → 查自己的补偿表 → 逆序执行(可测试)
协同:失败点 → 指望某个服务订阅了失败事件 → 没有就静默漏掉
反模式
| 反模式 | 后果 |
|---|---|
| 用编制实现 “强顺序 + 补偿” 的业务 | 补偿逻辑散落各处,漏一个就资损 |
| 用编排实现 “谁都不关心” 的通知 | 协调者变成瓶颈与单点 |
| 编排里写业务规则 | 流程引擎变成第二套业务系统 |
| 编制没有全局追踪 | 出问题无法回答 “卡在哪一步” |
| 两种模式混用且无边界 | 一半在流程里、一半在事件里,排障成本翻倍 |
| 补偿动作不幂等 | 重试导致重复退款/重复释放 |
| 协同用“文档约定谁订阅”代替契约测试 | 新人加步骤时漏订阅,线上才发现 | | 编排的补偿表与流程分两处维护 | 加步骤忘加补偿,回滚不完整 |
三条铁律
🔑 要补偿、要可见 → 编排;要解耦、要扩展 → 编制。
🔑 同一业务链路只用一种主模式,另一种只做边缘通知。
🔑 补偿必须幂等,否则重试会把 “纠正” 变成 “二次事故”。
自检
Q1 为什么 “订单 → 支付 → 履约” 更适合编排?
▸ 答案:因为它有强顺序、多个失败点、必须补偿:库存要释放、款项要退回、履约要取消。编排把这些 “下一步与补偿” 集中在一个可测试的流程里;编制则要求每个服务自己知道 “失败后该通知谁”,漏掉一条就是资损。
Q2 编制(choreography)的真正优势是什么?
▸ 答案:参与方解耦:新增一个 “积分服务” 只需订阅 OrderCompleted,不用改流程定义。它适合 “一对多广播、参与方互不关心” 的场景。代价是 “全流程可见性” 变差,需要额外的追踪与补偿设计。
Q3 编排的 “单点” 怎么解决?
▸ 答案:用持久化工作流引擎 + 多副本:流程状态落库(可恢复),引擎本身做高可用;协调者崩溃后由其他实例接管并继续推进。注意 “流程状态” 也是数据,需要备份与版本兼容。
Q4 编排与 “Saga” 是什么关系?
▸ 答案:Saga 是补偿式事务的模式,编排与编制都可以实现它:编排版把 Saga 步骤写在流程里;编制版靠事件驱动、每个服务自己触发下一步与补偿。区别在 “谁掌握流程知识”,不在 “有没有补偿”(见 24.5 Saga (编排 / 协同 / 补偿))。
Q5 怎么给编制补上 “可见性”?
▸ 答案:三件事:统一事件规范(同一 trace_id/saga_id 贯穿)、集中式追踪(链路 + 事件表)、超时看门狗(某步长时间未完成即告警/补偿)。没有这三样,编制在生产里就是 “黑盒异步”。
Q6 什么时候该从编制迁到编排?
▸ 答案:当出现 “必须按序、必须补偿、必须知道进度” 的需求时(如涉及资金/库存/合规审批)。判据是 “出问题时,能否在一个地方回答 ‘这单卡在哪、下一步谁负责’”;答不上来,就该迁。
Q7 协同模式怎么发现 “漏订阅”?
▸ 答案:三条硬手段:契约测试(每个失败事件必须至少有一个消费者,且在 CI 校验)、端到端故障注入(在测试环境让某步失败,断言补偿全部发生)、生产对账(按 saga_id 检查 “失败单是否都有补偿记录”)。缺任何一条,漏订阅都只能靠事故暴露。
故障复盘(典型模式,脱敏)
| 阶段 | 现象 | 判断 | 动作 |
|---|---|---|---|
| T+0 | 履约失败后,部分订单未退款 | 协同漏订阅失败事件 | 手工补退款 |
| T+1h | 补退款 300 单,其中 12 单重复退 | 补偿不幂等 | 按幂等键去重 |
| T+1d | 对账发现仍有 5 单未退 | 无全局 saga_id | 按业务单号回溯 |
| 之后 | 加编排 + 补偿幂等 + 对账 | 可发现、可修复 | 周级对账常态化 |
根因:用协同实现 “必须补偿” 的资金流程,且补偿不幂等。代价:300 单人工 + 12 单重复。根治:资金链路用编排;补偿必须幂等 + 可对账。