Skip to content

你在调用别人接口的时候有考虑过失败情况吗? ​

核心观点(原文) ​

我们在构建分布式系统的时候,对开发人员会有一个很重要的要求。即使你的上下游挂了,你的系统不能挂。这充分体现了分布式系统健康保障的基本思路。它从服务级别去控制故障的蔓延,防止系统性崩溃的出现。

排除自身逻辑中的作死行为。从方法上来说,在复杂的分布式系统中,要保持自身的牛逼属性,简单来说就两点:

  • 做好限流:提供给别人的接口,不能因为被频繁调用,把自己压死

  • 做好熔断:调用别人接口的时候,不能因为别人影响慢了,把自己拖死

理论上,分布式系统中的每个接口调用都能做到上面两点。要出现系统性故障,只有大范围的整体性故障才会造成,比如整个机房挂了这类重大灾难出现。因为上下游都有保护,个别服务的作死情况是可以安全的抵御故障蔓延而研发的系统性故障的。

但是,理论总是美好,而实践总有各种意外。笔者所经历的各种故障,也都是没有做好上面两点所造成了。

一、为什么"调别人的接口"是高危动作 ​

上游故障不会安静地待在上游,它会沿着线程池、连接池向上游传染:

上游 C 挂了/慢了
   │ 每个请求都要等满超时时间
   ▼
你的工作线程被占满 ──► 线程池拒绝新任务 ──► 你自己也无法响应
   │                                    (依赖 C 的接口和无关接口一起死)
   ▼
你的上游(D、E)调你也超时 ──► 故障沿调用链向上蔓延 ──► 级联雪崩

所以"考虑失败情况"不是 catch 个异常那么简单,而是一套措施:超时、重试与幂等、错误分类、降级熔断。

二、超时:防御的第一道闸门 ​

  • 每一次远程调用都必须显式设置连接超时和读超时,禁止使用"默认无限等待"的客户端配置。这是无数事故的第一根引线。
  • 超时值参考上游接口 P99 的 2~3 倍设上限:太短会把"偶发慢"误杀成失败,太长等于自己给自己埋雷。
  • 超时要外层大于内层:网关 > 服务 > 数据库。外层先超时会导致内层白白执行完还继续占用资源,且调用方已放弃,结果被丢弃。
  • 拿不到 P99 数据时先设一个保守值上线,再根据监控收敛。没有超时和"超时不精确"是两回事,前者是事故,后者是优化项。

三、重试与幂等 ​

重试只对特定错误有效,先分类再重试:

错误表现例子可否重试
网络类失败连接超时、连接被重置可以
对端临时过载502/503/504、限流 429可以(但要配合退避)
请求/契约错误400、401、403、参数校验失败不可以,重试一万次也一样
明确的业务失败码余额不足、库存不足不可以,应走业务分支

重试的三条纪律:

  1. 次数有限:一般不超过 2~3 次;
  2. 指数退避 + 抖动:1s、2s、4s 并加随机量,避免所有调用方在同一时刻齐步重试;
  3. 只重试幂等操作:查询天然幂等。写操作必须先设计幂等键(业务单号、唯一约束、状态机)。否则重试等于重复扣款。

重试的放大效应要警惕:上游短暂抖动,N 个下游各重试 M 次。恢复瞬间的流量是正常值的数倍(重试风暴)。这可能把刚缓过来的上游二次压垮。所以重试必须配合熔断,熔断打开期间不重试。

四、错误分类处理 ​

拿到失败之后,处理策略按业务语义选,而不是一律抛异常:

场景策略
强依赖,无法继续fail fast,快速失败并给出明确错误码
弱依赖,有旧数据读缓存/上一次成功结果兜底(推荐列表、配置类)
弱依赖,可默认返回默认值/降级文案(头像挂了给默认头像)
可延后完成落库/进 MQ 异步补偿,事后对账
资金/订单类绝不静默吞掉:告警 + 人工介入通道

反模式:catch (Exception e) { log.warn(...); return null; }——吞掉异常返回 null,把上游故障转嫁成下游 NPE,且监控上完全不可见。

五、降级与熔断 ​

熔断器是自动化的"保险丝",三个状态循环:

            失败率超阈值
  ┌─────────┐ ──────────► ┌─────────┐
  │ Closed  │             │  Open   │ 快速失败,不再真正调用上游
  │ 正常放行 │ ◄───────── │ (熔断中) │
  └─────────┘   探测成功   └────┬────┘
      ▲        ▲   冷却时间到   │ 放行少量探测请求
      │        └───────────────┴──► Half-Open(半开试探)
  • 熔断解决"别再打了":失败率/慢调用率超阈值就断开。冷却后半开试探,恢复再闭合(Sentinel、resilience4j、Hystrix 均为此模型)。
  • 降级解决"断了之后给什么":默认值、缓存旧值、简化逻辑、砍次要功能保主流程、异步化+对账补偿。降级路径同样要有监控,否则降级本身变成新故障源。

六、防御性调用清单(Code Review 对照) ​

  • 超时:连接/读超时是否显式设置,外层是否大于内层;
  • 错误:是否按错误类别分别处理,有没有裸 catch 后 return null;
  • 重试:是否只对幂等+可重试错误重试,有无退避与抖动,次数是否有限;
  • 幂等:写接口有无幂等键/唯一约束/状态机防重;
  • 熔断:依赖方有无熔断规则,失败率阈值与半开恢复策略是否合理;
  • 隔离:核心与非核心依赖是否线程池/信号量隔离,慢依赖会不会拖垮快依赖;
  • 限流:自己作为服务方有无限流保护(对应原文"不能把自己压死");
  • 可观测:日志是否带 traceId 与耗时拆分,失败率有无分级告警;
  • 兜底:每个弱依赖是否明确降级方案与兜底数据来源。

面试追问链:

  • 超时怎么设?
  • 重试风暴怎么避免?
  • 为什么写操作必须幂等?
  • 熔断三个状态怎么流转?
  • 降级预案怎么验证有效(混沌工程/预案演练)?

2026-10 扩充(原内容过薄)。