你的日志可能吃掉了 40% 的一个 CPU 核

你的日志可能吃掉了 40% 的一个 CPU 核

一个 10 万 QPS 的服务,每个请求打 5 条日志。实测这 50 万条/秒的日志要花多少 CPU:

erlang 复制代码
写法                           ns/条      10 万 QPS 时      占一个核的
------------------------------------------------------------------
Printf + %v (走 String())      802.1     0.401 秒/秒        40.1%
Printf + 显式字段               230.9     0.115 秒/秒        11.5%
KV (interface{} 传参)          479.1     0.240 秒/秒        24.0%
Fast (零分配链式)               110.1     0.055 秒/秒         5.5%

最差的写法和最好的写法差了 7.3 倍------从 40% 的一个核,到 5.5%。

单条日志的成本拆解

输出到 /dev/null(排除磁盘影响),只看格式化本身:

scss 复制代码
写法                                        ns/op    B/op   allocs/op
----------------------------------------------------------------------
Printf("order %d user %s amount %d")        430.3      32      3
Printf("%v", order)   走 String()            997.5     168      8
KV(msg, "id", id, "user", user)             819.0     216     10
Fast(msg).Int().Str().Int().Done()          282.2       0      0

成本的来源非常清楚------看 allocs/op 这一列:

① 可变参数要装箱。 Printf(format, args ...any) 里每个参数都要塞进一个 interface{}。int64 和 string 装箱都要堆分配。

② %v 会走反射或 String() 方法。 997 ns、8 次分配------因为 Order.String() 内部又是一次 Sprintf,而且 Items []string 要再格式化一遍。

③ 格式串要在运行时解析。 %d、%s 这些占位符每次都要重新扫一遍。

④ 结果字符串要分配。

而零分配版本做的事情是:往一个复用的 []byte 上 append ,类型在编译期就确定了(.Int() / .Str() 分开),不经过 interface{},不解析格式串。

go

scss 复制代码
lg.Fast(INFO, "order placed").
   Int("id", o.ID).
   Str("user", o.User).
   Int("amount", o.Amount).
   Done()

这就是 zap / zerolog 的 API 为什么长这样。 它看起来啰嗦,但那个啰嗦正是性能的来源------每个字段的类型都是静态已知的。

被过滤掉的 DEBUG 日志,不一定免费

scss 复制代码
写法                                          ns/op    allocs/op
----------------------------------------------------------------
Printf(DEBUG, "%d %s", id, user)               40.1        2
Printf(DEBUG, "%v", order)                      2.1        0
Printf(DEBUG, order.String())                 594.4        9    ← 坑
if Enabled(DEBUG) { ... }                       1.9        0
Fast(DEBUG, ...) 链式在 nil 上短路                5.0        0

第三行是真正的陷阱:594 ns,是第四行的 313 倍。

go

c 复制代码
log.Debug("order " + order.String())        // ❌ String() 一定会执行
log.Debug("order %v", order)                // ✅ 只在真要输出时才格式化

函数参数在调用前就求值了。 日志级别的判断发生在函数内部 ------等它判断出「这条不该输出」的时候,order.String() 早就跑完了。

这是个非常隐蔽的坑:你在生产环境把级别设成 INFO,以为所有 DEBUG 日志都是零成本,实际上只要参数里有函数调用或字符串拼接,这部分开销一分不少。

顺带解释第一行和第二行的差异:"%d %s", o.ID, o.User 要把 int64 和 string 各装进一个 interface{}------2 次堆分配 ;而 "%v", o 里 o 本身就是指针,装箱时指针直接当数据存,不需要分配。

所以「被过滤的日志」的成本,取决于你传了什么参数。

写到哪里,差 7 倍

javascript 复制代码
输出目标                        ns/op
-----------------------------------------
/dev/null                     268.9
直接写文件 (每条一次 write)      525.1
64KB 缓冲写                     72.7     ← 最快

带缓冲写文件,比写 /dev/null 还快 3.7 倍。

因为 /dev/null 虽然不落盘,但每条日志依然是一次 write 系统调用。而 64KB 缓冲把几百条日志攒成一次系统调用------系统调用的固定开销被摊薄了几百倍。

这和之前那篇讲系统调用的文章是同一个结论:打扰内核的次数,比传输的数据量重要得多。

代价是崩溃时会丢失缓冲区里的日志。所以:

  • ERROR 级别立即 flush(它们最可能和崩溃相关)
  • INFO/DEBUG 走缓冲 + 定时 flush(比如每 100 ms)
  • 关键路径用异步 writer,让业务线程只管往 channel 里塞

实践清单

1. 用结构化日志库的强类型 API。 zap 的 logger.Info("msg", zap.Int64("id", id))、zerolog 的 log.Info().Int64("id", id).Msg("msg")。别用 Sugar() / Printf 风格------那正是 interface{} 装箱的来源。

2. DEBUG 日志的参数里不要有计算。 要么用延迟求值(传闭包),要么显式判断:

go

c 复制代码
if log.Enabled(DEBUG) {
    log.Debug("state: " + expensiveDump(x))
}

3. 输出一定要带缓冲。 7 倍,而且几乎没有代价。

4. 高频日志要采样。 一个每秒打 10 万条的 WARN,前 100 条和后 99900 条提供的信息量完全一样。zap 自带 Sampler。

5. 别在日志里 %v 一个大对象。 997 ns 一条,而且输出的东西通常没人看。要打就打关键字段。

6. 定期检查日志量本身。 日志的下游成本(采集 agent 的 CPU、网络带宽、ES 的存储和索引)往往是写日志本身的几倍到几十倍。

最后

日志是唯一一种在系统正常时持续消耗资源、只在出事时才产生价值的东西。

正因如此它很容易被忽视------压测时大家关掉日志跑出漂亮数字,上线后日志全开,然后困惑为什么性能对不上。

而这次实测里,仅仅是把 Printf("%v", obj) 换成强类型字段,就省下了 35% 的一个 CPU 核。

那是实打实的机器钱。

相关推荐
志尊宝2 小时前
AI原生前端开发(018):端侧AI性能优化:模型加载、推理加速
人工智能·性能优化·ai-native
打工仔折腾 AI4 小时前
Linux 网络编程:从零掌握 TCP Socket 编程与 C++ 实战落地
linux·网络·人工智能·后端·python·tcp/ip·性能优化
打工仔折腾 AI5 小时前
深入验证 DeepSeek 新模型开源声明:KV 缓存指标与按次计费的后端落地评估
缓存·性能优化·开源·ai agent 实战
隔窗听雨眠10 小时前
开源为骨自研为魂:存储统一管理平台的混合架构实践
性能优化·实战教程·原理解析·踩坑记录
科威网络18 小时前
企业网站技术方案:响应式、SEO 结构与性能优化实践
性能优化·需求分析·软件需求
爱喝水的鱼丶18 小时前
SAP-ABAP:ECC→S/4HANA ABAP 迁移升级指南——程序改造、语法兼容与性能优化 导语
运维·开发语言·性能优化·sap·abap·经验交流·开发交流
传奇开心果编程1 天前
【Compose Multiplatform跨端声明式UI实用技术】第17课:性能优化与基准测试
性能优化·移动开发·跨平台·编程语言理论·开发范式·编程框架·声明式ui
科威网络1 天前
APP 技术选型:原生、React Native、Flutter 怎么选?
性能优化·软件需求·web app
硅基手札1 天前
SPI技术系列 12 · SPI 设备驱动实战
linux·驱动开发·单片机·嵌入式硬件·mcu·性能优化·系统架构