log 默认同步写入导致主线程阻塞,提速关键在于用带缓冲的 chan string + 单 goroutine 消费,且日志内容需提前拼好;因 os.File.Write 是阻塞系统调用,HTTP handler 中打日志会拖慢整个请求。

标准库 log 默认同步写入,主线程必然阻塞;不换库也能提速,关键在加一层带缓冲的 chan string + 单 goroutine 消费,且日志内容必须提前拼好。
为什么 log.Printf 一调用就卡住
因为 log 包底层直接调用 w.Write(),而 os.File.Write 是系统调用,要等磁盘或内核缓冲确认。HTTP handler 里打一条日志,整个请求就得等它落盘——尤其在 SSD 性能差、文件系统忙、或用了 lumberjack 轮转时更明显。
常见错误现象:log.Println("start") 和 log.Println("end") 时间戳间隔远大于业务逻辑耗时;pprof 显示大量时间花在 syscall.Syscall 上。
- 不是 log 格式化慢,是写入动作本身阻塞
- 即使把
log.SetOutput指向bufio.NewWriter,也只是缓冲,Flush()仍会阻塞 - 多个 goroutine 同时调用
log.Printf,还会因内部锁串行化
用 chan string + goroutine 最简实现
不依赖第三方,50 行内搞定,核心是解耦“发日志”和“写文件”两个动作。
立即学习“go语言免费学习笔记(深入)”;
实操建议:
- channel 必须带缓冲:
logCh := make(chan string, 4096);无缓冲会立刻阻塞发送方 - 日志字符串由主流程提前拼好(含时间、级别、换行),后台 goroutine 只做
file.WriteString(msg),避免在消费端做time.Now()或fmt.Sprintf - 发送时用
select { case logCh ,防止队列满时卡死;default 分支可降级为 <code>fmt.Fprintln(os.Stderr, "[WARN] log dropped") - 后台 goroutine 必须包
recover,否则file.Writepanic 会导致整个日志通道失效
示例关键片段:
func (l *AsyncLogger) Info(msg string) {
s := fmt.Sprintf("[%s] INFO %s\n", time.Now().UTC().Format("2006-01-02T15:04:05Z"), msg)
select {
case l.ch <- s:
default:
fmt.Fprintln(os.Stderr, "[DROP] log queue full")
}
}
<p>func (l *AsyncLogger) run() {
defer func() {
if r := recover(); r != nil {
fmt.Fprintln(os.Stderr, "[FATAL] logger panic:", r)
}
}()
for s := range l.ch {
if _, err := l.file.WriteString(s); err != nil {
fmt.Fprintln(os.Stderr, "[ERROR] write log failed:", err)
}
}
}用 zerolog + diode 快速接入异步
如果你已在用 zerolog,不用改日志调用方式,只需替换 io.Writer 层。
实操建议:
- 别直接传
os.Stdout给diode.NewWriter,应传复用的*os.File,避免频繁 open/close - 缓冲区大小设为 2048–8192,太小易丢,太大内存浪费;溢出回调里别只打印,建议触发告警或 metrics 计数
-
diode.OneToOne比ManyToOne更轻量,适合单写者场景(即你只有一个 logger 实例) - 不要省略
file.Sync()——diode不自动 flush,需自己控制落盘时机,否则进程崩溃必丢最后几条
典型配置:
file, _ := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
wr := diode.NewWriter(file, 4096, 10*time.Millisecond, func(missed int) {
atomic.AddInt64(&dropCount, int64(missed))
})
logger := zerolog.New(wr).With().Timestamp().Logger()进程退出时日志丢失怎么防
这是异步日志最常被忽略的点:os.Exit(0) 或 panic 退出时,channel 里积压的日志根本来不及消费。
必须做三件事:
- 收到
os.Interrupt或syscall.SIGTERM信号后,先close(logCh) - 启动一个
sync.WaitGroup,在后台 goroutine 启动时wg.Add(1),退出前wg.Done() - 主流程调用
wg.Wait(),但加超时(如 2 秒),超时则强制os.Exit(1),避免无限等待
注意:for range ch 会自动退出,但若 channel 未 close,goroutine 就永远阻塞在 range 上——这正是优雅关闭失效的根源。


















