Java 应用全链路追踪与分布式 Context 传递
大家好,今天我们来聊聊 Java 应用的全链路追踪与分布式 Context 传递。随着微服务架构的普及,一个请求往往需要经过多个服务才能完成,这使得问题排查变得异常困难。全链路追踪和 Context 传递就是解决这个问题的关键技术。
1. 全链路追踪的必要性与基本概念
在单体应用时代,一个请求的执行路径通常比较简单,我们可以通过日志、调试等手段快速定位问题。但在微服务架构下,一个请求可能需要经过多个服务,每个服务又可能调用多个数据库、缓存等组件。如果某个环节出现问题,很难快速定位到具体是哪个服务或组件导致的。
全链路追踪的核心思想是将一个请求的处理过程串联起来,形成一条完整的链路。通过对链路上的每个节点进行监控和记录,我们可以清晰地了解请求的执行路径、耗时、状态等信息,从而快速定位问题。
全链路追踪涉及以下几个关键概念:
- Trace ID: 全局唯一的 ID,用于标识一次完整的请求链路。
- Span ID: 用于标识链路中的一个单元,例如一个服务调用、一个数据库查询等。
- Parent Span ID: 用于标识当前 Span 的父 Span。通过 Parent Span ID,我们可以将 Span 组织成树状结构,从而还原请求的执行路径。
- Annotations: 用于记录 Span 的事件,例如请求开始、请求结束、异常发生等。
- Tags: 用于记录 Span 的元数据,例如服务名称、主机 IP、HTTP 状态码等。
2. 全链路追踪的实现原理
全链路追踪的实现原理可以概括为以下几个步骤:
- 注入 Trace ID: 在请求进入系统时,生成一个全局唯一的 Trace ID,并将其传递给后续的服务。
- 创建 Span: 在每个服务中,当开始处理请求时,创建一个 Span,并设置 Parent Span ID。
- 记录 Span 信息: 在 Span 的生命周期内,记录 Annotations 和 Tags,用于描述 Span 的状态和元数据。
- 传递 Context: 将 Trace ID、Span ID 等信息传递给下游服务,以便下游服务可以创建子 Span。
- 收集和展示: 将所有 Span 信息收集起来,通过可视化界面展示请求的执行路径和性能指标。
3. 常用的全链路追踪框架
目前有很多优秀的全链路追踪框架可供选择,例如:
- Zipkin: Twitter 开源的分布式追踪系统,支持多种数据存储方式,例如 Elasticsearch、Cassandra 等。
- Jaeger: Uber 开源的分布式追踪系统,支持 OpenTracing 标准,可以与多种编程语言和框架集成。
- SkyWalking: 国产开源的 APM 系统,功能强大,除了全链路追踪外,还支持指标监控、告警等功能。
- OpenTelemetry: CNCF 孵化的可观测性工具,提供统一的 API 和 SDK,可以与多种后端存储集成。
4. 基于 Spring Cloud Sleuth 和 Zipkin 的全链路追踪实践
Spring Cloud Sleuth 是 Spring Cloud 官方提供的全链路追踪组件,它可以与 Zipkin 等追踪系统集成。下面我们以 Spring Cloud Sleuth 和 Zipkin 为例,演示如何实现全链路追踪。
4.1 环境准备
-
安装 Zipkin:可以通过 Docker 快速启动 Zipkin:
docker run -d -p 9411:9411 openzipkin/zipkin
-
创建 Spring Boot 项目:创建三个 Spring Boot 项目,分别命名为
service-a
、service-b
、service-c
。
4.2 添加依赖
在每个 Spring Boot 项目的 pom.xml
文件中添加以下依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
4.3 配置 application.yml
在每个 Spring Boot 项目的 application.yml
文件中添加以下配置:
spring:
application:
name: service-a # 将 service-a 替换为实际的服务名称
zipkin:
base-url: http://localhost:9411
sleuth:
sampler:
probability: 1.0 # 设置采样率,1.0 表示全部采样
server:
port: 8081 # 设置服务端口,每个服务端口不同
4.4 创建 Controller
在 service-a
项目中创建 Controller:
@RestController
public class ServiceAController {
@Autowired
private RestTemplate restTemplate;
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder.build();
}
@GetMapping("/a")
public String serviceA() {
String response = restTemplate.getForObject("http://localhost:8082/b", String.class);
return "Hello from Service A, Response from Service B: " + response;
}
}
在 service-b
项目中创建 Controller:
@RestController
public class ServiceBController {
@Autowired
private RestTemplate restTemplate;
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder.build();
}
@GetMapping("/b")
public String serviceB() {
String response = restTemplate.getForObject("http://localhost:8083/c", String.class);
return "Hello from Service B, Response from Service C: " + response;
}
}
在 service-c
项目中创建 Controller:
@RestController
public class ServiceCController {
@GetMapping("/c")
public String serviceC() {
return "Hello from Service C";
}
}
4.5 启动服务
分别启动 service-a
、service-b
、service-c
三个 Spring Boot 项目。
4.6 测试
访问 http://localhost:8081/a
,可以看到以下输出:
Hello from Service A, Response from Service B: Hello from Service B, Response from Service C: Hello from Service C
4.7 查看 Zipkin
打开 Zipkin 的 Web 界面 http://localhost:9411
,可以看到请求的链路信息。通过 Zipkin,我们可以清晰地了解请求的执行路径和耗时。
5. 分布式 Context 传递的实现
在全链路追踪中,我们需要将 Trace ID、Span ID 等信息传递给下游服务。常用的 Context 传递方式有以下几种:
- HTTP Headers: 将 Context 信息放在 HTTP Headers 中传递。
- Message Queues: 将 Context 信息放在消息的 Headers 中传递。
- ThreadLocal: 将 Context 信息放在 ThreadLocal 中传递。
5.1 基于 HTTP Headers 的 Context 传递
Spring Cloud Sleuth 默认使用 HTTP Headers 传递 Context 信息。它会将 Trace ID、Span ID 等信息放在 X-B3-*
Headers 中传递。
例如,当 service-a
调用 service-b
时,service-a
会将以下 Headers 传递给 service-b
:
X-B3-TraceId
: Trace IDX-B3-SpanId
: Span IDX-B3-ParentSpanId
: Parent Span IDX-B3-Sampled
: 是否采样
5.2 自定义 Context 传递
除了 Spring Cloud Sleuth 默认提供的 Context 传递方式外,我们还可以自定义 Context 传递方式。例如,我们可以将 Context 信息放在自定义的 HTTP Headers 中传递。
以下是一个示例,演示如何将 Context 信息放在自定义的 HTTP Headers 中传递:
// 定义 Context 信息
public class MyContext {
private String userId;
private String requestId;
// getter and setter
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getRequestId() {
return requestId;
}
public void setRequestId(String requestId) {
this.requestId = requestId;
}
}
// 创建 Context Holder
public class MyContextHolder {
private static final ThreadLocal<MyContext> contextHolder = new ThreadLocal<>();
public static MyContext getContext() {
return contextHolder.get();
}
public static void setContext(MyContext context) {
contextHolder.set(context);
}
public static void clearContext() {
contextHolder.remove();
}
}
// 创建 Interceptor,用于在请求中添加 Context 信息
@Component
public class MyRequestInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
MyContext context = MyContextHolder.getContext();
if (context != null) {
request.getHeaders().add("X-User-Id", context.getUserId());
request.getHeaders().add("X-Request-Id", context.getRequestId());
}
return execution.execute(request, body);
}
}
// 配置 RestTemplate,添加 Interceptor
@Configuration
public class RestTemplateConfig {
@Autowired
private MyRequestInterceptor myRequestInterceptor;
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder.interceptors(myRequestInterceptor).build();
}
}
// 创建 Filter,用于在请求进入时设置 Context 信息
@Component
public class MyFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String userId = httpServletRequest.getHeader("X-User-Id");
String requestId = httpServletRequest.getHeader("X-Request-Id");
MyContext context = new MyContext();
context.setUserId(userId);
context.setRequestId(requestId);
MyContextHolder.setContext(context);
try {
chain.doFilter(request, response);
} finally {
MyContextHolder.clearContext();
}
}
}
6. 异步场景下的 Context 传递
在异步场景下,例如使用线程池、消息队列等,Context 传递会变得更加复杂。因为异步任务通常会在不同的线程中执行,而 ThreadLocal 只能在同一个线程中传递数据。
以下是一些常用的异步场景下的 Context 传递方式:
- TransmittableThreadLocal: Alibaba 开源的 TransmittableThreadLocal (TTL) 可以解决线程池场景下的 ThreadLocal 数据传递问题。
- MDC (Mapped Diagnostic Context): MDC 是 log4j 和 logback 提供的功能,可以将 Context 信息添加到日志中。
- 手动传递: 在创建异步任务时,手动将 Context 信息传递给异步任务。
6.1 使用 TransmittableThreadLocal
TransmittableThreadLocal 可以将 ThreadLocal 中的数据传递给线程池中的线程。
首先,添加 TTL 的依赖:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.12.2</version>
</dependency>
然后,将 ThreadLocal 替换为 TransmittableThreadLocal:
// 将 ThreadLocal 替换为 TransmittableThreadLocal
private static final TransmittableThreadLocal<MyContext> contextHolder = new TransmittableThreadLocal<>();
7. 全链路追踪的最佳实践
- 统一 Trace ID 生成方式: 保证 Trace ID 的全局唯一性。
- 合理设置采样率: 根据实际情况设置采样率,避免采样数据过多或过少。
- 规范 Span 名称: 使用清晰的 Span 名称,方便理解和分析。
- 添加必要的 Tags: 添加必要的 Tags,例如服务名称、主机 IP、HTTP 状态码等,方便过滤和分析。
- 处理异常情况: 在发生异常时,记录异常信息到 Span 中。
- 监控追踪系统: 监控追踪系统的性能和可用性,确保其正常运行。
8. 示例:结合 Spring Cloud Gateway 进行全链路追踪
在微服务架构中,API Gateway 通常作为流量入口,负责请求的路由、鉴权、限流等功能。将全链路追踪与 Spring Cloud Gateway 集成,可以更好地监控和管理整个系统的流量。
在 Spring Cloud Gateway 中集成全链路追踪非常简单,只需要添加 Spring Cloud Sleuth 和 Zipkin 的依赖即可。Spring Cloud Gateway 会自动将 Trace ID 和 Span ID 传递给下游服务。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
9. 总结:有效追踪,快速定位问题
我们学习了全链路追踪的必要性、实现原理、常用框架以及最佳实践。通过全链路追踪,我们可以清晰地了解请求的执行路径、耗时、状态等信息,从而快速定位问题,提高系统的可维护性和可靠性。