三、链路追踪原理归纳总结
链路追踪这件事,说白了就是把一次请求或一个操作,从起点到终点走过的所有路径串起来。我刚开始做 ANR 监控时,以为只要抓个堆栈就够了,结果查了好几个问题都查不到根因——后来才意识到,没有链路,你只看到症状,看不到传播路径。
3.1 从一次请求到一条链路
核心概念就三个:Trace、Span、SpanContext。
- Trace:一次完整的事件流。比如一次用户点击 -> 主线程 dispatch -> 子线程 IO -> 加锁等待。整条线就是一个 Trace。
- Span:Trace 里的一个独立工作单元。每个 Span 有开始时间、结束时间、操作名称、标签、日志。
- SpanContext:身份令牌。里面装着 traceId、spanId、采样标记。靠它才能把上下游 Span 串起来。
我习惯把 Span 想象成乐高积木,Trace 就是搭好的模型。每一块积木(Span)都知道自己连接着谁(parentSpanId)。
3.2 Span 的传播与上下文传递
链路信息要怎么传给下一个 Span?靠的是 传播机制。常见的有两种格式:
| 协议 | 传播载体 | 示例 |
|---|---|---|
| W3C Trace Context | HTTP 头:traceparent / tracestate | 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01 |
| B3 | HTTP 头:X-B3-TraceId / X-B3-SpanId / X-B3-Sampled | X-B3-TraceId: 0af7651916cd43dd8448eb211c80319c |
| Zipkin 多标头 | 多个 HTTP 头 | 与 B3 类似但分拆更细 |
在 Android 端,我们没有 HTTP 头,那就用 ThreadLocal 或者 协程上下文 传递。我曾经踩过一个坑:子线程里没法拿到主线程的 TraceId,因为 ThreadLocal 不跨线程传递。后来改用 TraceContext 容器,手动在创建线程时注入一次。
3.3 采样策略与性能取舍
全量采集所有 Span?你觉得可能吗?一次 ANR 可能产生成百上千个 Span,全采集的话手机都得卡死。所以采样是必须的。
常用四种策略:
- 固定采样率:比如 10% 的请求录。简单粗暴,但低流量场景容易漏掉关键 ANR。
- 动态采样:根据错误率、延迟等条件动态调整。比如 CPU 飙高时提高采样率。我在项目中实现过,效果不错,但逻辑复杂。
- 头采样:在 Trace 开头就决定采样。简单,但万一后面出错了呢?
- 尾采样:Trace 全部跑完再决定是否保留。能精准保留错误 Trace,但需要缓冲存储。我比较推荐这个,配合一个滑动窗口的 10% 预算。
核心原则: 采样策略的触发点要放在 Span 创建前,不要在 Span 结束后再检查。否则浪费 CPU 不说,还可能因为来不及丢弃而导致内存泄漏。
3.4 关键埋点时机
链路数据要想有用,得埋在正确的点上。对于 ANR 追踪,我总结出四个必须埋的点:
- 输入事件分发:
dispatchInputEvent开始和结束。差值就是响应延迟。 - 广播接收:
BroadcastReceiver.onReceive入口和出口。注意要在主线程。 - 服务生命周期:
Service.onCreate/onStartCommand/onDestroy。特别是 Intent 处理时间。 - 锁等待:
synchronized或ReentrantLock的 acquire 和 release。这个在 ANR 里特别关键。
代码示例(简化版):
void dispatchInputEvent(InputEvent event) {
// 创建 Span
Span span = tracer.buildSpan("inputEvent")
.withTag("eventType", event.getType())
.start();
try (Scope scope = tracer.activateSpan(span)) {
// 实际分发逻辑
handleEvent(event);
} finally {
span.finish(); // 结束 Span
}
}
3.5 链路还原与根因定位
所有 Span 上报到后端后,后端会按 traceId 和 parentSpanId 构建一棵 Span 树。你想想看,一棵树的根 Span 就是主线程的起始事件,下面挂着的子 Span 是每次调用、每次锁等待。哪个子 Span 花了很长时间,直接在树上标红就可以。
我遇到过一个问题:有一个 ANR 在树里显示成两棵独立的树,后来发现是因为我们内部把 traceId 重新生了,导致父子链断裂。所以一定要确保 traceId 在整个 Trace 生命周期内是同一个。
Intent 里跨进程传递,结果遇到 PendingIntent 时 traceId 被覆盖了。后来改成了用 ActivityManager 的额外 Bundle 来传。嗯,这地方容易忽略。
下面这张图概括了整个链路追踪的工作流程:
嗯,以上就是链路追踪的核心原理。记住:Trace 定边界,Span 记录单元,上下文负责传递。有了这三样,再结合合适的采样策略,你就能在 ANR 爆发时迅速找到是哪条路径堵住了。我推荐团队在项目中先实现一个轻量级的手动 Span 植入,跑通后再上自动 AOP 埋点。