并发写大文件失控会导致 too many open files、磁盘 i/o 归零、内存暴涨;应使用固定数量 worker pool,按分片文件各自开闭文件,或单文件时由专属 writer 串行消费 channel 数据,worker 数建议 4–16。

写入大文件时并发失控的典型现象
直接为每个数据块 go writeChunk(),很快会遇到:too many open files、磁盘 I/O 趋近于 0、程序内存暴涨甚至被 OOM kill。这不是因为 goroutine 本身重,而是所有协程共用同一套系统资源(文件描述符、内核缓冲区、磁盘队列),无节制并发会让调度和竞争开销反超吞吐收益。
用 worker pool 控制写入并发最稳妥
核心是预启动固定数量的长期 worker,每个只负责写入一个独立目标文件,或串行写入共享文件。关键不在于“怎么起 goroutine”,而在于“谁持有 *os.File”和“谁决定写入顺序”。
- 若写入多个分片文件(如按哈希分桶):每个 worker 自己
os.OpenFile(..., os.O_CREATE|os.O_WRONLY|os.O_APPEND),处理完file.Close(),完全无共享、无锁 - 若必须写入单个大文件:所有 worker 不能直接操作该
*os.File;改用一个专属 writer goroutine 从chan []byte消费,其余 worker 只做计算/编码,避免竞态 - worker 数量建议设为
4–16,取决于磁盘类型(SSD 可稍高,HDD 建议 ≤8)和单次写入大小(bufio.NewWriterSize(file, 1 推荐)
别用带缓冲 channel 当信号量来限写入并发
像 sem := make(chan struct{}, 10) + sem 这类信号量模式,适合 IO 等待短、任务粒度细的场景(如 HTTP 请求)。但对文件写入——尤其是大块写入——它无法解决根本问题:
- 信号量只控制“开始写”的数量,不控制“正在写的字节数”,buffered writer 的 flush 仍可能堆积大量未落盘数据
- 一旦某个写入卡在
Write()(如磁盘满、NFS 挂起),整个信号量会被长期占用,后续任务无限等待 - 没有天然的背压反馈,上游读取协程继续往 channel 塞数据,极易 OOM
channel 缓冲区大小不是并发数,别混淆
常见误解:把 make(chan []byte, 100) 的 100 当成并发上限。其实这只是内存缓冲深度,和实际同时执行的 goroutine 数量无关。真正控制并发的是 worker 数量,不是 channel 容量。
- 缓冲太小(如
1):读协程频繁阻塞,吞吐上不去 - 缓冲太大(如
10000):内存占用不可控,尤其当每个[]byte是几 MB 的分块时 - 推荐值:
make(chan []byte, 8–32),配合 4–16 个 worker,平衡延迟与内存
真正容易被忽略的是:writer goroutine 必须用 bufio.NewWriterSize 包装,并显式调用 Flush() 或靠 defer 关闭触发落盘;否则你以为写进去了,其实还卡在用户态 buffer 里。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











