bigywater.tech
gRPC · 6

context 取消:客户端取消后服务端 goroutine 怎么停

客户端取消或超时后,服务端的 goroutine 怎么停?线上是什么帧?流式 RPC 呢?取消刚好撞上响应会怎样?这篇逐项抓包和计数。

0 对 50
50 个调用超时返回后,服务端仍在运行的 handler 数:监听 ctx.Done() 与不监听
RST_STREAM(CANCEL)
客户端 cancel() 在线上的样子;deadline 到期时服务端和客户端各发一个
0 个
调用完成后再 cancel() 在线上多发的帧

线上的取消

实测 · 本机执行输出
  cancel:客户端得到 Canceled;线上帧:
      C→S HEADERS    流 3 :method: POST; :scheme: http; :path: /demo.Demo/Unary; :authority: 127.0.0.1:9801; content-type: application/grpc; user-agent: grpc-go/1.84.0; te: trailers
      C→S DATA       流 3 00000000070a057761746368
      C→S RST_STREAM 流 3 错误码 CANCEL
  deadline:客户端得到 DeadlineExceeded;线上帧:
      C→S HEADERS    流 5 :method: POST; :scheme: http; :path: /demo.Demo/Unary; :authority: 127.0.0.1:9801; content-type: application/grpc; user-agent: grpc-go/1.84.0; te: trailers; grpc-timeout: 99928041n
      C→S DATA       流 5 00000000070a057761746368
      S→C RST_STREAM 流 5 错误码 CANCEL
      C→S RST_STREAM 流 5 错误码 CANCEL

取消就是 RST_STREAM 帧(错误码 CANCEL)。cancel() 由客户端发出;deadline 到期则两边都会计时并各发一个。

handler 要自己停

实测 · 本机执行输出
  watch   客户端 50 个调用全部超时返回时:服务端还在运行的 handler 0 个
            再过 100 ms:0 个
            再过 300 ms:0 个
            再过 600 ms:0 个
  ignore  客户端 50 个调用全部超时返回时:服务端还在运行的 handler 50 个
            再过 100 ms:50 个
            再过 300 ms:0 个
            再过 600 ms:0 个

服务端的 ctx 会被取消,但 handler 不检查就继续跑:50 个不监听 ctx 的 handler 在客户端全部返回时仍在运行,直到睡眠结束。长耗时的 handler 要在阻塞处 select ctx.Done()。

流式

实测 · 本机执行输出
  客户端取消后下一次 Recv 立刻返回:code=Canceled (0.1 ms)
  服务端:第 5 次 Send 失败:rpc error: code = Canceled desc = context canceled;st.Context().Err() = context canceled;仍在运行的 handler 0 个

客户端取消后,服务端流的 Send 会失败并返回带 Canceled 的错误;检查 Send 的返回值才能及时退出。

竞态

实测 · 本机执行输出
  handler 约 1000 µs,客户端  500 µs 后取消:成功   0、Canceled 400、其它 0
  handler 约 1000 µs,客户端 1000 µs 后取消:成功   0、Canceled 400、其它 0
  handler 约 1000 µs,客户端 1500 µs 后取消:成功 396、Canceled   4、其它 0
  handler 约 1000 µs,客户端 2000 µs 后取消:成功 400、Canceled   0、其它 0
  handler 约 1000 µs,客户端 4000 µs 后取消:成功 400、Canceled   0、其它 0

取消和响应恰好撞上时,结果是成功还是 Canceled 取决于时序:取消不能保证请求没被执行,需要幂等。

defer cancel()

实测 · 本机执行输出
  调用完成后线上 RST_STREAM 0 个;再调用 cancel() 之后 0 个

调用结束后再 cancel() 什么帧都不发,所以 defer cancel() 是安全且免费的。

沿调用链

实测 · 本机执行输出
  客户端在 101.0 ms 时取消;A 在 101.2 ms、B 在 101.2 ms、C 在 101.3 ms 发现自己的 ctx 被取消(距取消分别 0.1 / 0.2 / 0.3 ms)

取消通过每一层传递 ctx 一路传到最下游,本机上每跳只要零点几毫秒;前提是每一层都传 ctx(第 5 篇)。

分步演示

取消
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1 / grpc-go v1.84.0 / protobuf-go v1.36.12 / golang.org/x/net v0.57.0 / protoc 36.2(Homebrew);服务端和客户端都是本机起的 Go 进程(127.0.0.1,端口 9800 以上,不访问任何外部服务);需要看线上字节时用我自己写的 TCP 抓包代理加 golang.org/x/net/http2 的 Framer 解析 HTTP/2 帧;计时用 time.Now 或 perf_counter,数字每次运行会有小幅抖动。第一节用抓包代理看帧;第二节的“正在运行的 handler 数”是服务端自己的原子计数器;第四节每个延迟 400 次、最多 8 个并发;第五节在线上安静 500 ms 之后测;第六节的 ms 数都是同进程里 time.Now 的差,单次测量
    • 没有验证:在真实网络上取消传播的延迟;服务端用 grpc.MaxConcurrentStreams 限制时取消对排队请求的影响;HTTP/2 RST_STREAM 的流控含义(被取消的流上已发送的数据是否还占流控窗口);客户端流和双向流里取消的细节;非 grpc-go 的服务端实现
    京ICP备2026067502号-1京公网安备11010502063487号