直接调用 log.println() 不是异步写盘,因其内部同步执行 file.write()、无缓冲、无批量、无背压控制,导致高 qps 下 p99 延迟飙升、进程退出时日志丢失;需用带缓冲 channel + 单写 goroutine + bufio.writersize(16kb) + 按需 sync,并确保 writer 线程安全。

为什么直接 go log.Println() 不算异步写盘
它只是把 goroutine 开在调用侧,log.Println() 内部仍同步调用 file.Write(),没缓冲、没批量、没背压控制。现象很直接:QPS 上去后 P99 延迟飙升,pprof 显示大量时间卡在 syscall.Syscall;更糟的是,进程退出时 channel 里积压的日志全丢,defer file.Close() 根本救不回来。
必须设带缓冲的 channel + 单写 goroutine
这是避免并发写冲突和 OOM 的底线。多 goroutine 直接往同一个 *os.File 写,即使开了 O_APPEND,ext4/xfs 下仍可能因内核页缓存导致行粘连或截断。缓冲大小不能拍脑袋定:
-
make(chan *LogEntry, 1024)是安全起点,低于 256 容易满丢日志 - 高于 8192 会拉高 P99 延迟(尤其磁盘慢时),实测 4096 是多数服务的平衡点
- 别用无缓冲 channel —— 一卡全卡,主流程直接被拖死
bufio.Writer 必须显式指定 buffer size
默认 bufio.NewWriter(file) 只给 4KB 缓冲,攒不满就 flush,频繁系统调用反而更慢。关键点:
- 用
bufio.NewWriterSize(file, 16*1024),16KB 能较好平衡延迟与内存占用 -
w.Flush()只刷到内核 buffer,对 ERROR/audit 级别日志,后面必须跟file.Sync() - 别在每次写完都
Sync()—— 那就退化成同步写,每秒 3k 条日志时毛刺明显
Zap 的 NewAsyncCore 不是套个 goroutine 就完事
很多人以为 go logger.Info() 就是异步,其实 zap.NewProduction() 底层仍是同步写。真要异步,必须用 zapcore.NewAsyncCore:
- 传入的
writer得是zapcore.WriteSyncer,比如zapcore.AddSync(zapcore.Lock(lumberjackLogger)) -
bufferSize设 4096~8192,太小丢日志,太大延迟不可控 - 高频行为日志(如点击、评分)可走异步通道,但 DB 连接失败这类关键错误,得单独配一个
os.O_SYNC的文件直写
最易被忽略的是 writer 的并发安全性 —— *os.File 本身不支持并发写,必须包一层 zapcore.Lock 或用 lumberjack.Logger 这类线程安全封装。否则哪怕用了 NewAsyncCore,日志内容照样错乱。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











