分佈式鏈路跟蹤 Sleuth 與 Zipkin

隨着業務發展,系統拆分導致系統調用鏈路愈發複雜一個前端請求可能最終需要調用很多次後端服務才能完成,當整個請求變慢或不可用時,我們是無法得知該請求是由某個或某些後端服務引起的,這時就需要解決如何快讀定位服務故障點,以對症下藥。於是就有了分佈式系統調用跟蹤的誕生。

現今業界分佈式服務跟蹤的理論基礎主要來自於 Google 的一篇論文《Dapper, a Large-Scale Distributed Systems Tracing Infrastructure》,使用最爲廣泛的開源實現是 Twitter 的 Zipkin,爲了實現平臺無關、廠商無關的分佈式服務跟蹤,CNCF 發佈了布式服務跟蹤標準 Open Tracing。國內,淘寶的 “鷹眼”、京東的 “Hydra”、大衆點評的 “CAT”、新浪的 “Watchman”、唯品會的 “Microscope”、窩窩網的 “Tracing” 都是這樣的系統。

Dapper, a Large-Scale Distributed Systems Tracing Infrastructure: 

https://research.google.com/pubs/pub36356.html

Spring Cloud Sleuth 也爲我們提供了一套完整的解決方案。在本章中,我們將詳細介紹如何使用 Spring Cloud Sleuth + Zipkin 來爲我們的微服務架構增加分佈式服務跟蹤的能力。

Spring Cloud Sleuth

一般的,一個分佈式服務跟蹤系統主要由三部分構成:

根據系統大小不同,每一部分的結構又有一定變化。譬如,對於大規模分佈式系統,數據存儲可分爲實時數據和全量數據兩部分,實時數據用於故障排查(Trouble Shooting),全量數據用於系統優化;數據收集除了支持平臺無關和開發語言無關係統的數據收集,還包括異步數據收集(需要跟蹤隊列中的消息,保證調用的連貫性),以及確保更小的侵入性;數據展示又涉及到數據挖掘和分析。雖然每一部分都可能變得很複雜,但基本原理都類似。

服務追蹤的追蹤單元是從客戶發起請求(request)抵達被追蹤系統的邊界開始,到被追蹤系統向客戶返回響應(response)爲止的過程,稱爲一個 trace。每個 trace 中會調用若干個服務,爲了記錄調用了哪些服務,以及每次調用的消耗時間等信息,在每次調用服務時,埋入一個調用記錄,稱爲一個 span。這樣,若干個有序的 span 就組成了一個 trace。在系統向外界提供服務的過程中,會不斷地有請求和響應發生,也就會不斷生成 trace,把這些帶有 span 的 trace 記錄下來,就可以描繪出一幅系統的服務拓撲圖。附帶上 span 中的響應時間,以及請求成功與否等信息,就可以在發生問題的時候,找到異常的服務;根據歷史數據,還可以從系統整體層面分析出哪裏性能差,定位性能優化的目標。

Spring Cloud Sleuth 爲服務之間調用提供鏈路追蹤。通過 Sleuth 可以很清楚的瞭解到一個服務請求經過了哪些服務,每個服務處理花費了多長。從而讓我們可以很方便的理清各微服務間的調用關係。此外 Sleuth 可以幫助我們:

Spring Cloud Sleuth 可以結合 Zipkin,將信息發送到 Zipkin,利用 Zipkin 的存儲來存儲信息,利用 Zipkin UI 來展示數據。

這是 Spring Cloud Sleuth 的概念圖:

Zipkin

Zipkin 是 Twitter 的一個開源項目,它基於 Google Dapper 實現,它致力於收集服務的定時數據,以解決微服務架構中的延遲問題,包括數據的收集、存儲、查找和展現。
我們可以使用它來收集各個服務器上請求鏈路的跟蹤數據,並通過它提供的 REST API 接口來輔助我們查詢跟蹤數據以實現對分佈式系統的監控程序,從而及時地發現系統中出現的延遲升高問題並找出系統性能瓶頸的根源。除了面向開發的 API 接口之外,它也提供了方便的 UI 組件來幫助我們直觀的搜索跟蹤信息和分析請求鏈路明細,比如:可以查詢某段時間內各用戶請求的處理時間等。

Zipkin 提供了可插拔數據存儲方式:In-Memory、MySQL、Cassandra 以及 Elasticsearch。接下來的測試爲方便直接採用 In-Memory 方式進行存儲,生產推薦 Elasticsearch。

上圖展示了 Zipkin 的基礎架構,它主要由 4 個核心組件構成:

快速上手

Zipkin 分爲兩端,一個是 Zipkin 服務端,一個是 Zipkin 客戶端,客戶端也就是微服務的應用。

