bigywater.tech
gRPC · 32

序列化成本:大消息的内存拷贝与 buffer 复用

一个 100 MiB 的 RPC 要在内存里复制多少份?小消息每次调用的固定开销是多少?string 和 bytes 的反序列化差多少?这篇用 testing.Benchmark 和 MemStats 把这些数字量出来。

3~4 倍
1 MiB 到 8 MiB 的 echo 调用,每次分配的字节数相对 payload 的倍数
约 11 KiB
1 KiB 小消息每次 RPC 的分配(固定开销)
568.5 µs vs 48.2 µs
1 MiB 中文 string 与 1 MiB bytes 的 Unmarshal 耗时

每次序列化都复制

实测 · 本机执行输出
  1 KiB    Marshal      0.2 µs、分配      1152 字节/次(1.12 倍 payload);Unmarshal      0.2 µs、分配      1120 字节/次(1.09 倍 payload)
  16 KiB   Marshal      1.9 µs、分配     18432 字节/次(1.12 倍 payload);Unmarshal      1.9 µs、分配     16480 字节/次(1.01 倍 payload)
  100 KiB  Marshal     10.5 µs、分配    106497 字节/次(1.04 倍 payload);Unmarshal     10.3 µs、分配    106592 字节/次(1.04 倍 payload)
  512 KiB  Marshal     53.3 µs、分配    532481 字节/次(1.02 倍 payload);Unmarshal     26.6 µs、分配    524384 字节/次(1.00 倍 payload)
  1 MiB    Marshal     74.0 µs、分配   1056770 字节/次(1.01 倍 payload);Unmarshal     52.6 µs、分配   1048674 字节/次(1.00 倍 payload)
  2 MiB    Marshal    124.7 µs、分配   2105346 字节/次(1.00 倍 payload);Unmarshal     86.5 µs、分配   2097254 字节/次(1.00 倍 payload)
  8 MiB    Marshal    279.9 µs、分配   8396815 字节/次(1.00 倍 payload);Unmarshal    260.9 µs、分配   8388709 字节/次(1.00 倍 payload)

Marshal 把字段拷进新缓冲,Unmarshal 把 bytes 字段拷出来。大消息每经过一次编码或解码就多一份拷贝。

整条 RPC 的倍数

实测 · 本机执行输出
  1 KiB    每次调用分配      11632 字节(= 11.36 倍 payload;请求加响应共 2048 字节 payload 来回);139 次内存分配;耗时 0.1 ms/次
  16 KiB   每次调用分配      47274 字节(= 2.89 倍 payload;请求加响应共 32768 字节 payload 来回);153 次内存分配;耗时 0.1 ms/次
  100 KiB  每次调用分配     422102 字节(= 4.12 倍 payload;请求加响应共 204800 字节 payload 来回);162 次内存分配;耗时 0.2 ms/次
  512 KiB  每次调用分配    1847324 字节(= 3.52 倍 payload;请求加响应共 1048576 字节 payload 来回);234 次内存分配;耗时 0.8 ms/次
  1 MiB    每次调用分配    3172576 字节(= 3.03 倍 payload;请求加响应共 2097152 字节 payload 来回);293 次内存分配;耗时 1.3 ms/次
  2 MiB    每次调用分配    6622418 字节(= 3.16 倍 payload;请求加响应共 4194304 字节 payload 来回);414 次内存分配;耗时 2.5 ms/次
  8 MiB    每次调用分配   27044150 字节(= 3.22 倍 payload;请求加响应共 16777216 字节 payload 来回);957 次内存分配;耗时 9.4 ms/次

echo 一次大消息,客户端和服务端合计分配的内存稳定在 payload 的 3~4 倍;小消息每次还有约 11 KiB 的框架固定开销。

string 与 bytes

实测 · 本机执行输出
  string(ASCII)  线上 1048580 字节;Unmarshal 62.8 µs/次,分配 1048674 字节/次
  string(中文)     线上 1048579 字节;Unmarshal 568.5 µs/次,分配 1048673 字节/次
  bytes          线上 1048580 字节;Unmarshal 48.2 µs/次,分配 1048676 字节/次
  string 字段里放非法 UTF-8(0xff 0xfe):proto.Unmarshal 的错误:string field contains invalid UTF-8

string 要校验 UTF-8,中文文本的校验明显更慢;不需要文本语义的数据用 bytes。

分步演示

序列化成本
本步的对象

    本文用到的工具与边界

    • 本机 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,数字每次运行会有小幅抖动。编解码部分用 testing.Benchmark(只测 protobuf-go);整条 RPC 的分配在同一进程内用 MemStats.TotalAlloc 的增量除以调用次数(客户端和服务端的分配合计,GC 前后各读一次);服务端调大到 64 MiB 的接收/发送上限,否则 8 MiB 的消息会被拒绝;缓冲池的档位读自 mem/buffer_pool.go,选项在包内是不导出的
    • 没有验证:“四次拷贝”是推断(没有逐步追踪每次分配);缓冲池对不同大小消息的实际命中率;对 GC 暂停、尾延迟的影响;零拷贝方案(自定义 codec、mem.Buffer 引用计数);流式分块与单条大消息的内存对比(第 24 篇);压缩开启时的额外分配
    京ICP备2026067502号-1京公网安备11010502063487号