链路追踪 的十种打开方式:工程实践笔记
前阵子线上一个下单接口偶发超时,用户把报错截图甩过来,我翻了半个多小时日志,订单服务一段、库存服务一段、支付服务一段,各写各的,中间是怎么串起来的全靠猜。最后定位到是库存服务里一条慢 SQL,但它跟下单接口隔了两层调用,日志里死活看不出关系。痛定思痛,花了一个多星期把 OpenTelemetry 接上了,顺手把踩过的坑记下来,凑成十种打开方式,权当工程笔记.
一、先把 trace_id 吐给前端
报障的人手里得有个能搜的东西,不然你让他从哪儿开始。Spring Boot 3 的话加个 filter,把 traceId 塞进响应头:
@Bean
public FilterRegistrationBean<Filter> traceIdFilter(Tracer tracer) {
FilterRegistrationBean<Filter> bean = new FilterRegistrationBean<>();
bean.setFilter((request, response, chain) -> {
Span span = tracer.currentSpan();
if (span != null) {
((HttpServletResponse) response).setHeader("X-Trace-Id", span.context().traceId());
}
chain.doFilter(request, response);
});
bean.addUrlPatterns("/*");
return bean;
}
后来用户第二次报障,直接把 F12 里看到的 X-Trace-Id 发了过来,贴进 Grafana 一搜,整条链路啪一下就出来了,这感觉是真的爽。
二、日志里必须带 trace_id
不然链路是链路,日志是日志,两边对不上等于白干。logback 的 pattern 里加一下 MDC:
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level [trace_id=%X{traceId} span_id=%X{spanId}] %logger{36} - %msg%n</pattern>
注意 micrometer-tracing 默认往 MDC 里放的 key 是 traceId,网上不少老文章写的是 %X{trace_id},抄过去会发现永远是空的,我就这么浪费了二十分钟...
三、跨服务调用,header 别弄丢
Spring 容器管的 RestTemplate、WebClient 会自动透传 W3C 的 header,但凡是自己在工具类里 new 出来的 client,一律断链。有次查断链,最后发现是某位同事在工具类里自己 new 了个 RestTemplate... 办法没什么花哨的,统一走容器里的 bean,或者手动加 interceptor。判断方法也简单:接口 200 正常但下游日志对不上号,八成就是这个。
四、异步线程池是第二大断链大户
@Async 和自建线程池,上下文不会自己跟过去,图上看就是一段一段的孤儿链路。加个 TaskDecorator 完事:
@Bean
public TaskDecorator taskDecorator() {
return new ContextPropagatingTaskDecorator();
}
然后 executor.setTaskDecorator(taskDecorator()),别只加一半。
五、消息队列也要传
MQ 是断链重灾区。spring-kafka 3.x 把 observation 打开就行:
spring:
kafka:
template:
observation-enabled: true # <--- 默认是关的,不打开 producer 不带 header
自己手写原生 client 的话,就把 traceparent 塞进 record header,消费端 extract 出来再开 span。之前 MQ 消费那一截在图上永远是断的,查了半天,发现消费者是隔壁组写的,压根没接,这就不是技术问题了... 配完过后,不出意外的话,图上就能看到完整的一条链了。
六、采样:全采和全丢都不行
一开始图省事,head sampling 直接 100%:
management:
tracing:
sampling:
probability: 1.0
三天后存储告警,场面一度十分尴尬。改成 1%,正常流量是省了,但偶发超时十次里九次采不到,等于没接。结论:正常请求低比例采,错误和慢请求必须全采。head sampling 按结果采不到,所以得上尾采样。
七、尾采样,用 OTel Collector 挂个 processor
collector 我直接 docker 起的,拉不动的可以自己配个加速域名,不过没准儿你用的时候那个域名已经不可用了:
docker run -d --name otel-collector \
-p 4317:4317 -p 4318:4318 \
-v ~/otel-collector.yaml:/etc/otelcol-contrib/config.yaml \
otel/opentelemetry-collector-contrib:0.96.0
配置里挂 tail_sampling:
processors:
tail_sampling:
decision_wait: 30s
policies:
- name: errors
type: status_code
status_code: {status_codes: [ERROR]}
- name: slow
type: latency
latency: {threshold_ms: 1000}
- name: normal
type: probabilistic
probabilistic: {sampling_percentage: 5}
decision_wait 别设太短,跨服务的链路没到齐就做决定,切出来一看只有半截,还以为是 bug.
八、span 属性别乱塞
user_id、订单号这种高基数的值别往 span attribute 里塞,塞了存储直接爆炸。之前有人把整个请求 body 塞进去"方便排查",一周后 Tempo 磁盘满,写入直接拒绝。真要用 tag 查,先想清楚会不会拿它做查询,不会就别放。顺带一提,手机号这类敏感字段记得先脱敏,别问我为什么专门强调这条.
九、存储和保留期提前想好
Tempo 后端我挂的 minio,保留期先压到 72 小时,真要长期留的再单独下沉:
compactor:
compaction:
block_retention: 72h
别拿本地盘硬扛,磁盘打满 Tempo 会拒绝写入,然后 ingress 一片报错,告警响一晚上,血泪教训。
十、告警和日志里直接挂链接
最后一步,让链路自己找上门。Grafana 的 Loki 数据源配个 derived field,把日志里的 trace_id 解析成 Tempo 的跳转链接。效果就是值班群里机器人贴出报错日志,点一下 trace_id 直达链路页,不用再回群里问"这条是哪个服务的"。十个点写完了,全是踩出来的,没什么高深理论,够用就行。下一步打算把入口的 RUM 也接上,接完再来记.
评论
还没有评论。
发表评论
提交后评论将经过自动审核,审核通过后公开展示。