io.copy在http下载中不走splice,因其目标http.responsewriter封装了header、chunked编码等逻辑,导致无法满足splice对裸socket和seekable文件的硬性要求。

io.Copy 不是零拷贝,超大文件流式下载必须绕过它直调 unix.Splice;否则 strace 里全是 read+write,CPU 飙高、吞吐卡在 100MB/s 以下。
为什么 io.Copy 在 HTTP 下载里根本不会走 splice
HTTP 服务(如 http.ResponseWriter)本身封装了 header 写入、chunked 编码、buffer 管理和连接状态跟踪,io.Copy 检测到目标不是裸 net.Conn 就直接放弃 splice 路径。哪怕你传的是 *os.File 作源,最终也退化为用户态内存搬运。
常见错误现象:
- 用
io.Copy(w, f)处理 GB 级文件时,goroutine 占用高、GC 频繁、top显示 CPU 持续 90%+ -
strace -e trace=sendfile,splice,read,write只看到read和write,没有splice调用 - 响应体被 chunked 编码后,原始文件数据必须先读进内存再切块——零拷贝路径彻底失效
unix.Splice 的硬性前提一个都不能少
Linux 下真正零拷贝转发,必须满足三要素:pipe 中转、fd 类型合法、内核支持。缺一即 fallback 到 read/write。
- 必须用
unix.Pipe2(0)创建一对 pipe fd,不能用os.Pipe()(它不保证O_CLOEXEC,且底层可能不兼容 splice) - 源 fd 必须是 seekable 文件(
*os.File),且挂载点启用noatime(否则内核 ≥4.5 才允许 file→pipe) - 目标 fd 必须是
*net.TCPConn底层 socket,需通过conn.(*net.TCPConn).SyscallConn()获取,不能用conn.(*net.TCPConn).Fd()(它会提前关闭 conn) - 内核版本必须 ≥2.6.17(基础 splice),≥3.15(socket 支持完整),建议 ≥4.5(避免 noatime 依赖)
手写 unix.Splice 循环的最小可靠模式
每次 unix.Splice 最多搬 64KiB(pipe buffer 默认大小),超长必须循环;返回值是实际字节数,不等于请求长度就得重试。
- 先调
unix.Splice(inFd, nil, pipeW, nil, 65536, unix.SPLICE_F_MOVE|unix.SPLICE_F_NONBLOCK)把文件推入 pipe - 再调
unix.Splice(pipeR, nil, outFd, nil, 65536, unix.SPLICE_F_MOVE|unix.SPLICE_F_NONBLOCK)把 pipe 推给 socket - 两次调用都得检查 errno:
EAGAIN表示 pipe 满/空,需epoll或轮询等待;EINTR可重试;EINVAL说明 fd 类型不匹配 - 别漏掉
unix.Close(pipeR)和unix.Close(pipeW),否则 fd 泄露
示例关键片段(省略 error 处理):
inPipeR, inPipeW, _ := unix.Pipe2(0)
defer unix.Close(inPipeR)
defer unix.Close(inPipeW)
var offset int64
for remain > 0 {
n, _ := unix.Splice(int(file.Fd()), &offset, inPipeW, nil, 65536, unix.SPLICE_F_MOVE|unix.SPLICE_F_NONBLOCK)
remain -= n
n, _ = unix.Splice(inPipeR, nil, int(conn.(*net.TCPConn).SyscallConn().Control(...)), nil, 65536, unix.SPLICE_F_MOVE|unix.SPLICE_F_NONBLOCK)
}
真正卡住性能的往往不是 Go 代码,而是环境配置
实测中,85% 的“splice 不生效”问题出在系统层:
- Docker/K8s 容器默认使用 user namespace,
unix.Splice跨命名空间失败,报EINVAL - 云主机挂载磁盘未加
noatime,导致 file→pipe 步骤静默 fallback - Go runtime 的 M 线程被阻塞在
unix.Splice上(没设SPLICE_F_NONBLOCK),整个调度器卡顿 -
http.ResponseWriter已经写了 header,但你又想 splice —— 不行,header 必须由你手动拼,"HTTP/1.1 200 OK\r\nContent-Length: ...\r\n\r\n"一行都不能错
最后提醒一句:如果业务需要 TLS、HTTP/2 或动态 header(如 ETag 计算),就别硬上 splice;那些协议层操作注定要把数据拉进用户态。零拷贝只适合纯静态文件透传或代理类场景。











