链路追踪、耗时拆分与无侵入监控实践
日志先要能回答问题
很多线上慢请求不是没有日志,而是日志只能告诉你:
1、这次失败了
2、总耗时很高
3、线程池里有异常
但当你继续追问“慢在哪一段”“是不是同一条请求”“异步线程里到底发生了什么”时,日志就答不上来了。
所以可观测性的第一步不是上多复杂的平台,而是先让日志结构能回答问题。
traceId 先解决串链
如果一次请求里会经过主线程、异步线程、下游调用,没有统一的 trace 标识,最后看到的只是一堆散日志。
比较实用的做法还是在入口统一生成或透传 traceId,异步线程里显式放回 MDC:
1 | String traceId = MDC.get("traceId"); |
这里最容易漏的是 finally 里的清理。不清理时,线程池复用线程后很容易把上一次请求的上下文带到下一次任务里。
总耗时不够,要拆分段耗时
如果日志最后只有一条:
1 | request finished, cost=1280ms |
那你根本不知道问题出在:
1、配置加载
2、数据库查询
3、远程调用
4、规则执行
5、异步结果收口
更有用的做法是分段记耗时:
1 | long start = System.currentTimeMillis(); |
这种日志不用一开始拆得很细,先把数据库、远程调用、规则执行这几段拆出来,通常就够第一轮定位了。
异步链路要补上下文
异步线程里最常见的问题其实不是异常,而是“异常没有上下文”。
最典型的表现是:
1、主线程有 traceId
2、子线程里只有异常堆栈
3、日志里没有业务主键、批次号、任务类型
这时就算知道线程池里报错了,也很难找到到底是哪一笔业务。
所以异步任务我现在至少会补:
1、traceId
2、业务单号或主键
3、批次号
4、任务类型
5、线程池名
定时任务先做无侵入监控
这类任务通常有两个特点:
1、入口固定
2、关键方法有限
如果每个方法都手工埋点,老代码会越来越难收,后面排查的人也不一定敢继续加。
这类场景我更愿意先用无侵入方式观察方法耗时:
1 | includeClassKeywords=CampaignResultNoticeTask|AbstractNcNoticeTask |
这种做法的核心价值不是替代日志,而是先把范围收住,确认到底是入口慢、某个查询慢,还是某个通知方法慢。
无侵入监控要输出什么
如果只是打印一句“方法开始了、方法结束了”,价值其实很有限。
我更看重的是输出里至少要带:
1、类名和方法名
2、耗时
3、入参里的关键业务字段
4、是否异常退出
5、必要时的返回结果摘要
这样才能把方法慢继续落到具体任务和具体批次,不然最后还是只能回到整段日志里翻。
慢链路排查顺序
1、先确认同一请求能不能靠 traceId 串起来
2、再看总耗时是不是已经拆成分段耗时
3、再看异步线程里有没有透传上下文
4、最后再补定时任务或批处理任务的无侵入方法耗时
这个顺序的重点是先把定位链路补起来,而不是先上平台、先堆图表。
小结
链路追踪的关键,不是日志多,而是日志能不能解释问题。
traceId 负责把一条链路串起来,分段耗时负责把慢点拆开,无侵入监控负责低成本观察老任务方法。把这三层补齐以后,很多线上排查第一次看日志就能先把范围收住。



