zipkin使用_使用Spring Cloud Sleuth实现微服务链路追踪

1. 介绍

在微服务项目中,一个请求到达后端后,在处理业务的过程中,可能还会调用其他多个微服务来实现功能,在这个过程中,整个请求的链路追踪就非常重要,我们需要知道每个节点的调用信息。通过这些信息我们能够在程序报错时快速定位到问题或者根据每个节点的请求情况进行性能优化。

Spring Cloud Sleuth是Spring Cloud官方提供的针对分布式链路追踪的解决方案。

1.1 基本术语

Spring Cloud Sleuth的术语借鉴了Google的Dapper。

Span:基本工作单元。最常见的就是发送一个HTTP请求调用另一个微服务。

Trace:一系列span组成的树状结构。

Annotation:用于记录链路中事件。

  • cs: Client Sent,客户端发送一个请求,此注解表示一个span的开始。
  • sr: Server Received,服务端接收到请求并开始处理,此时时间戳减去cs的时间戳即可得到网络延迟。
  • ss: Server Sent,请求处理完成时的注解,即发送响应给客户端时。此时时间戳减去sr的时间戳即可得到服务端处理请求所需的时间。
  • cr: Client Received,客户端成功接收到服务端的响应,表明了一个span的结束。此时时间戳减去cs的时间戳即可得到整个请求所需的时间。

下面是一个官方示意图,展现了一个完整的请求链路,每个颜色表示一个span。

ca05f472d5def517f919d2ed79d95b39.png

2. 快速使用

2.1 引入依赖

<dependency>
    <groupId>org.springframework.cloudgroupId>
    <artifactId>spring-cloud-starter-sleuthartifactId>
dependency>

启动项目后,会出现类似下面的日志:

2020-11-03 17:36:29.472  INFO [foo,a8cfa21613384b70,a8cfa21613384b70,true] 22392 --- [     parallel-1] ...
2020-11-03 17:36:29.474  INFO [foo,a8cfa21613384b70,a8cfa21613384b70,true] 22392 --- [     parallel-1] ...

引入依赖后,Spring Cloud Sleuth就会将traceId和spanId等信息添加到Slf4J MDC中。此功能主要通过TraceEnvironmentPostProcessor修改了logging.pattern.level属性。上面的[foo,a8cfa21613384b70,a8cfa21613384b70,true]分别为:

  • applicationName,应用名
  • traceId
  • spanId
  • exportable,是否导出,比如发送到Zipkin

2.2 属性配置

spring:
  sleuth:
    enabled: true # 开启sleuth,默认true
    trace-id128: false # 生成128位id,默认false生成64位
    sampler:
      probability: 1.0 # 采样率,此处设置为100%采样,优先于限流
      rate: 10 # 限流,每秒钟次数,默认10
    log:
      slf4j:
        enabled: true # 开启Slf4J MDC功能,默认true

2.3 组件支持

  • OpenTracing
  • Runnable和Callable
  • Spring Cloud CircuitBreaker
  • Hystrix
  • Rxjava
  • HTTP
  • HTTP Client,包括RestTemplateWebClient
  • Feign
  • gRPC
  • 异步方法,包括@Async@Scheduled注解的方法以及ExecutorExecutorServiceScheduledExecutorService
  • Messaging,包括RabbitMQKafkaJMS
  • Zuul
  • Redis
  • Quartz
  • Reactor

3. Zipkin

生成Trace信息后,我们还需要一个系统来查看和分析链路信息,这时候我们就需要Zipkin了。Zipkin是一个分布式跟踪系统。它可以根据收集到的数据来分析系统中的延迟问题,而且还可以根据条件进行查找。

dd1cb58e0f7b8782d5769afe65360a89.png

Zipkin UI还提供了一个依赖关系图,能够显示每个应用的具体请求情况。

2b67e1748a0de2afac8d07fb7a9b96b7.png

3.1 引入依赖

如果我们在项目中引入了依赖,那么当一个span关闭的时候,它就会异步发送到Zipkin服务器。

<dependency>
    <groupId>org.springframework.cloudgroupId>
    <artifactId>spring-cloud-starter-zipkinartifactId>
dependency>

3.2 属性配置

3.2.1 Web

