← 中文资源

简体中文版本

Go — 客户端断开引发的 context canceled 级联

区分 context deadline exceeded 与 context canceled,追查客户端断开在调用链中的级联,并把用户发起的取消与系统错误分开。

阅读英文原文

症状

  两个错误值在日志里长得一模一样、含义却相反:context canceled 是有人调用了 cancel——最常见是 HTTP 层因客户端关闭连接而取消;context deadline exceeded 是计时器到点。级联的签名在时间线上:任取一条 canceled 日志,同一秒的边缘日志里必有断开记录;五个服务同时打 canceled 只是一个用户离开,五个服务同时打 DeadlineExceeded 才是真有东西慢。把传播信号当组件故障处理,取消风暴就会污染错误率并误派值班——这正是它常被当成“全线故障”上报的原因。

诊断思路

  先精确分类:grep context.Canceled 与 DeadlineExceeded,二者成因、响应、指标都不同;再做时间戳关联,canceled 与边缘断开同步即证实是传播而非独立故障;最后找错误派生:从 r.Context() 派生后台工作上下文的代码,请求一死后台全死,需要独立根上下文。代理超时取消其上游请求上下文、级联进健康依赖,是另一常见入口。

根因

  请求上下文携带来自客户端连接的取消信号,从它派生的每个操作都继承该信号——一次断开就取消整棵在途调用树。把这种传播信号归类为组件故障,等于把正常的用户行为误判为宕机。

修复与验证

  在错误浮出的边界分类处理:errors.Is(err, context.Canceled) 且请求上下文已取消的,按预期处理——info 级日志、独立计数器(不进错误率)、面向客户端的场合可报 499;DeadlineExceeded 单独归类并保持告警。其次停止从请求上下文派生长命工作:后台作业用 context.Background() 加自己的超时与 cancel,响应内的工作才跟随请求生命周期。铁律是:用户永远看不到结果的东西可以跟随请求上下文;副作用活得比连接久的必须有自己的根。验证时搭一个在请求中途随机断开客户端的测试装置,断言三点:canceled 指标计数上涨而错误率仪表盘不动、后台副作用在分离上下文下完整完成、DeadlineExceeded 路径照常告警——证明分类真的把超时与放弃拆开了,而不是把两者一起藏掉。

代码示例

rpc error: code = Canceled desc = context canceled
rpc error: code = DeadlineExceeded desc = context deadline exceeded
if errors.Is(err, context.Canceled) && req.Context().Err() != nil {
    // client walked away: expected, not a failure
    log.Info("request abandoned by client", "trace", traceID)
    metrics.Cancellations.Add(1)      // separate counter
    return
}
if errors.Is(err, context.DeadlineExceeded) {
    // real timeout: alert-worthy
    metrics.Timeouts.Add(1)
}
func handler(w http.ResponseWriter, r *http.Request) {
    // background job must survive the request
    jobCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
    defer cancel()
    queue.Submit(jobCtx, payload)

    // response work correctly follows the request lifetime
    render(w, r.Context())
}