好的,下面我将以讲座的形式,详细阐述Java微服务接口链路过长时,结合Sleuth自动生成TraceId的上下游治理方案。
大家好,今天我们来聊聊微服务架构下接口链路追踪的问题,以及如何利用Spring Cloud Sleuth来自动生成TraceId,并进行上下游治理。
微服务架构的优势在于解耦和独立部署,但也引入了分布式追踪的复杂性。当请求跨越多个服务时,排查问题变得困难。我们需要一种机制来追踪请求的整个生命周期,这就是链路追踪。
一、链路追踪的必要性
在一个典型的微服务架构中,一个用户请求可能需要经过多个服务的处理才能完成。如果某个服务出现问题,我们需要能够快速定位到问题所在,而链路追踪就能帮助我们做到这一点。
- 性能分析: 了解请求在每个服务上的耗时,找到瓶颈。
- 错误定位: 追踪请求的整个调用链,找出错误发生的具体服务。
- 依赖关系分析: 了解服务之间的依赖关系,优化服务架构。
二、Spring Cloud Sleuth 简介
Spring Cloud Sleuth 是一个分布式追踪解决方案,它通过自动生成 TraceId 和 SpanId,并将这些信息传递到下游服务,从而实现链路追踪。Sleuth 可以与 Zipkin、Jaeger 等追踪系统集成,将追踪数据收集并可视化。
主要概念:
- Trace: 代表一个完整的请求链路,包含一个或多个 Span。
- Span: 代表请求链路中的一个操作,例如一个HTTP请求、一个数据库查询等。每个Span 都有一个唯一的 SpanId,并且属于一个 Trace。
- TraceId: 标识一个 Trace 的唯一ID,贯穿整个请求链路。
- SpanId: 标识一个 Span 的唯一ID。
- ParentId: 指向父 Span 的 SpanId,表示 Span 之间的父子关系。
三、Sleuth 集成与TraceId自动生成
-
添加依赖:
在每个微服务的
pom.xml文件中添加 Sleuth 和 Spring Cloud Starter OpenTelemetry 的依赖:<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-sleuth</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>io.opentelemetry</groupId> <artifactId>opentelemetry-exporter-zipkin</artifactId> </dependency>spring-cloud-starter-sleuth:Sleuth 的核心依赖。spring-cloud-starter-openfeign:如果使用 Feign 作为服务间的调用工具,需要添加此依赖,Sleuth 会自动集成 Feign 的链路追踪。io.opentelemetry:opentelemetry-exporter-zipkin: 用于将追踪数据导出到 Zipkin。
-
配置追踪信息导出:
在
application.properties或application.yml文件中配置 Sleuth 和 Zipkin:spring: application: name: your-service-name # 服务名称 sleuth: sampler: probability: 1.0 # 采样率,1.0 表示全部采样 zipkin: enabled: true base-url: http://localhost:9411 # Zipkin 服务器地址spring.application.name: 服务名称,用于在 Zipkin 中标识服务。spring.sleuth.sampler.probability: 采样率,决定了多少比例的请求会被追踪。生产环境可以适当降低采样率,以减少性能开销。spring.zipkin.enabled: 是否启用 Zipkin。spring.zipkin.base-url: Zipkin 服务器的地址。
-
自动生成 TraceId 和 SpanId:
Sleuth 会自动为每个进入服务的请求生成 TraceId 和 SpanId,并将这些信息添加到请求的Header中。对于Spring MVC 控制器,Sleuth 会自动拦截请求并创建 Span。 对于 Feign 客户端,Sleuth 会自动将 TraceId 和 SpanId 传递到下游服务。
示例代码:
@RestController public class MyController { private final RestTemplate restTemplate; public MyController(RestTemplate restTemplate) { this.restTemplate = restTemplate; } @GetMapping("/api/hello") public String hello() { String response = restTemplate.getForObject("http://another-service/api/world", String.class); return "Hello " + response; } }在这个例子中,当访问
/api/hello接口时,Sleuth 会自动生成 TraceId 和 SpanId。当RestTemplate调用another-service的/api/world接口时,Sleuth 会自动将 TraceId 和 SpanId 传递到下游服务。 -
自定义 Span:
有时候,我们需要手动创建 Span,例如追踪一些耗时的操作。可以使用
Tracer对象来实现:import brave.Tracer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class MyService { @Autowired private Tracer tracer; public void doSomething() { tracer.startScopedSpan("doSomethingSpan"); try { // 执行一些耗时的操作 Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } finally { tracer.endScope(); } } }tracer.startScopedSpan("doSomethingSpan"): 创建一个名为 "doSomethingSpan" 的 Span,并将其设置为当前线程的活动 Span。tracer.endScope(): 结束当前线程的活动 Span。
四、上下游治理方案
仅仅集成 Sleuth 只能实现TraceId的自动生成和传递,要实现完整的链路追踪,还需要对上下游服务进行治理,确保TraceId能够正确传递,并进行统一的异常处理。
-
统一请求头传递:
确保所有服务都能够正确地接收和传递 Sleuth 生成的 TraceId 和 SpanId。Sleuth 默认会将 TraceId 和 SpanId 放在 HTTP Header 中传递,Header 的名称为
X-B3-TraceId、X-B3-SpanId、X-B3-ParentSpanId、X-B3-Sampled和X-B3-Flags。-
Feign 客户端: 如果使用 Feign 作为服务间的调用工具,Sleuth 会自动处理请求头的传递。
-
RestTemplate: 如果使用 RestTemplate,需要手动添加拦截器,将请求头传递到下游服务。
import org.springframework.http.HttpRequest; import org.springframework.http.client.ClientHttpRequestExecution; import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.http.client.ClientHttpResponse; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import java.io.IOException; @Component public class RestTemplateInterceptor implements ClientHttpRequestInterceptor { @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attributes != null) { HttpServletRequest httpRequest = attributes.getRequest(); String traceId = httpRequest.getHeader("X-B3-TraceId"); String spanId = httpRequest.getHeader("X-B3-SpanId"); String parentId = httpRequest.getHeader("X-B3-ParentSpanId"); String sampled = httpRequest.getHeader("X-B3-Sampled"); String flags = httpRequest.getHeader("X-B3-Flags"); if (traceId != null) { request.getHeaders().add("X-B3-TraceId", traceId); } if (spanId != null) { request.getHeaders().add("X-B3-SpanId", spanId); } if (parentId != null) { request.getHeaders().add("X-B3-ParentSpanId", parentId); } if (sampled != null) { request.getHeaders().add("X-B3-Sampled", sampled); } if (flags != null) { request.getHeaders().add("X-B3-Flags", flags); } } return execution.execute(request, body); } }配置 RestTemplate:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestTemplate; import java.util.Collections; @Configuration public class RestTemplateConfig { @Bean public RestTemplate restTemplate(RestTemplateInterceptor restTemplateInterceptor) { RestTemplate restTemplate = new RestTemplate(); restTemplate.setInterceptors(Collections.singletonList(restTemplateInterceptor)); return restTemplate; } } -
消息队列: 如果使用消息队列进行服务间的通信,需要手动将 TraceId 和 SpanId 添加到消息的 Header 中。具体实现方式取决于使用的消息队列类型。
-
-
统一异常处理:
在微服务架构中,异常处理是一个重要的问题。我们需要一种机制来统一处理异常,并将异常信息添加到 Span 中,方便排查问题。
-
全局异常处理器: 可以使用 Spring 的
@ControllerAdvice注解来创建全局异常处理器,捕获所有未处理的异常,并将异常信息添加到 Span 中。import brave.Tracer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; @ControllerAdvice public class GlobalExceptionHandler { @Autowired private Tracer tracer; @ExceptionHandler(Exception.class) public ResponseEntity<String> handleException(Exception e) { tracer.currentSpan().tag("error", e.getMessage()); return new ResponseEntity<>("Internal Server Error: " + e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); } }在这个例子中,当发生异常时,
handleException方法会被调用,并将异常信息添加到当前 Span 的 "error" tag 中。 -
自定义异常: 可以创建自定义异常类,并在异常中包含 TraceId 和 SpanId,方便追踪异常的来源。
-
-
日志规范
服务日志中增加 TraceId 和 SpanId,便于关联请求链路。可以通过logback 或者 log4j 的 MDC (Mapped Diagnostic Context)来实现。
<!-- logback-spring.xml -->
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - [%X{traceId},%X{spanId}] - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>
在代码中,Sleuth会自动将TraceId和SpanId放入MDC中,无需手动设置。
五、Zipkin 可视化
Sleuth 会将追踪数据发送到 Zipkin 服务器,Zipkin 提供了一个 Web 界面,可以查看请求的整个调用链。
-
启动 Zipkin:
可以从 Zipkin 的官网下载 Zipkin 的可执行 JAR 文件,然后使用以下命令启动 Zipkin:
java -jar zipkin.jar也可以使用 Docker 启动 Zipkin:
docker run -d -p 9411:9411 openzipkin/zipkin -
访问 Zipkin Web 界面:
在浏览器中访问
http://localhost:9411,即可打开 Zipkin 的 Web 界面。 -
查看追踪数据:
在 Zipkin 的 Web 界面中,可以根据服务名称、时间范围等条件搜索追踪数据。点击一个 Trace,可以查看该 Trace 的详细信息,包括每个 Span 的耗时、服务名称、请求头等。
六、最佳实践
- 采样率: 在生产环境中,可以适当降低采样率,以减少性能开销。但是,采样率不宜过低,否则可能会丢失重要的追踪信息。
- Tagging: 可以使用 Sleuth 的 Tagging 功能,为 Span 添加自定义的标签,例如请求参数、用户信息等。这些标签可以帮助我们更好地理解请求的上下文。
- 日志: 在日志中包含 TraceId 和 SpanId,方便关联请求链路。
- 监控: 可以使用监控系统,例如 Prometheus 和 Grafana,监控 Sleuth 的性能指标,例如 Span 的创建速度、追踪数据的发送速度等。
七、注意事项
- 版本兼容性: 确保 Sleuth 和 Spring Cloud 的版本兼容。
- 配置: 正确配置 Sleuth 和 Zipkin,确保追踪数据能够正确发送到 Zipkin 服务器。
- 性能: 链路追踪会对性能产生一定的影响,需要根据实际情况进行优化。
- 安全: 注意保护追踪数据的安全,防止泄露敏感信息。
八、代码示例
为了方便大家理解,这里提供一个完整的代码示例,包含两个微服务:service-a 和 service-b。
service-a:
-
pom.xml:<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.17</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example</groupId> <artifactId>service-a</artifactId> <version>0.0.1-SNAPSHOT</version> <name>service-a</name> <description>service-a</description> <properties> <java.version>1.8</java.version> <spring-cloud.version>2021.0.9</spring-cloud.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</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-openfeign</artifactId> </dependency> <dependency> <groupId>io.opentelemetry</groupId> <artifactId>opentelemetry-exporter-zipkin</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project> -
application.yml:spring: application: name: service-a sleuth: sampler: probability: 1.0 zipkin: enabled: true base-url: http://localhost:9411 server: port: 8080 -
ServiceAController.java:import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class ServiceAController { @Autowired private ServiceBClient serviceBClient; @GetMapping("/api/a") public String serviceA() { return "Service A -> " + serviceBClient.serviceB(); } } -
ServiceBClient.java:import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; @FeignClient(name = "service-b", url = "http://localhost:8081") public interface ServiceBClient { @GetMapping("/api/b") String serviceB(); } -
ServiceAApplication.java:import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.openfeign.EnableFeignClients; @SpringBootApplication @EnableFeignClients public class ServiceAApplication { public static void main(String[] args) { SpringApplication.run(ServiceAApplication.class, args); } }
service-b:
-
pom.xml: (与 service-a 类似,只需修改 artifactId 和 name) -
application.yml:spring: application: name: service-b sleuth: sampler: probability: 1.0 zipkin: enabled: true base-url: http://localhost:9411 server: port: 8081 -
ServiceBController.java:import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class ServiceBController { @GetMapping("/api/b") public String serviceB() { return "Service B"; } } -
ServiceBApplication.java: (与 service-a 类似,只需修改类名)
运行示例:
- 启动 Zipkin (使用
java -jar zipkin.jar或 Docker)。 - 启动
service-a和service-b。 - 访问
http://localhost:8080/api/a。 - 在 Zipkin Web 界面中查看追踪数据。
九、总结一下
通过集成 Spring Cloud Sleuth,我们可以自动生成 TraceId 和 SpanId,并通过上下游治理,确保 TraceId 能够在整个请求链路中正确传递。结合 Zipkin 等追踪系统,我们可以可视化请求的调用链,快速定位问题,优化服务架构。希望今天的分享对大家有所帮助。