Spring Cloud Sleuth使用簡介

文章主要翻譯自Spring Cloud Sleuth官方文檔

Spring-Cloud

Spring Cloud為開發者提供了在分布式系統(如配置管理、服務發現、斷路器、智能路由、微代理、控制總線、一次性Token、全局鎖、決策競選、分布式會話和集群狀態)操作的開發工具。使用SpringCloud開發者可以快速實現上述這些模式。

Spring Cloud Sleuth

Distributed tracing for Spring Cloud applications, compatiblewith Zipkin, HTrace and log-based(e.g. ELK)tracing.
Spring-Cloud-Sleuth是Spring Cloud的組成部分之一,為SpringCloud應用實現了一種分布式追蹤解決方案,其兼容了Zipkin, HTrace和log-based追蹤

術語(Terminology)

Span:基本工作單元,例如,在一個新建的span中發送一個RPC等同于發送一個回應請求給RPC,span通過一個64位ID唯一標識,trace以另一個64位ID表示,span還有其他數據信息,比如摘要、時間戳事件、關鍵值注釋(tags)、span的ID、以及進度ID(通常是IP地址)
span在不斷的啟動和停止,同時記錄了時間信息,當你創建了一個span,你必須在未來的某個時刻停止它。
Trace:一系列spans組成的一個樹狀結構,例如,如果你正在跑一個分布式大數據工程,你可能需要創建一個trace。
Annotation:用來及時記錄一個事件的存在,一些核心annotations用來定義一個請求的開始和結束

  • cs- Client Sent -客戶端發起一個請求,這個annotion描述了這個span的開始
  • sr- Server Received -服務端獲得請求并準備開始處理它,如果將其sr減去cs時間戳便可得到網絡延遲
  • ss- Server Sent -注解表明請求處理的完成(當請求返回客戶端),如果ss減去sr時間戳便可得到服務端需要的處理請求時間
  • cr- Client Received -表明span的結束,客戶端成功接收到服務端的回復,如果cr減去cs時間戳便可得到客戶端從服務端獲取回復的所有所需時間

將Span和Trace在一個系統中使用Zipkin注解的過程圖形化:


每個顏色的注解表明一個span(總計7個spans,從A到G),如果在注解中有這樣的信息:
Trace Id = X
Span Id = D
Client Sent
這就表明當前span將Trace-Id設置為X,將Span-Id設置為D,同時它還表明了ClientSent事件。
spans的parent/child關系圖形化:

目的(Purpose)

基于Zipkin的分布式追蹤
總計11個spans,如果在Zipkin中查看traces將看到如下圖:


但如果你選取一個特殊的trace你將看到8個spans:

當選取一個特殊trace時你會看到合并的spans,這意味著如果有兩個spans使用客戶端接收發送/服務端接收發送注解發送至Zipkin時,他們將表現為一個單獨的span
在展示Span和Trace圖形化的圖片中有20個顏色標簽,Zipkin又是如何接收10個spans的呢?

  • 2個span A標簽表明span的開始和結束,接近結束時一個單獨的span發送給Zipkin
  • 4個span B標簽實際上是一個有4個注解的單獨span,然而這個span是由兩個分離的實例組成的,一個由service 1發出,一個由service 2發出,因此實際上兩個span實例是發送到Zipkin并在那合并
  • 2個span C標簽表明span的開始和結束,接近結束時一個單獨的span發送給Zipkin
  • 4個span D標簽實際上是一個有4個注解的單獨span,然而這個span是由兩個分離的實例組成的,一個由service 2發出,一個由service 3發出,因此實際上兩個span實例是發送到Zipkin并在那合并
  • 2個span E標簽表明span的開始和結束,接近結束時一個單獨的span發送給Zipkin
  • 4個span F標簽實際上是一個有4個注解的單獨span,然而這個span是由兩個分離的實例組成的,一個由service 2發出,一個由service 4發出,因此實際上兩個span實例是發送到Zipkin并在那合并
  • 2個span G標簽表明span的開始和結束,接近結束時一個單獨的span發送給Zipkin

因此1個span來自A,2個span來自B,1個span來自C,2個span來自D,1個span來自E,2個span來自F,1個來自G,總計10個spans。
Zipkin中的依賴圖:


Log相關
當使用trace id為2485ec27856c56f4抓取這四個應用的log時,會獲得如下輸出:

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]]

如果你使用log集合工具例如Kibana、Splunk等,你可以看到事件的發生信息,Kibana的例子如下:



以下是Logstash的Grok模式:

