分布式追踪:Spring Cloud Sleuth 与 Zipkin 实践

分布式追踪:Spring Cloud Sleuth 与 Zipkin 实践 —— 像侦探一样追踪你的微服务

大家好,我是你们的老朋友,一个在代码世界里摸爬滚打多年的老兵。今天,咱们来聊聊一个在微服务架构中至关重要,却常常被忽略的话题:分布式追踪。

想象一下,你正在侦破一个复杂的案件,线索分散在各个角落,而你必须把它们拼凑起来才能找到真相。微服务架构就像这个案件,不同的服务就像不同的嫌疑人,请求就像线索,而分布式追踪就像你手中的放大镜和笔记本,帮你理清头绪,找到性能瓶颈,揪出幕后黑手。

如果没有分布式追踪,你的微服务就像一群无头苍蝇,嗡嗡嗡地飞来飞去,出了问题你却不知道从何查起。所以,今天我们就来学习如何使用 Spring Cloud Sleuth 和 Zipkin 这两个利器,让你的微服务变得透明,让问题无处遁形。

1. 为什么要用分布式追踪?

在单体应用时代,我们debug就像在自己家后院散步,随便打个断点,就能看到整个调用链。但是到了微服务时代,一个请求可能要经过十几个甚至几十个服务,每个服务都可能由不同的团队维护,使用不同的技术栈。这时候,如果某个请求出了问题,你可能需要:

  • 登录所有服务器,查看日志,大海捞针。
  • 询问所有相关团队,互相甩锅,扯皮半天。
  • 祈祷问题能够自动消失,听天由命。

这种场景想想都让人头皮发麻。分布式追踪就是来解决这个问题的,它可以:

  • 监控服务调用链: 让你知道一个请求经过了哪些服务,每个服务花费了多少时间。
  • 定位性能瓶颈: 找出哪个服务是瓶颈,哪个服务拖慢了整体速度。
  • 分析错误原因: 快速定位错误发生的位置,减少排查时间。
  • 优化系统性能: 通过分析追踪数据,找到可以优化的地方,提升系统性能。
  • 建立服务依赖关系: 了解服务之间的依赖关系,方便进行容量规划和故障隔离。

简单来说,分布式追踪就是给你的微服务装上了一个“摄像头”,让你能够随时监控它们的一举一动,出了问题也能第一时间发现并解决。

2. Spring Cloud Sleuth:追踪的“传感器”

Spring Cloud Sleuth就像一个精密的传感器,它能够自动地为你的 Spring Boot 应用添加追踪信息。它主要做了以下几件事:

  • 生成 Trace ID: 每个请求都会被分配一个唯一的 Trace ID,用来标识整个调用链。
  • 生成 Span ID: 每个服务调用都会被分配一个 Span ID,用来标识该服务在调用链中的位置。
  • 注入追踪信息: 将 Trace ID 和 Span ID 注入到 HTTP Header 中,传递给下游服务。
  • 收集追踪数据: 将追踪数据发送给追踪系统(比如 Zipkin)。

使用 Sleuth 非常简单,只需要在你的 Spring Boot 项目中添加以下依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>

添加依赖后,Sleuth 就会自动生效,不需要做任何额外的配置。它会自动为你的应用添加一个 TraceFilter,拦截所有的 HTTP 请求,并生成 Trace ID 和 Span ID。

示例:一个简单的 REST API

假设我们有一个简单的 REST API,提供一个 /hello 接口:

@RestController
public class HelloController {

    private static final Logger logger = LoggerFactory.getLogger(HelloController.class);

    @GetMapping("/hello")
    public String hello() {
        logger.info("Received request to /hello");
        return "Hello, world!";
    }
}

启动这个应用后,访问 /hello 接口,你会在日志中看到类似这样的信息:

2023-10-27 10:00:00.000  INFO [your-application,e4a2b3c1d5f6g7h8,9c8b7a6f5e4d3c2b,false] 12345 --- [nio-8080-exec-1] com.example.HelloController : Received request to /hello
  • your-application 是你的应用名称。
  • e4a2b3c1d5f6g7h8 是 Trace ID。
  • 9c8b7a6f5e4d3c2b 是 Span ID。
  • false 表示是否是抽样出来的,如果是true,表明是被抽样的。抽样是为了减少数据量,避免对性能产生影响。

3. Zipkin:追踪数据的“存储和展示中心”

有了 Sleuth 这个“传感器”,我们还需要一个“存储和展示中心”来收集和分析追踪数据。Zipkin 就是这样一个工具,它可以:

  • 收集追踪数据: 接收来自各个服务的追踪数据。
  • 存储追踪数据: 将追踪数据存储在数据库中(比如 MySQL、Cassandra)。
  • 展示追踪数据: 提供一个 Web 界面,让你能够方便地查看和分析追踪数据。

你可以使用 Docker 来快速启动 Zipkin:

docker run -d -p 9411:9411 openzipkin/zipkin

启动后,访问 http://localhost:9411,你就可以看到 Zipkin 的 Web 界面了。

4. 将 Sleuth 和 Zipkin 连接起来

现在,我们需要将 Sleuth 和 Zipkin 连接起来,让 Sleuth 将追踪数据发送给 Zipkin。只需要在你的 application.propertiesapplication.yml 文件中添加以下配置:

spring.zipkin.baseUrl=http://localhost:9411
spring.sleuth.sampler.probability=1.0
  • spring.zipkin.baseUrl 指定 Zipkin 的地址。
  • spring.sleuth.sampler.probability 指定采样率。设置为 1.0 表示所有请求都进行追踪。在生产环境中,通常会将采样率设置为一个较小的数值(比如 0.1),以减少对性能的影响。

重启你的应用后,再次访问 /hello 接口,你就可以在 Zipkin 的 Web 界面中看到追踪数据了。

