采样器与高性能写入路径
日志系统最怕的不是慢,而是被高频日志拖垮。zap 用两个组件解决这个问题:sampler 控制数量,bufferedWriteSyncer 控制写入频率。这一篇是系列的收尾,也是性能路径的总结。
sampler:计数采样
gozapcore/sampler.go(简化)
type sampler struct {
core Core
// 每个级别的计数数组(固定大小,避免哈希)
counts [maxInteger]uint64
ticks uint64
tick uint64
first uint64
rand *rand.Rand
// ...
}
采样算法要点:
- 按消息计数:每条日志按「级别 + 消息内容」哈希到固定槽位
- 时间窗口:每个窗口(默认 1 秒)内同一消息最多输出
N条(默认 100) - 突发放行:窗口内前
N条全部放行,之后概率放行少量样本,并输出Dropped计数
text采样后的输出
{"level":"warn","msg":"slow query","count":100}
{"level":"warn","msg":"slow query dropped","count":3420}
Dropped 条目让「被丢弃的日志」也可被查询到——这是采样设计里最容易忽略、却最见功底的一点。
⚠️ 注意: 采样是有损的。审计类日志、计费日志永远不该走采样路径,应单独使用不采样的 Core。
高频路径的三级缓存
zap 对「高频写」的优化是分层级的:
| 层级 | 机制 | 解决的问题 |
|---|---|---|
| 编码层 | 字节级追加 + 类型分派 | CPU 开销 |
| 写入层 | bufferedWriteSyncer |
系统调用次数 |
| 同步层 | Sync() 合并 |
刷盘频率 |
bufferedWriteSyncer:攒一批再写
gozapcore/write_syncer.go(简化)
type bufferedWriteSyncer struct {
ws WriteSyncer
size int
clock Clock
mutex sync.Mutex
stopc chan struct{}
// ...
}
核心机制:
- 日志先写入内存缓冲(默认 256KB)
- 缓冲满或到达刷新间隔(默认 5 秒)时,一次性
Write到底层 - 通过 channel 通知后台刷新协程,避免主路径阻塞
代价是延迟:日志可能最多滞后一个刷新周期才落盘。崩溃时未刷新的日志会丢失——这也是为什么 zap 提供 Sync() 让应用在退出前主动刷盘。
完整写入路径
Logger.Info(...)
→ core.Check() // 级别过滤 + 采样,快速失败
→ core.Write() // encoder 编码到 buffer
→ WriteSyncer.Write // buffered 合并写入
→ (后台协程) io.Write + Sync
整条路径上没有锁竞争(采样器除外)、没有反射、没有中间对象——这就是 zap 吞吐量高的全部秘密。
小结
回到系列第一篇的问题:zap 凭什么高性能又可扩展?现在答案完整了:
- 架构:双 API + 三层解耦,扩展靠组合
- 编码:强类型 Field + 字节级编码,零反射
- 写入:缓冲合并 + 后台刷新,减少系统调用
- 控制:原子级别 + 采样器,量价可控
如果你在设计自己的日志内核,这套组合拳是经过生产验证的范本。