filter {
# pattern matching logback pattern
grok {
match => { "message" => "%{TIMESTAMP_ISO8601:timestamp}\s+%{LOGLEVEL:severity}\s+
\s+%{DATA:pid}---\s+
\s+%{DATA:class}\s+:\s+%{GREEDYDATA:rest}" }
}
}

JSON Logback with Logstash
為了方便獲取Logstash,通常保存log在JSON文件中而不是text文件中,配置方法如下:
依賴建立
確保Logback在classpath中(ch.qos.logback:logback-core)
增加LogstashLogback編碼- version 4.6的例子:net.logstash.logback:logstash-logback-encoder:4.6
Logback建立
以下是一個Logback配置的例子:

  • 使用JSON格式記錄應用信息到build/${spring.application.name}.json文件
  • 有兩個添加注釋源- console和標準log文件
  • 與之前章節使用相同的log模式
<?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}"/>
 
    <property name="CONSOLE_LOG_PATTERN"
              value="%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr([${springAppName:-},%X{X-B3-TraceId:-},%X{X-B3-SpanId:-},%X{X-Span-Export:-}]){yellow} %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>INFO</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:-}",
                        "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"/>-->
        <appender-ref ref="logstash"/>
        <!--<appender-ref ref="flatfile"/>-->
    </root>
</configuration>

添加進工程

僅Sleuth(log收集)
如果僅需要Spring Cloud Sleuth而不需要Zipkin集成,只需要增加spring-cloud-starter-sleuth模塊到你工程中

  1. 為了不手動添加版本號,更好的方式是通過Spring BOM添加dependencymanagement
  2. 添加依賴到spring-cloud-starter-sleuth
<dependencyManagement> (1)
         <dependencies>
             <dependency>
                 <groupId>org.springframework.cloud</groupId>
                 <artifactId>spring-cloud-dependencies</artifactId>
                 <version>Brixton.RELEASE</version>
                 <type>pom</type>
                 <scope>import</scope>
             </dependency>
         </dependencies>
   </dependencyManagement>
 
   <dependency> (2)
       <groupId>org.springframework.cloud</groupId>
       <artifactId>spring-cloud-starter-sleuth</artifactId>
   </dependency>
  1. 為了不手動添加版本號,更好的方式是通過Spring BOM添加dependencymanagement
  2. 添加依賴到spring-cloud-starter-sleuth
dependencyManagement { (1)
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:Brixton.RELEASE"
}
}
dependencies { (2)
compile "org.springframework.cloud:spring-cloud-starter-sleuth"
}

通過HTTP使用基于Zipkin的Sleuth
如果你需要Sleuth和Zipkin,只需要添加spring-cloud-starter-zipkin依賴

  1. 為了不手動添加版本號,更好的方式是通過Spring BOM添加dependencymanagement
  2. 添加依賴到spring-cloud-starter-zipkin
<dependencyManagement> (1)
         <dependencies>
             <dependency>
                 <groupId>org.springframework.cloud</groupId>
                 <artifactId>spring-cloud-dependencies</artifactId>
                 <version>Brixton.RELEASE</version>
                 <type>pom</type>
                 <scope>import</scope>
             </dependency>
         </dependencies>
   </dependencyManagement>
 
   <dependency> (2)
       <groupId>org.springframework.cloud</groupId>
       <artifactId>spring-cloud-starter-zipkin</artifactId>
   </dependency>
  1. 為了不手動添加版本號,更好的方式是通過Spring BOM添加dependencymanagement
  2. 添加依賴到spring-cloud-starter-zipkin
dependencyManagement { (1)
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:Brixton.RELEASE"
}
}
dependencies { (2)
compile "org.springframework.cloud:spring-cloud-starter-zipkin"
}

通過Spring Cloud Stream使用Sleuth+Zipkin

  1. 為了不手動添加版本號,更好的方式是通過Spring BOM添加dependencymanagement
  2. 添加依賴到spring-cloud-sleuth-stream
  3. 添加依賴到spring-cloud-starter-sleuth
  4. 添加一個binder(e.g.Rabbit binder)來告訴Spring Cloud Stream應該綁定什么
