Spring Cloud Sleuth介绍

Spring Cloud Sleuth为Spring Cloud实现了分布式追踪解决方案。

术语

Spring Cloud Sleuth借用了Dapper的术语。

Span:基本工作单元,例如,发送RPC是一个新的span,就像向RPC发送响应一样,Span由span的唯一64位ID标识,另一个64位ID标识其所属的Trace。Span还有其他数据,例如描述、带时间戳的事件、键值annotations(标签),导致它们的span的ID以及进程ID(通常是IP地址)。

span可以启动和停止,它们可以追踪自己的时间信息,创建span后,必须在将来的某个时刻停止它。

启动Trace的初始span称为root span,该span的ID值等于trace ID。

Trace:一组span形成的树状结构,例如,如果运行分布式大数据存储,则可能由PUT请求形成trace。

Annotation:用于及时记录事件的存在,使用Brave工具,不再需要为Zipkin设置特殊的事件来了解客户端和服务器是谁、请求在哪里开始以及在哪里结束,然而,出于学习目的,标记这些事件以突出发生了什么类型的操作。

  • cs:Client Sent,客户端发起了一个请求,这个annotation表示span的开始。
  • sr:Server Received,服务器端获得了请求并开始处理它,从此时间戳中减去cs时间戳会显示网络延迟。
  • ss:Server Sent,在请求处理完成时注释(当响应被发送回客户端时),从此时间戳中减去sr时间戳会显示服务器端处理请求所需的时间。
  • cr:Client Received,表示span的结束,客户端已成功从服务器端收到响应,从此时间戳中减去cs时间戳会显示客户端从服务器接收响应所需的全部时间。

下图显示了系统中的SpanTrace,以及Zipkin annotations:

trace-id.png

标记的每种颜色表示一个span(有七个span — 从AG),请考虑以下标记:

Trace Id = X
Span Id = D
Client Sent

此标记表示当前span的Trace Id设置为XSpan Id设置为D,此外,还发生了Client Sent事件。

下图显示了span的父—子关系:

parents.png

用途

以下部分参考上图中显示的示例。

使用Zipkin进行分布式追踪

这个例子有七个span,如果你进入Zipkin中的trace,你可以在第二个trace中看到这个数字,如下图所示:

zipkin-traces.png

但是,如果选择特定trace,则可以看到四个span,如下图所示:

zipkin-ui.png

选择特定trace时,你会看到合并的span,这意味着,如果通过Server Received和Server Sent或Client Received和Client Sent annotations向Zipkin发送了两个span,则它们将显示为单个span。

在这种情况下,为什么七个和四个span之间存在差异?

  • 一个span来自http:/start span,它具有Server Received(sr)和Server Sent(ss)annotations。
  • 两个span来自从service1service2http:/foo端点的RPC调用,Client Sent(cs)和Client Received(cr)事件发生在service1端,Server Received(sr)和Server Sent(ss)事件发生在service2端,这两个span形成一个与RPC调用相关的逻辑span。
  • 两个span来自从service2service3http:/bar端点的RPC调用,Client Sent(cs)和Client Received(cr)事件发生在service2端,Server Received(sr)和Server Sent(ss)事件发生在service3端,这两个span形成一个与RPC调用相关的逻辑span。
  • 两个span来自从service2service4http:/baz端点的RPC调用,Client Sent(cs)和Client Received(cr)事件发生在service2端,Server Received(sr)和Server Sent(ss)事件发生在service4端,这两个span形成一个与RPC调用相关的逻辑span。

因此,如果我们计算物理span,我们有一个来自http:/start,两个来自service1调用service2,两个来自service2调用service3,两个来自service2调用service4,总之,我们总共有七个span。

从逻辑上讲,我们看到四个总Span的信息,因为我们有一个与传入请求到service1相关的span和三个与RPC调用相关的span。

可视化错误

Zipkin允许你可视化trace中的错误,当抛出异常而没有被捕获时,在span上设置了适当的标签,然后Zipkin可以正确地着色,你可以在trace列表中看到一条红色的trace,出现这种情况是因为抛出了异常。

如果单击该trace,则会看到类似的图片,如下所示:

zipkin-error-traces.png

如果你随后单击其中一个span,则会看到以下内容:

zipkin-error-trace-screenshot.png

span显示错误的原因以及与之相关的整个堆栈追踪。

Brave的分布式追踪

从版本2.0.0开始,Spring Cloud Sleuth使用Brave作为追踪库,因此,Sleuth不再负责存储上下文,而是将该工作委托给Brave。

由于Sleuth与Brave有不同的命名和标记约定,Spring决定从现在开始遵循Brave的约定,但是,如果要使用遗留的Sleuth方法,可以将spring.sleuth.http.legacy.enabled属性设置为true

