log.Printf在高并发下会卡住,因其内部使用同步写盘和全局互斥锁,每条日志都需串行获取锁并执行os.File.Write,导致goroutine大量阻塞在mu.Lock(),引发延迟陡增与锁竞争瓶颈。

为什么直接用 log.Printf 在高并发下会卡住?
因为默认的 log.Logger 内部用的是同步写盘 + 全局互斥锁,每条日志都得串行走 os.File.Write。QPS 上千时,大量 goroutine 会堵在 mu.Lock() 上,CPU 没跑满,磁盘 I/O 和锁竞争成了瓶颈。
典型现象:压测时 p99 延迟陡增、goroutine 数持续上涨、runtime/pprof 显示大量 goroutine 卡在 log.(*Logger).Output。
- 别试图靠加缓冲区(比如把
log.SetOutput换成bufio.Writer)来“优化”——底层仍要锁,只是延迟了阻塞点 -
log.SetFlags(0)或关闭时间戳只能省点格式化开销,不解决根本问题 - 用
sync.Pool缓存log.Logger实例也没用,锁是 logger 自己持有的,不是创建开销问题
用 zap 替代标准库时要注意哪些配置陷阱?
zap 的高性能核心在于结构化日志 + 零分配编码 + 异步写盘,但默认配置可能悄悄退化成同步模式。
- 必须用
zap.NewProductionConfig().AddCaller()这类明确启用异步的配置;zap.NewDevelopment()默认是同步的,只适合调试 -
EncoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder看似只是改大小写,但若用错 encoder(比如混用consoleEncoder和jsonEncoder),会导致EncodeEntry调用路径变长,拖慢队列消费速度 - 异步写盘依赖
zapcore.NewCore传入的WriteSyncer是否真正支持非阻塞——os.Stdout支持,但某些封装过的rotatelogs实现可能内部加锁,需实测 - 务必调用
logger.Sync()在进程退出前刷盘,否则最后几条日志会丢失
如何安全地自建日志队列而不丢日志?
自己用 chan *logEntry + goroutine 消费看似简单,但边界情况极多:队列满怎么办?消费者 panic 怎么恢复?OOM 怎么防?
立即学习“go语言免费学习笔记(深入)”;
- 通道长度不能设太小(如
make(chan, 100)),建议至少make(chan, 10000),并配select{ case ch 防死锁 - 消费者 goroutine 必须 recover,否则 panic 后队列永久卡死;且要定期检查
len(ch),超过阈值(如 80% 容量)触发告警 - 日志结构体里别存指针或大对象(如
*http.Request),序列化前先提取必要字段,否则 GC 压力大、内存泄漏风险高 - 写盘失败时,不要重试(可能无限积压),记录错误到 stderr 并丢弃该条——日志系统本身不该影响主业务
滚动文件 + 并发写同一个文件是否可行?
多个 goroutine 直接往同一个 *os.File 写,即使加了锁,也会因系统调用(write(2))和磁盘 seek 导致性能崩坏,且 rotatelogs 类库多数不保证并发安全。
- 正确做法是:所有日志统一走单个 writer goroutine,它负责判断是否滚动、打开新文件、原子替换软链接(如用
os.Rename+os.Symlink) - 避免用
lumberjack的Rotate回调做耗时操作(如压缩旧日志),应另起 goroutine 处理,否则阻塞主写入流 - 如果必须多进程写同一目录下的不同文件(如按 service name 分片),确保文件名带唯一标识(pid 或 uuid),防止冲突
真正难的不是“怎么写快”,而是“怎么在丢日志、吃内存、卡主线程这三者之间划出可接受的界线”。每个业务对这三者的容忍度不同,得拿真实流量压出来,而不是看 benchmark 数字。


















