← 中文资源

简体中文版本

Go — context deadline exceeded

定位 Go 调用链中哪个截止时间到期,区分调用方超时与依赖超时,修复传播而不是盲目调大超时。

阅读英文原文

截止时间不止一个

context deadline exceeded 表示链路上某个截止时间到期,但没说哪一个。入站 HTTP 服务器可能有读超时,客户端可能设置了 2 秒超时,数据库驱动可能另有超时,或者父 context 的截止时间比操作所需更短。在每个边界记录操作名和 ctx.Err(),才能确定失败层。

ctx, cancel := context.WithTimeout(r.Context(), 1500*time.Millisecond)
defer cancel()

row := db.QueryRowContext(ctx, query, id)
if err := row.Scan(&value); err != nil {
    if errors.Is(err, context.DeadlineExceeded) {
        return fmt.Errorf("query user %s: budget exhausted: %w", id, err)
    }
    return fmt.Errorf("query user %s: %w", id, err)
}

子 context 派生自 r.Context(),所以调用方取消也会取消查询。此处使用 context.Background() 会丢弃调用方截止时间,让查询在客户端离开后继续运行。

不要越过截止时间重试

重试循环在开始下一次尝试前应检查剩余预算。如果截止时间只剩 30 毫秒而操作需要 500 毫秒,重试只会增加负载。根据剩余时间计算单次尝试超时,低于有用下限时停止。

if deadline, ok := ctx.Deadline(); ok && time.Until(deadline) < 50*time.Millisecond {
    return ctx.Err()
}

区分慢与卡住

慢依赖可能需要优化、缓存或更大预算;卡住的依赖需要取消和降级。比较操作正常延迟分布与截止时间。如果 p99 接近预算,普通尾延迟就会在负载下产生超时;如果通常 50 毫秒完成、偶尔挂起数分钟,问题在缺少取消或连接阻塞,而不是超时太小。

验证端到端取消

发送请求后取消客户端,确认 handler 和下游查询停止。使用一个阻塞到 context 取消的测试查询或服务。比较取消前后的 goroutine 数量;取消后数量持续增长说明有工作忽略了 context。

修复并验证

修复方式是让每个子 context 派生自入站请求,传给每个 I/O 调用,并在重试前检查剩余预算。用测试验证:请求中途取消客户端,断言 handler 迅速返回、下游调用被取消、goroutine 数量回到基线。