实例

pws.png

点击这里查看实例!

Zipkin中的依赖关系图应类似于以下图像:

dependencies.png

日志相关

当使用grep通过扫描等于(例如)2485ec27856c56f4的trace ID来读取这四个应用程序的日志时,你将获得类似于以下内容的输出:

service1.log:2016-02-26 11:15:47.561  INFO [service1,2485ec27856c56f4,2485ec27856c56f4,true] 68058 --- [nio-8081-exec-1] i.s.c.sleuth.docs.service1.Application   : Hello from service1. Calling service2
service2.log:2016-02-26 11:15:47.710  INFO [service2,2485ec27856c56f4,9aa10ee6fbde75fa,true] 68059 --- [nio-8082-exec-1] i.s.c.sleuth.docs.service2.Application   : Hello from service2. Calling service3 and then service4
service3.log:2016-02-26 11:15:47.895  INFO [service3,2485ec27856c56f4,1210be13194bfe5,true] 68060 --- [nio-8083-exec-1] i.s.c.sleuth.docs.service3.Application   : Hello from service3
service2.log:2016-02-26 11:15:47.924  INFO [service2,2485ec27856c56f4,9aa10ee6fbde75fa,true] 68059 --- [nio-8082-exec-1] i.s.c.sleuth.docs.service2.Application   : Got response from service3 [Hello from service3]
service4.log:2016-02-26 11:15:48.134  INFO [service4,2485ec27856c56f4,1b1845262ffba49d,true] 68061 --- [nio-8084-exec-1] i.s.c.sleuth.docs.service4.Application   : Hello from service4
service2.log:2016-02-26 11:15:48.156  INFO [service2,2485ec27856c56f4,9aa10ee6fbde75fa,true] 68059 --- [nio-8082-exec-1] i.s.c.sleuth.docs.service2.Application   : Got response from service4 [Hello from service4]
service1.log:2016-02-26 11:15:48.182  INFO [service1,2485ec27856c56f4,2485ec27856c56f4,true] 68058 --- [nio-8081-exec-1] i.s.c.sleuth.docs.service1.Application   : Got response from service2 [Hello from service2, response from service3 [Hello from service3] and from service4 [Hello from service4]]

如果你使用日志聚合工具(例如KibanaSplunk等),你可以排序发生的事件,Kibana的一个例子类似于下图:

kibana.png

如果要使用Logstash,以下列表显示Logstash的Grok模式:

filter {
       # pattern matching logback pattern
       grok {
              match => { "message" => "%{TIMESTAMP_ISO8601:timestamp}\s+%{LOGLEVEL:severity}\s+\[%{DATA:service},%{DATA:trace},%{DATA:span},%{DATA:exportable}\]\s+%{DATA:pid}\s+---\s+\[%{DATA:thread}\]\s+%{DATA:class}\s+:\s+%{GREEDYDATA:rest}" }
       }
}
如果要将Grok与Cloud Foundry中的日志一起使用,则必须使用以下模式:
filter {
       # pattern matching logback pattern
       grok {
              match => { "message" => "(?m)OUT\s+%{TIMESTAMP_ISO8601:timestamp}\s+%{LOGLEVEL:severity}\s+\[%{DATA:service},%{DATA:trace},%{DATA:span},%{DATA:exportable}\]\s+%{DATA:pid}\s+---\s+\[%{DATA:thread}\]\s+%{DATA:class}\s+:\s+%{GREEDYDATA:rest}" }
       }
}
JSON化Logback与Logstash一起使用

通常,你不希望将日志存储在文本文件中,而是存储在Logstash可以立即选择的JSON文件中,为此,你必须执行以下操作(为了便于阅读,在groupId:artifactId:version notation中传递依赖项)。

依赖关系设置

  1. 确保Logback位于类路径上(ch.qos.logback:logback-core)。
  2. 添加Logstash Logback encode,例如,要使用版本4.6,请添加net.logstash.logback:logstash-logback-encoder:4.6

Logback设置

