Spring Cloud Sleuth 为 Spring Cloud 实现了一个分布式跟踪解决方案。 Spring Cloud Sleuth 借用了 Dapper 的术语。 Span: 基本工作单位。例如,发送一个 RPC 是一个新的范围,发送一个对 RPC 的响应也是如此。 Span 由 Span 的唯一 64 位 ID 和 Span 所属跟踪的另一个 64 位 ID 标识。 Span 还具有其他数据,例如描述、时间戳事件、键值注释(标记)、导致这些数据的 Span 的 ID 以及进程 ID(通常为 IP 地址)。 Span 可以开始和停止,并且它们可以跟踪它们的时间信息。一旦你创建了一个 Span ,你必须在将来的某个时候停止它。
Trace: 形成树形结构的一组 Span 。例如,如果你运行一个分布式大数据存储,那么可能会通过一个 Annotation: 用于及时记录事件的存在。使用 Brave 工具,我们不再需要为 Zipkin 设置特殊事件来了解客户端和服务器是谁、请求的开始位置和结束位置。然而,为了学习的目的,我们标记这些事件来突出发生了什么样的行为。
下图展示了系统中的 Span 和 Trace,以及 Zipkin 注解: ![]() 便条签的每种颜色都表示一个 Span (有七个 Span - 从 A 到 G)。请考虑以下注意事项: Trace Id = X Span Id = D Client Sent
此说明表明当前 Span 的 Trace Id 设置为 X,而 Span Id 设置为 D。此外, 下图显示了 Span 的父子关系: ![]() 以下各节参考上图中所示的示例。 这个例子有七个 Span 。如果你转到 Zipkin 中的跟踪,可以在第二个跟踪中看到这个数字,如下图所示: ![]() 但是,如果选择特定的跟踪,则可以看到四个 Span ,如下图所示: ![]()
在这种情况下,为什么七个和四个 Span 有区别?
因此,如果计算物理 Span ,我们有一个来自
从逻辑上讲,我们看到了四个总 Span 的信息,因为我们有一个 Span 与传入的 Zipkin 可以让你在跟踪中可视化错误。当一个异常被抛出并且没有被捕获时,我们在 Span 上设置了适当的标记,然后 Zipkin 可以正确地着色。可以在记录道列表中看到一条红色的记录道。这是因为引发了异常。 如果单击该跟踪,则会看到类似的图片,如下所示: ![]() 如果单击其中一个 Span ,将看到以下内容 ![]() 该 Span 显示了错误的原因以及与之相关的整个堆栈跟踪。
从
由于 Sleuth 的命名和标记约定不同于 Brave,我们决定从现在开始遵循 Brave 的约定。但是,如果要使用传统的 Sleuth 方法,可以将 Zipkin 中的依赖关系图应类似于下图: ![]()
当使用 grep 通过扫描等于(例如) 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]] 如果使用日志聚合工具(如 Kibana、Splunk 和其他工具),则可以对发生的事件进行排序。Kibana 的一个例子类似于下图: ![]() 如果要使用 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}" } } }
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 文件中,Logstash 可以立即选择该文件。为此,必须执行以下操作(为了可读性,我们在 依赖项设置
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 配置文件:
Span 上下文是必须跨流程边界传播到任何子 Span 的状态。 Span 上下文的一部分是 Baggage。跟踪和 Span ID 是 Span 上下文的必需部分。Baggage 是可选部分。
Baggage 是存储在 Span 上下文中的一组 key:value 对。Baggage 随跟踪一起移动,并附在每个 Span 上。Spring Cloud Sleuth 理解,如果 HTTP 头前面加上
下面的示例展示在 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 都包含其父 Span 的 baggage)。Zipkin 不知道 baggage,也不接收这些信息。
标签附加到特定 Span 。换言之,它们只针对特定的 Span 呈现。但是,可以通过标记搜索来查找跟踪,假定存在具有搜索标记值的 Span 。 如果你希望能够基于 baggage 查找一个 Span ,那么应该在根 Span 中添加一个相应的条目作为标记。
以下列展示了使用 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")); 本节介绍如何使用 Maven 或 Gradle 向项目中添加 Sleuth。
如果你只想使用 Spring Cloud Sleuth 而不使用 Zipkin 集成,请将 下面的示例展示如何使用 Maven 添加 Sleuth: Maven. <dependencyManagement> 下面的示例展示如何使用 Gradle 添加 Sleuth: Gradle. dependencyManagement {
如果同时需要 Sleuth 和 Zipkin,请添加 下面的示例展示了如何使用 Maven 执行此操作: Maven. <dependencyManagement> 下面的示例展示了如何使用 Gradle 执行此操作: Gradle. dependencyManagement {
如果要使用 RabbitMQ 或 Kafka 而不是 HTTP,请添加
如果使用 Kafka,则必须相应地设置属性 spring.zipkin.sender.type: kafka
如果你希望在 RabbitMQ 上使用 Sleuth,请添加 下面的示例展示了如何使用 Gradle 执行此操作: Maven. <dependencyManagement>
Gradle. dependencyManagement {
Spring Cloud Sleuth 支持从 2.1.0 版开始向多个跟踪系统发送跟踪。为了使其正常工作,每个跟踪系统都需要有一个 @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); } } } |