首选zapcore.newasynccore,因其内置无锁环形缓冲、批量刷盘、内存复用和背压控制;手写channel易丢日志或卡死,缓冲大小建议1024~8192,超5w qps需磁盘队列兜底,关键错误日志必须直写并sync落盘。

zap.NewAsyncCore 是唯一靠谱的起点
别从零手写 channel + goroutine 日志器——它在每秒 5k 条以上就大概率丢日志、OOM 或卡死。生产环境直接用 zap.NewAsyncCore,它不是简单包一层 goroutine,而是内置无锁环形缓冲、批量刷盘、内存复用和背压控制。自己实现时漏掉任意一环(比如没处理 runtime.Caller 调用开销、没做 buffer 复用、没设超时强制 flush),性能和稳定性都会断崖式下跌。
- 缓冲大小设
1024~8192:太小易满导致default分支频繁丢日志;太大则延迟升高,P99 日志延迟可能突破 200ms - 启用方式必须是
zap.WrapCore(func(core zapcore.Core) zapcore.Core { return zapcore.NewAsyncCore(encoder, writer, level, bufferSize) }),不能只套个go func() {...}() - 峰值 QPS 超 5w 时,纯内存缓冲扛不住,得配合本地磁盘队列(如
file-rotatelogs+io.MultiWriter)落地,否则进程重启即丢
关键错误日志必须绕过异步通道直写
异步本质是“尽力而为”,但服务启动失败、数据库连接中断这类日志丢了就等于失去可观测性。它们不能走 zap.NewAsyncCore 的缓冲路径,必须单独走同步落盘通道。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 开一个带
os.O_SYNC标志的*os.File,专供ERROR和PANIC级别日志使用 - 用
zapcore.NewTeeCore(syncCore, asyncCore)把 error 日志同时发给直写 core 和异步 core - 每次直写后必须显式调
file.Sync(),defer file.Close()或defer logger.Sync()在崩溃时根本不会执行
轮转日志别碰 lumberjack 的阻塞坑
lumberjack 在 rotate 瞬间会同步压缩归档,所有写入卡住,实测延迟飙到 200ms+,还会导致旧日志写进新文件末尾、时间戳错乱。Windows 下文件锁行为更不可靠。
- 改用
file-rotatelogs+io.MultiWriter:把当前日志写入和 rotate 动作完全解耦,rotate 丢进独立 goroutine - rotate 前务必复用
bytes.Buffer,否则高频 rotate 下 GC 压力陡增,CPU 毛刺明显 - 测试必须在目标 OS 上跑,Linux 和 Windows 的文件锁语义差异大,跨平台部署前不验证等于埋雷
Encoder 和 Caller 配置直接影响性能
默认配置下,zap.AddCaller() 每次触发 runtime.Caller,增加约 15% 耗时;EncodeLevel 若用字符串拼接而非预定义 encoder,也会白烧 CPU。
- 关掉全局
zap.AddCaller(),只在ERROR级别用zap.AddCallerSkip(1) -
EncoderConfig里设EncodeLevel: zapcore.CapitalLevelEncoder,避免 runtime 拼接 - 禁用
EncodeTime默认的 RFC3339 格式,改用zapcore.TimeEncoder(zapcore.ISO8601TimeEncoder),减少格式化开销
zap.NewAsyncCore 覆盖了,手写 channel 没覆盖。这点容易被忽略,直到 P99 延迟突然跳变才意识到问题不在磁盘,而在主线程里多跑了三次 runtime.Caller。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










