《吃透微服務(wù)》- 服務(wù)追蹤之Sleuth
本文轉(zhuǎn)載自微信公眾號(hào)「小菜良記」,作者蔡不菜丶。轉(zhuǎn)載本文請(qǐng)聯(lián)系小菜良記公眾號(hào)。
這么這篇我們繼續(xù)我們的主題 《吃透微服務(wù)》 ,繼續(xù)了解分布式系統(tǒng)中解決鏈路追蹤的方案 --- Sleuth。吃透 當(dāng)然可以作為一個(gè)噱頭,但也可以作為一個(gè)目標(biāo)!
微服務(wù)的好壞我們不再爭(zhēng)議,最近看到很多反面宣傳微服務(wù)的特性,不知道是否大數(shù)據(jù)精準(zhǔn)投喂~ ps:估計(jì)后臺(tái)人物畫像已經(jīng)標(biāo)簽為一個(gè)反微分子了。這當(dāng)然是一句玩笑話,結(jié)束玩笑,我們來(lái)為微服務(wù)的學(xué)習(xí)添磚加瓦!
微服務(wù)的關(guān)鍵在于拆分服務(wù),但是關(guān)鍵不在 微 ,而在于合適的大小。當(dāng)我們跨出拆分服務(wù)的那一刻起,我們就得對(duì)我們拆分后的服務(wù)負(fù)責(zé),這個(gè)是一個(gè)程序員應(yīng)有的擔(dān)當(dāng)!當(dāng)服務(wù)出現(xiàn)問(wèn)題的時(shí)候,你是否還苦于排查,而無(wú)從定位。線上的代碼我們無(wú)法 debug,找出問(wèn)題的時(shí)刻往往出現(xiàn)以下對(duì)話:
除了無(wú)奈的苦笑,只能盲目的摸索,終于有一天程序員小菜不再妥協(xié),力求解決該問(wèn)題。咱們能不能搞個(gè)類似監(jiān)控的功能,當(dāng)請(qǐng)求進(jìn)來(lái)的時(shí)候,分配一個(gè)身份令牌的給它,當(dāng)它在微服務(wù)中游走的時(shí)候,每個(gè)服務(wù)都會(huì)留下它的痕跡,當(dāng)它出現(xiàn)問(wèn)題的時(shí)候,我們只需要檢查游走到哪一個(gè)微服務(wù),相當(dāng)于畫一個(gè)行為軌跡,通過(guò)行為軌跡追蹤服務(wù)的調(diào)用問(wèn)題!沾沾自喜的同時(shí)犯嘀咕了,這說(shuō)起來(lái)容易,實(shí)現(xiàn)起來(lái)好像有點(diǎn)困難~退堂鼓即將敲響,便想到微服務(wù)中怎么可能沒有人想到該問(wèn)題,于是便游走于微服務(wù)組件之中,力求找到解決問(wèn)題的實(shí)現(xiàn)。終于,Sleuth 出現(xiàn)了。
Sleuth
一、認(rèn)識(shí) Sleuth
前面做了那么多的鋪墊,就是想引出 Sleuth 這玩意。洋洋灑灑說(shuō)了一堆,那就簡(jiǎn)單用一句話描述一下它的作用:在分布式系統(tǒng)中提供追蹤解決方案。它大量借用了 Google Dapper 的設(shè)計(jì),難道又是個(gè)換殼的玩意?
我們先來(lái)了解一下 Sleuth 中的術(shù)語(yǔ)和相關(guān)概念,只為了讓我們更加專業(yè),更好 吹著牛x談架構(gòu)
- Trace
由一組 Trace Id 相同的 Span 串聯(lián)形成一個(gè)樹狀結(jié)構(gòu)。為了實(shí)現(xiàn)請(qǐng)求追蹤,當(dāng)請(qǐng)求到達(dá)分布式系統(tǒng)的入口端點(diǎn)時(shí),只需要服務(wù)跟蹤框架為該請(qǐng)求創(chuàng)建一個(gè)唯一的標(biāo)識(shí)(即 Trace Id),同時(shí)在恩不是系統(tǒng)內(nèi)部流轉(zhuǎn)的時(shí)候,框架是中保持傳遞該唯一值,直到整個(gè)請(qǐng)求的返回。那么我們就可以使用該唯一標(biāo)識(shí)將所有的請(qǐng)求串聯(lián)起來(lái),形成一條完整的請(qǐng)求鏈路。
- Span
代表了一組基本的工作單元。為了統(tǒng)計(jì)各處理單元的延遲,當(dāng)請(qǐng)求到達(dá)各個(gè)服務(wù)組件的時(shí)候,也通過(guò)一個(gè)唯一標(biāo)識(shí)(SpanId)來(lái)標(biāo)記它的開始,具體過(guò)程和結(jié)束。通過(guò) SpanId 的開始和結(jié)束時(shí)間戳,就能統(tǒng)計(jì)該 Span 的調(diào)用時(shí)間,除此之外,我們還可以獲取如事件的名稱,請(qǐng)求信息等元數(shù)據(jù)
- Annotation
用于記錄一段時(shí)間內(nèi)的事件。內(nèi)部使用的重要注釋如下:
- cs(Client Send):客戶端發(fā)出請(qǐng)求,開始一個(gè)請(qǐng)求的事件
- sr(Server Received) :服務(wù)端接受到請(qǐng)求開始進(jìn)行處理。sr - cs = 網(wǎng)絡(luò)延遲(服務(wù)調(diào)用的時(shí)間)
- ss(Server Send):服務(wù)端處理完畢準(zhǔn)備發(fā)送到客戶端。ss -sr = 服務(wù)器上的請(qǐng)求處理時(shí)間
- cr(Client Reveived):客戶端接受到服務(wù)端的響應(yīng),請(qǐng)求結(jié)束。cr - sr = 請(qǐng)求的總時(shí)間
理解完三個(gè)概念,我們下面就直接上手認(rèn)識(shí)!
二、掌握 Sleuth
我們老樣子請(qǐng)出我們的微服務(wù)項(xiàng)目:
簡(jiǎn)簡(jiǎn)單單幾個(gè)服務(wù)做個(gè)自我介紹
- store-gateway: 服務(wù)網(wǎng)關(guān)
- store-order: 訂單服務(wù)
- store-product: 產(chǎn)品服務(wù)
然后我們需要在父工程中引入 Sleuth 依賴
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-starter-sleuth</artifactId>
- </dependency>
直接在父工程的 dependencies 中引入,這樣子每個(gè)子服務(wù)就可以不同重復(fù)引入該依賴
然后開始我們的簡(jiǎn)易代碼模式:
store-order服務(wù)
OrderController:
ProductService:
store-product 服務(wù)
ProductController:
我們?cè)谟唵畏?wù) 中使用 Feign 遠(yuǎn)程調(diào)用 產(chǎn)品服務(wù) 中的接口,然后啟動(dòng)服務(wù)調(diào)用后看控制臺(tái)打印:
注意看我圈出來(lái)的部分,有沒有發(fā)現(xiàn)了些許不同,沒錯(cuò)!這一串字符中包含的信息有 服務(wù)名 / TraceId / SpanId。依次調(diào)用有一個(gè)全局的 TraceId,將調(diào)用鏈路穿起來(lái)。通過(guò)分析微服務(wù)的日志,不難看出請(qǐng)求的具體過(guò)程。但是有個(gè)弊端,但服務(wù)數(shù)量增多,或者日志數(shù)量增多,從日志文件中撈出某個(gè)請(qǐng)求的調(diào)用過(guò)程,可并非是件易事,那么有沒有一個(gè)可以全文檢索和可視化展示的插件幫助我們解決該問(wèn)題?既然提出問(wèn)題,小菜自然已經(jīng)找到解決問(wèn)題的方法!那就是 ZipKin
三、使用 ZipKin
ZipKin 是 Twitter 開源的一個(gè)項(xiàng)目,它也是基于 Google Dapper 實(shí)現(xiàn)的,主要作用便是解決我們上面提到的問(wèn)題:收集服務(wù)的定時(shí)數(shù)據(jù),以解決微服務(wù)架構(gòu)中的延遲問(wèn)題,包括數(shù)據(jù)的收集,存儲(chǔ),查找和展現(xiàn)
我們可以使用它來(lái)收集各個(gè)服務(wù)器上請(qǐng)求鏈路的跟蹤數(shù)據(jù),并通過(guò)它提供的 REST API 接口來(lái)輔助我們查詢跟蹤數(shù)據(jù)以實(shí)現(xiàn)對(duì)分布式系統(tǒng)的監(jiān)控程序,從而及時(shí)地發(fā)現(xiàn)系統(tǒng)中出現(xiàn)的延遲升高問(wèn)題并找出系統(tǒng)性能瓶頸的根源!
我們除了面向開發(fā)的 API 接口之外, ZipKin 也提供了方便的 UI 組件來(lái)幫我們更加直觀的搜索跟蹤信息和分析請(qǐng)求鏈路明細(xì),比如:可以查詢某段時(shí)間內(nèi)各用戶請(qǐng)求的處理時(shí)間等。
聽到 UI 組件是不是感到眼前一亮,說(shuō)明我們可以通過(guò)控制臺(tái)更好的管理鏈路跟蹤
不僅如此,ZipKin 還提供了可插拔式的數(shù)據(jù)存儲(chǔ)方式,例如:In-Memory、MySQL、Cassandra以及Elasticsearch。支持方式不算少,總有一種你喜歡的!
我們來(lái)看看 ZipKin 的基礎(chǔ)架構(gòu)圖:
可以看出 ZipKin 結(jié)構(gòu)并不復(fù)雜,有四個(gè)核心組件構(gòu)成:
- Collector:收集器組件。主要用于處理外部系統(tǒng)發(fā)現(xiàn)過(guò)來(lái)的跟蹤信息,然后將這些信息轉(zhuǎn)換為 ZipKin 內(nèi)部處理的 Span 格式,以便支持后續(xù)的存儲(chǔ)、分析、展示等功能
- Storage: 存儲(chǔ)組件。主要用于對(duì)處理收集器接收到的跟蹤信息,默認(rèn)會(huì)將這些信息存儲(chǔ)到內(nèi)存中,我們可以修改存儲(chǔ)策略,將其存儲(chǔ)到我們的其他存儲(chǔ)倉(cāng)庫(kù)中
- RestFul API:API 組件。用于提供外部訪問(wèn)的接口,比如客戶端的跟蹤信息,或外接系統(tǒng)的訪問(wèn)信息
- WebUI:UI 組件。基于 API 組件實(shí)現(xiàn)的上層應(yīng)用,通過(guò) UI 組件用戶可以方便而直觀地查詢和分析跟蹤信息
因此 ZiPKin 使用起來(lái)有些類似我們上篇說(shuō)到的 Sentinel ,它分為 服務(wù)端 和 客戶端。
客戶端就是指微服務(wù)中的應(yīng)用,在客戶端中配置服務(wù)端的 URL 地址,一旦發(fā)生服務(wù)間的調(diào)用的時(shí)候,會(huì)被配置在微服務(wù)中的 Sleuth 的監(jiān)聽器監(jiān)聽,并生相應(yīng)的 Trace 和 Span 信息發(fā)送給服務(wù)端。
1. ZipKin 服務(wù)端
服務(wù)端是一個(gè)現(xiàn)成的 SpringBoot 服務(wù),我們只需要下載 jar 包 ,便可以直接運(yùn)行!下載地址
然后我們通過(guò)命令行的方式啟動(dòng)即可:
- java -jar zipkin-server-2.12.9-exec.jar
成功啟動(dòng)后通過(guò)訪問(wèn) http://localhost:9411 可以看到以下頁(yè)面:
到這步為止,我們服務(wù)端就已經(jīng)成功安裝。然后回到我們的客戶端集成上去
2. ZipKin 客戶端
我們需要在每個(gè)服務(wù)中引入 ZipKin 的依賴
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-starter-zipkin</artifactId>
- </dependency>
然后在配置文件中進(jìn)行配置:
- spring:
- zipkin:
- base-url: http://127.0.0.1:9411/ #zipkin 服務(wù)端地址
- discoveryClientEnabled: false #讓nacos把它當(dāng)成一個(gè)URL,而不要當(dāng)做服務(wù)名
- sleuth:
- sampler:
- probability: 1.0 #采樣的百分比
然后我們回到項(xiàng)目中,啟動(dòng)項(xiàng)目通過(guò)訪問(wèn)我們上面已經(jīng)定義好的接口訪問(wèn)微服務(wù),之后在 ZipKin 的UI界面進(jìn)行觀察
可以看到已經(jīng)出現(xiàn)了我們剛剛訪問(wèn)微服務(wù)的請(qǐng)求鏈路了,點(diǎn)擊其中一條記錄可以查看具體的訪問(wèn)路線。
通過(guò)對(duì)請(qǐng)求鏈路進(jìn)行追蹤,就可以確定服務(wù)的哪一個(gè)模塊更耗時(shí),進(jìn)而可以進(jìn)行優(yōu)化或者排查 Bug。
3. ZipKin 持久化
在 ZipKin 中默認(rèn)會(huì)將鏈路跟蹤的數(shù)據(jù)保存到內(nèi)存中,但是這種方式明顯不適合于生產(chǎn)環(huán)境。因此在 ZipKin 中支持將追蹤鏈路的數(shù)據(jù)持久化到 mysql 數(shù)據(jù)庫(kù)中或 elasticsearch 中。
MySQL
- 首先我們需要配置數(shù)據(jù)庫(kù)信息
- 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,
- `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'
- ) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;
- ALTER TABLE zipkin_spans ADD UNIQUE KEY(`trace_id_high`, `trace_id`, `id`) COMMENT 'ignore insert on duplicate';
- ALTER TABLE zipkin_spans ADD INDEX(`trace_id_high`, `trace_id`, `id`) COMMENT 'for joining with zipkin_annotations';
- 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(`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';
- ALTER TABLE zipkin_annotations ADD INDEX(`a_key`) COMMENT 'for getTraces';
- 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
- ) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;
- ALTER TABLE zipkin_dependencies ADD UNIQUE KEY(`day`, `parent`, `child`);
- 啟動(dòng)時(shí)指定 mysql 信息
- java -DSTORAGE_TYPE=mysql -DMYSQL_HOST=127.0.0.1 -DMYSQL_TCP_PORT=3306 -DMYSQL_DB=zipkin -DMYSQL_USER=root -DMYSQL_PASS=root -jar zipkin-server-2.12.9-exec.jar
ElasticSearch
- 啟動(dòng) ES
- 啟動(dòng)時(shí)指定 ES 信息
- java -jar zipkin-server-2.12.9-exec.jar --STORAGE_TYPE=elasticsearch --ES-HOST=localhost:9200
END
到這里我們就完成了分布式系統(tǒng)中服務(wù)鏈路跟蹤的解決!那為我們解決了問(wèn)題呢?
- 跨微服務(wù)的API調(diào)用發(fā)生異常,快速定位出問(wèn)題出在哪里。
- 跨微服務(wù)的API調(diào)用發(fā)生性能瓶頸,迅速定位出性能瓶頸。







































