Go错误日志采样不能只靠log.Printf,因其同步、无缓冲、无上下文、无等级区分,高并发下易拖垮系统;需用zerolog+定制Sampler按错误指纹聚合采样,并在业务层实现计数与告警。

Go 错误日志采样为什么不能只靠 log.Printf
因为 log.Printf 是同步写、无缓冲、无上下文、不区分错误等级的“裸输出”,高并发下直接打满磁盘 I/O 或阻塞 goroutine。真实服务里一个 panic 可能每秒触发上千次,全量记录既没意义又拖垮系统。
- 采样不是“随机丢日志”,而是对相同错误模式(如相同
err.Error()+ 相近堆栈前缀)做频次聚合后,按阈值控制输出节奏 - 直接用
fmt.Sprintf拼接错误信息再传给log,会丢失原始error类型,无法做类型判断或提取码(如errors.Is(err, io.EOF)) - 标准库
log不支持字段结构化,后续没法按service、trace_id过滤或告警
用 zerolog + Sampler 实现带上下文的错误采样
zerolog 是 Go 生态中少数原生支持采样策略的日志库,关键在它把采样逻辑下沉到 Sampler 接口,而不是简单计数器。
- 不要用
zerolog.New(os.Stderr).Sample(&zerolog.BasicSampler{N: 100})—— 这是对所有日志统一采样,错误和 info 混在一起,漏掉关键异常 - 正确做法是:只对
LevelError日志启用采样,并绑定错误指纹(例如fmt.Sprintf("%s|%s", err.Error(), strings.TrimSuffix(string(debug.Stack()), "\n")[:64])) - 示例中
sampler := &zerolog.BurstSampler{Burst: 5, Period: time.Second}更实用:允许突发 5 条相同错误,之后静默 1 秒,比固定 N 更抗毛刺
logger := zerolog.New(os.Stderr).
Level(zerolog.DebugLevel).
Sample(&zerolog.BurstSampler{Burst: 5, Period: time.Second}).
With().Timestamp().Logger()
<p>// 记录错误时显式加 error 字段
logger.Err(err).Str("op", "db_query").Int64("user_id", uid).Send()聚合错误需绕过日志库,自己维护内存状态
日志库本身不负责聚合,它只负责输出;聚合必须在业务层完成——否则你永远不知道“同一个错误已发生 237 次”,只能看到 237 行重复日志。
- 用
sync.Map存错误指纹 → 计数 + 首次时间 + 最近一次堆栈(避免每次 dump 全栈) - 指纹生成别用完整堆栈:
runtime.Caller(1)获取文件/行号 +err.Error()即可,太长的字符串影响 map 查找性能 - 定时(比如每 30 秒)遍历
sync.Map,把计数 > 10 的错误推送到告警通道,并清空计数;注意遍历时用LoadAndDelete避免漏统计 - 别把聚合状态存在全局变量里——多实例部署时各进程独立计数,聚合失效
采样率调太高或太低都会掩盖问题
设成 1% 看似安全,但若某次部署引入了新 panic,它可能只在 1% 请求里触发,而你的采样又恰好没捕获到那 1%,就等于完全没观测到。
立即学习“go语言免费学习笔记(深入)”;
- 对
panic和http.Status5xx类错误,采样率建议设为 100%(即不采样),它们本就不该高频出现 - 对
io.Timeout或context.DeadlineExceeded这类预期中的错误,可设BurstSampler{Burst: 3, Period: 5 * time.Second},既防刷屏又保可观测性 - 上线前用
go test -bench=.测下采样逻辑本身开销:如果单次采样判断耗时 > 500ns,说明用了正则或反射,得换哈希方式
真正难的是定义“相同错误”——err.Error() 相同但堆栈不同,算不算?用户 ID 不同但路径相同,要不要合并?这些边界没银弹,得贴着你的监控告警链路去对齐。