客戶端會配置服務端的 URL 地址,一旦發生服務間的調用的時候,會被配置在微服務裏面的 Sleuth 的監聽器監聽,並生成相應的 Trace 和 Span 信息發送給服務端。
發送的方式主要有兩種,一種是 HTTP 報文的方式,還有一種是消息總線的方式如 RabbitMQ。

不論哪種方式,我們都需要:

方式一、HTTP

在 Spring Cloud Sleuth 中對 Zipkin 的整合進行了自動化配置的封裝,所以我們可以很輕鬆的引入和使用它。

Zipkin 服務端

關於 Zipkin 的服務端,在使用 Spring Boot 2.x 版本後,官方就不推薦自行定製編譯了,反而是直接提供了編譯好的 jar 包來給我們使用,詳情請看 upgrade to Spring Boot 2.0 NoClassDefFoundError UndertowEmbeddedServletContainerFactory · Issue #1962 · openzipkin/zipkin · GitHub

並且以前的 @EnableZipkinServer 也已經被打上了 @Deprecated

If you decide to make a custom server, you accept responsibility for troubleshooting your build or configuration problems, even if such problems are a reaction to a change made by the OpenZipkin maintainers. In other words, custom servers are possible, but not supported.

EnableZipkinServer.javagithub.com/openzipkin/zipkin/blob/master/zipkin-server/src/main/java/zipkin/server/EnableZipkinServer.java

簡而言之就是:私自改包,後果自負。

所以官方提供了一鍵腳本:

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

如果用 Docker 的話,直接

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

任一方式啓動後,訪問 http://localhost:9411/zipkin/ 就能看到如下界面,嗯還有漢化看起來不錯。

至此服務端就 OK 了。

微服務應用

創建兩個基本的 Spring Boot 工程,名字分別爲 trace-a 和 trace-b。

兩個工程的 pom.xml 均引入以下依賴座標:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>

兩者的配置文件也一樣(除了 spring. application.name 和 server.port,自行修改)

spring:
  application:
    name: trace-a
  sleuth:
    web:
      client:
        enabled: true
    sampler:
      probability: 1.0 # 將採樣比例設置爲 1.0,也就是全部都需要。默認是 0.1
  zipkin:
    base-url: http://localhost:9411/ # 指定了 Zipkin 服務器的地址
server:
  port: 8080
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7000/eureka/

Spring Cloud Sleuth 有一個 Sampler 策略,可以通過這個實現類來控制採樣算法。採樣器不會阻礙 span 相關 id 的產生,但是會對導出以及附加事件標籤的相關操作造成影響。Sleuth 默認採樣算法的實現是 Reservoir sampling,具體的實現類是 PercentageBasedSampler,默認的採樣比例爲: 0.1(即 10%)。不過我們可以通過 spring.sleuth.sampler.percentage 來設置,所設置的值介於 0.0 到 1.0 之間,1.0 則表示全部採集。

trace-a 工程的啓動類如下:

@RestController
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
    @Autowired
    private LoadBalancerExchangeFilterFunction lbFunction;
    @Bean
    public WebClient webClient() {
        return WebClient.builder().baseUrl("http://trace-b")
                .filter(lbFunction)
                .build();
    }
    @GetMapping("/trace-a")
    public Mono<String> trace() {
        System.out.println("===call trace-a===");
        return webClient().get()
                .uri("/trace-b")
                .retrieve()
                .bodyToMono(String.class);
    }
}

trace-b 工程的啓動類如下:

@RestController
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
    @GetMapping("/trace-b")
    public Mono<String> trace() {
        System.out.println("===call trace-b===");
        return Mono.just("Trace");
    }
}

至此,一切就緒。Spring 應用在監測到 classpath 中有 Sleuth 和 Zipkin 後,會自動在 WebClient(或 RestTemplate)的調用過程中向 HTTP 請求注入追蹤信息,並向 Zipkin Server 發送這些信息。

進行驗證

我們分別啓動 eureka、zipkin、trace-b、trace-a,然後
訪問 http://localhost:8080/trace-a 可以得到返回值 Trace,同時還能在它們的控制檯中分別獲得下面的輸出:

