通過之前的 Spring Cloud 組件學習, 實際上我們已經能夠通過使用它們搭建起一 個基礎的微服務架構系統來實現業務需求了。 但是, 隨着業務的發展, 系統規模也會變得越來越大, 各微服務間的調用關系也變得越來越錯綜復雜。 通常 一 個由客戶端發起的請求在后端系統中會經過多個不同的微服務調用來協同產生最后的請求結果, 在復雜的微服務架構系統中, 幾乎每 一 個前端請求都會形成 一 條復雜的分布式服務調用鏈路, 在每條鏈路中任何 一 個依賴服務出現延遲過高或錯誤的時候都有可能引起請求最后的失敗。這時候,對於每個請求, 全鏈路調用的跟蹤就變得越來越重要, 通過實現對請求調用的跟蹤可以幫助我們快速發現錯誤根源以及監控分析每條請求鏈路上的性能瓶頸等。針對上面所述的分布式服務跟蹤問題, Spring Cloud Sleuth 提供了一 套完整的解決方案。 在本文中, 我們將詳細介紹如何使用 Spring Cloud Sleuth 來為微服務架構增加分布式服務跟蹤的能力。
快速入門(實現跟蹤):
在引入 Sleuth 之前, 我們先按照之前章節學習的內容來做 一 些准備工作, 構建 一 些基礎的設施和應用。
- 服務注冊中心: eureka-server, 這里不做贅述, 直接使用之前構建的工程即可。
- 微服務應用: trace-1, 實現 一 個 REST 接口 /trace-1, 調用該接口后將觸發對 trace-2 應用的調用。 具體實現如下所述。
1.創建 一 個基礎的 Spring Boot 應用, 在 pom.xml 中增加下面的依賴:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-ribbon</artifactId>
<version>1.4.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
2.實現 /trace-1 接口, 並使用 RestTemplate 調用 trace-2應用的接口。 具體如下:
@Configuration public class ConfigBean { @Bean @LoadBalanced // ribbon是客戶端 的負載均衡工具 //默認算法是輪詢算法 核心組件IRule
public RestTemplate getRestTemplate() { return new RestTemplate(); }
} @RestController public class TestController { private final Logger logger = Logger.getLogger(getClass().getName()); @Autowired private RestTemplate restTemplate; @RequestMapping(value = "/trace-1", method = RequestMethod.GET) public String trace() { logger.info(" === call trace- 1 === "); return restTemplate.getForEntity("http://trace-2/trace-2", String.class).getBody(); } }
3.添加主類:
@SpringBootApplication @EnableDiscoveryClient public class Trace1App { public static void main(String[] args) { SpringApplication.run(Trace1App.class, args); } }
4.增加配置
server.port = 9101 eureka.client.serviceUrl.defaultZone = http://localhost:7001/eureka/,http://localhost:7002/eureka/
spring.application.name = trace-1
5. 創建 一 個基礎的 Spring Boot 微服務應用:trace-2, 實現 一 個 REST 接口 /trace-2, 供 trace-1 調用。pom.xml 中的依賴與 trace-1 相同。
@RestController public class TestController { private final static Logger log = LoggerFactory.getLogger(TestController.class); @RequestMapping(value = "/trace-2", method = RequestMethod.GET) public String trace() { log.info(" = = = <call trace-2> == = "); return " Return ===call trace-2 === "; } }
6.配置如下
server.port = 9102 eureka.client.serviceUrl.defaultZone = http://localhost:7001/eureka/,http://localhost:7002/eureka/
spring.application.name = trace-2
在實現了上面的內容之后, 我們可以將 eureka-server、 trace-1、 trace-2應用都啟動起來, 並對 trace-1 的接口發送請求http://localhost:9101/trace-1 。 可以得到返回值 Trace.
7.實現跟蹤,在完成了准備工作之后, 為上面的 trace-1和 trace-2 添加服務跟蹤功能。 通過 Spring Cloud Sleuth 的封裝, 我們為應用增加服務跟蹤能力的操作非常簡單, 只需在 trace-1 和 trace-2 的 porn.xrnl 依賴管理中增加spring-cloud-starter-sleuth 依賴即可,具體如下:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
到這里, 實際上我們已經為 trace-1 和 trace-2 實現服務跟蹤做好了基礎的准備,重啟 trace-1 和 trace-2, 再對 trace-1 的接口發送請求 http://localhost:9101/ trace-1 。 此時, 我們可以從它們的控制台輸出中窺探到 Sleuth 的 一 些端倪。
從上面的控制台輸出內容 中 , 我 們可 以看到多了一些形如[[trace-1,dfb2da015596c556,864c49f71578d260,false]的日志信息, 而這些元素正是實現分布式服務跟蹤的重要組成部分,每個值的含義如下所述。
- 第一 個值: trace-1, 它記錄了應用的名稱,也就是 application.properties中 spring.application.name參數配置的屬性。
- 第二個值: dfb2da015596c556, Spring Cloud Sleuth生成的 一 個ID,稱為Trace ID,它用來標識 一 條請求鏈路。一 條請求鏈路中包含 一 個TraceID, 多個SpanID。
- 第三個值: 864c49f71578d260, Spring Cloud Sleuth生成的另外 一 個ID, 稱為Span ID, 它表示 一 個基本的工作單元, 比如發送 一 個HTTP請求。
- 第四個值: false, 表示是否要將該信息輸出到Zipkin等服務中來收集和展示 。
上面四個值中的TraceID和SpanID是Spring Cloud Sleuth實現分布式服務跟蹤的核心。 在 一 次服務請求鏈路的調用過程中, 會保待並傳遞同 一 個Trace ID, 從而將整個分布於不同微服務進程中的請求跟蹤信息串聯起來。 以上面輸出內容為例, trace-1 和 trace-2同屬於 一 個前端服務請求來源,所以它們的TraceID是相同的,處於同 一 條請求鏈路中。
跟蹤原理:
分布式系統中的服務跟蹤在理論上並不復雜, 它主要包括下面兩個關鍵點。
- 為了實現請求跟蹤, 當請求發送到分布式系統的入口端點時, 只需要服務跟蹤框架為該請求創建 一 個唯 一 的跟蹤標識, 同時在分布式系統內部流轉的時候,框架始終保待傳遞 該唯 一 標識, 直到返回給請求方為止, 這個唯 一 標識就是前文中提到的Trace ID。 通過TraceID的記錄, 我們就能將所有請求過程的日志關聯起來。
- 為了統計各處理單元的時間延遲, 當請求到達各個服務組件時, 或是處理邏輯到達某個狀態時,也通過 一 個唯 一 標識來標記它的開始、 具體過程以及結束, 該標識就是前文中提到的SpanID。 對於每個Span來說, 它必須有開始和結束 兩個節點, 通過記錄開始 Span和結束Span的時間戳,就能統計出該Span的時間延遲,除了時間戳記錄之外,它還可以包含 一 些其他元數據, 比如事件名稱、 請求信息等。
抽樣收集:
通過Trace ID和Span ID已經實現了對分布式系統中的請求跟蹤, 而記錄的跟蹤信息最終會被分析系統收集起來, 並用來實現對分布式系統的監控和分析功能, 比如, 預警延遲過長的請求鏈路、 查詢請求鏈路的調用明細等。 此時, 我們在對接分析系統時就會碰到一個問題: 分析系統在收集跟蹤信息的時候, 需要收集多少跟蹤信息才合適呢?理論上來說, 我們收集的跟蹤信息越多就可以越好地反映出系統的實際運行情況, 並給出更精准的預警和分析。 但是在高並發的分布式系統運行時, 大量的請求調用會產生海量的跟蹤日志信息, 如果收集過多的跟蹤信息將會對整個分布式系統的性能造成 一 定的影響, 同時保存大量的日志信息也需要不少的存儲開銷。 所以, 在 Sleuth 中采用了抽象收集的方式來為跟蹤信息打上收集標記,也就是我們之前在日志信息中看到的第4個布爾類型的值,它代表了該信息是否要被后續的跟蹤信息收集器獲取和存儲。
Sleuth 中的抽樣收集策略是通過 Sampler (1.3.5.RELEASE)抽象類實現的,通過實現 isSarnpled 方法, Spring Cloud Sleuth 會在產生跟蹤信息的時候調用它來為跟蹤信息生成是否要被收集的標志。 需要注意的是, 即使 isSampled 返回了 false, 它僅代表該跟蹤信息不被輸出到后續對接的遠程分析系統(比如 Zipkin), 對於請求的跟蹤活動依然會進行,所以我們在日志中還是能看到收集標識為 false 的記錄。默認情況下, Sleuth 會使用 PercentageBasedSarnpler 實現的抽樣策略,以請求百分比的方式配置和收集跟蹤信息。 我們可以通過在 application.properties 中配置
下面的參數對其百分比值進行設置, 它的默認值為0 .1, 代表收集10%的請求跟蹤信息。
spring.sleuth.sampler.percentage = 0.1
與Logstash整合:
通過之前的准備與整合,我們已經為trace-1和trace-2引入了Spring Cloud Sleuth的基礎模塊spring-cloud-starter-sleuth, 實現了在各個微服務的日志信息中添加跟蹤信息的功能。 但是, 由於日志文件都離散地存儲在各個服務實例的文件系統之上, 僅僅通過查看日志文件來分析我們的請求鏈路依然是 一 件相當麻煩的事, 所以我們還需要 一些工具來幫助集中收集、 存儲和搜索這些跟蹤信息。 引入基於日志的分析系統是 一 個不錯的選擇, 比如ELK平台, 它可以輕松地幫助我們收集和存儲這些跟蹤日志, 同時在需要的時候我們也可以根據Trace ID來輕松地搜索出對應請求鏈路 相關的明細日志。
ELK平台主要由ElasticSearch、 Logstash和 Kibana三個開源工具組成。
- ElasticSearch是 一 個開源分布式搜索引擎, 它的特點有: 分布式, 零配置, 自動發現, 索引自動分片, 索引副本機制,RESTful風格接口, 多數據源, 自動搜索負載等。
- Logstash是 一 個完全開源的工具, 它可以對日志進行收集、 過濾, 並將其存儲供以后使用。
- Kibana 也是 一 個開源和免費的工具, 它可以為Logstash 和ElasticSearch提供日志分析友好的Web界面, 可以幫助匯總、 分析和搜索重要數據日志。Spring Cloud Sleuth 在與ELK平台整合使用時, 實際上只要實現與負責日志收集的Logstash完成數據對接即可, 所以我們需要為Logstash准備JSON格式的日志輸出。 由於Spring Boot 應用默認使用logback來記錄日志,而Logstash自身也有對logback日志工具的支持工具, 所以我們可以直接通過在logback的配置中增加對Logstash的Appender, 就能非常方便地將日志轉換成以JSON的格式存儲和輸出了。
下面我們來詳細介紹 一 下在快速入門示例的基礎上, 如何實現面向Logstash的日志輸出配置。
1.在pom.xml依賴中引入logstash-logback-encoder依賴, 具體如下:
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-ribbon</artifactId>
<version>1.4.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions><!-- 去掉springboot默認配置 -->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>4.6</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.1.11</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.1.11</version>
</dependency>
</dependencies>
2.在工程 /resource 目錄下創建 bootstrap.properties 配置文件,將 spring.application.name = trace-1 配置移動到該文件中去。由於 logback-spring.xml 的加載在 application.properties 之前, 所以之前的配置 logbackspring.xml 無法獲取 spring.application.name 屬性, 因此這里將該屬性移動到最先加載的 bootstrap.properties 配置文件中。
3.在工程 /resource 目錄下創建 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="sleuth" source="spring.application.name"/>
<!-- 日志在工程中的輸出位置 -->
<property name="LOG_FILE" value="${BUILD_FOLDER:-build}/${sleuth}"/>
<!-- 控制台的日志輸出樣式 -->
<property name="CONSOLE_LOG_PATTERN" value="%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr([${sleuth:-},%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 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
<charset>utf8</charset>
</encoder>
</appender>
<!-- 為logstash輸出的json格式的Appender -->
<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": "${sleuth:-}", "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"/>
</root>
</configuration>
完成上面的改造之后, 我們再將快速入門的示例運行起來, 並發起對 trace-1 的接口訪間。 此時可以在 trace-1 和 trace-2 的工程目錄下發現有 一 個 build 目錄, 下面分別創建了以各自應用名稱命名的 JSON 文件, 該文件就是在 logback-spring.xml中配置的名為 logstash 的 Appender 輸出的日志文件,其中記錄了類似下面格式的 JSON:
與Zipkin整合:
雖然通過 ELK 平台提供的收集、存儲、 搜索等強大功能, 我們對跟蹤信息的管理和使用已經變得非常便利。但是,在 ELK 平台中的數據分析維度缺少對請求鏈路中各階段時間延遲的關注, 很多時候我們追溯請求鏈路的 一 個原因是為了找出整個調用鏈路中出現延遲過高的瓶頸源, 或為了實現對分布式系統做延遲監控等與時間消耗相關的需求, 這時候類似 ELK 這樣的日志分析系統就顯得有些乏力了。 對於這樣的問題, 我們就可以引入 Zipkin來得以輕松解決。
下圖展示了 Zipkin 的基礎架構, 它主要由 4 個核心組件構成。
- Collector: 收集器組件, 它主要處理從外部系統發送過來的跟蹤信息, 將這些信息轉換為Zipkin內部處理的Span格式, 以支待后續的存儲、 分析、 展示等功能。
- Storage: 存儲組件, 它主要處理收集器接收到的跟蹤信息, 默認會將這些信息存儲在內存中。 我們也可以修改此存儲策略, 通過使用其他存儲組件將跟蹤信息存儲到數據庫中。
- RESTful API: API組件, 它主要用來提供外部訪問接口。 比如給客戶端展示跟蹤信息, 或是外接系統訪問以實現監控等。
- Web UI: UI組件, 基於API組件實現的上層應用。 通過UI組件, 用戶可以方便而又直觀地查詢和分析跟蹤信息。
HTTP收集:
在Spring Cloud Sleuth中對Zipkin的整合進行了自動化配置的封裝, 所以我們可以很輕松地引入和使用它。 下面我們來詳細介紹 一 下Sleuth與Zip如n的基礎整合過程, 主要分為以下兩步。
1.創建 一 個基礎的 Spring Boot 應用, 命名為 zipkin-server, 並在 pom.xml 中引入 Zipkin Server 的相關依賴, 具體如下:
<dependencies>
<dependency>
<groupId>io.zipkin.java</groupId>
<artifactId>zipkin-server</artifactId>
<version>2.11.8</version>
</dependency>
<dependency>
<groupId>io.zipkin.java</groupId>
<artifactId>zipkin-autoconfigure-ui</artifactId>
<version>2.11.8</version>
</dependency>
</dependencies>
2.創建應用主類 ZipkinApplication, 使用 @EnableZipkinServer 注解來啟動Zipkin Server, 具體如下:
@SpringBootApplication @EnableZipkinServer public class ZikpinApp { private final static Logger log = LoggerFactory.getLogger(ZikpinApp.class); public static void main(String[] args) { SpringApplication.run(ZikpinApp.class, args); log.info("服務啟動成功"); } }
3.在application.properties中做一 些簡單配置,比如設置 服務端口號為9411(客戶端整合時,自動化配置會連接9411端口,所以在服務端設置了端口為9411的話, 客戶端可以省去這個配置)。
spring.application.name=zipkin-server server.port=9411
4.創建完上述工程之后, 我們將其啟動起來,並訪問http://localhost: 9411/, 可以看到如下圖所示的Zipkin管理頁面:
第二步: 為應用引入和配置Zipkin服務
在完成了Zipkin Server的搭建之后, 我們還需要對應用做一 些配置, 以實現將跟蹤信息輸出到ZipkinServer。 我們以之前實現的七race-1和trace-2為例, 對它們做以下改造。
1.在trace-1和trace-2的pom.xml中引入spring-cloud-sleuth-zipkin依賴, 具體如下所示。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-zipkin</artifactId>
</dependency>
2.在trace-1和trace-2的application.properties中增加ZipkinServer的配置信息,具體如下所示(如果在zip-server應用中,我們將其端口設置為9411,並且均在本地調試的話, 也可以不配置該參數, 因為默認值就是http://localhost:9411)。
spring.zipkin.base-url=http://localhost:9411
測試與分析:
到這里我們已經完成 了接入 Zipkin Server 的所有基本工作,可以繼續將eureka-server、 trace-1和trace-2啟動起來, 然后做一 些測試,以對它的運行機制有 一 些初步的理解。我們先來向trace-1的接口發送幾個請求http://localhost: 9101/trace-1。當在日志中出現 跟蹤信息的最后 一 個值為true的時候, 說明該跟蹤信息會輸出給ZipkinServer, 所以此時可以在ZipkinServer的管理頁面中選擇合適的查詢條件, 單擊FindTraces按鈕, 就可以查詢出剛才在日志中出現的跟蹤信息了(也可以根據日志中的TraceID, 在頁面右上角的輸入框中來搜索), 頁面如下所示。
單擊下方trace-1端點的跟蹤信息, 還可以得到Sleuth跟蹤到的詳細信息, 其中包括我們關注的請求時間消耗等。
單擊導航欄中的Dependencies菜單, 還可以查看ZipkinServer根據跟蹤信息分析生成的系統請求鏈路依賴關系圖, 如下圖所示。
其他關於RabbitMQ收集就不說明了 ,springboot2.0以后官方不建議自己搭建 Zipkin-Server,建議使用 Jar 運行,詳細可以查詢官方文檔。這個東西的說明以及最新版本的配置的資料不多,以后有需要了再深入看看。