必须用stream关键字定义流式rpc(如rpc upload(stream chunk) returns (uploadresult))传大文件,单次unary调用会oom;chunk仅含data和offset,chunk size推荐256kb;服务端需校验seq、缓存乱序块、聚合写盘并sha256校验。

gRPC 流式 RPC 怎么定义才能传大文件
必须用 stream 关键字声明双向流或服务端流,单次请求响应(unary)会直接 OOM。二进制大文件不能塞进 bytes 字段走普通 RPC,得靠流把文件切块推过去。
常见错误是写成 rpc Upload(FileRequest) returns (FileResponse) —— 这种定义下,哪怕你客户端分块发,服务端也等不到完整请求不开始处理,gRPC 底层会攒全量再解包,内存爆掉是必然的。
- 正确做法:定义为
rpc Upload(stream Chunk) returns (UploadResult)(客户端流)或rpc Upload(stream Chunk) returns (stream ChunkAck)(双向流) -
Chunk消息里只放bytes data和uint64 offset,别塞文件名、元数据到每块里,统一在首块或单独 RPC 传 - Protobuf 中避免使用
optional或嵌套深的结构,序列化开销会放大流式传输延迟
客户端分块发送时怎么控制 chunk size 和 buffer
默认 gRPC Go 客户端流没有自动缓冲,Send() 是同步阻塞调用,但底层 TCP 写入可能被节流;chunk 太小(如 4KB)会导致 syscall 过多、gRPC header 开销占比飙升;太大(如 16MB)则容易触发 max_receive_message_size 限制或内存抖动。
- 推荐 chunk size 在
64KB–1MB之间,实测 256KB 在千兆内网和云上 SSD 场景下吞吐较稳 - 用
bufio.NewReader包一层文件 reader,每次Read()到固定长度[]byte,别用io.CopyN直接塞流——它不保证精确字节数,边界容易错位 - 发送前检查
len(data) == 0,空 chunk 会让某些服务端解析 panic(尤其没做防御性解包时) - 别在循环里反复 new
Chunk{}然后赋值——小对象逃逸+GC 压力明显,复用一个实例 +proto.Clone()更省
服务端接收流时如何防丢包、保顺序、防超时
gRPC 流本身不保证跨 chunk 的原子性,网络抖动或客户端崩溃可能导致中间 chunk 缺失,而服务端 Recv() 返回 io.EOF 只代表流结束,不代表数据完整。
立即学习“go语言免费学习笔记(深入)”;
- 必须在每个
Chunk中带单调递增的seq字段,服务端用 map[int]*Chunk 缓存乱序到达的块,收到seq == 0才初始化 session,seq == -1或特殊 flag 标记结束 - 设置
context.WithTimeout时长要远大于单个 chunk 传输时间(比如设 5 分钟),否则流中途卡顿就会整个 ctx cancel,已收 chunk 全丢 - 不要在
Recv()循环里直接写磁盘——频繁 fsync 会拖垮吞吐;先写入内存 buffer(如bytes.Buffer),累积到 4MB 再 flush 到临时文件,最后流结束 rename - 注意
grpc.Server默认MaxConcurrentStreams是 100,上传并发高时需显式调大,否则新流被拒绝,报错是transport: max stream limit reached
Go 服务端怎么安全落地分块文件到磁盘
直接拼接 os.WriteFile 到同一路径会覆盖,用 os.OpenFile(..., os.O_APPEND) 又无法随机写入指定 offset,且多个 chunk 并发写同一 file handle 极易错位。
- 方案一(推荐):每个上传流分配唯一
upload_id,所有 chunk 写入/tmp/upload_${upload_id}_${seq},流结束后用io.MultiReader按 seq 排序合并读取,再os.Rename到目标路径 - 方案二:用
mmap(通过golang.org/x/sys/unix)创建稀疏文件,按offset直接写对应位置,但 Windows 不支持,云环境可能被容器 runtime 禁用 - 务必校验最终文件的
sha256sum,和客户端上传前计算并随首块传来的 hash 对比——这是唯一能确认“没丢字节、没错位”的方式 - 临时文件写完后,记得
os.Chmod(path, 0644),否则某些 k8s initContainer 或 sidecar 可能因权限问题读不了
真正难的不是分块发,而是怎么让每一块都落盘可追溯、可验证、可回滚。chunk 序号、校验 hash、临时文件隔离,这三样漏任何一样,线上出问题就只能翻日志猜。










