根本原因是未控制chunk大小导致内存暴涨;须手动分块(64KB~1MB)、分批读取、显式调用CloseSend()、并在循环中监听stream.Context().Done()防止goroutine泄漏。

gRPC流式传输时内存暴涨,根本原因是没控制 chunk 大小
客户端或服务端直接把整个文件读进 bytes 再塞进 stream,一传 1GB 文件就 OOM。gRPC 的 stream.Send() 不会自动分块,它只管把传入的 message 序列化后发出去——你给多大,它就发多大。
实操建议:
- 必须手动按固定字节切片,推荐
64KB ~ 1MB每块(65536是常见安全值,兼顾网络 MTU 和 GC 压力) - 不要用
os.ReadFile()或bytes.Buffer.Bytes()加载全量数据 - 改用
os.Open()+io.ReadFull()或bufio.Reader.Read()分批读取 - 每块封装成独立的 streaming message,例如
FileChunk{Data: []byte, Offset: int64}
Go 客户端写流时卡住或超时,大概率是没调 stream.CloseSend()
gRPC 的 client-streaming 或 bidirectional streaming 要求客户端显式结束写入,否则服务端 stream.Recv() 会一直阻塞在 EOF 等待。这不是 bug,是协议设计:stream 生命周期由两端共同管理。
常见错误现象:
- 服务端日志停在 “waiting for next chunk” 后无响应
- 客户端
ctx.Done()触发context deadline exceeded - Wireshark 可见 TCP 连接空闲但未 FIN
实操建议:
- 所有写循环结束后,必须紧跟
stream.CloseSend() - 把它包进
defer不安全——异常路径可能跳过;应放在正常流程末尾 +if err != nil分支外 - 加日志:
log.Printf("sent %d chunks, closing send"),确认执行到这一步
服务端处理大文件时 goroutine 泄漏,问题出在未设置 stream.Context().Done() 监听
客户端断连、取消或超时后,服务端若还在往已关闭的 stream 写响应(比如回传校验失败),会触发 rpc error: code = Canceled desc = context canceled,但更危险的是:不检查上下文,goroutine 就卡死在 stream.Send(),且无法被回收。
使用场景:文件校验、转码、加密等需耗时处理的流式服务。
实操建议:
- 每个处理循环内必须用
select监听stream.Context().Done() - 避免在
for循环里直接调stream.Send(),改成select { case - 对 chunk 处理逻辑本身也设超时,比如用
context.WithTimeout(ctx, 30*time.Second)
Java gRPC 客户端用 StreamObserver 传大文件,onNext() 报 java.lang.OutOfMemoryError: Direct buffer memory
Netty 默认用堆外内存(DirectByteBuffer)做 gRPC 编解码缓冲区,但 JVM 的 -XX:MaxDirectMemorySize 通常默认只有 0(即等同于堆大小),而大文件 chunk 频繁分配释放 DirectBuffer,容易触顶。
参数差异:
- 不设参数时,Netty 使用
PooledByteBufAllocator,但池子上限受 JVM 直接内存限制 -
ManagedChannelBuilder.maxInboundMessageSize()控制单条 message 上限,但不影响缓冲区总用量
实操建议:
- 启动时加 JVM 参数:
-XX:MaxDirectMemorySize=2g(根据并发流数估算,如 10 并发 × 128MB 缓冲 ≈ 1.2g) - 客户端发送前,确保
chunk.getData().capacity() ,否则服务端直接拒收 - 避免在
onNext()中做深拷贝或 Base64 编码——这些操作额外吃堆内存
真正难的不是“怎么分块”,而是让每一块都落在内存水位线之下,同时不因频繁 syscall 拖慢吞吐。很多人调大 buffer 后上传变快了,却在高并发下批量 OOM——那是因为没把 stream.Context() 和 runtime.GC() 的节奏对齐。