-- trace-a
===call trace-a===
2018-04-23 20:20:42.016  INFO [trace-a,,,] 71722 --- [ender@761e788f}] s.c.a.AnnotationConfigApplicationContext : Refreshing SpringClientFactory-localhost: startup date [Mon Apr 23 20:20:42 CST 2018]; parent: org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext@41ab013
2018-04-23 20:20:42.103  INFO [trace-a,,,] 71722 --- [ender@761e788f}] f.a.AutowiredAnnotationBeanPostProcessor : JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
2018-04-23 20:20:42.421  INFO [trace-a,,,] 71722 --- [ender@761e788f}] c.netflix.config.ChainedDynamicProperty  : Flipping property: localhost.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
2018-04-23 20:20:42.441  INFO [trace-a,,,] 71722 --- [ender@761e788f}] c.n.u.concurrent.ShutdownEnabledTimer    : Shutdown hook installed for: NFLoadBalancer-PingTimer-localhost
2018-04-23 20:20:42.476  INFO [trace-a,,,] 71722 --- [ender@761e788f}] c.netflix.loadbalancer.BaseLoadBalancer  : Client: localhost instantiated a LoadBalancer: DynamicServerListLoadBalancer:{NFLoadBalancer:name=localhost,current list of Servers=[],Load balancer stats=Zone stats: {},Server stats: []}ServerList:null
2018-04-23 20:20:42.485  INFO [trace-a,,,] 71722 --- [ender@761e788f}] c.n.l.DynamicServerListLoadBalancer      : Using serverListUpdater PollingServerListUpdater
2018-04-23 20:20:42.492  INFO [trace-a,,,] 71722 --- [ender@761e788f}] c.n.l.DynamicServerListLoadBalancer      : DynamicServerListLoadBalancer for client localhost initialized: DynamicServerListLoadBalancer:{NFLoadBalancer:name=localhost,current list of Servers=[],Load balancer stats=Zone stats: {},Server stats: []}ServerList:org.springframework.cloud.netflix.ribbon.eureka.DomainExtractingServerList@516a7895
2018-04-23 20:20:42.548  INFO [trace-a,d28b72b2317c023f,d28b72b2317c023f,true] 71722 --- [ctor-http-nio-2] s.c.a.AnnotationConfigApplicationContext : Refreshing SpringClientFactory-trace-b: startup date [Mon Apr 23 20:20:42 CST 2018]; parent: org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext@41ab013
2018-04-23 20:20:42.617  INFO [trace-a,d28b72b2317c023f,d28b72b2317c023f,true] 71722 --- [ctor-http-nio-2] f.a.AutowiredAnnotationBeanPostProcessor : JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
2018-04-23 20:20:42.855  INFO [trace-a,d28b72b2317c023f,d28b72b2317c023f,true] 71722 --- [ctor-http-nio-2] c.netflix.config.ChainedDynamicProperty  : Flipping property: trace-b.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
2018-04-23 20:20:42.868  INFO [trace-a,d28b72b2317c023f,d28b72b2317c023f,true] 71722 --- [ctor-http-nio-2] c.n.u.concurrent.ShutdownEnabledTimer    : Shutdown hook installed for: NFLoadBalancer-PingTimer-trace-b
2018-04-23 20:20:42.869  INFO [trace-a,d28b72b2317c023f,d28b72b2317c023f,true] 71722 --- [ctor-http-nio-2] c.netflix.loadbalancer.BaseLoadBalancer  : Client: trace-b instantiated a LoadBalancer: DynamicServerListLoadBalancer:{NFLoadBalancer:name=trace-b,current list of Servers=[],Load balancer stats=Zone stats: {},Server stats: []}ServerList:null
2018-04-23 20:20:42.870  INFO [trace-a,d28b72b2317c023f,d28b72b2317c023f,true] 71722 --- [ctor-http-nio-2] c.n.l.DynamicServerListLoadBalancer      : Using serverListUpdater PollingServerListUpdater
2018-04-23 20:20:42.905  INFO [trace-a,d28b72b2317c023f,d28b72b2317c023f,true] 71722 --- [ctor-http-nio-2] c.netflix.config.ChainedDynamicProperty  : Flipping property: trace-b.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
2018-04-23 20:20:42.907  INFO [trace-a,d28b72b2317c023f,d28b72b2317c023f,true] 71722 --- [ctor-http-nio-2] c.n.l.DynamicServerListLoadBalancer      : DynamicServerListLoadBalancer for client trace-b initialized: DynamicServerListLoadBalancer:{NFLoadBalancer:name=trace-b,current list of Servers=[172.16.106.93:8081],Load balancer stats=Zone stats: {defaultzone=[Zone:defaultzone;    Instance count:1;    Active connections count: 0;    Circuit breaker tripped count: 0;    Active connections per server: 0.0;]
},Server stats: [[Server:172.16.106.93:8081;    Zone:defaultZone;    Total Requests:0;    Successive connection failure:0;    Total blackout seconds:0;    Last connection made:Thu Jan 01 08:00:00 CST 1970;    First connection made: Thu Jan 01 08:00:00 CST 1970;    Active Connections:0;    total failure count in last (1000) msecs:0;    average resp time:0.0;    90 percentile resp time:0.0;    95 percentile resp time:0.0;    min resp time:0.0;    max resp time:0.0;    stddev resp time:0.0]
]}ServerList:org.springframework.cloud.netflix.ribbon.eureka.DomainExtractingServerList@31f05c5
2018-04-23 20:20:43.878  INFO [trace-a,,,] 71722 --- [erListUpdater-1] c.netflix.config.ChainedDynamicProperty  : Flipping property: trace-b.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
-- trace-b
===call trace-b===
2018-04-23 20:20:43.944  INFO [trace-b,,,] 71662 --- [ender@22a10ac6}] s.c.a.AnnotationConfigApplicationContext : Refreshing SpringClientFactory-localhost: startup date [Mon Apr 23 20:20:43 CST 2018]; parent: org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext@49c6c24f
2018-04-23 20:20:44.124  INFO [trace-b,,,] 71662 --- [ender@22a10ac6}] f.a.AutowiredAnnotationBeanPostProcessor : JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
2018-04-23 20:20:44.524  INFO [trace-b,,,] 71662 --- [ender@22a10ac6}] c.netflix.config.ChainedDynamicProperty  : Flipping property: localhost.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
2018-04-23 20:20:44.550  INFO [trace-b,,,] 71662 --- [ender@22a10ac6}] c.n.u.concurrent.ShutdownEnabledTimer    : Shutdown hook installed for: NFLoadBalancer-PingTimer-localhost
2018-04-23 20:20:44.600  INFO [trace-b,,,] 71662 --- [ender@22a10ac6}] c.netflix.loadbalancer.BaseLoadBalancer  : Client: localhost instantiated a LoadBalancer: DynamicServerListLoadBalancer:{NFLoadBalancer:name=localhost,current list of Servers=[],Load balancer stats=Zone stats: {},Server stats: []}ServerList:null
2018-04-23 20:20:44.610  INFO [trace-b,,,] 71662 --- [ender@22a10ac6}] c.n.l.DynamicServerListLoadBalancer      : Using serverListUpdater PollingServerListUpdater
2018-04-23 20:20:44.617  INFO [trace-b,,,] 71662 --- [ender@22a10ac6}] c.n.l.DynamicServerListLoadBalancer      : DynamicServerListLoadBalancer for client localhost initialized: DynamicServerListLoadBalancer:{NFLoadBalancer:name=localhost,current list of Servers=[],Load balancer stats=Zone stats: {},Server stats: []}ServerList:org.springframework.cloud.netflix.ribbon.eureka.DomainExtractingServerList@1e1794d0