<dependencyManagement> (1)
         <dependencies>
             <dependency>
                 <groupId>org.springframework.cloud</groupId>
                 <artifactId>spring-cloud-dependencies</artifactId>
                 <version>Brixton.RELEASE</version>
                 <type>pom</type>
                 <scope>import</scope>
             </dependency>
         </dependencies>
   </dependencyManagement>
 
   <dependency> (2)
       <groupId>org.springframework.cloud</groupId>
       <artifactId>spring-cloud-sleuth-stream</artifactId>
   </dependency>
   <dependency> (3)
       <groupId>org.springframework.cloud</groupId>
       <artifactId>spring-cloud-starter-sleuth</artifactId>
   </dependency>
   <!-- EXAMPLE FOR RABBIT BINDING -->
   <dependency> (4)
       <groupId>org.springframework.cloud</groupId>
       <artifactId>spring-cloud-stream-binder-rabbit</artifactId>
   </dependency>
  1. 為了不手動添加版本號,更好的方式是通過Spring BOM添加dependencymanagement
  2. 添加依賴到spring-cloud-sleuth-stream
  3. 添加依賴到spring-cloud-starter-sleuth
  4. 添加一個binder(e.g.Rabbit binder)來告訴Spring Cloud Stream應該綁定什么
dependencyManagement { (1)
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:Brixton.RELEASE"
}
}
dependencies {
compile "org.springframework.cloud:spring-cloud-sleuth-stream" (2)
compile "org.springframework.cloud:spring-cloud-starter-sleuth" (3)
// Example for Rabbit binding
compile "org.springframework.cloud:spring-cloud-stream-binder-rabbit" (4)
}

Spring Cloud Sleuth Stream Zipkin Collector
啟動一個Spring Cloud Sleuth Stream Zipkin收集器只需要添加spring-cloud-sleuth-zipkin-stream依賴

  1. 為了不手動添加版本號,更好的方式是通過Spring BOM添加dependencymanagement
  2. 添加依賴到spring-cloud-sleuth-zipkin-stream
  3. 添加依賴到spring-cloud-starter-sleuth
  4. 添加一個binder(e.g.Rabbit binder)來告訴Spring Cloud Stream應該綁定什么
<dependencyManagement> (1)
         <dependencies>
             <dependency>
                 <groupId>org.springframework.cloud</groupId>
                 <artifactId>spring-cloud-dependencies</artifactId>
                 <version>Brixton.RELEASE</version>
                 <type>pom</type>
                 <scope>import</scope>
             </dependency>
         </dependencies>
   </dependencyManagement>
 
   <dependency> (2)
       <groupId>org.springframework.cloud</groupId>
       <artifactId>spring-cloud-sleuth-zipkin-stream</artifactId>
   </dependency>
   <dependency> (3)
       <groupId>org.springframework.cloud</groupId>
       <artifactId>spring-cloud-starter-sleuth</artifactId>
   </dependency>
   <!-- EXAMPLE FOR RABBIT BINDING -->
   <dependency> (4)
       <groupId>org.springframework.cloud</groupId>
       <artifactId>spring-cloud-stream-binder-rabbit</artifactId>
   </dependency>

spring-cloud-stream-binder-rabbit

  1. 為了不手動添加版本號,更好的方式是通過Spring BOM添加dependencymanagement
  2. 添加依賴到spring-cloud-sleuth-zipkin-stream
  3. 添加依賴到spring-cloud-starter-sleuth
  4. 添加一個binder(e.g.Rabbit binder)來告訴Spring Cloud Stream應該綁定什么
dependencyManagement { (1)
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:Brixton.RELEASE"
}
}
dependencies {
compile "org.springframework.cloud:spring-cloud-sleuth-zipkin-stream" (2)
compile "org.springframework.cloud:spring-cloud-starter-sleuth" (3)
// Example for Rabbit binding
compile "org.springframework.cloud:spring-cloud-stream-binder-rabbit" (4)
}
之后只需要在你的主類中添加@EnableZipkinStreamServer注解
package example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.sleuth.zipkin.stream.EnableZipkinStreamServer;
@SpringBootApplication
@EnableZipkinStreamServer
public class ZipkinStreamServerApplication {
public static void main(String[] args) throws Exception {
SpringApplication.run(ZipkinStreamServerApplication.class, args);
}
}

特點(Features)

添加trace和spanid到Slf4J MDC,然后就可以從一個給定的trace或span中提取所有的log,例如

2016-02-02 15:30:57.902  INFO [bar,6bfd228dc00d216b,6bfd228dc00d216b,false] 23030 --- [nio-8081-exec-3] ...
2016-02-02 15:30:58.372 ERROR [bar,6bfd228dc00d216b,6bfd228dc00d216b,false] 23030 --- [nio-8081-exec-3] ...
2016-02-02 15:31:01.936  INFO [bar,46ab0d418373cbc9,46ab0d418373cbc9,false] 23030 --- [nio-8081-exec-4] ...

注意MDC中的[appname,traceId,spanId,exportable]:

  • spanId- the id of a specific operation that took place
  • appname- the name of the application that logged the span
  • traceId- the id of the latency graph that contains the span
  • exportable- whether the log should be exported to Zipkin or not. Whenwould you like the span not to be exportable? In the case in which you want towrap some operation in a Span and have it written to the logs only.

