SDK重试与超时概述:为什么需要重试与超时?
说实话,干了这么多年后端,每次带新人第一个要讲的,就是这两个概念。
记得我刚开始写 SDK 那会儿,总是把网络请求想得太完美。直到有一次线上事故,一个第三方支付接口偶尔超时,我那个没加重试的 SDK 直接让整批订单卡住不动了。嗯,凌晨三点被叫起来修复的感觉,真不好受。
从那以后,我深刻明白了一个道理:在分布式系统里,请求失败是常态,不是异常。我们写 SDK 的,必须为这种 常态 设计好兜底策略。
为什么请求会失败?
我们先来盘点一下,一个网络请求到底能遇到多少坑。我把它们分了类,你看看遇到过几个:
| 失败类型 | 典型场景 | 能不能重试? |
|---|---|---|
| 网络抖动 | TCP 丢包、DNS 解析失败 | 通常可以,短时间后可能恢复 |
| 服务端过载 | HTTP 503、502,连接被重置 | 可以,但需要退避 |
| 服务端超时 | 请求处理太久,客户端等不及 | 需谨慎,可能已经处理了 |
| 资源竞争 | 数据库死锁、Redis 熔断 | 可以,等锁释放后再试 |
| 数据冲突 | 乐观锁更新版本号不符 | 可以,需要重新读取最新数据 |
| 幂等性违规 | 重复支付、订单重复创建 | 不行,必须靠幂等令牌防重 |
| 永久性错误 | HTTP 400 参数错误、401 鉴权过期 | 不重试,修复代码或刷新凭据 |
看到没?上面这些失败里,有 5 类是可以尝试重试的。但你往下看,如果重试策略写不好,反而会把系统搞崩。
没有重试机制的风险链
我举个例子,你在 SDK 里不加重试。用户调用一次失败了,直接抛异常。用户自己想「再点一次」。好嘛,这一点的结果可能就是 雪崩。
我曾经遇到过一个真实案例:某 SDK 调用内部网关超时,没做指数退避。客户端 1 秒内重试 100 次,直接把网关打挂了。那个网关后面还连着数据库,数据库瞬间连接池爆满,整个系统 5 分钟才缓过来。所以,不设重试是找死,乱设重试是作死。
没有重试的后果很明显:一次瞬时故障,就直接转成 失败结果。用户体验差不说,开发排障的成本也高。但更可怕的是一种连锁反应:
- 客户端频繁重试 -> 服务端负载升高
- 服务端来不及处理 -> 更多请求排队超时
- 超时引发更多客户端的重试 -> 负载进一步升高
- 最终结果:服务雪崩
那超时机制又解决了什么?
重试解决的是「失败了怎么办」,超时解决的是「什么时候放弃」。没有超时,你的线程可能永远挂在一个慢请求上。
想想看,一个 HTTP 连接池默认可能没有超时。如果服务端进程卡死、不返回也不断开,你的调用线程就一直等在那。一次两次还好,QPS 一上来,连接池被占满,新的请求进不来,整个 SDK 就阻塞了。
我个人习惯:在 SDK 设计初期就明确三个超时维度——连接超时(建立 TCP 连接最久等多久)、读取超时(等待服务端返回数据的最长时间)、写入超时(发送请求体的时限)。这三个定好了,再谈重试策略。
举个具体的例子:你写一个微服务间的 RPC 调用 SDK。如果设置了 500ms 的读取超时,那当上游服务慢查询时,最多影响 500ms 的线程资源。结合重试(比如重试 2 次,每次间隔 200ms),一次调用最多耗时 500ms × 3 + 400ms 间隔 = 1900ms。你看,这个时间可控了,不会出现「等 30 秒还没响应」的尴尬。
为什么需要两者配合?
说白了,重试和超时是两兄弟。缺了谁都不行。我个人把它们看作一个 完整决策链路:
- 发起请求 -> 带着连接超时和读取超时
- 遇到超时或错误 -> 判断错误类型是不是可重试的
- 决定重试 -> 按照预设的退避策略等待(比如指数退避 + 随机抖动)
- 累计超时检查 -> 如果整体耗时超过了总的最大等待时长,放弃
- 重试耗尽 -> 最终报告失败,抛出异常或返回错误码
这个决策链路,我稍后会作为 SDK 核心流程的核心骨架 来展开讲。现在先记住一点:没有超时的重试是盲目的,没有重试的超时是脆弱的。
核心逻辑关系图
构建策略前先想清楚的问题
好,既然我们知道了「为什么需要」,那我建议你在写代码前,先问自己三个问题:
- 这次调用幂等吗? 不幂等的接口不要自动重试。你可以提供一个重试策略接口,让调用方自己决定是否重试。
- 我能忍受的最大延迟是多少? 这决定了你的总超时时间和重试次数。用户界面请求最多 2-3 秒,后台批处理可以久一些。
- 错误是不是永久性的? 比如 401 鉴权失败,重试 100 次也没用。这种情况直接失败,让上层重新获取 。
一句话总结: 重试解决瞬态失败的恢复问题,超时解决资源释放和系统保护的边界问题。两者配合,才能构建一个健壮的 SDK 调用层。后面我会详细讲具体的策略算法、代码实现和实战中的避坑经验。