zap.NewProduction()默认不是异步的,它同步写入os.Stderr或裸*os.File,无goroutine、缓冲区或队列,磁盘慢时直接阻塞业务;真异步需手动组合Encoder、加锁封装的WriteSyncer(如lumberjack)和Sampler。

zap.NewProduction() 默认不是异步的,它同步写入 os.Stderr 或你传入的裸 *os.File,磁盘慢或日志量大时会直接阻塞业务 goroutine。
为什么 zap.NewProduction() 不能解决 I/O 阻塞
它只是启用了 JSON 编码、纳秒时间戳和采样,底层仍用同步 WriteSyncer,每次 logger.Info() 都触发系统调用 write()。压测时 pprof 明显显示卡在 syscall.Syscall 或 runtime.futex —— 这不是 CPU 瓶颈,是 I/O 等待。
-
zap.NewProduction()不含 goroutine、缓冲区、队列或超时控制 - 若输出目标是慢设备(如机械盘、NFS)或远程 sink(如 Kafka、Loki),延迟会直接传导到业务逻辑
- 所谓“高性能”仅指编码和字段序列化路径零分配,不等于 I/O 并发能力
手动构造真正异步 Core 的三个必要组件
异步不是加个 go logger.Info() 就行——那只是并发打日志,writer 仍是同步阻塞。Zap 要求你显式组装 zapcore.Core,且以下三者缺一不可:
-
Encoder:必须用
zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig());自定义EncodeTime函数会触发time.Now().Format(),引发 GC -
WriteSyncer:不能直接传
*os.File,得套两层:zapcore.AddSync(file)+zapcore.Lock(...);写文件优先用lumberjack.Logger替代裸os.OpenFile -
Sampler:用
zapcore.NewSampler(core, time.Second, 100, 10)控制高频 error 日志;注意第一个参数是原始 core,不是 logger 实例
如何避免日志丢失与缓冲区溢出
异步写入引入了内存缓冲,但没配好就容易丢日志或 OOM:
立即学习“go语言免费学习笔记(深入)”;
- 进程退出前必须显式调用
logger.Sync(),否则缓冲区内容静默丢失;建议在main()结尾或defer中执行 -
zapcore.NewSampler的第三个参数(burst)别设太大,比如1000在高错误率下可能吃光内存;生产环境推荐100起步 - 不要用
os.Exit(0)退出,改用return让defer logger.Sync()生效;若需响应信号,监听os.Interrupt后再Sync() - 字段过多(>10 个)会显著抬高内存分配,
zap.String("k", v)每个都分配一个zap.Field结构体,高频场景下应精简上下文
双输出且格式不同:用 NewTee 而非 NewMultiCore
zapcore.NewMultiCore 是并发写入同一份字段,无法实现“控制台人类可读、文件机器可解析”。要格式分离,必须用 zapcore.NewTee:
- 控制台 Core:用
zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()),LevelEnabler设为DebugLevel - 文件 Core:用
zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),LevelEnabler设为InfoLevel - 两者共用同一个
WriteSyncer时,需分别封装:控制台用zapcore.Lock(os.Stdout),文件用zapcore.Lock(lumberjack.NewLogger(...))
最易被忽略的是:zapcore.NewTee 的每个子 Core 必须独立配置 LevelEnabler,否则低级别日志会漏掉或误写入文件。



















