SpringCloud實戰之路 | 應用篇(八)分佈式鏈路追蹤技術Spring Cloud Sleuth + Zipkin
問題場景
在微服務架構下,一次請求少則經過三四次服務調用完成,多則跨越幾十個甚至是上百個服務節點。在調用關係如此繁雜的情況下,引出的問題:
- 如何清晰的展示出服務之間的調用關係?
- 如何解決在服務間調用鏈路過程中出現的錯誤進行排查?
- 如何對服務調用的耗時進行統計進行優化?
正是基於上述問題才提出了分佈式鏈路追蹤技術的解決方案
核心思想
本質: 分佈式鏈路追蹤本質就是通過在各個服務的調用鏈路節點記錄日誌的方式,並最終聚合集中化展示出來。
分佈式鏈路追蹤當中有幾個核心概念:
- Trace: 服務追蹤的追蹤單元是從客戶發起請求(request)抵達被追蹤系統的邊界開始,到被追蹤系統
向客戶返回響應(response)爲止的過程,一個調用鏈路的過程就是Trace,一個請求對應一個Trace。 - Trace ID: 爲了實現請求跟蹤,當請求發送到分佈式系統的入口端點時,只需要服務跟蹤框架爲該請求
創建一個唯一的跟蹤標識Trace ID,同時在分佈式系統內部流轉的時候,框架始終保持該唯一標識,直
到返回給請求方 - Span: 跨度,可以認爲是一個日誌數據結構,在一些特殊的時機點會記錄了一些日誌信息,比如有時間戳、spanId、TraceId,parentIde等,例如A->B->C,在調用B進入時的節點叫做入口Span,完成調用時出去的節點叫做出口Span,若干個有序的 Span 就組成了一個Trace。
- Span ID: 爲了統計各處理單元的時間延遲,當請求到達各個服務組件時,也是通過一個唯一標識Span
ID來標記它的開始,具體過程以及結束。對每一個Span來說,它必須有開始和結束兩個節點,通過記錄
開始Span和結束Span的時間戳,就能統計出該Span的時間延遲,除了時間戳記錄之外,它還可以包含
一些其他元數據,比如時間名稱、請求信息等。 - Parent ID: 指向另一個SpanId,用於表明父子的關係,即依賴關係。
Span中也抽象出了另外幾個概念,叫做事件,核心事件如下:
- CS : client send/start 客戶端/消費者發出一個請求,描述的是一個span開始
- SR: server received/start 服務端/生產者接收請求 SR - CS = 請求發送的網絡延遲
- SS: server send/finish 服務端/生產者發送應答 SS - SR = 服務端消耗時間
- CR: client received/finished 客戶端/消費者接收應答 CR - SS = 回覆需要的時間(響應的網絡延遲)
解決方案
Spring Cloud Sleuth (追蹤服務框架): 可以追蹤服務之間的調用,Sleuth可以記錄一個服務請求經過哪些服務、服務處理時間等,根據這些,我們能夠理清各微服務間的調用關係及進行問題追蹤分析(本質就是通過記錄日誌的方式來記錄蹤跡數據的)
- 耗時分析:通過 Sleuth 瞭解採樣請求的耗時,分析服務性能問題(哪些服務調用比較耗時)
- 鏈路優化:發現頻繁調用的服務,針對性優化等
Zikpin: 爲分佈式鏈路調用監控系統,可以對鏈路蹤跡的數據進行展示和存儲。
我們通常會Spring Cloud Sleuth + Zipkin一起使用,通過Sleuth記錄下鏈路追蹤的日誌數據,Zipkin Client採集到這些數據信息然後發送給Zipkin Server端進行聚合統計展示。
代碼實現
Zipkin 包括Zipkin Server和 Zipkin Client兩部分,Zipkin Server是一個單獨的服務,Zipkin Client就是
具體的微服務。
(一)構建Zipkin Server服務
引入maven依賴
<dependency>
<groupId>io.zipkin.java</groupId>
<artifactId>zipkin-server</artifactId>
<version>2.12.3</version>
<exclusions>
<!--排除掉log4j2的傳遞依賴,避免和springboot依賴的⽇志組件衝突-->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--zipkin-server ui界⾯依賴座標-->
<dependency>
<groupId>io.zipkin.java</groupId>
<artifactId>zipkin-autoconfigure-ui</artifactId>
<version>2.12.3</version>
</dependency>
創建入口啓動類
package com.cloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import zipkin2.server.internal.EnableZipkinServer;
@SpringBootApplication
@EnableZipkinServer // 開啓Zipkin Server功能
public class ZipkinServerApplication {
public static void main(String[] args) {
SpringApplication.run(ZipkinServerApplication.class,args);
}
}
創建配置文件yml
server:
port: 9411
management:
metrics:
web:
server:
auto-time-requests: false # 關閉⾃動檢測請求
訪問 http://localhost:9411/zipkin/ 完成Zipkin server端構建
(二)構建Zipkin Client服務
每個微服務引入maven依賴
<!--鏈路追蹤-->
<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>
修改微服務application.yml配置文件,添加日誌級別
server:
port: 8080
#註冊到Eureka服務中心
eureka:
client:
service-url:
# 註冊到集羣,就把多個Eurekaserver地址使用逗號連接起來即可;註冊到單實例(非集羣模式),那就寫一個就ok
defaultZone: http://EurekaServerA:8761/eureka,http://EurekaServerB:8762/eureka
instance:
prefer-ip-address: true
instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@
spring:
application:
name: cloud-service-user
zipkin:
base-url: http://127.0.0.1:9411 # zipkin server的請求地址
sender:
# web 客戶端將蹤跡日誌數據通過網絡請求的方式傳送到服務端,另外還有配置
# kafka/rabbit 客戶端將蹤跡日誌數據傳遞到mq進行中轉
type: web
sleuth:
sampler:
# 採樣率 1 代表100%全部採集 ,默認0.1 代表10% 的請求蹤跡數據會被採集
# 生產環境下,請求量非常大,沒有必要所有請求的蹤跡數據都採集分析,對於網絡包括server端壓力都是比較大的,可以配置採樣率採集一定比例的請求的蹤跡數據進行分析即可
probability: 1
management:
endpoints:
web:
exposure:
include: "*"
# 暴露健康接口的細節
endpoint:
health:
show-details: always
#分佈式鏈路追蹤
logging:
level:
org.springframework.web.servlet.DispatcherServlet: debug
org.springframework.cloud.sleuth: debug
至此完成Zipkin Client創建
測試
對服務發送一次請求,然後刷新 http://localhost:9411/zipkin/
Zipkin server頁面方便我們查看服務調用依賴關係及一些性能指標和異常信息
點擊進入可以觀察到調用情況
追蹤數據持久化
通過上面的操作可以完成Sleuth + Zipkin基本搭建,由於鏈路追蹤的信息是存儲在內存當中的,所以當對服務端進行重啓後會導致之前的鏈路追蹤的數據丟失,爲此Zipkin提供了兩種持久化方式,持久化到mysql或elasticsearch當中;
以持久化到mysql爲例
mysql中創建名稱爲zipkin的數據庫,並執⾏如下sql語句(官⽅提供)
--
-- Copyright 2015-2019 The OpenZipkin Authors
--
-- Licensed under the Apache License, Version 2.0 (the "License"); you
may not use this file except
-- in compliance with the License. You may obtain a copy of the
License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
distributed under the License
-- is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express
-- or implied. See the License for the specific language governing
permissions and limitations under
-- the License.
--
CREATE TABLE IF NOT EXISTS zipkin_spans (
`trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this
means the trace uses 128 bit traceIds instead of 64 bit',
`trace_id` BIGINT NOT NULL,
`id` BIGINT NOT NULL,
`name` VARCHAR(255) NOT NULL,
`remote_service_name` VARCHAR(255),
`parent_id` BIGINT,
`debug` BIT(1),
`start_ts` BIGINT COMMENT 'Span.timestamp(): epoch micros used for
endTs query and to implement TTL',
`duration` BIGINT COMMENT 'Span.duration(): micros used for
minDuration and maxDuration query',
PRIMARY KEY (`trace_id_high`, `trace_id`, `id`)
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE
utf8_general_ci;
ALTER TABLE zipkin_spans ADD INDEX(`trace_id_high`, `trace_id`)
COMMENT 'for getTracesByIds';
ALTER TABLE zipkin_spans ADD INDEX(`name`) COMMENT 'for getTraces and
getSpanNames';
ALTER TABLE zipkin_spans ADD INDEX(`remote_service_name`) COMMENT 'for
getTraces and getRemoteServiceNames';
ALTER TABLE zipkin_spans ADD INDEX(`start_ts`) COMMENT 'for getTraces
ordering and range';
CREATE TABLE IF NOT EXISTS zipkin_annotations (
`trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this
means the trace uses 128 bit traceIds instead of 64 bit',
`trace_id` BIGINT NOT NULL COMMENT 'coincides with
zipkin_spans.trace_id',
`span_id` BIGINT NOT NULL COMMENT 'coincides with zipkin_spans.id',
`a_key` VARCHAR(255) NOT NULL COMMENT 'BinaryAnnotation.key or
Annotation.value if type == -1',
`a_value` BLOB COMMENT 'BinaryAnnotation.value(), which must be
smaller than 64KB',
`a_type` INT NOT NULL COMMENT 'BinaryAnnotation.type() or -1 if
Annotation',
`a_timestamp` BIGINT COMMENT 'Used to implement TTL;
Annotation.timestamp or zipkin_spans.timestamp',
`endpoint_ipv4` INT COMMENT 'Null when Binary/Annotation.endpoint is
null',
`endpoint_ipv6` BINARY(16) COMMENT 'Null when
Binary/Annotation.endpoint is null, or no IPv6 address',
`endpoint_port` SMALLINT COMMENT 'Null when
Binary/Annotation.endpoint is null',
`endpoint_service_name` VARCHAR(255) COMMENT 'Null when
Binary/Annotation.endpoint is null'
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE
utf8_general_ci;
ALTER TABLE zipkin_annotations ADD UNIQUE KEY(`trace_id_high`,
`trace_id`, `span_id`, `a_key`, `a_timestamp`) COMMENT 'Ignore insert
on duplicate';
ALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`,
`span_id`) COMMENT 'for joining with zipkin_spans';
ALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`)
COMMENT 'for getTraces/ByIds';
ALTER TABLE zipkin_annotations ADD INDEX(`endpoint_service_name`)
COMMENT 'for getTraces and getServiceNames';
ALTER TABLE zipkin_annotations ADD INDEX(`a_type`) COMMENT 'for
getTraces and autocomplete values';
ALTER TABLE zipkin_annotations ADD INDEX(`a_key`) COMMENT 'for
getTraces and autocomplete values';
ALTER TABLE zipkin_annotations ADD INDEX(`trace_id`, `span_id`,
`a_key`) COMMENT 'for dependencies job';
CREATE TABLE IF NOT EXISTS zipkin_dependencies (
`day` DATE NOT NULL,
`parent` VARCHAR(255) NOT NULL,
`child` VARCHAR(255) NOT NULL,
`call_count` BIGINT,
`error_count` BIGINT,
PRIMARY KEY (`day`, `parent`, `child`)
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE
utf8_general_ci;
引入maven相關依賴
<dependency>
<groupId>io.zipkin.java</groupId>
<artifactId>zipkin-autoconfigure-storagemysql</artifactId>
<version>2.12.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
添加配置信息
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/zipkin?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true
username: root
password: 123456
druid:
initialSize: 10
minIdle: 10
maxActive: 30
maxWait: 50000
# 指定zipkin持久化介質爲mysql
zipkin:
storage:
type: mysql
啓動類中注入事務管理器
@Bean
public PlatformTransactionManager txManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
至此完成對鏈路追蹤信息持久化到mysql的操作