grpc流式传输传大文件易卡死或oom,因unary调用将整个数据加载内存;须用bidistreaming分块传输,客户端需手动分片send,服务端须及时落盘而非缓存。

gRPC流式传输为什么传大文件容易卡死或 OOM
因为默认 gRPC 的 Unary 调用会把整个请求/响应体加载进内存,文件一超过几十 MB,ClientConn 或 Server 就可能触发 GC 压力、超时、甚至 out of memory。流式(Streaming)不是“自动变快”,而是把大文件切片成小块,靠 Send() / Recv() 逐步推拉 —— 但前提是客户端和服务端都用对了流类型。
必须用 ServerStreaming 或 BidiStreaming,不能用 Unary
单次上传大文件用 ServerStreaming(服务端返回进度/结果)不够,得用 BidiStreaming(双向流),否则无法边传边校验、断点续传或实时反馈错误。定义 proto 时要明确写:
rpc UploadFile(stream FileChunk) returns (stream UploadStatus);
其中 FileChunk 至少含 bytes data 和 int64 offset;UploadStatus 含 int32 code 和 string message。别偷懒复用 message File —— 那还是 Unary 思维。
-
BidiStreaming允许客户端按需Send()分块,服务端随时Recv()并异步落盘 - 如果只用
ServerStreaming,客户端还得先把整个文件读进内存再一次性Send(),没意义 - Go 客户端调用后得到的是
UploadFileClient接口,不是普通函数,必须自己控制Send()循环
客户端分块发送必须手动控制 buffer 大小和 flush 时机
常见错误是直接 os.ReadFile() 整个文件再塞进一个 FileChunk 发出去 —— 这等于又回到 Unary 模式。正确做法是开固定 buffer(比如 32 * 1024 字节),循环 io.ReadFull() 或 bufio.Reader.Read(),每次构造新 FileChunk 并调用 Send():
立即学习“go语言免费学习笔记(深入)”;
buf := make([]byte, 32*1024)
for {
n, err := file.Read(buf)
if n > 0 {
chunk := &pb.FileChunk{
Data: buf[:n],
Offset: int64(offset),
}
if err := client.Send(chunk); err != nil {
return err // 注意:Send 可能因网络中断提前失败
}
offset += n
}
if err == io.EOF { break }
}- buffer 太小(如 4KB)会导致 RPC 调用太频繁,增加 gRPC header 开销
- buffer 太大(如 1MB)可能让单次
Send()阻塞过久,且易触发maxMessageSize限制(默认 4MB) - 别依赖
client.CloseSend()触发服务端结束 —— 要显式发一个FileChunk{Eof: true}标记
服务端接收时必须及时 write 到磁盘,不能攒在内存里
服务端 Recv() 到的每个 FileChunk,应该立刻 os.WriteAt() 或追加到 *os.File,而不是 append 到 [][]byte。否则内存占用随文件大小线性增长。
- 用
os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644)打开文件,避免重复创建 - 用
file.WriteAt(chunk.Data, chunk.Offset)支持乱序到达(比如重传某块),但要注意并发安全 —— 建议用sync.Mutex包一层写操作 - 收到
Eof: true后才做校验(如sha256)、重命名、清理临时文件 - 别在
Recv()循环里做耗时操作(如数据库写入),会阻塞整个流
真正麻烦的从来不是“怎么发”,而是怎么保证每一块都可靠落地、出错可回退、重启可续传 —— 这些得靠 offset、checksum、临时文件名和幂等接口共同兜底,gRPC 流本身不提供这些。










