朕喜技术(ZhenxiJS)
2026.09.12
1 分钟阅读

gRPC 报错 RESOURCE_EXHAUSTED: gRPC message exceeds maximum size 4194304 怎么解决

关键词:gRPC / grpc-java / protobuf / RESOURCE_EXHAUSTED / gRPC message exceeds maximum size 4194304 / 4194304 是什么 / maxInboundMessageSize / maxOutboundMessageSize / message too large / 4MiB 默认消息上限 / unary 改流式 / 大文件上报 / 服务端日志没有记录

一、问题背景

一个指标聚合上报链路:各业务机 Agent 把一整天的监控指标攒成一个 gRPC unary 请求,一次性上报给聚合服务。

以前指标很小,一切正常。接入「压缩采样点」后单条指标涨到约 3MB,3 条打包后请求体约 9MiB,于是客户端稳定报错:

❌ 调用失败
   status = RESOURCE_EXHAUSTED
   description = gRPC message exceeds maximum size 4194304: 9437275

让人困惑的地方:

  • 没人配过 4194304,这个数字是哪来的?
  • 9437275 又是什么?
  • 报错在客户端抛出,但服务端应用日志里一条记录都没有,请求到底发出去没有?
  • 在客户端调大 maxInboundMessageSize,毫无作用,还是同一个 4194304。

技术栈:grpc-java 1.67.1、protobuf-java 3.25.5、JDK 21、Docker Compose。

二、复现步骤

最小可复现环境已放在仓库(scenario/2026-09-12-0000)。核心就两点:两侧都不显式设置消息上限(保持默认),然后发一个 >4MiB 的 unary 请求。

git fetch origin && git checkout 2026-09-12-0000
cd scenario/2026-09-12-0000

docker compose up -d --build

# 一键复现 + 定位 + 验证
docker compose exec -T app bash ./scripts/run_all.sh

关键输出:

    protobuf 序列化后请求体 = 9437275 bytes (9.00 MiB)
❌ 调用失败
   status = RESOURCE_EXHAUSTED
   description = gRPC message exceeds maximum size 4194304: 9437275

临界点:3 条各 3MB(9MiB)挂;3 条各 1MB(3MiB)正常。分界线就是 4MiB。

三、根本原因

1. 4194304 是 gRPC 默认的单条消息上限 = 4MiB

4 × 1024 × 1024 = 4194304。grpc-java 里 ServerBuilder / ManagedChannelBuilder 的默认 maxInboundMessageSize 就是 4MiB。环境里两侧都没设置,于是都走默认。

2. 拦你的是「服务端入站」,不是客户端

这是最容易被误判的一点。看服务端日志:

WARNING: Exception processing message
io.grpc.StatusRuntimeException: RESOURCE_EXHAUSTED: gRPC message exceeds maximum size 4194304: 9437229
    at io.grpc.internal.MessageDeframer.processHeader(MessageDeframer.java:392)
    at io.grpc.netty.shaded.io.grpc.netty.NettyServerStream$TransportState deframeFailed(...)

结论:

  • 请求已经到达服务端(TCP 上来了、netty 开始 deframe);
  • 是在服务端解析消息帧头时被判超限,直接 fail 掉 stream;
  • 根本没执行到你的业务方法,所以应用日志里没有这条请求 —— 不是「没发出去」,而是「发到了没进门」。

3. 9437275 就是 protobuf 序列化后的请求体大小

和客户端打印的 request.getSerializedSize() 完全一致,可自证。

4. 为什么调客户端 maxInboundMessageSize 没用

入站、出站是两个独立的校验。maxInboundMessageSize 管的是响应/入站方向;你发出的这条大请求要过的是服务端入站那道门。服务端没放开,客户端怎么调都没用。

5. 决定性实验:只改服务端上限,同一请求立刻成功

docker compose exec -T app bash -c '
CP="build/classes:$(ls /opt/grpc-libs/*.jar | tr "\n" ":")"
setsid nohup env GRPC_MAX_INBOUND_MB=32 java -cp "$CP" com.example.report.ReportServer 50096 0 >/tmp/big.log 2>&1 < /dev/null &
'
docker compose exec -T app bash -c '
CP="build/classes:$(ls /opt/grpc-libs/*.jar | tr "\n" ":")"
java -cp "$CP" com.example.report.ReportClient localhost:50096 3 3 unary 0
'

结果:客户端一个参数都没改,9MiB 请求通过了。

