根本原因是系统调用碎片化、缓冲区过小(默认32kb)、单线程无法压满带宽、页缓存冗余拷贝;实测需显式增大读缓冲(如bufio.newreadersize配1mb),才能释放nvme磁盘性能。

为什么默认文件备份IO带宽上不去
Go 默认用 os.ReadFile 或 io.Copy 做备份时,常卡在 30~70MB/s(哪怕 SSD 跑满是 500MB+/s),根本原因不是磁盘慢,而是:系统调用太碎、缓冲区太小、单线程压不住带宽、没绕过页缓存冗余拷贝。实测发现,io.Copy 内部用的 32KB 缓冲在机械盘尚可,在 NVMe 上明显吃不饱。
用 bufio.NewReaderSize + io.Copy 替代裸 io.Copy
直接 io.Copy(dst, src) 看似简洁,但它包装的 io.Reader 没法控制底层缓冲区大小;而备份场景下,读端瓶颈远大于写端,必须显式放大读缓冲。
- 把源文件用
bufio.NewReaderSize(file, 1(256KB)包装,再传给 <code>io.Copy - 目标文件用
bufio.NewWriterSize(outputFile, 1,并在最后 <code>Flush() - 避免用
os.Create,改用os.OpenFile(name, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644),省掉 chmod 系统调用 - 若目标是普通磁盘,缓冲区设为 64KB~128KB 更稳;NVMe 可试 256KB~512KB
并发分片读写但不争抢同一文件描述符
对单个大文件做并发读写,不是开 goroutine 就行——os.File 本身不是线程安全的读写位置(ReadAt/WriteAt 才是)。盲目并发会导致数据错位或 panic。
- 只对「多个独立备份任务」(如备份 10 个不同目录)启用 goroutine,并用
sem := make(chan struct{}, 4)控制总并发数(4 是 SATA SSD 的典型最优值) - 对单个超大文件(>1GB),用
file.Seek(offset, 0)定位后,配合io.CopyN分段读取,每个 goroutine 处理固定字节范围 - 写入端必须用
file.WriteAt(data, offset),不能用file.Write,否则所有 goroutine 会挤在同一个 write cursor 上 - 注意:Windows 下
WriteAt性能较差,Linux/macOS 更适合此模式
绕过内核页缓存直写(仅限 Linux)
备份时若已确认源文件不会被其他进程修改,且目标盘是高性能 SSD,可以跳过内核页缓存,减少一次内存拷贝——但这不是万能药,开错会反降速甚至报 invalid argument。
- 打开源文件时加
syscall.O_DIRECT:需用unix.Open(非os.Open),且缓冲区地址、偏移、长度都必须是 512B 对齐 - 目标文件也需同样对齐,并用
unix.Write替代file.Write - 缓冲区必须用
unix.Mmap或unsafe.AlignedAlloc分配,普通make([]byte, n)不行 - 别在 ext4 上用
O_DIRECT写小文件(
真正起效的前提是:你清楚自己在绕过哪一层、对齐是否到位、以及是否值得为这点吞吐牺牲可移植性。多数业务备份,bufio + 合理分片已经够用,O_DIRECT 是最后半个百分点的抠门优化。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