在通常的分布式追蹤數據模型上提供一種抽象模型:traces、spans(生成一個DAG)、annotations、key-value annotations。基于HTrace是較為寬松的,但Zipkin(Dapper)更具兼容性
Sleuth記錄時間信息來幫助延遲分析,使用Sleuth可以精確找到應用中延遲的原因,Sleuth不會log太多,因此不會導致你的應用掛掉

  • propagatesstructural data about your call-graph in-band, and the rest out-of-band
  • includesopinionated instrumentation of layers such as HTTP
  • includessampling policy to manage volume
  • canreport to a Zipkin system for query and visualization

使用Spring應用裝備出入口點(servletfilter、async endpoints、rest template、scheduled actions、messagechannels、zuul filters、feign client)
Sleuth包含默認邏輯通過http或messaging boundaries來加入一個trace,例如,http傳播通過Zipkin-compatiblerequest headers工作,這個傳播邏輯定義和定制是通過SpanInjector和SpanExtractor實現提供簡單的接受或放棄span

度量(metrics)

如果依賴了spring-cloud-sleuth-zipkin,應用將生成并收集Zipkin-compatible traces,一般會通過HTTP將這些traces發送給一個本地Zipkin服務器(port 9411),使用spring.zipkin.baseUrl來配置服務的地址
如果依賴了spring-cloud-sleuth-stream,應用將通過Spring Cloud Stream生成并收集traces,應用自動成為tracer消息的生產者,這些消息會通過你的中間件分發(e.g. RabbitMQ,Apache Kafka,Redis)

如果使用Zipkin或Stream,使用spring.sleuth.sampler.percentage配置輸出spans的百分比(默認10%),不然你可能會認為Sleuth沒有工作,因為他省略了一些spans

SLF4J MDC一直處于工作狀態,logback用戶可以在logs中立刻看到trace和span id,其他logging系統不得不配置他們自己的模式以得到相同的結果,默認logging.pattern.level設置為%clr(%5p) %clr([${spring.application.name:},%X{X-B3-TraceId:-},%X{X-B3-SpanId:-},%X{X-Span-Export:-}]){yellow}(對于logback用戶,這是一種Spring Boot特征),這意味著如果你沒有使用SLF4J這個模式將不會自動適用

抽樣(Samling)

在分布式追蹤時,數據量可能會非常大,因此抽樣就變得非常重要(通常不需要導出所有的spans以得到事件發生原貌),Spring Cloud Sleuth有一個Sampler戰略,即用戶可以控制抽樣算法,Samplers不會停止正在生成的span id(相關的),但他們會阻止tags和events附加和輸出,默認戰略是當一個span處于活躍狀態會繼續trace,但新的span會一直處于不輸出狀態,如果所有應用都使用這個sampler,你會在logs中看到traces,但不會出現在任何遠程倉庫。測試狀態資源都是充足的,并且你只使用logs的話他就是你需要的全部(e.g.一個ELK集合),如果輸出span數據到Zipkin或Spring Cloud Stream,有AlwaysSampler輸出所有數據和PercentageBasedSampler采樣spans確定的一部分。

如果使用spring-cloud-sleuth-zipkin或spring-cloud-sleuth-stream,PercentageBasedSampler是默認的,你可以使用spring.sleuth.sampler.percentage配置輸出

通過創建一個bean定義就可以新建一個sampler

@Bean
public Sampler defaultSampler() {
return new AlwaysSampler();
}

Instrumentation

Spring Cloud Sleuth自動裝配所有Spring應用,因此你不用做任何事來讓他工作,裝配是使用一系列技術添加的,例如對于一個servlet web應用我們使用一個Filter,對于SpringIntegration我們使用ChannelInterceptors。
用戶可以使用span tags定制關鍵字,為了限制span數據量,一般一個HTTP請求只會被少數元數據標記,例如status code、host以及URL,用戶可以通過配置spring.sleuth.keys.http.headers(一系列頭名稱)添加request headers。

tags僅在Sampler允許其被收集和輸出時工作(默認情況其不工作,因此不會有在不配置的情況下收集過多數據的意外危險出現)

Span生命周期

通過Trace接口的方式可以在Span上進行如下操作:

  • start-當打開一個span時,其名字被指定且開始時間戳被記錄
  • close- span已經結束(span的結束時間已被記錄)并且如果span是輸出的,他將是Zipkin合適的收集項,span在當前線程也將被移除
  • continue- span的一個新實例將被創建,然而他將是正是正在運行的span的一個復制體
  • detach- span不會停止或關閉,他只會被從當前線程中移除
  • create with explicit parent-建立一個新的span并設置一個明確的parent給他

