bigywater.tech
gRPC · 8

服务端流:发送缓冲、流控与慢消费者

服务端流里,服务端发得比客户端读得快会怎样?Send 在什么时候阻塞?窗口调大有用吗?阻塞时超时怎么处理?这篇用慢消费者实验一项项量。

0.4 对 31 ms
服务端前 4 次 Send 与第 5 次起每次 Send 的耗时(消费者每条睡 30 ms)
195584 字节
客户端不读时,默认设置下服务端阻塞前能 Send 的字节数
64 KiB
grpc-go 每个流的 defaultWriteQuota(读源码)

背压

实测 · 本机执行输出
  客户端 1292 ms 内收了 40 条;服务端此时已 Send 完成(返回)43 条
  服务端前 20 次 Send 耗时(ms):[0.0 0.5 0.3 0.4 31.3 30.9 30.7 30.6 31.2 31.1 31.1 30.7 31.5 30.5 33.5 30.7 30.9 31.2 30.3 31.2]

Send 不是“放进队列就走”:传输层只允许每个流有有限的缓冲和流控窗口。前几条消息瞬间返回,之后 Send 要等客户端读走数据放出窗口才返回,生产者自动被消费者拖慢。

阻塞点

实测 · 本机执行输出
  默认窗口:600 ms 时已成功 Send 195584 字节(191 条 × 1 KiB),再过 300 ms 还是 195584 字节(已经阻塞);取消后服务端:第 191 次 Send 失败:rpc error: code = Canceled desc = context canceled

客户端完全不读时,默认设置下服务端先发出约 191 KiB 就阻塞。阻塞点约等于流控窗口加不到 64 KiB 的写缓冲(defaultWriteQuota)。

调窗口

实测 · 本机执行输出
  窗口 65536    字节:阻塞前 Send 了 130048 字节(2.0 倍窗口)
  窗口 262144   字节:阻塞前 Send 了 324608 字节(1.2 倍窗口)
  窗口 1048576  字节:阻塞前 Send 了 1102848 字节(1.1 倍窗口)
  窗口 4194304  字节:阻塞前 Send 了 4214784 字节(1.0 倍窗口)

显式设置 WithInitialWindowSize 或 WithInitialConnWindowSize 后,阻塞点相应上移(1 MiB 窗口阻塞前约 1.1 MiB);同时会关闭 BDP 动态窗口,所以默认值有时反而更大。窗口怎么影响吞吐见第 10 篇。

阻塞时的超时

实测 · 本机执行输出
  服务端:第 3 次 Send 失败:rpc error: code = Canceled desc = context canceled;客户端的 deadline 是 400 ms,被阻塞的 Send 在 deadline 到了之后才返回

被阻塞的 Send 要等到客户端放弃才返回,错误码是 Canceled。流式 handler 必须检查 Send 的返回值并退出。

先消息后状态

实测 · 本机执行输出
  客户端收到 100 条(每条 20000 字节),顺序正确:true,最后得到 code=Aborted message="发完就报错"

handler 返回后,已经交给传输层的消息仍会全部送达,状态在最后。

并发 Send

实测 · 本机执行输出
  两个 goroutine 各 Send 200 条:客户端收到 400 条,内容与编号对不上的 0 条

官方文档说同一个流的 SendMsg 不能在多个 goroutine 里并发;实验没触发问题,不能当作安全的证据。

分步演示

流控与慢消费者
本步的对象

    本文用到的工具与边界

    • 本机 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,数字每次运行会有小幅抖动。第一节的 Send 耗时是服务端自己用 time.Now 量的;第二、三节用“600 ms 内成功 Send 的字节数”表示阻塞点,不 Recv 的客户端;“defaultWriteQuota”“StaticWindowSize”“SendMsg 并发”三点是读了 grpc-go v1.84.0 源码(internal/transport/defaults.go、dialoptions.go、stream.go)得到的;其余是测量结果
    • 没有验证:阻塞点具体由哪几部分相加(流窗口、连接窗口、写配额、操作系统 socket 缓冲)——只有总数;动态窗口 BDP 把窗口调到多大;多个流共享连接窗口时的分配;客户端 Recv 的内部缓冲;非 grpc-go 实现的阻塞点
    京ICP备2026067502号-1京公网安备11010502063487号