JAVA 微服务接口链路过长?结合 Sleuth 自动生成 TraceId 的上下游治理方案

好的,下面我将以讲座的形式,详细阐述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自动生成

  1. 添加依赖:

    在每个微服务的 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。
  2. 配置追踪信息导出:

    application.propertiesapplication.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 服务器的地址。
  3. 自动生成 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 传递到下游服务。

  4. 自定义 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能够正确传递,并进行统一的异常处理。

  1. 统一请求头传递:

    确保所有服务都能够正确地接收和传递 Sleuth 生成的 TraceId 和 SpanId。Sleuth 默认会将 TraceId 和 SpanId 放在 HTTP Header 中传递,Header 的名称为 X-B3-TraceIdX-B3-SpanIdX-B3-ParentSpanIdX-B3-SampledX-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 中。具体实现方式取决于使用的消息队列类型。

  2. 统一异常处理:

    在微服务架构中,异常处理是一个重要的问题。我们需要一种机制来统一处理异常,并将异常信息添加到 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,方便追踪异常的来源。

  3. 日志规范

服务日志中增加 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 界面,可以查看请求的整个调用链。

  1. 启动 Zipkin:

    可以从 Zipkin 的官网下载 Zipkin 的可执行 JAR 文件,然后使用以下命令启动 Zipkin:

    java -jar zipkin.jar

    也可以使用 Docker 启动 Zipkin:

    docker run -d -p 9411:9411 openzipkin/zipkin
  2. 访问 Zipkin Web 界面:

    在浏览器中访问 http://localhost:9411,即可打开 Zipkin 的 Web 界面。

  3. 查看追踪数据:

    在 Zipkin 的 Web 界面中,可以根据服务名称、时间范围等条件搜索追踪数据。点击一个 Trace,可以查看该 Trace 的详细信息,包括每个 Span 的耗时、服务名称、请求头等。

六、最佳实践

  • 采样率: 在生产环境中,可以适当降低采样率,以减少性能开销。但是,采样率不宜过低,否则可能会丢失重要的追踪信息。
  • Tagging: 可以使用 Sleuth 的 Tagging 功能,为 Span 添加自定义的标签,例如请求参数、用户信息等。这些标签可以帮助我们更好地理解请求的上下文。
  • 日志: 在日志中包含 TraceId 和 SpanId,方便关联请求链路。
  • 监控: 可以使用监控系统,例如 Prometheus 和 Grafana,监控 Sleuth 的性能指标,例如 Span 的创建速度、追踪数据的发送速度等。

七、注意事项

  • 版本兼容性: 确保 Sleuth 和 Spring Cloud 的版本兼容。
  • 配置: 正确配置 Sleuth 和 Zipkin,确保追踪数据能够正确发送到 Zipkin 服务器。
  • 性能: 链路追踪会对性能产生一定的影响,需要根据实际情况进行优化。
  • 安全: 注意保护追踪数据的安全,防止泄露敏感信息。

八、代码示例

为了方便大家理解,这里提供一个完整的代码示例,包含两个微服务:service-aservice-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 类似,只需修改类名)

运行示例:

  1. 启动 Zipkin (使用 java -jar zipkin.jar 或 Docker)。
  2. 启动 service-aservice-b
  3. 访问 http://localhost:8080/api/a
  4. 在 Zipkin Web 界面中查看追踪数据。

九、总结一下

通过集成 Spring Cloud Sleuth,我们可以自动生成 TraceId 和 SpanId,并通过上下游治理,确保 TraceId 能够在整个请求链路中正确传递。结合 Zipkin 等追踪系统,我们可以可视化请求的调用链,快速定位问题,优化服务架构。希望今天的分享对大家有所帮助。

发表回复

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