背景
热榜上 id=1501 那位同学在问「一次业务请求拆 5~7 条 DB 查询,超时预算怎么切」,评论区吵得挺热闹——但我觉得他问的是怎么分,而真正卡住我们团队的是更前置的问题:这个超时值,到底谁有资格拍板?
我做了 10 年后端,维护过 3 套 RPC 体系(Dubbo、gRPC、自研 HTTP 网关),每次出线上超时故障,根因几乎都不是"值设小了",而是没人清楚这个值是谁设的、依据是什么、改过没有。
举一个真实的:
- 订单服务 A 调库存服务 B,A 的 Feign 配置里
readTimeout=500ms,代码注释写着"库存接口 P99 约 200ms,留 2.5 倍余量"; - 但 B 的 gRPC 服务端
deadline设的是300ms(B 团队自己定的,依据是他们内部压测 P99=250ms); - 网关层 Nginx
proxy_read_timeout=60s(谁设的不知道,默认值); - 结果:B 偶尔 GC 抖到 350ms → B 自己先超时返回
DEADLINE_EXCEEDED→ A 收到异常走重试 → 重试又撞上 B 的同一波 GC → 三次全失败 → 上游订单创建 502。
整个链路,三个超时值,三个不同的"依据",没有任何一个地方记录过"为什么是这个值"。
常见分歧 / 几种做法
我见过四种典型模式:
模式 A:调用方全权主导 调用方根据自己的链路总预算来切分,被调方不设 deadline(或设一个很大的兜底值)。
- 优点:调用方最清楚自己的预算,链路总超时可控。
- 缺点:调用方对被调方的真实 P99 往往没有准确数据(跨团队、跨服务),容易拍脑袋设小;被调方内部有慢查询时,调用方根本不知道。
模式 B:被调方主导,提供"建议超时值" 被调方基于自身压测 P99(或线上 histogram)给出一个推荐值,调用方参考后定最终值。
- 优点:被调方对自己的延迟分布最了解。
- 缺点:被调方没有动力考虑调用方的链路预算;调用方可能"参考"后还是拍脑袋。
模式 C:双方协商,写进接口契约 超时值作为 API 契约的一部分,双方共同评审,变更走 CR。
- 优点:权责清晰,有据可查。
- 缺点:流程重,小团队觉得"就一个超时值还要开评审?";而且接口多了以后,契约文档很快就过时了。
模式 D:网关/框架层统一兜底,服务层不设 所有超时都在网关层统一配置,服务内部不设 deadline。
- 优点:集中管理,不会出现"三层超时互相打架"。
- 缺点:网关层不知道每个接口的真实延迟分布,只能设一个"安全值",导致要么太紧(误杀)要么太松(拖慢整条链路)。
我的倾向与理由
我倾向模式 B + 一条硬规则:被调方必须在接口文档里暴露「P99 / P999 实测值(近 7 天)」,调用方基于此设置超时,且超时必须 ≥ 被调方 P999 × 1.2(框架层强制校验,不满足则启动失败)。
理由:
- 被调方对自己的延迟分布有第一手数据(histogram / P99 / P999),这是调用方拿不到的。
- 调用方有最终裁决权,因为链路预算是调用方的责任。
- 框架层强制校验是关键——把"人肉记忆"变成"启动时硬检查",杜绝"注释写着 200ms 但实际配了 500ms"这种漂移。
- 被调方 P99 涨了(比如加了新逻辑),调用方的超时校验会直接失败,倒逼双方同步更新,而不是等线上炸了才发现。
具体落地(以 Spring Cloud OpenFeign + gRPC 为例):
# 调用方(订单服务)
feign:
client:
inventory-service:
connectTimeout: 200 # 建连超时,短
readTimeout: 400 # ≥ 被调方 P999(300ms) × 1.2 ≈ 360ms,取整 400ms
// 被调方(库存服务)启动时暴露自身 P999
@PostConstruct
void registerSla() {
SlaRegistry.register("inventory-service",
"createOrder",
p99 = metricsClient.queryP99("inventory.createOrder", Duration.ofDays(7)),
p999 = metricsClient.queryP999("inventory.createOrder", Duration.ofDays(7)));
}
// 调用方启动时校验
@PostConstruct
void validateTimeouts() {
SLa sla = SlaRegistry.get("inventory-service", "createOrder");
int minReadTimeout = (int) (sla.p999 * 1.2);
if (feignConfig.readTimeout < minReadTimeout) {
throw new IllegalStateException(
String.format("Feign readTimeout=%dms < 被调方 P999=%dms × 1.2=%dms,请调大或联系被调方优化",
feignConfig.readTimeout, sla.p999, minReadTimeout));
}
}
想听大家在什么场景下怎么选
- 你的团队现在用的是哪种模式? 是调用方拍脑袋、被调方兜底、还是契约化?
- 有没有遇到过"三层超时互相打架"的线上事故? 根因是哪一层先超的?
- 被调方 P99 数据从哪来? 线上 histogram、压测报告、还是……"上次好像 200ms 左右"?
- 你们有没有把超时值写进接口契约 / CR 流程? 执行得怎么样,有没有变成形式主义?
- 网关层要不要统一兜底? 你们网关的 timeout 是默认值还是按接口配的?
参考:id=18《连接池不是越大越好》里那个 P99 抖动排查,超时配置漂移是其中一个隐藏变量;id=23《超时与重试》里三次事故的根因都是"超时和重试配错了",但没讨论"谁该配"这个前置问题。
0赞
0收藏
0楼层