bigywater.tech
gRPC · 13

max_concurrent_streams:单连接的并发上限与排队

一条 HTTP/2 连接上最多能同时跑多少个调用?超过了是被服务端拒绝还是排队?排队的时间算谁的?这篇用 MaxConcurrentStreams 和抓包代理逐项实测。

4 个 HEADERS
限制 4 时,前 100 ms 内客户端实际发出的请求数(其余 16 个在客户端排队)
1007.1 ms
限制 4 时 20 个 200 ms 调用的总耗时(不限制是 202.2 ms)
8 成功 / 12 超时
排队 + 500 ms deadline 时每一轮的结果(3 轮一致)

这个限制在哪

实测 · 本机执行输出
  MaxConcurrentStreams(0):服务端第一个 SETTINGS:MAX_FRAME_SIZE=16384
  MaxConcurrentStreams(4):服务端第一个 SETTINGS:MAX_FRAME_SIZE=16384 MAX_CONCURRENT_STREAMS=4

HTTP/2 的 SETTINGS_MAX_CONCURRENT_STREAMS 是接收方告诉发送方“我最多同时接受这么多流”。grpc-go 服务端默认不设置(不限制),用 grpc.MaxConcurrentStreams(n) 设置。

超出的怎么办

实测 · 本机执行输出
  不限制:服务端 handler 同时在处理的峰值 20;总耗时 202.2 ms;前 100 ms 内客户端发出的 HEADERS 20 个;RST_STREAM 0 个;所有 20 个都成功
      完成时刻分布:200–400 ms:20 个
  限制 4:服务端 handler 同时在处理的峰值 4;总耗时 1007.1 ms;前 100 ms 内客户端发出的 HEADERS 4 个;RST_STREAM 0 个;所有 20 个都成功
      完成时刻分布:200–400 ms:4 个;400–600 ms:4 个;600–800 ms:4 个;800–1000 ms:4 个;1000–1200 ms:4 个

客户端读到这个设置后,同时在途的流达到上限就把新调用留在本地排队,有流结束再发。线上没有 RST_STREAM,服务端也不知道有人在排队。

排队与 deadline

实测 · 本机执行输出
  第 1 轮:成功 8 个,DeadlineExceeded 12 个,其它 0 个
      服务端 handler 实际被调用 14 次;客户端发出的 HEADERS 16 个;客户端发出的 RST_STREAM 8 个
      客户端发出 HEADERS 的时刻分布:0–100 ms 4 个;100–300 ms 4 个;300–450 ms 4 个;450 ms 之后 4 个
  第 2 轮:成功 8 个,DeadlineExceeded 12 个,其它 0 个
      服务端 handler 实际被调用 12 次;客户端发出的 HEADERS 15 个;客户端发出的 RST_STREAM 7 个
      客户端发出 HEADERS 的时刻分布:0–100 ms 4 个;100–300 ms 4 个;300–450 ms 4 个;450 ms 之后 3 个
  第 3 轮:成功 8 个,DeadlineExceeded 12 个,其它 0 个
      服务端 handler 实际被调用 13 次;客户端发出的 HEADERS 15 个;客户端发出的 RST_STREAM 7 个
      客户端发出 HEADERS 的时刻分布:0–100 ms 4 个;100–300 ms 4 个;300–450 ms 4 个;450 ms 之后 3 个

排队的时间算在调用自己的 deadline 里:排队太久得到的是 DeadlineExceeded,不是“服务端忙”。deadline 到期的瞬间,有的调用刚好发出又被取消,每次运行数量略有不同。

每条连接

实测 · 本机执行输出
   1 条连接:服务端同时处理的峰值 4;40 个调用总耗时 1013.3 ms
   2 条连接:服务端同时处理的峰值 8;40 个调用总耗时 509.1 ms
   4 条连接:服务端同时处理的峰值 16;40 个调用总耗时 306.4 ms
  10 条连接:服务端同时处理的峰值 40;40 个调用总耗时 102.3 ms

限制作用在每条连接上。客户端建 n 条连接,并发上限就是 n 倍;但每多一条连接就多一份连接的开销(第 38 篇讨论连接池)。

长流占名额

实测 · 本机执行输出
  2 个长流占满名额 600 ms 后:这个 Unary 调用已到达服务端的次数 0(没有到达就还在客户端排队)
  长流结束(释放)之后,Unary 调用在开始后 603.8 ms 才完成;服务端 handler 共被调用 1 次

一个流从开始到结束都占一个名额,不管它传了多少数据。几个长期存活的流(订阅、双向流)可能把名额占满,使同一条连接上的普通调用排队。

不设限制

实测 · 本机执行输出
  全新的连接上直接发 300 个并发调用(没有预热):峰值 300;总耗时 210.4 ms

服务端不设置时客户端的配额是 MaxUint32,300 个并发调用全部同时在处理。限制是保护服务端的手段,不是默认存在的。

分步演示

max_concurrent_streams
本步的对象

    本文用到的工具与边界

    • 本机 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 里的原子计数器;HEADERS、RST_STREAM 数量和时刻来自代理对 HTTP/2 帧的解析;第三节重复了 3 轮,因为 deadline 到期瞬间的竞争每轮不同;“超出的调用在客户端排队”由“前 100 ms 内只发出 4 个 HEADERS、没有 RST_STREAM、服务端到达次数为 0”间接得出,并核对了 grpc-go 源码(http2_client.go 的 streamQuota 与 waitingStreams)
    • 没有验证:其它语言的客户端是否也在本地排队;服务端降低限制(SETTINGS 动态变小)时已有流的处理;多个客户端同时连一个服务端时的公平性;负载均衡策略下每个子连接各自的配额(第 14 篇);把 MaxConcurrentStreams 设得很大时的内存与 goroutine 开销(第 31 篇)
    京ICP备2026067502号-1京公网安备11010502063487号