默认情况下,应用通过Web的方式发送数据到Zipkin,可以通过属性配置Zipkin服务器地址。

spring:
  zipkin:
    sender:
      type: web
    base-url: http://localhost:9411/
    compression:
      enabled: true #压缩
3.2.2 RabbitMQ

需要引入spring-rabbit依赖,默认发送到zipkin队列。

spring:
  zipkin:
    sender:
      type: rabbit
    rabbitmq:
      queue: zipkin
3.2.3 Kafka

需要引入spring-kafka依赖,默认发送到zipkin主题。

spring:
  zipkin:
    sender:
      type: kafka
    kafka:
      topic: zipkin

3.3 Zipkin服务器搭建

3.3.1 启动

可执行jar包方式(最低Java 8)

curl -sSL https://zipkin.io/quickstart.sh | bash -s
java -jar zipkin.jar

docker方式

docker run -d -p 9411:9411 openzipkin/zipkin
3.3.2 配置

Zipkin配置主要包含2个方面:数据收集和数据存储。

一般情况下我们还会生成依赖关系图,所以可以使用docker-compose的方式一起启动zipkinzipkin-dependencies,这里以RabbitMQ收集ElasticSearch存储为例:

version: '2'

services:
  # The zipkin process services the UI, and also exposes a POST endpoint that
  # instrumentation can send trace data to. Scribe is disabled by default.
  zipkin:
    image: openzipkin/zipkin:2.22.0
    container_name: zipkin
    # Environment settings are defined here https://github.com/openzipkin/zipkin/blob/master/zipkin-server/README.md#environment-variables
    environment:
      # collector-rabbitmq
      - RABBIT_ADDRESSES=http://localhost:5672
      - RABBIT_USER=guest
      - RABBIT_PASSWORD=guest
      - RABBIT_VIRTUAL_HOST=/
      # zipkin-storage-elasticsearch
      - STORAGE_TYPE=elasticsearch
      - ES_HOSTS=http://localhost:9200
      - ES_USERNAME=elastic
      - ES_PASSWORD=elastic
      - ES_DATE_SEPARATOR=.
      - ES_INDEX_SHARDS=1
      - ES_INDEX_REPLICAS=0
      # Uncomment to see requests to and from elasticsearch
      # - ES_HTTP_LOGGING=BODY
      # Uncomment to enable scribe
      # - SCRIBE_ENABLED=true
      # Uncomment to enable self-tracing
      # - SELF_TRACING_ENABLED=true
      # Uncomment to enable debug logging
      # - JAVA_OPTS=-Dlogging.level.zipkin2=DEBUG
    restart: always
    ports:
      # Port used for the Zipkin UI and HTTP Api
      - 9411:9411
      # Uncomment if you set SCRIBE_ENABLED=true
      # - 9410:9410

  # Adds a cron to process spans since midnight every hour, and all spans each day
  # This data is served by http://192.168.99.100:8080/dependency
  #
  # For more details, see https://github.com/openzipkin/docker-zipkin-dependencies
  zipkin-dependencies:
    image: openzipkin/zipkin-dependencies:2.3.2
    container_name: zipkin-dependencies
    entrypoint: crond -f
    environment:
      - STORAGE_TYPE=elasticsearch
      - ES_HOSTS=http://localhost:9200
      - ES_USERNAME=elastic
      - ES_PASSWORD=elastic
      - ES_DATE_SEPARATOR=.
      # Uncomment to see dependency processing logs
      # - ZIPKIN_LOG_LEVEL=DEBUG
      # Uncomment to adjust memory used by the dependencies job
      # - JAVA_OPTS=-verbose:gc -Xms1G -Xmx1G
    restart: always

4. MySQL、MongoDB集成

4.1 MySQL

Spring Cloud Sleuth默认没有集成对MySQL的支持,通过查看brave-instrumentation-mysql8文档,可以通过在数据库连接地址后追加queryInterceptors=brave.mysql8.TracingQueryInterceptor&exceptionInterceptors=brave.mysql8.TracingExceptionInterceptor两个参数来集成,当然同时也要引入依赖:

<dependency>
    <groupId>io.zipkin.bravegroupId>
    <artifactId>brave-instrumentation-mysql8artifactId>
dependency>

