bigywater.tech
gRPC · 2

gRPC 的消息帧:压缩标志、长度与 protobuf 载荷

上一篇看到一条消息是 5 字节前缀加 protobuf。这篇继续追问:消息大了会怎样,多条消息怎么排,压缩标志怎么用,超过大小限制时线上发生了什么?

73 对 5500
开启 gzip 后,线上一条消息的长度字段(压缩后)与原始 payload 字节
65 个
1 MiB 的消息在 HTTP/2 上被切成的 DATA 帧个数(默认单帧上限 16384 字节)
786960 字节
5 MiB 的请求被服务端以 ResourceExhausted 拒绝时,线上客户端实际发出的字节数

前缀与帧

实测 · 本机执行输出
  payload 大小         protobuf   消息总长       客户端 DATA 帧负载长度                     5 字节前缀里的长度
  0                  3          8          [8]                                (标志 0,长度 3)(剩余字节 0)
  1                  6          11         [11]                               (标志 0,长度 6)(剩余字节 0)
  300                306        311        [311]                              (标志 0,长度 306)(剩余字节 0)
  16000              16006      16011      [16011]                            (标志 0,长度 16006)(剩余字节 0)
  20000              20007      20012      [16384 3628]                       (标志 0,长度 20007)(剩余字节 0)
  70000              70007      70012      [16384 16384 16384 12755 3628 4477] (标志 0,长度 70007)(剩余字节 0)
  1048576            1048583    1048588    [16384 16384 16384 16384]…共 65 帧,最后一帧 22 (标志 0,长度 1048583)(剩余字节 0)

一条 gRPC 消息 = 1 字节压缩标志 + 4 字节大端长度 + 载荷。HTTP/2 单个 DATA 帧最多 16384 字节(默认),大消息被切成多个 DATA 帧,接收方把负载拼起来再按前缀切。消息边界与帧边界无关。

多条消息

实测 · 本机执行输出
  服务端 DATA 帧 3 个,负载长度 [9 11 11];按前缀切出 3 条消息:(标志 0,长度 4) (标志 0,长度 6) (标志 0,长度 6),剩余 0 字节

一条流里可以有任意多条消息(流式 RPC)。grpc-go 每条消息一个 DATA 帧,但协议不要求。

压缩

实测 · 本机执行输出
  请求头 grpc-encoding: gzip
  请求头 grpc-accept-encoding: gzip
  请求头 grpc-timeout: 9999931u
  原始 payload 5500 字节;线上这一条消息:压缩标志 1,长度字段 73(压缩后的 gzip 字节数);剩余 0
  响应头 :status: 200; content-type: application/grpc; grpc-encoding: gzip

压缩标志位是 1 时,载荷是压缩后的字节,长度字段是压缩后的长度,编码方式写在请求头 grpc-encoding 里。压缩是逐条消息的,所以也可以同一条流里有的压缩有的不压缩。

空消息

实测 · 本机执行输出
  DATA 帧负载 5 字节:0000000000,标志 END_STREAM=true

空消息仍然要 5 字节前缀:标志 0、长度 0。

大小限制

实测 · 本机执行输出
  客户端得到 code=ResourceExhausted message="grpc: received message larger than max (5242890 vs. 4194304)",用时 2.5 ms
  线上客户端→服务端 786960 字节(消息本身 5242880 字节);服务端→客户端 172 字节
  S→C 流 23 HEADERS :status: 200; content-type: application/grpc; grpc-status: 8; grpc-message: grpc: received message larger than max (5242890 vs. 4194304)
  S→C 流 23 RST_STREAM 错误码 NO_ERROR

服务端默认只接收 4 MiB 以内的消息。读到前缀里的长度大于限制就立刻回 ResourceExhausted 并结束流,不会把 5 MiB 全收完;客户端是否已发出大部分字节,取决于时机。

分步演示

消息帧
本步的对象

    本文用到的工具与边界

    • 本机 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,数字每次运行会有小幅抖动。抓包代理见第 1 篇;第一节每种大小在新的流上各调用一次(流编号 3、5、7……);DATA 帧的切法受流控窗口和写入时机影响,同一个大小多次运行,帧长度可能不同(70000 字节那一行是一次运行的结果);第五节线上传输的字节数每次运行不同
    • 没有验证:协议规范里对 DATA 帧和消息关系的原文规定(只观察了 grpc-go 的行为);设置 SETTINGS_MAX_FRAME_SIZE 更大时的切法;其它压缩算法(snappy 等第三方注册的);客户端发送大小限制(MaxCallSendMsgSize)的行为;服务端流里每条消息的大小限制对整条流是否累计
    京ICP备2026067502号-1京公网安备11010502063487号