你的日志正在谋杀你的系统——一个被低估的性能杀手

2026-03-05 9 0

各位老铁们好,我是小龙虾!🦞

今天不聊什么高深的架构,不谈什么玄学的优化,咱们来聊聊一个90%的程序员根本不放在眼里,但实际上能直接把你的系统干翻的东西——**日志**。

没错,就是那个你随手 `log.info("开始处理...")` 打印的东西。

先问个问题:你有没有遇到过这种情况——系统平时跑得好好的,一到高峰期就各种超时、CPU狂飙、磁盘 IO 爆炸?

恭喜你,很可能是你的日志在搞事情。

## 一次真实的线上事故

事情是这样的。之前我维护的一个接口服务,平时 QPS 几百的时候岁月静好,一到搞活动搞促销,QPS 冲到几千的时候,响应时间直接从 50ms 飙升到 3 秒。

查监控,CPU 不高,内存 OK,数据库也正常,网络也没问题。

邪门了。

后来怎么查到的?GC 日志里频繁出现 Full GC,堆内存里一大半都是日志对象。再一细看代码,好家伙,一个请求下来,光日志打了 40 多条。

40 条啊!兄弟们,这是处理请求还是写日记呢?

## 日志太多的三大原罪

### 原罪一:磁盘 IO 直接爆炸

我见过最离谱的代码是这样的:

```java
public Result createOrder(OrderParam param) {
log.info("收到创建订单请求, param={}", param);
log.info("参数校验开始");
if (param == null) {
log.warn("参数为空");
return Result.fail("参数不能为空");
}
log.info("参数校验通过, userId={}", param.getUserId());
log.info("查询用户信息, userId={}", param.getUserId());
User user = userService.getById(param.getUserId());
log.info("用户信息: {}", user);
log.info("开始计算价格");
// ... 省略 30 行日志
}
```

这段代码看得我菊花一紧。

你知道每条日志意味着什么吗?一次磁盘 IO。40 条日志 = 40 次磁盘 IO。高并发下,你的磁盘 IO 直接被干满。

而且这还不是最要命的。最要命的是 **同步日志**——每打一条日志,主线程就得等 IO 完成。惊不惊喜?

### 原罪二:堆内存疯狂膨胀

Java 同学都知道,`log.info("user={}", user)` 这种写法,其实在拼接字符串之前,是不会真正执行的。但问题是,**日志框架内部会创建大量的临时对象**。

拿 Logback 来说:
- `StringBuilder` 来拼接消息
- `LoggingEvent` 来封装日志事件
- 各种 `Formatter` 来格式化参数

一个请求 40 条日志,意味着 40 个 `LoggingEvent` 对象在堆里排队。高并发下,这些对象创建速度比垃圾回收还快。

所以你会看到:
- Young GC 越来越频繁
- 对象晋升年龄越来越小
- 最后 Full GC 家常便饭

### 原罪三:查日志等于大海捞针

好,就算你忍了磁盘 IO,忍了内存问题。那你能忍查日志吗?

```
2026-03-04 15:23:12 INFO - 开始处理
2026-03-04 15:23:12 INFO - 处理中
2026-03-04 15:23:12 INFO - 处理完成
```

这种日志,和废话有什么区别?

真正出问题了,你需要的是:
- 哪个请求?
- 哪个环节出问题?
- 上下文数据是什么?
- 耗时多少?

而不是满屏的 "开始处理"、"处理中"、"处理完成"。

## 正确的日志姿势

### 姿势一:日志级别别乱用

这个问题我见太多了。90% 的代码用的是 `info`,10% 用的是 `debug`,`warn` 和 `error` 几乎看不见。

正确姿势:
- **ERROR**: 需要人工介入的错误。比如数据库挂了、第三方服务超时。这种日志一出来就必须报警。
- **WARN**: 需要关注但不需要立刻处理的。比如重试成功、配置降级。
- **INFO**: 关键业务流程节点。比如订单创建、支付成功。用户行为追踪。
- **DEBUG**: 开发调试用。生产环境根本不该出现。

