SpringCloud(八)鏈路追蹤Sleuth
在分佈式系統中,完成一個外部請求可能需要多個應用之間相互協作,形成複雜的調用鏈路。而一旦出現問題,更是難以定位問題,也難以直觀地獲取到各個服務之間的依賴關係。Spring Cloud Sleuth的出現正是爲了實現分佈式系統的鏈路追蹤。
文章目錄
Sleuth的基本概念
Sleuth借鑑了Google Dapper的術語。
有以下幾個要素:
Span
:基本的工作單元。比如發送一個RPC以及返回一個RPC應答分別是一個新的Span。Span是根據一個唯一的64-bit ID和所屬Trace的64-bit ID區分的。
Trace
:一系列的Span組成的樹結構的圖
Annotation
:用來記錄事件存在的時間。
- cs - Client Sent - client端發送一個請求。Span的開始。
- sr - Server Received - server端接收到請求,並且準備處理
- ss - Server Sent - server端請求處理完成
- cr - Client Received - client端接收到server端的響應結果。Span的結束。
上圖中七種顏色分別代表了從A-G7個Span。
將他們轉換爲父子關係圖,如下圖所示:
引入Sleuth
我們對web-app-service
,user-service
和sms-service
這三個服務增加Sleuth的支持。
在pom.xml
文件中引入spring-cloud-starter-sleuth
依賴。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
在application.yml
文件中設置
spring:
sleuth:
sampler:
percentage: 1.0
表示設置Sleuth的採樣率爲100%,即全部採樣。
我們在web-app-service
新建一個用戶註冊的接口,分別請求user-service
和sms-service
的接口,涉及到的接口增加Log輸出。
@RequestMapping("/register")
public String register() {
logger.info("user is registering");
userService.register();
smsService.sendRgister();
return "success";
}
請求web-app-service
的/user/register
接口(http://localhost:8101/user/register
),返回成功後可以在三個服務的控制檯分別看到的日誌打印。
# web-app-service
2018-10-10 16:51:11.454 INFO [web-app-service,dd3aaab11dddbca1,dd3aaab11dddbca1,true] 3760 --- [nio-8101-exec-9] c.c.s.web.controller.UserController : user is registering
# user-service
2018-10-10 16:51:11.463 INFO [user-service,dd3aaab11dddbca1,436fe40b80a2bdb1,true] 8608 --- [nio-8002-exec-9] c.c.s.user.controller.UserController : invoke user register endpoint
# sms-service
2018-10-10 16:51:11.983 INFO [sms-service,dd3aaab11dddbca1,ff559b20a835e679,true] 5920 --- [nio-8001-exec-3] c.c.s.controller.SMSController : send sms to registered user
注意日誌的輸出分別代表[appname,traceId,spanId,exportable]
。
exportable
:表示日誌是否應該導入到Zipkin中。
Sleuth集成ELK實現日誌分析
對於分佈式系統,我們希望將日誌納入統一管理。ELK(Elasticsearch+Logstash+Kibana)是日誌管理分析的一系列組件,實現了日誌收集、檢索和可視化查詢。我們通過logback
和logstash-logback-encoder
將日誌按照指定格式傳輸給Logstash,經過處理後傳輸給Elasticsearch,再通過Kibana的web界面進行日誌查詢。
在Logstash上設置輸入和輸出,並重啓Logstash服務。在輸入配置裏,tcp.host
就是我們的Logstash所在IP,tcp.port
是我們使用TCP傳輸日誌時的指定端口。輸出配置我們的Elasticsearch IP和端口,並指定索引(不能使用大寫字母)。
input {
tcp {
mode => "server"
host => "192.168.126.128"
port => 3333
}
}
filter {
}
output {
elasticsearch {
action => "index"
hosts => "192.168.126.128:9200"
index => "spring_cloud_demo_logs"
}
}
logback日誌配置文件logback-spring.xml
,destination
需要配置上面的Logstash IP和TCP端口,並指定編碼器LoggingEventCompositeJsonEncoder
及格式。
<?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}"/>
<!--Logstash 服務器地址和TCP端口-->
<property name="LOGSTASH_TCP_DESTINATION" value="192.168.126.128:3333" />
<!-- 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 name="logstash"
class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>${LOGSTASH_TCP_DESTINATION}</destination>
<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>
請求web-app-service
的/user/register
接口(http://localhost:8101/user/register
),自動向ELK發送日誌。
在Kibana web界面創建索引
採集到的日誌就是我們各個服務中的輸出。
根據某一個traceId
,對e7df1589e5a2a73f
進行搜索顯示整個調用鏈路的日誌信息。
展開某一條日誌後可以看到其詳細信息
使用Sleuth和Zipkin實現鏈路追蹤
Zipkin作爲分佈式鏈路跟蹤系統,提供了可視化頁面極大地方便了對請求的監控,更容易明確服務之間的依賴。新建一個zipkin-server
服務作爲鏈路追蹤服務。我們需要將Sleuth採集的日誌信息通過Http或者Spring Cloud Stream方式傳輸給Zipkin服務端。
通過Http傳輸Sleuth信息
zipkin-server
的pom.xml
需要引入zipkin-server
和zipkin-autoconfigure-ui
依賴。
<!--註冊到Eureka,可以自動發現Zipkin服務-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<!--使用Http方式獲取鏈路信息-->
<dependency>
<groupId>io.zipkin.java</groupId>
<artifactId>zipkin-server</artifactId>
</dependency>
<!--zipkin ui頁面-->
<dependency>
<groupId>io.zipkin.java</groupId>
<artifactId>zipkin-autoconfigure-ui</artifactId>
</dependency>
將服務端口設置爲8400並註冊到eureka,保證其他服務能夠發現Zipkin服務。
使用@EnableZipkinServer
啓用Zipkin服務。
@EnableEurekaClient
@EnableZipkinServer
@SpringBootApplication
public class ZipkinApplicationStarter {
public static void main(String[] args) {
SpringApplication.run(ZipkinApplicationStarter.class, args);
}
}
啓動成功後我們訪問http://localhost:8400
就能直接訪問Zipkin服務的首頁了。
接着改造我們的web-app-service
,user-service
和sms-service
服務使其支持使用Http向Zipkin傳輸鏈路信息。pom.xml
去掉之前的spring-cloud-starter-sleuth
依賴增加spring-cloud-sleuth-zipkin
(包含spring-cloud-starter-sleuth
)。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-zipkin</artifactId>
</dependency>
然後需要在application.yml
中配置Zipkin服務的地址和Sleuth的採樣率。我們可以使用Eureka發現Zipkin服務或者使用url配置Zipkin的路徑。Sleuth的採樣率繼續使用1,保證所有的請求都會被收集。
spring:
zipkin:
locator:
discovery:
enabled: true
#base-url: http://localhost:8400
sleuth:
sampler:
percentage: 1.0
請求web-app-service
的/user/register
接口(http://localhost:8101/user/register
)
此時我們打開Zipkin首頁,查看收集到的信息。圖中藍色的都是正常的請求,紅色則是出現錯誤的請求。
打開一個正常的請求可以看到請求的服務接口及請求時間。如圖我們可以看出web-app-service
請求了user-service
之後再請求了sms-service
。
點開user-service
Span顯示請求狀況
關閉sms-service
服務,並重新發起請求。觀察異常請求鏈路,發現sms-service
關閉後,請求異常後又重試了6次,最終失敗。
點擊下面紅色的Span查看錯誤信息。從日誌中不難發現出現錯誤的原因是/sms/sendRegister
超時了。
點擊Dependencies頁面顯示服務之間的依賴關係。
點開某個服務可以看到被哪些應用使用
使用SpringCloud Sleuth Stream傳輸Sleuth信息
使用Http方式可以很容易的實現Sleuth信息的上傳,但是當服務訪問量較高時Http的勢必會影響服務性能。再者,當我們的Zipkin服務端出現異常,這些Sleuth信息也不能正常上傳,影響服務的監控。這時候使用消息隊列就能輕鬆的解決高吞吐量和異步解耦這兩個問題。
Spring Cloud Stream是一個構建基於消息微服務的框架,通過簡單的註解方式就能實現消息的發送。目前已經支持Kafka和RabbitMQ。Spring Cloud Sleuth Stream正是利用Spring Cloud Stream實現使用消息隊列傳輸Sleuth信息。
下面我們演示使用RabbitMQ作爲中間件,實現Stream傳輸Sleuth信息。
在zipkin-server
增加Spring Cloud Sleuth Stream的依賴。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-zipkin-stream</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-rabbit</artifactId>
</dependency>
在啓動類上增加@EnableZipkinStreamServer
註解。
@EnableEurekaClient
//@EnableZipkinServer //Http方式傳輸sleuth信息
@EnableZipkinStreamServer //Stream方式傳輸Sleuth信息
@SpringBootApplication
public class ZipkinApplicationStarter {
public static void main(String[] args) {
SpringApplication.run(ZipkinApplicationStarter.class, args);
}
}
在application.yaml
文件中增加RabbitMQ的配置
spring:
rabbitmq:
host: 172.16.4.39
username: rabbitmq
password: rabbitmq
服務端完成,啓動之。
我們發現RabbitMQ的中已經有了一個叫做sleuth
的exchange。
在web-app-service
,user-service
和sms-service
這三個服務中增加Sleuth Stream採集支持。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-stream</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-rabbit</artifactId>
</dependency>
同樣地,增加RabbitMQ地址、用戶名和密碼配置。這三個應用集成配置中心的時候已經進行了配置。
啓動這三個服務,繼續請求我們的測試接口,觀察到RabbitMQ的web界面有消息的生產和消費,打開Zipkin界面也能看到調用鏈路信息。
信息存儲方式
默認地,Zipkin將鏈路信息存儲在內存中。在正式使用中這種做法是不可取的,隨着數據的堆積會大量消耗內存,一旦服務重啓所有的監控信息都將隨之消失。我們需要將鏈路信息進行持久化。
打開io.zipkin.java:zipkin-server
pom依賴,我們可以看到Zipkin支持的存儲方式有mysql,cassandra,elasticsearch。
下面演示以mysql作爲信息的存儲方式。我們在Zipkin服務的pom.xml
文件中引入mysql存儲支持
<!--使用mysql存儲Span信息-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>io.zipkin.java</groupId>
<artifactId>zipkin-autoconfigure-storage-mysql</artifactId>
</dependency>
在application.yaml
中設置數據源、存儲方式以及Zipkin DDL。
spring:
datasource:
schema: classpath:ysql.sql
# zipkin-autoconfigure-storage-mysql包中默認包含了HikariDataSource
type: com.zaxxer.hikari.HikariDataSource
url: jdbc:mysql://localhost:3306/test
username: chenxyz
password: 123
# Switch this on to create the schema on startup:
initialize: true
continueOnError: true
# 設置存儲類型爲mysql
zipkin:
storage:
type: mysql
服務啓動的時候會自動生成三張表。
後面我們再傳輸鏈路信息,Zipkin會將Span信息持久化到mysql中。