Java `Distributed Tracing` (`OpenTelemetry`, `Zipkin`) `Context Propagation` 跨服务调用追踪

各位观众老爷们,大家好!今天咱们聊聊Java分布式追踪的那些事儿,保证让大家听得明白,学得会,还能顺手解决几个线上问题!

开场白:故事的开端

话说在很久很久以前(其实也没多久,也就十几年),咱们的应用程序都是单体架构,那时候日子过得挺滋润,一个Tomcat就能搞定一切。但随着业务的膨胀,单体架构渐渐hold不住了,于是乎,微服务架构横空出世!

微服务架构,听起来高大上,但带来的问题也是real实在:服务拆分了,调用链路变得无比复杂,一旦线上出了问题,想定位到是哪个服务出的幺蛾子,简直比大海捞针还难!

这时候,救星来了,它就是——分布式追踪

什么是分布式追踪?

简单来说,分布式追踪就是记录每一次请求在各个服务之间的流转路径,并把这些信息收集起来,形成一个完整的调用链。就像警察叔叔追踪罪犯一样,咱们追踪请求在各个服务之间的“犯罪”轨迹。

分布式追踪的核心概念

要理解分布式追踪,首先要搞清楚几个核心概念:

  • Trace (追踪):一个完整的请求链路,从用户发起请求开始,到最终返回响应结束。可以理解为一次完整的用户操作。

  • Span (跨度):Trace中的一个基本单元,代表一次服务调用。比如,服务A调用服务B,就是一个Span。每个Span都有一个开始时间和结束时间,记录了这次调用的耗时。

  • Context Propagation (上下文传递):在服务调用过程中,将Trace ID和Span ID等信息传递给下游服务,保证整个Trace能够串联起来。这就像接力赛跑,每个服务拿到接力棒(Context),然后传递给下一个服务。

OpenTelemetry:追踪界的扛把子

在分布式追踪领域,有很多技术方案,比如Zipkin、Jaeger、SkyWalking等等。但是,今天咱们重点介绍OpenTelemetry,因为它实在是太火了,几乎成了行业标准。

OpenTelemetry是一个开源的可观测性框架,提供了一套统一的API、SDK和工具,用于生成、收集和导出遥测数据(包括Traces、Metrics和Logs)。它的目标是让开发者能够轻松地为应用程序添加追踪功能,而无需关心底层使用的具体技术。

OpenTelemetry的优势

  • 厂商无关性:OpenTelemetry不依赖于任何特定的追踪系统,你可以随意选择Zipkin、Jaeger等作为后端存储。
  • 标准化:OpenTelemetry提供了一套标准的API和协议,避免了各种追踪系统之间的不兼容性问题。
  • 可扩展性:OpenTelemetry支持自定义扩展,可以根据业务需求添加额外的追踪信息。

实战:用OpenTelemetry搞事情

接下来,咱们通过一个简单的例子,演示如何在Java应用中使用OpenTelemetry进行分布式追踪。

1. 引入OpenTelemetry依赖

首先,在你的Maven或Gradle项目中,引入OpenTelemetry相关的依赖:

<!-- Maven -->
<dependency>
  <groupId>io.opentelemetry</groupId>
  <artifactId>opentelemetry-api</artifactId>
  <version>1.33.0</version>
</dependency>
<dependency>
  <groupId>io.opentelemetry</groupId>
  <artifactId>opentelemetry-sdk</artifactId>
  <version>1.33.0</version>
</dependency>
<dependency>
  <groupId>io.opentelemetry</groupId>
  <artifactId>opentelemetry-exporter-zipkin</artifactId>
  <version>1.33.0</version>
</dependency>

<!-- Gradle -->
dependencies {
  implementation 'io.opentelemetry:opentelemetry-api:1.33.0'
  implementation 'io.opentelemetry:opentelemetry-sdk:1.33.0'
  implementation 'io.opentelemetry:opentelemetry-exporter-zipkin:1.33.0'
}

2. 初始化OpenTelemetry SDK