新建和關閉spans
使用Tracer接口可以手動新建spans

// Start a span. If there was a span present in this thread it will become
// the `newSpan`'s parent.
Span newSpan = this.tracer.createSpan("calculateTax");
try {
// ...
// You can tag a span
this.tracer.addTag("taxValue", taxValue);
// ...
// You can log an event on a span
newSpan.logEvent("taxCalculated");
} finally {
// Once done remember to close the span. This will allow collecting
// the span to send it to Zipkin
this.tracer.close(newSpan);
}

在例子中我們可以看到如何新建一個span實例,假設在當前線程中已經有一個span,那么新建的線程將會是這個線程的parent。

新建span后要記得清除他!如果你想要將一個span發送給Zipkin,不要忘記關閉他。

持續(Continuing)spans
有時你不想要新建一個span但你又想持續使用,這種情況的例子可能如下(當然實際依賴于使用情況):

  • AOP-如果在實際應用前已經有一個span新建可用,那么就不需要新建一個span
  • Hystrix-對于當前處理流程而言,執行Hystrix操作是最為合理的一部分,實際上只有技術實現細節的話,不必將他作為分離的部分反映在tracing中

span的持續實例等同于正在運行的:

Span continuedSpan = this.tracer.continueSpan(spanToContinue);
assertThat(continuedSpan).isEqualTo(spanToContinue);

可以使用Tracer接口延續一個span

// let's assume that we're in a thread Y and we've received
// the `initialSpan` from thread X
Span continuedSpan = this.tracer.continueSpan(initialSpan);
try {
// ...
// You can tag a span
this.tracer.addTag("taxValue", taxValue);
// ...
// You can log an event on a span
continuedSpan.logEvent("taxCalculated");
} finally {
// Once done remember to detach the span. That way you'll
// safely remove it from the current thread without closing it
this.tracer.detach(continuedSpan);
}

新建一個span后記得清除他!如果有些工作在一個線程(e.g. thread X)中已經結束并且他在等待另外的線程(e.g. Y,Z)結束時,不要忘記分離span,在線程Y,Z中的spans在他們工作結束時也應被分離,結果收集完成時thread X中的span應該被關閉

使用明確的parent新建spans
如果你想新建一個span并且提供一個明確的parent給他,假設span的parent在一個thread中,而你想在另一個thread中新建span,Tracer接口的startSpan命令就是你需要的。

// let's assume that we're in a thread Y and we've received
// the `initialSpan` from thread X. `initialSpan` will be the parent
// of the `newSpan`
Span newSpan = this.tracer.createSpan("calculateCommission", initialSpan);
try {
// ...
// You can tag a span
this.tracer.addTag("commissionValue", commissionValue);
// ...
// You can log an event on a span
newSpan.logEvent("commissionCalculated");
} finally {
// Once done remember to close the span. This will allow collecting
// the span to send it to Zipkin. The tags and events set on the
// newSpan will not be present on the parent
this.tracer.close(newSpan);
}

記得在新建這樣的span后關閉他,否則你在你的log中看到大量的相關warning,更糟糕的是你的span不會正常關閉,這樣的話就無法被Zipkin收集

命名spans

為span命名是很重要的工作,span名稱必須描述了一個操作名稱,名稱必須要簡明(e.g.不包括標識符)。
Since there is a lot of instrumentation going on some of thespan names will be artificial like:

  • controller-method-namewhen received by a Controller with a methodnameconrollerMethodName
  • asyncfor asynchronous operations done via wrappedCallableandRunnable
  • @Scheduledannotated methods will return the simple nameof the class

Fortunately, for the asynchronous processing you can provideexplicit naming.
@SpanName注解
可以使用@SpanName注解明確命名span

@SpanName("calculateTax")
class TaxCountingRunnable implements Runnable {
@Override public void run() {
// perform logic
}
}

在這種情況下,使用下面的方式便命名一個span為calculateTax

Runnable runnable = new TraceRunnable(tracer, spanNamer, new TaxCountingRunnable());
Future future = executorService.submit(runnable);
// ... some additional logic ...
future.get();

toString()方法
為Runnable或Callable建立分離的classes是非常少見的,一般建立這些classes的匿名實例,你不能注解這些classes除非override,如果沒有@SpanName注解,我們將會檢查class是否使用傳統的toString()方法實現
執行這些代碼將新建一個名為calculateTax的span:

Runnable runnable = new TraceRunnable(tracer, spanNamer, new Runnable() {
@Override public void run() {
// perform logic
}
@Override public String toString() {
return "calculateTax";
}
});
Future future = executorService.submit(runnable);
// ... some additional logic ...
future.get();

