时序图栏目 ·

接口调用时序图怎么画:超时、重试、降级、熔断、补偿的画法(研发向)

面向研发/测试/架构:把一次接口调用里最容易出事故的超时、重试、降级、熔断、补偿机制用 UML 时序图画清楚。含规范、反例修正、测试清单与 FAQ。

先把话说透:接口调用“画得专业”的关键,不是把箭头画得多,而是把 5 个边界画清楚

如果你正在画“服务 A 调服务 B”的时序图,目标通常不是给新人科普箭头长什么样,而是为了落地实现、设计评审、测试用例、故障演练

真正让一张图从“看起来对”变成“上线能用”的,是这 5 个边界——你只要把它们在 UML 时序图里表达清楚,后面再加细节就不会走偏:

  1. 超时边界:谁在计时?是连接超时、读超时、还是整体 deadline(timeout budget)?
  2. 重试边界:重试什么条件?重试几次?间隔怎么退避?有没有抖动(jitter)?
  3. 熔断边界:失败率/慢调用达到阈值后,什么时候“直接拒绝”?半开怎么探测?
  4. 降级边界:兜底返回什么?返回“默认值”还是“可用但不完整”?是否标记为降级结果?
  5. 补偿边界:业务已经部分成功怎么办?如何回滚/对账/异步补偿?幂等键是什么?

很多事故都不是“没做重试”,而是重试做错;不是“没做超时”,而是超时设置错层级;不是“没画图”,而是图里没有把边界画出来,导致评审时大家以为是一回事。

想省时间的话:你可以先用这个在线时序图生成器把骨架画出来,再逐段补充超时/重试/熔断/降级片段: 手打时序图 - 时序图在线制作

左侧编辑器支持点选“生命线/消息/组合片段(alt/opt/loop/par)”,右侧实时预览;写完可一键自动排版,并可导出 SVG/PNG/JPEG/draw.io。你也可以让 AI 先生成初稿,再按你的系统约束微调。


一、你到底在画什么:一次“接口调用”其实是一个小协议

把接口调用画成一根同步箭头(A → B → 返回)会隐藏掉最关键的信息:

  • 网络是不可靠的:超时不等于失败,失败也不等于对方没执行。
  • 中间层会改变语义:SDK、网关、LB、服务网格可能会重试、限流、熔断。
  • 业务动作常常不可逆:扣库存、扣款、发券、发短信、写 DB 都可能“部分成功”。

所以一张“能指导实现与测试”的时序图,通常需要把接口调用拆成三个层次:

  1. 调用层(Call):请求怎么发出去、超时怎么计、返回怎么解析。
  2. 治理层(Resilience):重试、超时预算、熔断、限流、降级。
  3. 业务层(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,结果整张图一坨。更稳的做法是:

  1. 先画一条不考虑失败的主链路(Happy Path)
  2. 再在明确的位置加上:超时、重试、熔断、降级、补偿

下面是一个推荐的“最小骨架”(用文字描述,你可以直接照着画):

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 个问题(图里也要体现)

  1. 重试依据是什么?

    • 网络超时(connect/read timeout)
    • 5xx / 网关超时
    • 明确的可重试错误码(例如 UNAVAILABLE
    • 业务错误(通常不该重试,比如参数错、余额不足)
  2. 重试是否安全?(幂等)

    • GET/查询通常幂等
    • 扣库存/扣款/创建资源通常非天然幂等,需要 idempotencyKey
  3. 重试预算是多少?

    • 总 deadline 2s,重试 3 次但每次 1s → 绝对画不出来“可用”,因为预算已经超了

你可以把这三个答案用注释或在消息上标注,例如:

  • deadline=2000ms
  • retry: max=2, backoff=100/300ms + jitter
  • retryOn: 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(或网关)生命线上,用 altstate 注释表达状态:

  • 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

**修正方式:**把它拆成两段:

  1. 熔断:决定“还打不打”
  2. 降级:当“不打”或“打失败”时,返回什么

六、把“补偿”画对:补偿不是重试的延长线,而是另一条链路

当你画到补偿(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 state
  • alt [status == NOT_FOUND] → retry later / alert

这段图的价值在于:测试能按图写出“超时后最终一致”的用例,排障也知道该看哪条异步链路。

6.3 补偿一定要和“幂等/状态机”绑在一起

补偿最怕重复执行导致二次伤害。所以你要在图里明确:

  • 补偿任务用什么 key 去幂等(requestId/outBizNo)
  • 状态机允许哪些跃迁(比如从 PROCESSING → SUCCESS,不允许 SUCCESS → PROCESSING)

如果你愿意更严谨:把“状态机校验”画成一条对 DB 的查询 + 条件更新(compare-and-set),比一句“更新状态”靠谱得多。


七、一步步把图画出来(适合写技术方案/画评审图)

这里给你一个可复用的 6 步流程,基本适用于“接口调用 + 稳定性治理”类图。

  1. 确定范围:这张图是“端到端(包含 DB/MQ)”,还是“调用方视角(只到被调方 API)”?范围越大,越要分图。
  2. 拆生命线:把“改变语义的组件”拆出来(SDK/网关/幂等存储/补偿任务)。
  3. 画 Happy Path:只画成功路径,但要把 requestId、deadline、idempotencyKey 标出来。
  4. 加超时:在真正计时的生命线处加 alt [timeout],并明确返回语义(错误/UNKNOWN/PROCESSING)。
  5. 加重试:用 loop 包住请求,并画出 backoff + jitter;标出 retryOn 条件。
  6. 加治理与补偿
    • 熔断:先判断 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,维护两版会轻很多。


收尾:你可以用这张图去做三件事(而不是只当“文档装饰”)

  1. 把接口契约写清楚:哪些错误码可重试、超时语义是什么、是否支持幂等键。
  2. 把测试用例列出来:按图覆盖 timeout/5xx/慢调用/熔断 Open/Half-Open/降级等路径。
  3. 把故障演练脚本写出来:注入延迟、注入 5xx、模拟下游不可用,验证熔断与降级是否按预期工作。

如果你愿意把“图”变成团队资产:建议把骨架模板沉淀下来(生命线 + 组合片段),以后每个关键接口只需要补业务差异。

需要一个能快速复用模板、并支持导出到设计评审文档的工具,可以直接用: 手打时序图 - 时序图在线制作

左侧点选生命线/消息/组合片段,右侧实时预览;自动排版;支持 SVG/PNG/JPEG/draw.io 导出;也支持 AI 生成骨架并按你的规范调整。