但是通过追加参数的配置方式,每次开启或关闭链路追踪都需要修改数据库配置,比较麻烦,所以想参考官方的方式,只需要配置一个enabled的属性即可控制。

4.1.1 EnvironmentPostProcessor

第一个想到的方案就是自定义一个EnvironmentPostProcessor,在Environment准备好后根据enabled的值来改写数据库连接的url达到目的,但是通过调试发现,此时读取到的Environment的值都是本地配置文件,在Spring Cloud项目中,很多环境下都是从config-server读取配置,而默认情况下远程配置的优先级是最高的,就算此时往Environment里添加PropertySource,改写后的数据库url也会被远程配置覆盖,所以此方法行不通。

EnvironmentPostProcessor的实现类需要在META-INF/spring.factories文件中注册。

# Environment Post Processors
org.springframework.boot.env.EnvironmentPostProcessor=\
com.example.TraceMySQLEnvironmentPostProcessor
4.2.2 BeanPostProcessor

上述方法不可行后,我们可以通过实现BeanPostProcessor接口,在DataSourceProperties调用初始化回调方法之前,设置一个新的url值以达到目的。

public class TraceMySQLBeanPostProcessor implements BeanPostProcessor, EnvironmentAware {

    private static final String URL_PREFIX = "jdbc:";
    private static final String QUERY_INTERCEPTORS_KEY = "queryInterceptors";
    private static final String QUERY_INTERCEPTORS_VALUE = "brave.mysql8.TracingQueryInterceptor";
    private static final String EXCEPTION_INTERCEPTORS_KEY = "exceptionInterceptors";
    private static final String EXCEPTION_INTERCEPTORS_VALUE = "brave.mysql8.TracingExceptionInterceptor";

    private static final boolean TRACE_MYSQL_PRESENT = ClassUtils.isPresent(QUERY_INTERCEPTORS_VALUE,
            TraceMySQLApplicationContextInitializer.class.getClassLoader());

    private Environment environment;

    @Override
    public void setEnvironment(@NonNull Environment environment) {
        this.environment = environment;
    }

    @Override
    public Object postProcessBeforeInitialization(@NonNull Object bean, @NonNull String beanName) {
        if (bean instanceof DataSourceProperties) {
            DataSourceProperties properties = (DataSourceProperties) bean;
            this.postProcessBean(properties);
        }
        return bean;
    }

    private void postProcessBean(DataSourceProperties properties) {
        // 默认不启用
        boolean enabled = environment.getProperty("hosjoy.sleuth.mysql.enabled", boolean.class, false);
        // 启用并且存在相关类
        if (!(enabled && TRACE_MYSQL_PRESENT)) {
            return;
        }
        // 首先获取配置的数据库url
        String url = properties.getUrl();
        DatabaseDriver databaseDriver = DatabaseDriver.fromJdbcUrl(url);
        if (StringUtils.isBlank(url)) {
            return;
        }
        // 不是mysql
        if (!DatabaseDriver.MYSQL.equals(databaseDriver)) {
            return;
        }
        String urlWithoutPrefix = url.substring(URL_PREFIX.length());
        UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(urlWithoutPrefix);
        MultiValueMap oldParams = uriComponentsBuilder.build().getQueryParams();if (!oldParams.containsKey(QUERY_INTERCEPTORS_KEY)) {
            uriComponentsBuilder.queryParam(QUERY_INTERCEPTORS_KEY, QUERY_INTERCEPTORS_VALUE);
        }if (!oldParams.containsKey(EXCEPTION_INTERCEPTORS_KEY)) {
            uriComponentsBuilder.queryParam(EXCEPTION_INTERCEPTORS_KEY, EXCEPTION_INTERCEPTORS_VALUE);
        }
        String newUrl = URL_PREFIX + uriComponentsBuilder.build().toUriString();
        properties.setUrl(newUrl);
    }
}

通过测试发现此方法可行,通过/actuator/configprops接口调用也能发现DataSourceProperties的值确实是新的url,但是调用/actuator/env接口还是老的url,即如果在程序中手动调用environment.getProperty("spring.datasource.url")方法,获取到的还是老的url,不是很完美。

4.2.3 ApplicationContextInitializer

