日志先要能回答问题

很多线上慢请求不是没有日志,而是日志只能告诉你:

1、这次失败了

2、总耗时很高

3、线程池里有异常

但当你继续追问“慢在哪一段”“是不是同一条请求”“异步线程里到底发生了什么”时,日志就答不上来了。

所以可观测性的第一步不是上多复杂的平台,而是先让日志结构能回答问题。

traceId 先解决串链

如果一次请求里会经过主线程、异步线程、下游调用,没有统一的 trace 标识,最后看到的只是一堆散日志。

比较实用的做法还是在入口统一生成或透传 traceId,异步线程里显式放回 MDC:

1
2
3
4
5
6
7
8
9
String traceId = MDC.get("traceId");
executor.execute(() -> {
MDC.put("traceId", traceId);
try {
handleTask();
} finally {
MDC.clear();
}
});

这里最容易漏的是 finally 里的清理。不清理时,线程池复用线程后很容易把上一次请求的上下文带到下一次任务里。

总耗时不够,要拆分段耗时

如果日志最后只有一条:

1
request finished, cost=1280ms

那你根本不知道问题出在:

1、配置加载

2、数据库查询

3、远程调用

4、规则执行

5、异步结果收口

更有用的做法是分段记耗时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
long start = System.currentTimeMillis();

loadConfig();
long afterConfig = System.currentTimeMillis();

queryData();
long afterQuery = System.currentTimeMillis();

invokeRemote();
long afterRemote = System.currentTimeMillis();

log.info("traceId={}, configCost={}, queryCost={}, remoteCost={}, totalCost={}",
traceId,
afterConfig - start,
afterQuery - afterConfig,
afterRemote - afterQuery,
afterRemote - start);

这种日志不用一开始拆得很细,先把数据库、远程调用、规则执行这几段拆出来,通常就够第一轮定位了。

异步链路要补上下文

异步线程里最常见的问题其实不是异常,而是“异常没有上下文”。

最典型的表现是:

1、主线程有 traceId

2、子线程里只有异常堆栈

3、日志里没有业务主键、批次号、任务类型

这时就算知道线程池里报错了,也很难找到到底是哪一笔业务。

所以异步任务我现在至少会补:

1、traceId

2、业务单号或主键

3、批次号

4、任务类型

5、线程池名

定时任务先做无侵入监控

这类任务通常有两个特点:

1、入口固定

2、关键方法有限

如果每个方法都手工埋点,老代码会越来越难收,后面排查的人也不一定敢继续加。

这类场景我更愿意先用无侵入方式观察方法耗时:

1
2
includeClassKeywords=CampaignResultNoticeTask|AbstractNcNoticeTask
includeMethods=notice|listCampaign|listCampaignUser|prepareContext|sendNcNotification|afterProcess

这种做法的核心价值不是替代日志,而是先把范围收住,确认到底是入口慢、某个查询慢,还是某个通知方法慢。

无侵入监控要输出什么

如果只是打印一句“方法开始了、方法结束了”,价值其实很有限。

我更看重的是输出里至少要带:

1、类名和方法名

2、耗时

3、入参里的关键业务字段

4、是否异常退出

5、必要时的返回结果摘要

这样才能把方法慢继续落到具体任务和具体批次,不然最后还是只能回到整段日志里翻。

慢链路排查顺序

1、先确认同一请求能不能靠 traceId 串起来

2、再看总耗时是不是已经拆成分段耗时

3、再看异步线程里有没有透传上下文

4、最后再补定时任务或批处理任务的无侵入方法耗时

这个顺序的重点是先把定位链路补起来,而不是先上平台、先堆图表。

小结

链路追踪的关键,不是日志多,而是日志能不能解释问题。

traceId 负责把一条链路串起来,分段耗时负责把慢点拆开,无侵入监控负责低成本观察老任务方法。把这三层补齐以后,很多线上排查第一次看日志就能先把范围收住。