6. 两种报错文案 = 两个不同的判定点(很好的区分信号)

报错文案谁判定的哪个配置
gRPC message exceeds maximum size 4194304: 9437275服务端入站 deframerServerBuilder.maxInboundMessageSize()
message too large 9437229 > 1048576客户端出站校验stub.withMaxOutboundMessageSize()

记住文案差异,就能一眼判断是服务端拦还是客户端拦。

四、解决办法

方案 A(治本,强烈推荐):业务层切片 + 流式

别再攒成一个大请求。按业务约束切片(如 1MiB/片),用流式 RPC 分片发送。单条消息恒定小于上限,服务端一个参数都不用改。

int chunkBytes = 1024 * 1024;
for (int off = 0; off < payload.length; off += chunkBytes) {
    int len = Math.min(chunkBytes, payload.length - off);
    byte[] slice = new byte[len];
    System.arraycopy(payload, off, slice, 0, len);
    reqObs.onNext(ReportChunk.newBuilder()
            .setTenantId("tenant-A")
            .setChunk(ByteString.copyFrom(slice))
            .setSeq(seq++)
            .build());
}
reqObs.onCompleted();

实测:服务端保持默认 4MiB,9MB 通过,50MB 也通过。

⚠️ 流式不是免死金牌:如果单片仍然 >4MiB,照样报同一个错。 实测 1 条 9MB 用流式发 → 依旧 RESOURCE_EXHAUSTED: ... 4194304: 9437199。 流式解决的是「一次请求总大小」,不解决「单条消息大小」。

方案 B(治标):服务端 + 客户端成对调大上限

只调一侧没用。两侧都要放开:

// 服务端
ServerBuilder.forPort(50051)
    .maxInboundMessageSize(32 * 1024 * 1024)
    .addService(new ReportServiceImpl())
    .build().start();
// 客户端(若继续用 unary,出站也要放开,否则撞 message too large)
var stub = ReportServiceGrpc.newBlockingStub(channel)
    .withMaxOutboundMessageSize(32 * 1024 * 1024);

方案 C(治本+):业务层掐体积

  • 单条 payload 上限校验,超限直接返回业务错误码;
  • 采样点分页 / 按时间窗口拆分上报;
  • 压缩异常先降采样,而不是原样上报。

原则:不要靠不停调大上限续命,那只是把「更晚崩」换成「更早 OOM」。

五、验证

docker compose exec -T app bash ./scripts/verify_fix.sh
1. 原始问题复现                      ✅
2. 反例:流式但单条 >4MiB 仍失败      ✅
3. 修复 A:切片 1MiB + 流式,9MB      ✅
4. 修复 A 加压:50MB                  ✅
5. 修复 B:服务端 env 调大 32MiB      ✅
通过 7 项 / 失败 0 项 —— 🎉 全部符合预期

六、经验与避坑

  1. 别只看客户端异常第一行。status 和 description 常常分两行输出: status = RESOURCE_EXHAUSTED / description = gRPC message exceeds maximum size 4194304: 9437275。 用 grep "RESOURCE_EXHAUSTED.*4194304" 会匹配不到(数字只在第二行)—— 我就这么写错过一次。
  2. 服务端应用日志没有记录 ≠ 请求没发出去。可能是被 transport 层 deframer 拦了,没进业务方法。 去翻 docker compose logs server 里的 deframeFailed / Exception processing message。
  3. 一定要拿报错里的数字和实际序列化体积对齐(request.getSerializedSize()), 否则容易把「服务端拒绝」误判成「客户端本地拒绝」。
  4. 调上限要两侧成对:ServerBuilder.maxInboundMessageSize + 出站 withMaxOutboundMessageSize。
  5. 流式改造要检查切片粒度,单条消息必须 < 上限,否则改了也白改。
  6. 服务端启动日志要如实打印生效的上限,以免出现「以为调了其实没生效」的排查黑洞。
  7. 工程细节:docker compose exec 里用 setsid nohup cmd >log 2>&1 </dev/null & 起后台进程, 否则 exec 会话一结束进程就被回收;pkill -f XXX 会误杀自己的 shell(返回 137),要用 ps -eo pid,args 精确定位。

版权声明

本文为博主原创文章,遵循 CC BY-NC-SA 4.0 许可协议。 转载请注明出处:http://localhost/blogs/2026-09-12-0000-grpc-resource-exhausted-4mib

分享这篇文章