在你的应用程序启动时,初始化OpenTelemetry SDK,并配置Zipkin作为后端存储:

import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.exporter.zipkin.ZipkinSpanExporter;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;

public class OpenTelemetryConfig {

    private static final String SERVICE_NAME = "my-java-service";
    private static final String ZIPKIN_URL = "http://localhost:9411/api/v2/spans";

    public static OpenTelemetry initOpenTelemetry() {
        // 配置 Zipkin Span Exporter
        ZipkinSpanExporter zipkinExporter = ZipkinSpanExporter.builder()
                .setEndpoint(ZIPKIN_URL)
                .build();

        // 配置 Tracer Provider
        SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
                .addSpanProcessor(SimpleSpanProcessor.create(zipkinExporter))
                .build();

        // 构建 OpenTelemetry SDK
        OpenTelemetrySdk openTelemetrySdk = OpenTelemetrySdk.builder()
                .setTracerProvider(tracerProvider)
                .buildAndRegisterGlobal();

        return openTelemetrySdk;
    }

    public static Tracer getTracer(OpenTelemetry openTelemetry) {
        return openTelemetry.getTracer(SERVICE_NAME, "1.0.0");
    }

    public static void main(String[] args) {
        OpenTelemetry openTelemetry = initOpenTelemetry();
        Tracer tracer = getTracer(openTelemetry);

        // ...你的业务代码...
    }
}

这段代码做了几件事:

  • 创建了一个 ZipkinSpanExporter,指定了Zipkin的地址。
  • 创建了一个 SdkTracerProvider,并将Zipkin Exporter添加到Span处理器中。
  • 构建了一个 OpenTelemetrySdk,并将Tracer Provider注册为全局默认的OpenTelemetry实例。
  • 定义了一个 getTracer 方法,用于获取 Tracer 实例。

3. 在代码中添加Span

现在,咱们可以在代码中添加Span,记录关键操作的耗时:

import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;

public class MyService {

    private final Tracer tracer;

    public MyService(OpenTelemetry openTelemetry) {
        this.tracer = OpenTelemetryConfig.getTracer(openTelemetry);
    }

    public String doSomething() {
        Span span = tracer.spanBuilder("doSomething").startSpan();
        try {
            // 模拟耗时操作
            Thread.sleep(100);
            return "Hello, OpenTelemetry!";
        } catch (InterruptedException e) {
            span.recordException(e); // 记录异常
            return "Error!";
        } finally {
            span.end(); // 结束Span
        }
    }
}

这段代码做了几件事:

  • 通过 tracer.spanBuilder("doSomething").startSpan() 创建了一个名为 "doSomething" 的Span。
  • 使用 try-catch-finally 块来确保Span能够正确结束,即使发生异常。
  • catch 块中,使用 span.recordException(e) 记录异常信息。

4. 跨服务调用:Context Propagation

重点来了!如何在跨服务调用中传递Trace ID和Span ID呢?OpenTelemetry提供了一种叫做 Context Propagation 的机制。

基于HTTP的Context Propagation

最常见的情况是基于HTTP的跨服务调用。OpenTelemetry提供了一组标准的HTTP Header,用于传递Context信息。

  • traceparent: 包含Trace ID、Span ID和Trace Flags。
  • tracestate: 包含厂商特定的Context信息。

示例:使用RestTemplate进行Context Propagation

import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import io.opentelemetry.context.propagation.TextMapSetter;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.web.client.RestTemplate;

import java.util.HashMap;
import java.util.Map;

public class ServiceA {

    private final Tracer tracer;
    private final RestTemplate restTemplate;
    private final OpenTelemetry openTelemetry;

    public ServiceA(OpenTelemetry openTelemetry, RestTemplate restTemplate) {
        this.tracer = OpenTelemetryConfig.getTracer(openTelemetry);
        this.restTemplate = restTemplate;
        this.openTelemetry = openTelemetry;
    }