5. 跨服务追踪:让追踪信息流动起来

上面的例子只是一个简单的单服务应用。在微服务架构中,一个请求通常会经过多个服务。为了能够追踪整个调用链,我们需要让追踪信息在服务之间流动起来。

Sleuth 会自动将 Trace ID 和 Span ID 注入到 HTTP Header 中,传递给下游服务。只要下游服务也使用了 Sleuth,它就会自动读取这些信息,并生成新的 Span ID,将自己加入到调用链中。

示例:服务 A 调用服务 B

假设我们有两个服务:服务 A 和服务 B。服务 A 调用服务 B 的 /hello 接口。

服务 A (application.properties):

spring.application.name=service-a
spring.zipkin.baseUrl=http://localhost:9411
spring.sleuth.sampler.probability=1.0
server.port=8081

服务 B (application.properties):

spring.application.name=service-b
spring.zipkin.baseUrl=http://localhost:9411
spring.sleuth.sampler.probability=1.0
server.port=8082

服务 A 的代码:

@RestController
public class ServiceAController {

    private static final Logger logger = LoggerFactory.getLogger(ServiceAController.class);

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/hello")
    public String hello() {
        logger.info("Received request to /hello in service A");
        String response = restTemplate.getForObject("http://localhost:8082/hello", String.class);
        logger.info("Received response from service B: {}", response);
        return "Hello from service A! " + response;
    }

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

服务 B 的代码:

@RestController
public class ServiceBController {

    private static final Logger logger = LoggerFactory.getLogger(ServiceBController.class);

    @GetMapping("/hello")
    public String hello() {
        logger.info("Received request to /hello in service B");
        return "Hello from service B!";
    }
}

启动服务 A 和服务 B 后,访问 http://localhost:8081/hello,你会在 Zipkin 的 Web 界面中看到一个包含两个 Span 的调用链:

  • 一个 Span 代表服务 A 的 /hello 接口。
  • 另一个 Span 代表服务 B 的 /hello 接口。

通过这个调用链,你可以清楚地看到请求从服务 A 流向服务 B 的过程,以及每个服务花费的时间。

6. 自定义 Span:精细化追踪

有时候,你可能需要追踪一些非 HTTP 请求的操作,比如数据库查询、消息队列发送等。这时候,你可以使用 Sleuth 提供的 API 来创建自定义 Span。

@Autowired
private Tracer tracer;

public void doSomething() {
    Span newSpan = tracer.nextSpan().name("doSomething");
    try (Tracer.SpanInScope ws = tracer.withSpan(newSpan.start())) {
        // Your code here
        logger.info("Doing something important...");
    } finally {
        newSpan.end();
    }
}
  • tracer.nextSpan().name("doSomething") 创建一个新的 Span,并设置 Span 的名称为 "doSomething"。
  • tracer.withSpan(newSpan.start()) 将新的 Span 设置为当前线程的活动 Span。
  • newSpan.end() 结束 Span。

try-with-resources 语句块中,你可以执行任何你想要追踪的操作。Sleuth 会自动将这些操作记录到 Span 中,并发送给 Zipkin。

示例:追踪数据库查询

@Autowired
private Tracer tracer;

@Autowired
private JdbcTemplate jdbcTemplate;

public List<User> getUsers() {
    Span newSpan = tracer.nextSpan().name("getUsersFromDatabase");
    try (Tracer.SpanInScope ws = tracer.withSpan(newSpan.start())) {
        logger.info("Querying database for users...");
        List<User> users = jdbcTemplate.query("SELECT * FROM users", new BeanPropertyRowMapper<>(User.class));
        return users;
    } finally {
        newSpan.end();
    }
}

7. Baggage:传递上下文信息

有时候,你需要在服务之间传递一些上下文信息,比如用户 ID、请求 ID 等。Sleuth 提供了 Baggage 功能,让你能够方便地传递这些信息。

@Autowired
private BaggageField userIdField;

public void doSomething(String userId) {
    userIdField.updateValue(userId);
    // ...
}

public String getUserId() {
    return userIdField.getValue();
}

首先,你需要定义一个 BaggageField

@Configuration
public class BaggageConfig {

    @Bean
    public BaggageField userIdField() {
        return BaggageField.create("user-id");
    }
}

然后,你就可以使用 BaggageField 来设置和获取 Baggage 的值。Baggage 的值会自动传递给下游服务。

8. 注意事项和最佳实践

  • 采样率: 在生产环境中,不要将采样率设置为 1.0,否则会对性能产生影响。建议根据实际情况设置一个合适的采样率。
  • 数据存储: Zipkin 默认使用内存存储数据,不适合生产环境。建议使用 MySQL、Cassandra 等数据库来存储数据。
  • 数据清理: 定期清理 Zipkin 中的数据,避免数据量过大。
  • 日志格式: 统一日志格式,方便分析日志。
  • 监控报警: 设置监控报警,及时发现问题。
  • 安全性: 确保 Zipkin 的安全性,避免未经授权的访问。

9. 总结

分布式追踪是微服务架构中不可或缺的一部分。Spring Cloud Sleuth 和 Zipkin 是两个强大的工具,可以帮助你轻松地实现分布式追踪。通过使用 Sleuth 和 Zipkin,你可以:

  • 监控服务调用链,定位性能瓶颈,分析错误原因。
  • 优化系统性能,建立服务依赖关系,方便进行容量规划和故障隔离。
  • 让你的微服务变得透明,让问题无处遁形。

希望这篇文章能够帮助你更好地理解和使用 Spring Cloud Sleuth 和 Zipkin。记住,优秀的程序员就像优秀的侦探,总是能够找到问题的根源,解决问题,让世界变得更加美好。

祝你编码愉快!

发表回复

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