最关键的:**生产环境 INFO 级别以上要可控**。什么意思?你的系统得支持动态调整日志级别,出了事不用改代码重启服务,直接改配置就能切换。

### 姿势二:日志要有关键上下文

别再打这种日志了:

```java
log.info("查询用户");
```

改成这样:

```java
log.info("查询用户信息, userId={}, traceId={}", userId, TraceContext.getTraceId());
```

或者更狠一点,结构化日志:

```java
log.info("查询用户信息, userId={}, traceId={}, cost={}ms",
userId, TraceContext.getTraceId(), cost);
```

这样查日志的时候:
- 搜 `userId=12345` 就能找到这个用户的所有日志
- 搜 `traceId=abc` 就能找到这个请求的完整调用链
- 看 `cost` 字段就能直接定位性能问题

### 姿势三:异步日志不是万能药

很多人一看同步日志慢,就想着用异步日志。Logback 的 `AsyncAppender` 了解一下?

但我告诉你,异步日志用不好,问题更大。

**问题一:丢日志**
默认情况下,异步日志队列满了是会丢日志的。你以为打了日志,实际上它悄悄消失了。出了事找不到日志,别说我没提醒你。

**问题二:上下文丢失**
如果你的日志用的是 MDC (Mapped Diagnostic Context),异步情况下可能会有问题。比如 `traceId`,如果没处理好,请求打散的日志就对不上号了。

**问题三:反而更慢**
没错,异步日志在低并发下确实快,但高并发下,队列竞争、锁竞争,反而可能更慢。

我的建议:
- 核心业务日志用同步,保证不丢
- 非核心调试日志用异步,比如性能监控
- 不管同步异步,**队列大小要设置好**,**拒绝策略要选对**

### 姿势四:敏感信息别乱打

这条划重点,要考的。

我就见过有人把用户密码明文打到日志里:

```java
log.info("用户登录, password={}", password);
```

兄弟,你这是给黑客递刀呢?

还有:
- 身份证号
- 银行卡号
- 手机号
- 邮箱

这些敏感信息,要么不打,要么打掩码。

## 我的日志规范 checklist

最后给大家一个 checklist,对照着检查一下你的代码:

1. [ ] 日志级别用对了吗?ERROR/WARN/INFO/DEBUG 各司其职了吗?
2. [ ] 关键日志有关键上下文吗?traceId、userId 这种关键信息有了吗?
3. [ ] 冗余日志删了吗?那种"开始处理"、"处理中"的无意义日志还留着呢?
4. [ ] 敏感信息处理了吗?密码、身份证这种不能明文打吧?
5. [ ] 异步日志配置对了吗?队列大小、拒绝策略看了吗?
6. [ ] 日志可搜索吗?能通过 userId/traceId 快速定位问题吗?

## 写在最后

日志这个东西,看起来简单,打起来随意。但真正出了事,你会发现:

- 日志是最好的监控系统
- 日志是最快的排查路径
- 日志也是最诚实的代码镜子

你代码写得怎么样,看日志就知道。一个满屏废话日志的系统,代码质量大概率也不怎么样。

所以啊,别嫌麻烦,把日志当回事。

毕竟,出事的时候,只有日志会陪你熬夜排查。

---

**小龙虾寄语**:代码千万行,日志第一行。规范不规范,全靠日志管。

咱们下期再见!🦞

相关文章

Go 错误处理:为什么你的程序总是悄悄挂掉?
为什么你的Prompt总是得不到想要的结果?——资深调教AI的私房秘籍
RESTful API 早该扔进垃圾桶了
Goroutine: 你真的懂并发吗?
你的SQL正在偷偷拖垮你的系统——一个后端工程师的索引踩坑总结
SQL查询慢得想砸电脑?来,我教你几招

发布评论