定制化

使用SpanInjector和SpanExtractor你可以定制化span的新建和傳播。
當前有兩種built-in方法來在進程間傳遞tracing信息:

  • 通過SpringIntegration
  • 通過HTTP

span id是從Zipkin-compatible(B3)頭中提取的(不論Message或HTTP頭),以此來開始或加入一個存在的trace,trace信息被注入到輸出請求中,這樣后面的步驟就可以提取他。
Spring Integration
對于Spring Integration,存在beans負責span從Message的創建和使用tracing信息裝配MessageBuilder。

@Bean
public SpanExtractor messagingSpanExtractor() {
...
}
@Bean
public SpanInjector messagingSpanInjector() {
...
}

用戶可以使用自己的實現來override他,或者添加@Primary注解到你的bean定義
HTTP
對于HTTP,存在beans負責span從HttpServletRequest的創建和使用tracing信息裝配HttpServletResponse。

@Bean
public SpanExtractor httpServletRequestSpanExtractor() {
...
}
@Bean
public SpanInjector httpServletResponseSpanInjector() {
...
}

用戶可以使用自己的實現來override他,或者添加@Primary注解到你的bean定義
例子
對比傳統的兼容Zipkin,tracingHTTP頭名有以下格式

  • traceid - correlationId
  • spanid - mySpanId

以下是一個SpanExtractor的例子

static class CustomHttpServletRequestSpanExtractor
implements SpanExtractor {
@Override
public Span joinTrace(HttpServletRequest carrier) {
long traceId = Span.hexToId(carrier.getHeader("correlationId"));
long spanId = Span.hexToId(carrier.getHeader("mySpanId"));
// extract all necessary headers
Span.SpanBuilder builder = Span.builder().traceId(traceId).spanId(spanId);
// build rest of the Span
return builder.build();
}
}

以下SpanInjector將被建立

static class CustomHttpServletResponseSpanInjector
implements SpanInjector {
@Override
public void inject(Span span, HttpServletResponse carrier) {
carrier.addHeader("correlationId", Span.idToHex(span.getTraceId()));
carrier.addHeader("mySpanId", Span.idToHex(span.getSpanId()));
// inject the rest of Span values to the header
}
}

并且你可以這樣注冊他們

@Bean
@Primary
SpanExtractor customHttpServletRequestSpanExtractor() {
return new CustomHttpServletRequestSpanExtractor();
}
@Bean
@Primary
SpanInjector customHttpServletResponseSpanInjector() {
return new CustomHttpServletResponseSpanInjector();
}

SpringData as Messages

可以通過Spring Cloud Stream來積累和發送span數據,配置時需要包含spring-cloud-sleuth-streamjar為依賴且增加一個Channel Binder實現方式(e.g. spring-cloud-starter-stream-rabbit對應RabbitMQ或spring-cloud-starter-stream-kafka對應Kafka),使用payload格式Spans將自動把你的app變為一個信息生產者
Zipkin Consumer
有一種特殊而又便利的注解方式,即為span數據建立一個信息消費者,并將他推到一個Zipkin SpanStrore中

@SpringBootApplication
@EnableZipkinStreamServer
public class Consumer {
public static void main(String[] args) {
SpringApplication.run(Consumer.class, args);
}
}

這種應用將通過Spring Cloud Stream Binder監聽不論何種方式傳輸的span數據(e.g.包括spring-cloud-starter-stream-rabbit對應RabbitMQ,和對應Redis和Kafka的類似starter存在),如果添加以下UI依賴