通过追踪Spring Boot的启动过程,发现Spring Cloud Config Client是通过PropertySourceBootstrapConfiguration类来从远程获取配置然后放入Environment中,PropertySourceBootstrapConfiguration就是实现了ApplicationContextInitializer接口。所以我们也可以实现一个自定义的ApplicationContextInitializer,在读取远程配置后,把链路追踪的数据库url添加到Environment中,并设置优先级最高。完整代码如下:

public class TraceMySQLApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {

    public static final String PROPERTY_SOURCE_NAME = "traceMySQLProperties";

    private static final String PROPERTY = "spring.datasource.url";
    private static final String URL_PREFIX = "jdbc:";
    private static final String QUERY_INTERCEPTORS_KEY = "queryInterceptors";
    private static final String QUERY_INTERCEPTORS_VALUE = "brave.mysql8.TracingQueryInterceptor";
    private static final String EXCEPTION_INTERCEPTORS_KEY = "exceptionInterceptors";
    private static final String EXCEPTION_INTERCEPTORS_VALUE = "brave.mysql8.TracingExceptionInterceptor";

    private static final boolean TRACE_MYSQL_PRESENT = ClassUtils.isPresent(QUERY_INTERCEPTORS_VALUE,
            TraceMySQLApplicationContextInitializer.class.getClassLoader());

    /**
     * 优先级最低, 在Spring Cloud Config Client加载完远程配置后再执行, 让traceMySQLProperties的优先级最高
     */
    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }

    @Override
    public void initialize(ConfigurableApplicationContext context) {
        ConfigurableEnvironment environment = context.getEnvironment();
        // 默认不启用
        boolean enabled = environment.getProperty("hosjoy.sleuth.mysql.enabled", boolean.class, false);
        // 启用并且存在相关类
        if (!(enabled && TRACE_MYSQL_PRESENT)) {
            return;
        }
        // 首先获取配置的数据库url
        String url = environment.getProperty(PROPERTY);
        if (StringUtils.isBlank(url)) {
            return;
        }
        DatabaseDriver databaseDriver = DatabaseDriver.fromJdbcUrl(url);
        // 不是mysql
        if (!DatabaseDriver.MYSQL.equals(databaseDriver)) {
            return;
        }
        String urlWithoutPrefix = url.substring(URL_PREFIX.length());
        UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(urlWithoutPrefix);
        MultiValueMap oldParams = uriComponentsBuilder.build().getQueryParams();if (!oldParams.containsKey(QUERY_INTERCEPTORS_KEY)) {
            uriComponentsBuilder.queryParam(QUERY_INTERCEPTORS_KEY, QUERY_INTERCEPTORS_VALUE);
        }if (!oldParams.containsKey(EXCEPTION_INTERCEPTORS_KEY)) {
            uriComponentsBuilder.queryParam(EXCEPTION_INTERCEPTORS_KEY, EXCEPTION_INTERCEPTORS_VALUE);
        }
        String newUrl = URL_PREFIX + uriComponentsBuilder.build().toUriString();
        ImmutableMap map = ImmutableMap.of(PROPERTY, newUrl);
        MapPropertySource propertySource = new MapPropertySource(PROPERTY_SOURCE_NAME, map);
        environment.getPropertySources().addFirst(propertySource);
    }
}

EnvironmentPostProcessor一样,ApplicationContextInitializer的实现类也需要在META-INF/spring.factories文件中注册。

# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\
com.example.TraceMySQLApplicationContextInitializer

4.2 MongoDB

相比于MySQL,MongoDB的集成就比较简单了,我们只需要根据条件配置一个Bean即可。

<dependency>
    <groupId>io.zipkin.bravegroupId>
    <artifactId>brave-instrumentation-mongodbartifactId>
dependency>
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({MongoClient.class, MongoDBTracing.class})
@ConditionalOnProperty(prefix = SleuthProperties.PREFIX, name = "mongodb.enabled")
@AutoConfigureAfter(org.springframework.cloud.sleuth.autoconfig.TraceAutoConfiguration.class)public class MongoDBConfiguration {

    @Bean
    public MongoClientSettingsBuilderCustomizer traceMongoClientSettingsBuilderCustomizer() {
        CommandListener listener = MongoDBTracing.create(Tracing.current()).commandListener();
        return builder -> builder.addCommandListener(listener);
    }

}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值