ZIPKIN链路追踪原理分享

316 阅读5分钟
  1. 链路追踪的基本原理

  • 追踪(trace):Trace 表示一次完整的分布式请求生命周期,它是一个全局上下文,包含了整个调用链所有经过的服务节点和调用路径。例如,用户发起一个请求,从前端服务到后端数据库的多次跨服务调用构成一个 Trace。
  • 跨度(Span):Span 是 Trace 中的一个基本单元,表示一次具体的操作或调用。一个 Trace 由多个 Span 组成,按时间和因果关系连接在一起。Span 内有描述操作的名称 span name、记录操作的开始时间和持续时间、Trace ID、当前 Span ID、父 Span ID(构建调用层级关系)等信息。

原理:为每个操作或调用记录一个跨度,一个请求内的所有跨度共享一个 trace id。通过 trace id,便可重建分布式系统服务间调用的因果关系。链路追踪(Trace)是由若干具有顺序、层级关系的跨度组成一棵追踪树(Trace Tree),核心是在服务调用过程中收集 trace 和 span 信息,并汇总生成追踪树结构。

  • Span之间存在层级关系,形成调用树

  1. 数据采集

目前,追踪系统的主流实现有俩种,具体如下:

  • 基于日志的追踪:直接将 Trace、Span 等信息输出到应用日志中,然后采集所有节点的日志汇聚到一起,再根据全局日志重建完整的调用链拓扑。这种方式的优点是没有网络开销、应用侵入性小、性能影响低;但其缺点是,业务调用与日志归集不是同时完成的,有可能业务调用已经结束,但日志归集不及时,导致追踪失真。

  • 基于服务的追踪:通过某些手段给目标应用注入追踪探针(Probe),然后通过探针收集服务调用信息并发送给链路追踪系统。探针通常被视为一个嵌入目标服务的小型微服务系统,具备服务注册、心跳检测等功能,并使用专用的协议将监控到的调用信息通过独立的 HTTP 或 RPC 请求发送给追踪系统。

    • 以 SkyWalking 的 Java 追踪探针为例,它实现的原理是将需要注入的类文件(追踪逻辑代码)转换成字节码,然后通过拦截器注入到正在运行的应用程序中。比起基于日志实现的追踪,基于服务的追踪在资源消耗和侵入性(但对业务工程师基本无感知)上有所增加,但其精确性和稳定性更高。现在,基于服务的追踪是目前最为常见的实现方式。
  1. zipkin 的实现原理

  1. zipkin 的核心数据模型

  1. Sleuth实现原理分析

原理

Sleuth 会把跟踪数据 (appname、traceId、spanId、exportable) 添加到 MDC ,MDC 的实现实际是将需要记录到日志的信息设置到当前线程的上下文(ThreadContext)

核心机制

  • 请求入口:拦截器注入 Trace 信息
  • Trace 信息注入到 MDC,链路信息写入线程上下文
MDC.put("traceId", traceId);
MDC.put("spanId", spanId);
  • Trace 信息跨线程传播

MDC 是线程本地(ThreadLocal)变量,不能支持线程切换(如异步任务、线程池)。Sleuth 提供了如下能力来支持:

  • RunnableCallableExecutorService 等进行了包装;

  • 使用 TraceRunnableTraceCallable 等将 Trace 信息复制到新线程上下文中;
  • 保证 traceId 在异步任务、线程池中的上下文仍能被正确使用
  • Trace 信息跨服务传播(HTTP Header)

    •   Sleuth 在调用其他服务(如使用 RestTemplate、FeignClient)时,会自动在请求头中注入 trace 信息
  1. 自实现链路追踪坑点错误场景分析

自定义线程池

@Component
public class DemoService {

    @Autowired
    private Tracer tracer;

    ExecutorService executor = Executors.newFixedThreadPool(1); // 未封装

    public void runAsync() {
        for (int i = 0; i < 3; i++) {
            TraceContext traceContext = tracer.currentSpan().context();
            executor.submit(() -> {
                // Sleuth 的 traceId 没有传播过来
                log.info("traceId: {}", tracer.currentSpan() == null ? "null" : t                  racer.currentSpan().context().traceIdString());
            });
        }
    }
}

  错误原因:

ThreadLocal的set/remove的上下文传递模式 在使用线程池等异步执行组件的情况下就失效了

  失效原因

  • ThreadLocal 是通过Thread.threadLocals实现的一个 Map(ThreadLocalMap),在线程池中线程是复用
  • ThreadLocal 没有跨线程传递机制。除非自己封装任务,显式传递上下文。
  • InheritableThreadLocal 是ThreadLocal的一个子类,只能在 新创建的 线程 将父线程的上下文拷贝一份

  解决方案(知识扩展)

  github.com/alibaba/tra…

  使用 TransmittableThreadLocal (TTL)

  • TransmittableThreadLocal 的核心方法

在与captured(抓取)/replay(回放)/restore(恢复)

Demo

执行结果

image.png 使用TransmittableThreadLocalTtlExecutors.getTtlExecutorService(executorService)装饰线程池之后,在每次调用任务的时,都会将当前的主线程的TransmittableThreadLocal数据copy到子线程里面,执行完成后,再清除掉。同时子线程里面的修改回到主线程时其实并没有生效

源码分析

public final class TtlRunnable implements Runnable {
    private final Runnable runnable;
    private final AtomicReference<Object> capturedRef;
    private final boolean releaseTtlValueReferenceAfterRun;

    // 构造时 capture 主线程上下文
    private TtlRunnable(Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
        this.runnable = runnable;
        // capturedRef 存的是主线程中所有 TransmittableThreadLocal 的快照(Map结构)
        this.capturedRef = new AtomicReference<>(TtlTransmittee.capture());
        this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
    }

    public static TtlRunnable get(Runnable runnable) {
        return new TtlRunnable(runnable, true);
    }

    @Override
    public void run() {
        // 1从主线程快照中获取 captured 上下文
        Object captured = capturedRef.get();

        // 防止重复运行
        if (captured == null || (releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null))) {
            throw new IllegalStateException("TTL value reference is released after run!");
        }

        // 2备份当前子线程的 TTL 上下文(可能已有设置)
        Object backup = TtlTransmittee.replay(captured);

        try {
            // 3执行异步任务(此时 TTL 已是主线程上下文)
            runnable.run();
        } finally {
            // 4 恢复原子线程上下文,防止污染线程池
            TtlTransmittee.restore(backup);
        }
    }
}

capture :捕获主线程中所有的 TransmittableThreadLocal 的值

replay :设置从主线程捕获到的上下文

restore :恢复这个线程原本的上下文

参考学习

juejin.cn/post/720577…

juejin.cn/post/721490…