gRPC Scan 超大数据报错:message too large
·
gRPC Scan 超大数据报错:message too large(>4GB)的根因分析与解决方案
摘要
在进行 Scan 性能测试时,当 Scan 范围较大(如 1,000,000 条记录)且单条 value 较大(如 16KB)时,gRPC 会直接报错:
rpc error: code = ResourceExhausted desc = grpc: message too large
即使在 Go 层将 MaxRecvMsgSize 设置为 20GB,该问题依然存在。
本文结合 实际压测场景 + gRPC 协议规范,分析该问题的根本原因,并给出两种可落地的解决方案。
一、问题背景
测试条件
- Scan 长度:1,000,000
- 单条 value 大小:16KB
- 理论返回数据量:约 15~16GB
- 测试时机:
- 已写入约 100GB 数据
- Full GC 执行完成两轮之后
- 测试工具(自己写的):
benchmark/scan_pro/scan_pro.go
报错信息
rpc error: code = ResourceExhausted desc = grpc: message too large (16014487524 bytes)
并且可以稳定复现一个现象:
当 scan_length × value_size 超过约 4GB 时,必然报错
二、根因分析:gRPC 协议层的 4GB 硬性限制
1️⃣ gRPC 消息 framing 机制(协议级)
gRPC 并不是直接“裸传” protobuf 数据,而是对每条消息进行 framing。
在 gRPC 官方协议文档中明确说明:
Each gRPC message is prefixed with a 5 byte header:
- 1 byte: compressed flag
- 4 bytes: unsigned integer message length (big-endian)
官方协议说明:
https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md
也就是说:
| 1 byte | 4 bytes | N bytes |
| flag | length | message payload |
2️⃣ 为什么单条 gRPC 消息无法超过 4GB
- 4 字节无符号整数最大可表示:
2^32 - 1 = 4,294,967,295 bytes ≈ 4GB - 这是 协议 framing 层的限制
- Go 层的:
只是 额外校验grpc.MaxRecvMsgSize(...) grpc.MaxSendMsgSize(...) - 无法突破协议对 message length 的表达上限
因此:
❌ 单条 gRPC Response 永远不可能超过 4GB
❌ 调大 MaxRecvMsgSize 不能解决问题
Scan 请求一次性返回约 16GB 数据,本质上是 一次性构造了一个超过 4GB 的 gRPC 消息,因此必然失败。
三、解决方案一:使用 gRPC 流式传输(推荐)
这是 gRPC 官方推荐的 大数据传输方式。
3.1 修改 Proto 定义
将普通 RPC 改为 Server Streaming RPC:
service KV {
rpc ScanRangeInRaft (ScanRangeRequest)
returns (stream ScanRangeResponse);
}
message ScanRangeResponse {
map<string, string> key_value_pairs = 1;
int32 LeaderId = 2;
string Err = 3;
}
3.2 服务端:边 Scan 边 Send
func (kvs *KVServer) ScanRangeInRaft(
req *kvrpc.ScanRangeRequest,
stream kvrpc.KV_ScanRangeInRaftServer,
) error {
batchSize := 1000
batch := make(map[string]string)
for iter.Seek(start); iter.Valid(); iter.Next() {
batch[key] = value
if len(batch) >= batchSize {
if err := stream.Send(&kvrpc.ScanRangeResponse{
KeyValuePairs: batch,
}); err != nil {
return err
}
batch = make(map[string]string)
}
}
if len(batch) > 0 {
_ = stream.Send(&kvrpc.ScanRangeResponse{
KeyValuePairs: batch,
})
}
return nil
}
3.3 客户端:循环 Recv
stream, err := client.ScanRangeInRaft(ctx, req)
if err != nil {
return err
}
for {
resp, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
return err
}
for k, v := range resp.KeyValuePairs {
// 处理或统计
}
}
优势:
- 避免 4GB 限制
- 极大降低内存峰值
- 更符合真实 Scan 吞吐模型
四、解决方案二:应用层分页 Scan(非流式)
如果短期内无法修改 proto,可采用分页方式:
- 每次 Scan 限制返回条数
- 记录最后一个 key
- 多轮 Scan 拼接结果
但该方案:
- 客户端逻辑复杂
- 吞吐测试不如 Streaming 真实
- 仍需人工控制单次返回大小
五、总结
- gRPC 单条消息存在不可突破的 4GB 协议上限
- Scan / Batch / Snapshot 等场景 必须拆分
- Streaming RPC 是最优解
- Benchmark 中一次性返回超大结果并不符合真实系统设计
参考资料
- gRPC Protocol over HTTP/2
https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md - gRPC 官方 Streaming 文档
https://grpc.io/docs/what-is-grpc/core-concepts/#streaming-rpc - gRPC message too large issue
https://github.com/grpc/grpc/issues/13841
温馨提示
本文基于实际项目与 gRPC 协议规范整理而成,如文中存在理解偏差或更优实现方式,欢迎私信或评论指出。
也希望本文能为后续遇到 gRPC 大规模 Scan / 批量数据传输问题的同学提供一些参考。
更多推荐



所有评论(0)