接口调用时序图怎么画:超时、重试、降级、熔断、补偿的画法(研发向)
面向研发/测试/架构:把一次接口调用里最容易出事故的超时、重试、降级、熔断、补偿机制用 UML 时序图画清楚。含规范、反例修正、测试清单与 FAQ。
先把话说透:接口调用“画得专业”的关键,不是把箭头画得多,而是把 5 个边界画清楚
如果你正在画“服务 A 调服务 B”的时序图,目标通常不是给新人科普箭头长什么样,而是为了落地实现、设计评审、测试用例、故障演练。
真正让一张图从“看起来对”变成“上线能用”的,是这 5 个边界——你只要把它们在 UML 时序图里表达清楚,后面再加细节就不会走偏:
- 超时边界:谁在计时?是连接超时、读超时、还是整体 deadline(timeout budget)?
- 重试边界:重试什么条件?重试几次?间隔怎么退避?有没有抖动(jitter)?
- 熔断边界:失败率/慢调用达到阈值后,什么时候“直接拒绝”?半开怎么探测?
- 降级边界:兜底返回什么?返回“默认值”还是“可用但不完整”?是否标记为降级结果?
- 补偿边界:业务已经部分成功怎么办?如何回滚/对账/异步补偿?幂等键是什么?
很多事故都不是“没做重试”,而是重试做错;不是“没做超时”,而是超时设置错层级;不是“没画图”,而是图里没有把边界画出来,导致评审时大家以为是一回事。
想省时间的话:你可以先用这个在线时序图生成器把骨架画出来,再逐段补充超时/重试/熔断/降级片段: 手打时序图 - 时序图在线制作
左侧编辑器支持点选“生命线/消息/组合片段(alt/opt/loop/par)”,右侧实时预览;写完可一键自动排版,并可导出 SVG/PNG/JPEG/draw.io。你也可以让 AI 先生成初稿,再按你的系统约束微调。
一、你到底在画什么:一次“接口调用”其实是一个小协议
把接口调用画成一根同步箭头(A → B → 返回)会隐藏掉最关键的信息:
- 网络是不可靠的:超时不等于失败,失败也不等于对方没执行。
- 中间层会改变语义:SDK、网关、LB、服务网格可能会重试、限流、熔断。
- 业务动作常常不可逆:扣库存、扣款、发券、发短信、写 DB 都可能“部分成功”。
所以一张“能指导实现与测试”的时序图,通常需要把接口调用拆成三个层次:
- 调用层(Call):请求怎么发出去、超时怎么计、返回怎么解析。
- 治理层(Resilience):重试、超时预算、熔断、限流、降级。
- 业务层(Business):状态机推进、幂等、防重、补偿与对账。
你不是在画“一个 HTTP 请求”,而是在画“一个可重放、可失败、可降级的小协议”。
二、参与者(生命线)怎么选:别把所有东西都画成“服务A/服务B”
要把边界画清楚,生命线建议至少拆到下面这些(根据你们实际架构增减):
- 调用方业务服务(Service A):真正决定业务语义的地方
- 客户端/治理组件(Client SDK / Resilience Layer):超时预算、重试、熔断常在这里
- 网关/LB/Service Mesh(可选):如果它会重试/超时/熔断,就值得画
- 被调方接入层(Service B API):鉴权、幂等、参数校验、限流
- 被调方业务层(Service B Core):写库、发消息、推进状态
- 存储(DB/Cache):状态落地与幂等约束
- 异步系统(MQ/EventBus):补偿、对账、最终一致
- 监控告警(Tracing/Metrics/Alert):请求ID、失败率、慢调用
一个简单但很有效的判断:
- 谁“可能改变语义”(比如自动重试、超时、熔断、降级)→ 单独画生命线。
- 谁“承担一致性责任”(比如幂等、状态机、补偿)→ 单独画生命线。
三、最小可交付画法:先画“成功路径”,但要在图里提前埋好超时与重试的位置
很多人一上来就画 alt/loop,结果整张图一坨。更稳的做法是:
- 先画一条不考虑失败的主链路(Happy Path)
- 再在明确的位置加上:超时、重试、熔断、降级、补偿
下面是一个推荐的“最小骨架”(用文字描述,你可以直接照着画):
3.1 主流程:A 调 B 成功
- Service A → Client SDK:callB(request, deadline)
- Client SDK → Service B API:HTTP/gRPC Request(带 requestId、deadline、idempotencyKey)
- Service B API → Service B Core:validate + auth + idempotency check
- Service B Core → DB:write / update(状态机推进)
- Service B Core → MQ(可选):emit event
- Service B API → Client SDK:200 OK / success
- Client SDK → Service A:return result
图上必须标注的 3 个字段(写在消息旁边就行):
requestId/traceId:用于串联日志与链路追踪deadline/timeoutBudget:用于表达“超时预算”idempotencyKey:用于表达“重复请求的语义”
这些不是“字段细节”,而是后续所有稳定性设计的锚点。
3.2 超时:在哪里画?
建议把“计时”画在你们系统真正执行计时的那一层:
- 如果超时由 SDK 控制:把 timeout 画在 Client SDK 上方,并在 SDK 内用 opt/alt 表达。
- 如果超时由网关/服务网格控制:单独画出 Mesh,并把超时画在 Mesh 上。
- 如果 B 内部也要做超时(例如调用 DB/第三方):那是 B 的内部时序图,不要混在 A→B 这张里,除非你专门画“端到端”全链路。
一句话:谁负责超时,谁的生命线上就应该出现“超时”片段。
四、把“超时 + 重试”画对:loop 不是重点,重点是“重试条件与幂等语义”
4.1 重试前先回答 3 个问题(图里也要体现)
-
重试依据是什么?
- 网络超时(connect/read timeout)
- 5xx / 网关超时
- 明确的可重试错误码(例如
UNAVAILABLE) - 业务错误(通常不该重试,比如参数错、余额不足)
-
重试是否安全?(幂等)
- GET/查询通常幂等
- 扣库存/扣款/创建资源通常非天然幂等,需要 idempotencyKey
-
重试预算是多少?
- 总 deadline 2s,重试 3 次但每次 1s → 绝对画不出来“可用”,因为预算已经超了
你可以把这三个答案用注释或在消息上标注,例如:
deadline=2000msretry: max=2, backoff=100/300ms + jitterretryOn: timeout, 5xx, UNAVAILABLE
4.2 UML 画法建议:loop + alt 组合
推荐结构:
- 在 Client SDK 生命线处放一个
loop [attempt <= maxRetries] - 在 loop 内放一个
alt:alt [success]→ 正常返回alt [retryable failure]→ wait(backoff) 再发请求alt [non-retryable failure]→ 直接返回错误
把“等待退避”画成一条 self-message(SDK 自己给自己发消息)通常更清晰:
- Client SDK → Client SDK:sleep(backoff + jitter)
这样测试同学一眼就能看出“你们到底有没有退避”。
4.3 反例:只画 loop 不画幂等
常见错误图:
- 画了
loop 重试 3 次 - 但没有
idempotencyKey - 也没画 B 侧的判重存储
这类图的隐含结论是:“每次重试都可能造成一次扣库存/扣款”。
**修正方式:**在 B 的接入层加入幂等片段(建议用 alt 表达):
- Service B API → DB:insert idempotencyKey(唯一索引)
alt [insert success]→ 进入业务处理alt [duplicate key]→ 读取历史结果并返回(或直接返回同样的状态)
注意:如果你们采用“幂等表 + 状态机”,建议把状态字段(PROCESSING/SUCCESS/FAILED)也标在注释里,图会更可执行。
五、把“熔断 + 降级”画对:不要把熔断画成一句“服务不可用”
熔断(Circuit Breaker)和降级(Fallback)经常被混用:
- 熔断:为了保护系统,当下游不健康时,主动快速失败,阻止雪崩。
- 降级:为了保住体验,在失败时返回“可接受的替代结果”。
5.1 熔断的 3 个状态,时序图里怎么表达
建议在 Client SDK(或网关)生命线上,用 alt 或 state 注释表达状态:
Closed:正常放行Open:直接拒绝,不发请求Half-Open:放少量探测请求
一种清晰的画法:
- Client SDK → Client SDK:checkCircuitState()
alt [state == Open]- Client SDK → Service A:return error (fast fail)
else [state != Open]- 继续发请求
然后在请求失败后画一条更新熔断器的 self-message:
- Client SDK → Client SDK:recordFailure(latency, error)
为什么要画这两条 self-message?
因为它把“熔断是客户端策略”明确出来:不是 B 回你一个“我被熔断了”,而是你决定不打过去。
5.2 降级的关键:返回值语义要明确(并能被上游识别)
降级不是随便返回空对象。一个可测试、可运营的降级通常要满足:
- 返回值在 API 契约里有定义(例如
data为空但degraded=true) - 上游能识别并做 UI/业务分支
- 日志/指标能统计“降级比例”
时序图建议这样画:
opt [fallback enabled]- Service A → Service A:buildFallbackResponse()
- Service A → Metrics:count(degraded)
如果降级发生在 SDK 层,也可以画成:
- Client SDK → Client SDK:fallback()
- Client SDK → Service A:return fallbackResult (degraded)
5.3 反例:熔断 + 降级画在一起,谁也看不懂
常见错误:
- 图里写“失败 → 熔断 → 降级”
- 但没有状态判断,没有阈值触发,没有 fast fail
**修正方式:**把它拆成两段:
- 熔断:决定“还打不打”
- 降级:当“不打”或“打失败”时,返回什么
六、把“补偿”画对:补偿不是重试的延长线,而是另一条链路
当你画到补偿(Compensation),说明你已经承认一件事:
超时/失败并不等于没执行,业务可能处于“部分成功”。
这时你需要在图里增加一个常被忽略的参与者:补偿/对账任务(Compensator)。
6.1 哪些情况需要补偿(建议在图里用 alt 标出来)
- A 侧超时,但 B 侧可能已经写库成功(典型:请求到达但响应丢失)
- B 侧写库成功,但下游事件发送失败(DB 成功、MQ 失败)
- A 侧收到失败,但实际上 B 已经进入 PROCESSING(异步处理)
6.2 推荐画法:主链路 + 异步补偿链路
主链路(同步调用):负责尽快给出一个“可接受的同步结果”,不要把补偿硬塞在主链路里。
补偿链路(异步):负责把系统拉回一致状态。
你可以这样画:
- Service A → Service B:call
alt [timeout]- Service A → MQ:publish(“CheckStatus”, requestId)
- Service A → Service A:return “UNKNOWN” / “PROCESSING”(视业务而定)
然后另起一段:
- Compensator → MQ:consume CheckStatus
- Compensator → Service B:queryStatus(requestId)
alt [status == SUCCESS]→ Service A DB:update local statealt [status == NOT_FOUND]→ retry later / alert
这段图的价值在于:测试能按图写出“超时后最终一致”的用例,排障也知道该看哪条异步链路。
6.3 补偿一定要和“幂等/状态机”绑在一起
补偿最怕重复执行导致二次伤害。所以你要在图里明确:
- 补偿任务用什么 key 去幂等(requestId/outBizNo)
- 状态机允许哪些跃迁(比如从 PROCESSING → SUCCESS,不允许 SUCCESS → PROCESSING)
如果你愿意更严谨:把“状态机校验”画成一条对 DB 的查询 + 条件更新(compare-and-set),比一句“更新状态”靠谱得多。
七、一步步把图画出来(适合写技术方案/画评审图)
这里给你一个可复用的 6 步流程,基本适用于“接口调用 + 稳定性治理”类图。
- 确定范围:这张图是“端到端(包含 DB/MQ)”,还是“调用方视角(只到被调方 API)”?范围越大,越要分图。
- 拆生命线:把“改变语义的组件”拆出来(SDK/网关/幂等存储/补偿任务)。
- 画 Happy Path:只画成功路径,但要把 requestId、deadline、idempotencyKey 标出来。
- 加超时:在真正计时的生命线处加
alt [timeout],并明确返回语义(错误/UNKNOWN/PROCESSING)。 - 加重试:用
loop包住请求,并画出 backoff + jitter;标出 retryOn 条件。 - 加治理与补偿:
- 熔断:先判断 state,再决定是否发请求
- 降级:明确返回值语义与统计
- 补偿:另起异步链路,绑定幂等与状态机
如果你不想从零搭框架,可以用在线生成器先选好:生命线、消息、alt/loop/opt/par 组合片段,让 AI 生成一个“骨架版”,再由你把公司内部的错误码、阈值、SLA 补上。
工具入口: 手打时序图 - 时序图在线制作
你会发现这种“先骨架后细化”的节奏,比一开始就手写 PlantUML 更不容易漏关键边界;并且导出 draw.io 后还能在评审会里现场改。
八、测试/评审用检查清单(建议直接贴到 PRD 或提测单里)
下面这份清单不是“理论正确”,而是为了让你上线后少背锅。你可以按模块挑重点。
8.1 超时检查
- 超时定义清楚:连接超时、读超时、整体 deadline 分别是多少?
- deadline 是否在链路中向下游传递(header/metadata)?
- 超时预算是否考虑了重试(总耗时不超 budget)?
- 超时返回语义是否明确(是否可能是 UNKNOWN/PROCESSING)?
- 超时后是否触发异步对账/补偿?
8.2 重试检查
- retryOn 条件明确:只对可重试错误重试(timeout/5xx/UNAVAILABLE),不对业务错误重试
- 次数/退避策略明确(指数/固定/自适应)并带 jitter
- 对非幂等操作是否有 idempotencyKey + 服务端判重
- 重试是否可能放大流量(重试风暴),是否与熔断/限流协同
8.3 熔断检查
- 熔断器位置明确(SDK/网关/mesh),状态与阈值可配置
- Open 时是否快速失败(不发请求)
- Half-Open 探测策略明确(放行比例/探测间隔)
- 指标可观测(失败率、慢调用、熔断打开次数)
8.4 降级检查
- 降级返回值语义明确(degraded 标记/默认值范围)
- 上游/前端能识别并正确展示(不要把降级当成功数据)
- 降级比例可统计并告警(超过阈值通知)
8.5 补偿/一致性检查
- 超时/失败场景是否有对账或补偿链路
- 补偿动作幂等(同一 requestId 重复执行不会二次伤害)
- 状态机跃迁受控(非法跃迁要拒绝并告警)
- “DB 成功、MQ 失败”是否有可靠投递方案(Outbox/事务消息/重试队列)
九、常见错误(反例)与修正:为什么你画的图“看起来对但就是不专业”
错误 1:把“超时”当成“失败”
症状:图里写 timeout -> failure,然后就结束。
问题:超时意味着“在预算内没等到响应”,不代表对方没处理。对非幂等写操作,超时后最危险。
修正:在超时分支里增加“查询状态/对账/补偿”链路,或返回 PROCESSING/UNKNOWN 并引导客户端轮询。
错误 2:重试不画退避与抖动
症状:loop 里连续画 3 次请求。
问题:不退避会加剧拥塞;无 jitter 会造成同步重试雪崩。
修正:在 loop 中加 self-message:sleep(backoff + jitter),并标注 backoff 策略。
错误 3:把熔断画成“B 返回 503”
症状:图里写“熔断 -> B 返回 503”。
问题:熔断通常是客户端/网关策略,Open 时压根不该打到 B。
修正:把熔断器画在 SDK/网关生命线上,Open 分支直接 fast fail。
错误 4:降级返回值语义不明确
症状:图里写“降级 -> 返回空列表”。
问题:空列表可能被上游当成“真实无数据”,造成业务误判;排障也无法知道是降级。
修正:在返回消息旁标注 degraded=true 或专门的错误码/字段,并画出 metrics 统计。
错误 5:把补偿塞进主链路,导致“同步接口永远不返回”
症状:图里超时后还继续画补偿、对账、发消息、重算等。
问题:同步链路要有确定的返回时点;补偿通常异步。
修正:主链路只负责“返回一个契约内的同步结果”,补偿另起异步链路。
十、FAQ(研发/测试经常会问的那些)
Q1:有了重试,是不是就可以把超时设得很短?
不一定。超时太短会导致大量“假失败”,触发重试放大流量;超时太长会占用线程/连接导致系统卡死。更合理的是:
- 用 整体 deadline 控制总预算
- 每次 attempt 有单次超时(比如读超时)
- 重试次数与单次超时要一起设计,让总耗时落在 SLA 内
Q2:所有写接口都要做幂等吗?
如果它会被重试(客户端重试、网关重试、用户重复提交、网络抖动),那它就应该具备幂等语义,至少在“可见副作用”层面幂等。
常见做法:
- 客户端生成
idempotencyKey(或业务流水号) - 服务端用唯一索引/幂等表记录处理结果
- 重复请求返回同一结果或同一状态
Q3:熔断和限流要不要画在同一张图里?
如果你的评审重点是“稳定性治理”,可以画在同一张图里,但建议把它们都放在同一层(SDK/网关)并各自独立成 opt/alt 片段,不要把消息线画得满天飞。
Q4:超时后返回 UNKNOWN/PROCESSING,会不会让上游很难处理?
会更复杂,但这是现实:对非幂等写操作,超时后你无法可靠判断对方是否执行。
工程上常见两种处理方式:
- 返回
PROCESSING并提供查询接口,让上游轮询/回查 - 返回错误,但内部必须启动对账/补偿,并保证最终一致
你在时序图里至少要把“返回语义”和“后续动作”画出来,否则测试无法覆盖、业务也无法接受。
Q5:一张图能同时给研发、测试、产品看吗?
可以,但要分层:
- 给产品:主链路 + 关键分支(超时/降级)即可
- 给研发:加上重试条件、熔断状态、幂等/补偿
- 给测试:补齐可观测点与用例触发条件(哪些错误码触发重试/熔断)
实操建议:同一份图导出两版(精简版/技术版)。如果你用在线生成器导出 SVG 或 draw.io,维护两版会轻很多。
收尾:你可以用这张图去做三件事(而不是只当“文档装饰”)
- 把接口契约写清楚:哪些错误码可重试、超时语义是什么、是否支持幂等键。
- 把测试用例列出来:按图覆盖 timeout/5xx/慢调用/熔断 Open/Half-Open/降级等路径。
- 把故障演练脚本写出来:注入延迟、注入 5xx、模拟下游不可用,验证熔断与降级是否按预期工作。
如果你愿意把“图”变成团队资产:建议把骨架模板沉淀下来(生命线 + 组合片段),以后每个关键接口只需要补业务差异。
需要一个能快速复用模板、并支持导出到设计评审文档的工具,可以直接用: 手打时序图 - 时序图在线制作
左侧点选生命线/消息/组合片段,右侧实时预览;自动排版;支持 SVG/PNG/JPEG/draw.io 导出;也支持 AI 生成骨架并按你的规范调整。