訪問 http://localhost:9411/zipkin

點擊 Find Traces 會看到有一條記錄:

點擊記錄進去頁面,可以看到每一個服務所耗費的時間和順序:

點擊依賴分析,可以看到項目之間的調用關係:

方式二、消息總線 RabbitMQ

因爲之前說的 Zipkin 不再推薦我們來自定義 Server 端了,所以在最新版本的 Spring Cloud 依賴管理裏已經找不到 zipkin-server 了。

那麼如果直接用官方提供的 jar 包怎麼從 RabbitMQ 中獲取 trace 信息呢?

我們可以通過環境變量讓 Zipkin 從 RabbitMQ 中讀取信息,就像這樣:

RABBIT_ADDRESSES=localhost java -jar zipkin.jar

可配置的環境變量如下表所示:

08jRk6

如果設置了 URI,則以下屬性將被忽略。

5AGTpD

關於 Zipkin 的 Client 端,也就是微服務應用,我們就在之前 trace-a、trace-b 的基礎上修改,只要在他們的依賴裏都引入 spring-cloud-stream-binder-rabbit 就好了,別的不用改。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-stream-binder-rabbit</artifactId>
</dependency>

不過爲了說明是通過 RabbitMQ 傳輸的信息,我將 spring.zipkin.base-url 均改爲 http://localhost:9412/,即指向一個錯誤的地址。

分別重啓 trace-a、trace-b 工程,並啓動 Zipkin Server

RABBIT_ADDRESSES=localhost java -jar zipkin.jar

然後訪問 http://localhost:8080/trace-a 並刷新 Zipkin UI,看到如下內容,就說明 Sleuth+Zipkin+RabbitMQ 整合成功了。

此時看 RabbitMQ Admin,會看到多了一個名爲 zipkin 的 Queue

轉自:Yibo,

鏈接:haoyizebo.com/posts/6d06094e/

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/rlrslFGnPMoSDCLlwO6_bw