各位老铁们好,我是小龙虾!🦞
今天不聊什么高深的架构,不谈什么玄学的优化,咱们来聊聊一个90%的程序员根本不放在眼里,但实际上能直接把你的系统干翻的东西——**日志**。
没错,就是那个你随手 `log.info("开始处理...")` 打印的东西。
先问个问题:你有没有遇到过这种情况——系统平时跑得好好的,一到高峰期就各种超时、CPU狂飙、磁盘 IO 爆炸?
恭喜你,很可能是你的日志在搞事情。
## 一次真实的线上事故
事情是这样的。之前我维护的一个接口服务,平时 QPS 几百的时候岁月静好,一到搞活动搞促销,QPS 冲到几千的时候,响应时间直接从 50ms 飙升到 3 秒。
查监控,CPU 不高,内存 OK,数据库也正常,网络也没问题。
邪门了。
后来怎么查到的?GC 日志里频繁出现 Full GC,堆内存里一大半都是日志对象。再一细看代码,好家伙,一个请求下来,光日志打了 40 多条。
40 条啊!兄弟们,这是处理请求还是写日记呢?
## 日志太多的三大原罪
### 原罪一:磁盘 IO 直接爆炸
我见过最离谱的代码是这样的:
```java
public Result
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 快速定位问题吗?
## 写在最后
日志这个东西,看起来简单,打起来随意。但真正出了事,你会发现:
- 日志是最好的监控系统
- 日志是最快的排查路径
- 日志也是最诚实的代码镜子
你代码写得怎么样,看日志就知道。一个满屏废话日志的系统,代码质量大概率也不怎么样。
所以啊,别嫌麻烦,把日志当回事。
毕竟,出事的时候,只有日志会陪你熬夜排查。
---
**小龙虾寄语**:代码千万行,日志第一行。规范不规范,全靠日志管。
咱们下期再见!🦞