正确做法是用zap.NewAsyncCore实现异步日志,配合无锁环形缓冲、批量刷盘和内存复用;关键错误日志需直写并强制落盘,且异步失败须监控告警。

别用 log.SetOutput 套 bufio.Writer 就算完事
这只能缓冲“写文件”那一小段,但日志格式化(比如拼接时间、调用栈、字段)、json.Marshal、甚至 runtime.Caller 都还在主 goroutine 里跑,业务照样被卡住。
- 现象:HTTP handler 里调一次
log.Printf,P99 延迟突然跳到 50ms+,尤其带fmt.Sprintf或结构体打印时 - 真正要解耦的是“记录动作”和“落盘动作”,不是“写动作”和“系统调用”
- 正确做法是让业务 goroutine 只做轻量级构造(如填字段),序列化和写入全交给独立 writer goroutine
- 自己手写
chan *LogEntry很容易漏掉背压控制——channel 满了不处理,要么 panic 要么丢日志,生产环境不可靠
每秒超 5k 条日志,直接上 zap 的异步 Core
zap 的 zapcore.NewAsyncCore 不是简单包一层 goroutine,它用无锁环形缓冲 + 批量刷盘 + 内存复用,比裸 channel 稳得多。
- 启用方式:
zap.WrapCore(func(core zapcore.Core) zapcore.Core { return zapcore.NewAsyncCore(encoder, writer, level, 1024) }) - 缓冲大小建议设为 1024~8192,太小易满,太大延迟高;峰值 5w QPS 以上就得考虑本地磁盘队列落地,纯内存扛不住
- 别开
zap.AddCaller()全局生效,它每次触发runtime.Caller,增加约 15% 耗时;只在error级别开更合理 - Encoder 配置里加
EncodeLevel: zapcore.CapitalLevelEncoder,避免字符串拼接开销
轮转日志别踩 lumberjack 的阻塞坑
lumberjack 在 rotate 瞬间会同步压缩归档,所有写入卡住,实测延迟可飙到 200ms+,还会导致日志时间戳乱序(旧日志写进新文件末尾)。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 必须设
LocalTime: true和Compress: false,压缩操作绝对不能在写路径里做 - 如果用
file-rotatelogs,得搭配sync.Pool复用bytes.Buffer,否则高频 rotate 下 GC 压力陡增 - Windows 下
file-rotatelogs文件锁行为和 Linux 不同,测试务必用目标 OS - 更稳的做法:用
io.MultiWriter把日志同时写到当前文件 + rotate goroutine,rotate 动作完全异步
关键错误日志不能走异步通道
异步本质是“尽力而为”,但像服务启动失败、数据库连接中断这类日志,丢了就难排查。必须绕过缓冲,直写并强制落盘。
立即学习“go语言免费学习笔记(深入)”;
- 方案:单独开一个带
os.O_SYNC的*os.File,专供error或panic级别日志使用 - 不要依赖
defer logger.Sync()—— 进程崩溃时它根本没机会执行;Sync()应该在每次关键写入后显式调用 - channel 满时用
select+default快速丢弃debug日志,但error日志应 fallback 到直写路径 - 异步 worker 本身也要监控:记录每秒写入量、flush 耗时、失败率,这些指标比日志内容还早暴露问题
最常被忽略的不是“怎么异步”,而是“异步失败了怎么办”。丢日志不可怕,可怕的是连丢日志这件事都无人知晓。

