请考虑以下Logback配置文件示例(名为logback-spring.xml)。

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    ​
    <springProperty scope="context" name="springAppName" source="spring.application.name"/>
    <!-- Example for logging into the build folder of your project -->
    <property name="LOG_FILE" value="${BUILD_FOLDER:-build}/${springAppName}"/>​

    <!-- You can override this to have a custom pattern -->
    <property name="CONSOLE_LOG_PATTERN"
              value="%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>

    <!-- Appender to log to console -->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <!-- Minimum logging level to be presented in the console logs-->
            <level>DEBUG</level>
        </filter>
        <encoder>
            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
            <charset>utf8</charset>
        </encoder>
    </appender>

    <!-- Appender to log to file -->​
    <appender name="flatfile" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_FILE}</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_FILE}.%d{yyyy-MM-dd}.gz</fileNamePattern>
            <maxHistory>7</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
            <charset>utf8</charset>
        </encoder>
    </appender>
    ​
    <!-- Appender to log to file in a JSON format -->
    <appender name="logstash" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_FILE}.json</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_FILE}.json.%d{yyyy-MM-dd}.gz</fileNamePattern>
            <maxHistory>7</maxHistory>
        </rollingPolicy>
        <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
            <providers>
                <timestamp>
                    <timeZone>UTC</timeZone>
                </timestamp>
                <pattern>
                    <pattern>
                        {
                        "severity": "%level",
                        "service": "${springAppName:-}",
                        "trace": "%X{X-B3-TraceId:-}",
                        "span": "%X{X-B3-SpanId:-}",
                        "parent": "%X{X-B3-ParentSpanId:-}",
                        "exportable": "%X{X-Span-Export:-}",
                        "pid": "${PID:-}",
                        "thread": "%thread",
                        "class": "%logger{40}",
                        "rest": "%message"
                        }
                    </pattern>
                </pattern>
            </providers>
        </encoder>
    </appender>
    ​
    <root level="INFO">
        <appender-ref ref="console"/>
        <!-- uncomment this to have also JSON logs -->
        <!--<appender-ref ref="logstash"/>-->
        <!--<appender-ref ref="flatfile"/>-->
    </root>
</configuration>

那个Logback配置文件:

  • 将来自应用程序的信息以JSON格式记录到build/${spring.application.name}.json文件中。
  • 已注释掉两个额外的appender:控制台和标准日志文件。
  • 具有与上一节中介绍的相同的日志记录模式。
如果使用自定义logback-spring.xml,则必须在bootstrap而不是application属性文件中传递spring.application.name,否则,你的自定义logback文件无法正确读取该属性。

传播span上下文

span上下文是必须传播到跨进程边界的任何子span的状态,span上下文的一部分是Baggage,trace和span ID是span上下文的必需部分,Baggage是可选部分。

Baggage是一组存储在span上下文中的键:值对,Baggage随着trace一起移动,并附在每一个span上,Spring Cloud Sleuth了解如果HTTP header以baggage-为前缀,则标题与行李相关,并且对于消息,它以baggage_开头。

目前对baggage条目的数量或大小没有限制,但是,请记住,太多可能会降低系统吞吐量或增加RPC延迟,在极端情况下,由于超出传输级别的消息或header容量,过多的baggage可能会使应用程序崩溃。

以下示例显示了在span上设置baggage:

Span initialSpan = this.tracer.nextSpan().name("span").start();
ExtraFieldPropagation.set(initialSpan.context(), "foo", "bar");
ExtraFieldPropagation.set(initialSpan.context(), "UPPER_CASE", "someValue");
Baggage与Span标签

Baggage随trace而行(每个子span包含其父级的baggage),Zipkin不知道baggage,也不接收这些信息。

从Sleuth 2.0.0开始,你必须在项目配置中明确传递baggage键名称。

标签附加到特定span,换句话说,它们仅针对特定span呈现,但是,你可以按标记搜索以查找trace,假设存在具有搜索标记值的span。

如果你希望能够根据baggage查找span,则应在根span中添加相应的条目作为标记。

span必须在scope内。

以下清单显示了使用baggage的集成测试:

设置

spring.sleuth:
  baggage-keys:
    - baz
    - bizarrecase
  propagation-keys:
    - foo
    - upper_case

代码

initialSpan.tag("foo",
        ExtraFieldPropagation.get(initialSpan.context(), "foo"));
initialSpan.tag("UPPER_CASE",
        ExtraFieldPropagation.get(initialSpan.context(), "UPPER_CASE"));

添加Sleuth到项目

本节介绍如何使用Maven或Gradle将Sleuth添加到项目中。

要确保你的应用程序名称在Zipkin中正确显示,请在bootstrap.yml中设置spring.application.name属性。

只有Sleuth(log相关)

如果你想在没有Zipkin集成的情况下仅使用Spring Cloud Sleuth,请将spring-cloud-starter-sleuth模块添加到你的项目中。

以下示例显示如何使用Maven添加Sleuth:

Maven

<dependencyManagement>
      <dependencies>
          <dependency>
              <groupId>org.springframework.cloud</groupId>
              <artifactId>spring-cloud-dependencies</artifactId>
              <version>${release.train.version}</version>
              <type>pom</type>
              <scope>import</scope>
          </dependency>
      </dependencies>