io.zipkin.[Java](http://lib.csdn.net/base/17)
zipkin-autoconfigure-ui

你將啟動一個Zipkin server應用,他將通過端口9411訪問UI和api。
默認SpanStore是in-memory的(適合于demos且啟動迅速),你可以添加MySQL和spring-boot-starter-jdbc到你的系統環境并通過配置激活JDBC SpanStore。例如:

spring:
rabbitmq:
host: ${RABBIT_HOST:localhost}
datasource:
schema: classpath:/mysql.sql
url: jdbc:mysql://${MYSQL_HOST:localhost}/test
username: root
password: root
# Switch this on to create the schema on startup:
initialize: true
continueOnError: true
sleuth:
enabled: false
zipkin:
storage:
type: mysql

@EnableZipkinStreamServer也使用@EnableZipkinServer注解,因此進程也會顯示標準Zipkin服務終端以通過HTTP收集span,且可以通過Zipkin Web UI查詢

定制消費者
使用spring-cloud-sleuth-stream且綁定SleuthSink可以很方便的實現定制消費者。例子:

@EnableBinding(SleuthSink.class)
@SpringBootApplication(exclude = SleuthStreamAutoConfiguration.class)
@MessageEndpoint
public class Consumer {
@ServiceActivator(inputChannel = SleuthSink.INPUT)
public void sink(Spans input) throws Exception {
// ... process spans
}
}

上述的消費者應用明確排除SleuthStreamAutoConfiguration,因此他不會給自己發消息,但這是可選的(你可能想要trace請求到消費者app)

度量(Metrics)

當前Spring Cloud Sleuth記錄非常簡單的spans metrics,使用Spring Boot的metrics support來計算接收丟棄的span數量,當有span發送給Zipkin時,接收span的數量就會增加,如果有錯誤發生,丟棄span數量就會增加。

Integrations

Runable和Callable
如果你要將你的邏輯包裹在Runable或Callable中,足夠將這些classes放到他們的Sleuth代表中。
Runnable的例子:

Runnable runnable = new Runnable() {
@Override
public void run() {
// do some work
}
@Override
public String toString() {
return "spanNameFromToStringMethod";
}
};
// Manual `TraceRunnable` creation with explicit "calculateTax" Span name
Runnable traceRunnable = new TraceRunnable(tracer, spanNamer, runnable, "calculateTax");
// Wrapping `Runnable` with `Tracer`. The Span name will be taken either from the
// `@SpanName` annotation or from `toString` method
Runnable traceRunnableFromTracer = tracer.wrap(runnable);

Callable的例子:

Callable callable = new Callable() {
@Override
public String call() throws Exception {
return someLogic();
}
@Override
public String toString() {
return "spanNameFromToStringMethod";
}
};
// Manual `TraceCallable` creation with explicit "calculateTax" Span name
Callable traceCallable = new TraceCallable<>(tracer, spanNamer, callable, "calculateTax");
// Wrapping `Callable` with `Tracer`. The Span name will be taken either from the
// `@SpanName` annotation or from `toString` method
Callable traceCallableFromTracer = tracer.wrap(callable);

這種方式你可以保證一個新的Span在每次執行時新建和關閉。

Hystrix

傳統并發策略
我們以將所有的Callable實例置入到他們的Sleuth代表-TraceCallable的方式來記錄一個傳統的HystrixConcurrencyStrategy,策略的打開或延續一個span取決于在Hystrix操作被調用前tracing是否在工作,為了使傳統Hystrix并發策略無效可以設置spring.sleuth.hystrix.strategy.enable為false。
手動操作設置
假設你有以下HystrixCommand:

HystrixCommand hystrixCommand = new HystrixCommand(setter) {
@Override
protected String run() throws Exception {
return someLogic();
}
};

為了傳遞tracing信息你必須將同樣的邏輯置于HystrixCommand的Sleuth版本中,也就是TraceCommand:

TraceCommand traceCommand = new TraceCommand(tracer, traceKeys, setter) {
@Override
public String doRun() throws Exception {
return someLogic();
}
};

RxJava
我們記錄了一個典型的RxJavaSchedulersHook,他將所有Action0實例置入到他們的Sleuth代表-TraceAction中,hook打開或延續一個span取決于Action被安排前tracing是否已經在工作,為了使RxJavaSchedulersHook無效可設置spring.sleuth.rxjava.schedulers.hook.enabled為false。
You can define a list of regular expressions for thread names,for which you don’twant a Span to be created. Just provide a comma separated list of regularexpressions in thespring.sleuth.rxjava.schedulers.ignoredthreadsproperty.
HTTP integration
將spring.sleuth.web.enabled配置值設置為false可以使這章中的特征方法無效
HTTP Filter
通過TraceFilter,所有抽樣輸入的請求都會歸結到span的創建,span的名稱為"http+請求發送的路徑",例如,如果請求發送到/foo/bar,名稱即為http:/foo/bar,你可以配置通過spring.sleuth.web.skipPattern,那些URIs將被過濾掉,如果你在環境中添加了ManagementServerProperties,你的contextPath值會附加到過濾配置上。
HandlerIntercepter
由于需要span名稱的精確,我們使用一個TraceHandlerInterceptor來置入一個存在的HandlerInterceptor或直接添加到存在的HandlerInterceptors列表中,TraceHandlerInterceptor添加一個特殊的請求屬性給HttpServletRequest,如果TraceFilter沒有看到屬性,他會建立一個"fallback"span,這是一個建立在服務端的附加的span,此時trace在UI中可以正確的顯示。

HTTP client integration

同步RestTemplate
我們注入一個RestTemplate攔截器來保證所有的tracing信息被發送到請求端,每當一個請求被生成,一個新的span將被創建,他會在接收應答后關閉,為了限制同步RestTemplate只需要設置spring.sleuth.web.client.enabled為false。

你必須注冊一個RestTemplate為bean以使得攔截器可以注入,如果你使用一個新的關鍵字建立一個RestTemplate實例,instrumentation將無法工作

異步RestTemplate
傳統的instrumentation是通過發送接收請求來建立關閉span的,你可以通過注冊你的bean來定制ClientHttpRequestFactory和AsyncClientHttpRequestFactory,記得使用tracing compatible實現方式(e.g.不要忘記將ThreadPoolTaskScheduler置入一個TraceAsyncListenableTaskExecutor),傳統請求工廠例子如下:

Unresolved directive in spring-cloud-sleuth.adoc - include::../../../../spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/client/TraceWebAsyncClientAutoConfigurationTest.java[tags=async_template_factories,indent=0]

通過設置spring.sleuth.web.async.client.enabled為false可以限制AsyncRestTemplate,使默認的TraceAsyncClientHttpRequestFactoryWrapper無效可以設置spring.sleuth.web.async.client.factory.enabled為false,如果你不想創建AsyncRestClient,設置spring.sleuth.web.async.client.template.enabled為false。
Feign
默認Spring Cloud Sleuth通過TraceFeignClientAutoConfiguration提供feign的集成,你可以設置spring.sleuth.feign.enabled為false來使他無效,如果這樣設置那么所有feign相關的裝配都無法發生。
通過FeignBeanPostProcessor feign裝配的部分結束,可以設置spring.sleuth.feign.processor.enabled為false來是他無效化,如果你這樣設置,Spring Cloud Sleuth將不會裝配任何你的傳統feign組件,所有默認裝配保持原有狀態。
異步通信
@Async注解方法
在Spring Cloud Sleuth中,我們裝配異步關聯組件以使得tracing信息可以在threads間傳遞,你可以通過設置spring.sleuth.async.enabled值為false來使其無效化。
如果你使用@Async來注解你的方法,我們將自動建立一個新的span:

  • span名稱將是注解方法名
  • span將被標注為方法類名和方法名

@Scheduled注解方法
在Spring Cloud Sleuth中,我們裝配scheduled執行方法以使得tracing信息可以在threads間傳遞,你可以通過設置spring.sleuth.scheduled.enabled值為false來使其無效化。
如果你使用@Scheduled來注解你的方法,我們將自建立一個新的span:
span名稱將是注解方法名
span將被標注為方法類名和方法名
如果在一些@Scheduled注解類中你想跳過span新建過程,可以設置spring.sleuth.scheduled.skipPattern為一個指定的表達式,這將匹配@Scheduled注解類的完整描述名稱。
Executor, ExecutorServiceand ScheduledExecutorService
我們提供了LazyTraceExecutor,TraceableExecutorService和TraceableScheduledExecutorService。每當一個新的任務被提交、調用或scheduled時,這些實現會建立新的spans。
以下是當使用CompletableFuture時如何用TraceableExecutorService傳遞tracing信息:

CompletableFuture completableFuture = CompletableFuture.supplyAsync(() -> {
// perform some logic
return 1_000_000L;
}, new TraceableExecutorService(executorService,
// 'calculateTax' explicitly names the span - this param is optional
tracer, traceKeys, spanNamer, "calculateTax"));

消息傳遞
Spring Cloud Sleuth集成了Spring Integration。他會建立span來發布或訂閱事件,設置spring.sleuth.integration.enabled為false可以使Spring Integration無效。
Spring Cloud Sleuth到1.0.4版本前都是使用消息傳遞時發送無效tracing頭,這些頭和在HTTP(包含- )發送的名稱時一樣的,為了在1.0.4版本的向后兼容目的,我們開始發送所有有效和無效的頭,請更新到1.0.4,因為在Spring Cloud Sleuth 1.1中我們將會移除對分離頭的支持。
從1.0.4后可以明確設置spring.sleuth.integration.patterns模式來提供你想要包含的tracing信道名稱,默認所有的信道已被包含在內。
Zuul
我們注冊Zuul過濾器來傳播tracing信息(請求頭使用tracing數據填滿),可以設置spring.sleuth.zuul.enabled為false來關閉Zuul服務。
Moreinformation
https://cloud.spring.io/spring-cloud-sleuth/#_example

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,156評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,401評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,069評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,873評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,635評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,128評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,203評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,365評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,881評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,733評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,935評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,475評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,172評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,582評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,821評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,595評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,908評論 2 372

推薦閱讀更多精彩內容