各位观众老爷们,大家好!今天咱们聊聊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 (调用方)