    public String callServiceB() {
        Span span = tracer.spanBuilder("callServiceB").startSpan();
        try (Scope scope = span.makeCurrent()) { // 非常重要!将Span设置为当前Context
            String url = "http://localhost:8081/serviceB"; // 假设ServiceB运行在8081端口

            // 1. 创建一个 TextMapSetter,用于将 Context 信息添加到 HTTP Header
            TextMapSetter<HttpHeaders> setter = new TextMapSetter<HttpHeaders>() {
                @Override
                public void set(HttpHeaders carrier, String key, String value) {
                    carrier.set(key, value);
                }
            };

            // 2. 创建一个 HttpHeaders 对象
            HttpHeaders headers = new HttpHeaders();

            // 3. 从当前 Context 中提取 Context 信息,并添加到 HTTP Header
            openTelemetry.getPropagators().getTextMapPropagator().inject(Context.current(), headers, setter);

            // 4. 使用 RestTemplate 发起 HTTP 请求,并将 HTTP Header 传递给 ServiceB
            HttpEntity<String> entity = new HttpEntity<>(headers);
            return restTemplate.exchange(url, HttpMethod.GET, entity, String.class).getBody();

        } catch (Exception e) {
            span.recordException(e);
            return "Error calling ServiceB!";
        } finally {
            span.end();
        }
    }
}

在ServiceB中,需要从HTTP Header中提取Context信息,并将其设置为当前Context:

import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import io.opentelemetry.context.propagation.TextMapGetter;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
public class ServiceB {

    private final Tracer tracer;
    private final OpenTelemetry openTelemetry;

    public ServiceB(OpenTelemetry openTelemetry) {
        this.tracer = OpenTelemetryConfig.getTracer(openTelemetry);
        this.openTelemetry = openTelemetry;
    }

    @GetMapping("/serviceB")
    public String handleRequest(@RequestHeader Map<String, String> headers) {
        // 1. 创建一个 TextMapGetter,用于从 HTTP Header 中提取 Context 信息
        TextMapGetter<Map<String, String>> getter = new TextMapGetter<Map<String, String>>() {
            @Override
            public String get(Map<String, String> carrier, String key) {
                return carrier.get(key);
            }

            @Override
            public Iterable<String> keys(Map<String, String> carrier) {
                return carrier.keySet();
            }
        };

        // 2. 从 HTTP Header 中提取 Context 信息
        Context context = openTelemetry.getPropagators().getTextMapPropagator().extract(Context.current(), headers, getter);

        // 3. 将提取到的 Context 设置为当前 Context
        try (Scope scope = context.makeCurrent()) {
            Span span = tracer.spanBuilder("ServiceB.handleRequest").startSpan();
            try {
                // 模拟耗时操作
                Thread.sleep(50);
                return "Hello from Service B!";
            } catch (InterruptedException e) {
                span.recordException(e);
                return "Error in Service B!";
            } finally {
                span.end();
            }
        }
    }
}

代码解释:

  • ServiceA:

    • TextMapSetter 接口用于将 Context 信息注入到 HTTP Header 中。
    • openTelemetry.getPropagators().getTextMapPropagator().inject() 方法将当前 Context 信息注入到 HttpHeaders 对象中。
    • RestTemplate 发送请求时,将 HttpHeaders 对象传递给 ServiceB。
  • ServiceB:

    • TextMapGetter 接口用于从 HTTP Header 中提取 Context 信息。
    • openTelemetry.getPropagators().getTextMapPropagator().extract() 方法从 HttpHeaders 对象中提取 Context 信息。
    • context.makeCurrent() 方法将提取到的 Context 设置为当前 Context,确保后续的Span都属于同一个Trace。

5. 运行程序,查看追踪结果

启动ServiceA和ServiceB,并访问ServiceA的接口。然后在Zipkin的Web界面(通常是 http://localhost:9411)上,你就可以看到完整的调用链了!

表格总结:Context Propagation的关键步骤

| 步骤 | ServiceA (调用方) & | * 服务A (调用方)

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注