</dependencyManagement>

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
  1. 建议你通过Spring BOM添加依赖关系管理,这样你就无需自行管理版本。
  2. 添加依赖spring-cloud-starter-sleuth

以下示例显示如何使用Gradle添加Sleuth:

Gradle

dependencyManagement {
    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${releaseTrainVersion}"
    }
}

dependencies {
    compile "org.springframework.cloud:spring-cloud-starter-sleuth"
}
  1. 建议你通过Spring BOM添加依赖关系管理,这样你就无需自行管理版本。
  2. 添加依赖spring-cloud-starter-sleuth

通过HTTP Sleuth与Zipkin一起使用

如果你想要Sleuth和Zipkin,请添加spring-cloud-starter-zipkin依赖项。

以下示例显示了如何为Maven执行此操作:

Maven

<dependencyManagement>
      <dependencies>
          <dependency>
              <groupId>org.springframework.cloud</groupId>
              <artifactId>spring-cloud-dependencies</artifactId>
              <version>${release.train.version}</version>
              <type>pom</type>
              <scope>import</scope>
          </dependency>
      </dependencies>
</dependencyManagement>

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

以下示例显示了如何为Gradle执行此操作:

Gradle

dependencyManagement {
    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${releaseTrainVersion}"
    }
}

dependencies {
    compile "org.springframework.cloud:spring-cloud-starter-zipkin"
}

在RabbitMQ或Kafka上Sleuth与Zipkin一起使用

如果你想使用RabbitMQ或Kafka而不是HTTP,添加spring-rabbitspring-kafka依赖项,默认目标名称是zipkin

如果使用Kafka,则必须相应地设置属性spring.zipkin.sender.type属性:

spring.zipkin.sender.type: kafka
spring-cloud-sleuth-stream已弃用且与这些目标不兼容。

如果你想在RabbitMQ上使用Sleuth,请添加spring-cloud-starter-zipkinspring-rabbit依赖项。

以下示例显示了如何为Maven执行此操作:

Maven

<dependencyManagement>
      <dependencies>
          <dependency>
              <groupId>org.springframework.cloud</groupId>
              <artifactId>spring-cloud-dependencies</artifactId>
              <version>${release.train.version}</version>
              <type>pom</type>
              <scope>import</scope>
          </dependency>
      </dependencies>
</dependencyManagement>

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.amqp</groupId>
    <artifactId>spring-rabbit</artifactId>
</dependency>
  1. 建议你通过Spring BOM添加依赖关系管理,这样你就无需自行管理版本。
  2. 添加依赖spring-cloud-starter-zipkin,这样,所有嵌套的依赖项都会被下载。
  3. 要自动配置RabbitMQ,请添加spring-rabbit依赖项。

Gradle

dependencyManagement {
    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${releaseTrainVersion}"
    }
}

dependencies {
    compile "org.springframework.cloud:spring-cloud-starter-zipkin" 2
    compile "org.springframework.amqp:spring-rabbit" 3
}

覆盖Zipkin的自动配置

Spring Cloud Sleuth从2.1.0版开始支持向多个追踪系统发送trace,为了使其工作,每个追踪系统都需要有一个Reporter<Span>Sender,如果要覆盖提供的bean,则需要为它们指定一个特定的名称,为此,你可以分别使用ZipkinAutoConfiguration.REPORTER_BEAN_NAMEZipkinAutoConfiguration.SENDER_BEAN_NAME

@Configuration
protected static class MyConfig {

    @Bean(ZipkinAutoConfiguration.REPORTER_BEAN_NAME)
    Reporter<zipkin2.Span> myReporter() {
        return AsyncReporter.create(mySender());
    }

    @Bean(ZipkinAutoConfiguration.SENDER_BEAN_NAME)
    MySender mySender() {
        return new MySender();
    }

    static class MySender extends Sender {

        private boolean spanSent = false;

        boolean isSpanSent() {
            return this.spanSent;
        }

        @Override
        public Encoding encoding() {
            return Encoding.JSON;
        }

        @Override
        public int messageMaxBytes() {
            return Integer.MAX_VALUE;
        }

        @Override
        public int messageSizeInBytes(List<byte[]> encodedSpans) {
            return encoding().listSizeInBytes(encodedSpans);
        }

        @Override
        public Call<Void> sendSpans(List<byte[]> encodedSpans) {
            this.spanSent = true;
            return Call.create(null);
        }

    }

}

额外的资源

你可以点击这里观看Reshmi KrishnaMarcin Grzejszczak关于Spring Cloud Sleuth和Zipkin的视频。

你可以在openzipkin/sleuth-webmvc-example存储库中检查Sleuth和Brave的不同设置。



博弈
2.5k 声望1.5k 粉丝

态度决定一切