這是SpringCloud實戰系列中第7篇文章,了解前面第兩篇文章更有助於更好理解本文內容:
①SpringCloud 實戰:引入Eureka組件,完善服務治理
②SpringCloud 實戰:引入Feign組件,發起服務間調用
③SpringCloud 實戰:使用 Ribbon 客戶端負載均衡
④SpringCloud 實戰:引入Hystrix組件,分布式系統容錯
⑤SpringCloud 實戰:引入Zuul組件,開啟網關路由
⑥SpringCloud 實戰:引入gateway組件,開啟網關路由功能
背景
近年來,隨着微服務架構的流行,很多公司都走上了微服務拆分之路。從而使系統變得越來越復雜,原本單體的系統被拆成很多個服務,每個服務之間通過輕量級的 HTTP 協議進行交互。
單體架構時,一個請求的調用鏈路非常清晰,一般由負載均衡器,比如 Nginx。將調用方的請求轉發到后端服務,后端服務進行業務處理后返回給調用方。而當架構變成微服務架構時,可能帶來一系列的問題,比如下面三個問題:
- 接口響應慢,怎么排查?
- 服務間的依賴關系如何查看?
- 請求貫穿多個微服務,如何將每個請求的日志串起來?
分布式鏈路跟蹤
分布式鏈路跟蹤原理在於如何能將請求經過的服務節點都關聯起來。當一個請求從客戶端到達網關后,相當於是第一個入口,這時就需要生成一個唯一的請求 ID,作為這次請求的標識。從網關到達服務 A 后,肯定是需要將請求 ID 傳遞到服務 A 中的,這樣才能將網關到服務 A 的請求關聯起來,依次類推,后面會經過多層服務,都需要將信息一層層傳遞。當然在每一層都需要將數據進行上報、統一存儲、展示等操作。
從我們對這個需求的理解來看,鏈路跟蹤並不是很復雜,而復雜的點在於如何實現這一套跟蹤框架,就拿請求信息傳遞這件事來說,服務之間交互,有的用的是 Feign 調用接口,有的用的是 RestTemplate 調用接口,要想將信息傳遞到下游服務,那么必須得擴展這些調用的框架才可以。
核心概念
-
Span
基本工作單元,例如,發送 RPC 請求是一個新的 Span,發送 HTTP 請求是一個新的 Span,內部方法調用也是一個新的 Span。
-
Trace
一次分布式調用的鏈路信息,每次調用鏈路信息都會在請求入口處生成一個
TraceId
。 -
Annotation
用於記錄事件的信息。在 Annotation 中會有 CS、SR、SS、CR 這些信息,前面的C表示客戶端,S表示服務器端; 后面的S表示sent,也就是發起請求時的動作,R表示Received,也就是接受到請求時的動作;下面分別介紹下這些信息的作用。
- CS
也就是Client Sent
,客戶端發起一個請求,這個 Annotation 表示 Span 的開始。 - CR
也就是Client Received
,表示 Span 的結束,客戶端已成功從服務器端收到響應,用 CR 的時間戳減去 CS 的時間戳就可以知道客戶端從服務器接收響應所需的全部時間。 - SS
也就是Server Sent
,在請求處理完成時將響應發送回客戶端,用 SS 的間戳減去 SR 的時間戳會顯示服務器端處理請求所需的時間。 - SR
也就是Server Received
,服務器端獲得請求並開始處理它,用 SR 的時間戳減去 CS 的時間戳會顯示網絡延遲時間。
- CS
請求追蹤過程分解
- 首先當一個請求訪問 SERVICE1 時,這時是沒有 Trace 和 Span 的,然后會生成 Trace 和 Span,如圖所示生成的 Trace ID 是 X,Span ID 是 A。
- 接着 SERVICE1 請求 SERVICE2,這是一次遠程請求,會生成一個新的 Span,Span ID 為 B,Trace ID 不變還是 X。Span B 處於 CS 狀態。
- 當請求到達 SERVICE2 后,Trade ID 和 Span ID 就被傳遞過來了,這時,SERVICE2 有內部操作,又生成了一個新的 Span,Span ID 為 C,Trace ID 不變還是 X。
- SERVICE2 處理完后向 SERVICE3 發起請求,同時產生新的 Span,Span ID 為 D,Span D 處於 CS 狀態,SERVICE3 接收到請求后,Span D 處於 SR 狀態,同時 SERVICE3 內部操作也會產生新的 Span,Span ID 為 E。
- 當 SERVICE3 處理完后,需要將結果響應給調用方,這時 Span D 就處於 SS 的狀態,當 SERVICE2 收到響應后,Span ID 為 D 的 Span 就是 CR 狀態,表示 Span 已經結束了。
Zipkin 介紹
Zipkin
是 Twitter
的一個開源項目,是一個致力於收集所有服務監控數據的分布式跟蹤系統,它提供了收集數據和查詢數據兩大接口服務。有了 Zipkin
我們就可以很直觀地查看調用鏈,並且可能很方便看出服務之間的調用關系,以及調用耗費的時間。
Zipkin還提供了可插拔數據存儲方式:In-Memory、MySql、Cassandra以及Elasticsearch。測試方便可直接采用In-Memory方式進行存儲,生產推薦使用Elasticsearch。
安裝 Zipkin
如果使用了 Java 8 或者更高的版本,可以獲取最新的可執行 jar 包來進行啟動。
-
下載jar包:
curl -sSL https://zipkin.io/quickstart.sh | bash -s
如果下載太慢,可以直接訪問Maven地址進行下載最新的jar。
其他方式安裝,可以查看官網的quickstart。
-
啟動服務
java -jar zipkin.jar
-
訪問Zipkin
成功啟動服務后,訪問http://127.0.0.1:9411/zipkin/即可。
Sleuth 介紹
Spring Cloud Sleuth
是一種分布式的服務鏈路跟蹤解決方案,通過使用 Spring Cloud Sleuth 可以讓我們快速定位某個服務的問題,以及厘清服務間的依賴關系。
Sleuth
可以添加鏈路信息到日志中,這樣的好處是可以統一將日志進行收集展示,並且可以根據鏈路的信息將日志進行串聯。
Sleuth 中的鏈路數據可直接上報給 Zipkin
,在 Zipkin
中就可以直接查看調用關系和每個服務的耗時情況.
Sleuth 中內置了很多框架的埋點,比如:Zuul、Feign、Hystrix、RestTemplate
等。正因為有了這些框架的埋點,鏈路信息才能一直往下傳遞。
通過 Http 結合Zipkin
-
在我們的微服務項目中添加Zipkin依賴
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zipkin</artifactId> </dependency>
-
配置Zipkin地址
spring.zipkin.base-url=http://127.0.0.1:9411/
-
配置采樣比例
實際使用中可能調用了 10 次接口,但是 Zipkin 中只有一條數據,這是因為收集信息是有一定比例的,Zipkin 中的數據條數與調用接口次數默認比例是 10:1,通過下面的配置來改變這個比例值:spring.sleuth.sampler.probability=1.0
-
驗證
啟動我們的微服務,訪問 http://localhost:9000/eureka-client/sayHello 接口,接口由網關路由到eureka-client 服務,eureka-client 服務再調用eureka-provider服務,接口返回eureka-provider服務的端口等信息。
然后訪問 http://127.0.0.1:9411/zipkin ,點擊查詢,即可查看到相關訪問記錄
點擊菜單上面的依賴,可以查看項目的依賴關系
使用 RabbitMQ or Kafka 代替 HTTP 發送調用鏈數據
數據的發送如果采用 HTTP 對性能還是有影響的。如果Zipkin 的服務端在重啟或者掛掉時,那么將丟失部分采集數據。為了解決這些問題,我們可以集成 RabbitMQ 或者Kafka 來發送采集數據,利用消息隊列來提高發送性能,保證數據不丟失;
-
如果要使用RabbitMQ或Kafka而不是HTTP,需要引入
spring-rabbit
orspring-kafka
相關依賴。<dependency> <groupId>org.springframework.amqp</groupId> <artifactId>spring-rabbit</artifactId> </dependency>
-
然后在配置文件修改相關配置:
# WEB、KAFKA、RABBIT、ACTIVEMQ spring.zipkin.sender.type=kafka
-
刪除之前配置的 spring.zipkin.base-url
-
配置kafka、rabbit
自定義 Zipkin 配置
每個跟蹤系統都需要具有Reporter <Span>
和Sender
,如果要覆蓋提供的bean,則需要給它們指定一個特定的名稱 ZipkinAutoConfiguration.REPORTER_BEAN_NAME
and ZipkinAutoConfiguration.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);
}
}
}