- 2010年谷歌發(fā)表了其內(nèi)部使用的分布式跟蹤系統(tǒng)Dapper的論文講述了Dapper在谷歌內(nèi)部兩年的演變和設(shè)計(jì)、運(yùn)維經(jīng)驗(yàn),Twitter也根據(jù)該論文開發(fā)了自己的分布式跟蹤系統(tǒng)Zipkin,并將其開源,但不知為啥沒有貢獻(xiàn)給Apache。其實(shí)還有很多的分布式跟蹤系統(tǒng),比如naver的Pinpoint、Apache的HTrace,阿里的鷹眼Tracing、京東的Hydra、新浪的Watchman等。
- 大型互聯(lián)網(wǎng)公司為什么需要分布式跟蹤系統(tǒng)?為了支撐日益增長的龐大業(yè)務(wù)量,我們會(huì)把服 務(wù)進(jìn)行整合、拆分,使我們的服務(wù)不僅能通過集群部署抵擋流量的沖擊,又能根據(jù)業(yè)務(wù)在其上進(jìn)行靈活的擴(kuò)展。一次請(qǐng)求少則經(jīng)過三四次服務(wù)調(diào)用完成,多則跨越幾十個(gè)甚至是上百個(gè)服務(wù)點(diǎn)。如何動(dòng)態(tài)展示服務(wù)的鏈路?如何分析服務(wù)鏈路的瓶頸并對(duì)其進(jìn)行調(diào)優(yōu)?如何快速進(jìn)行服務(wù)鏈路的故障發(fā)現(xiàn)?這就是服務(wù)跟蹤系統(tǒng)存在的目的和意義。
- 即使作為分布式系統(tǒng)的開發(fā)者,也很難清楚的說出某個(gè)服務(wù)的調(diào)用鏈路,況且服務(wù)調(diào)用鏈路還是動(dòng)態(tài)變化的,這時(shí)候只能咬咬牙翻代碼了。接下來,我們看看Zipkin是如何做到這一點(diǎn)的。在這之前,我們先來簡(jiǎn)單討論一下分布式跟蹤系統(tǒng)的設(shè)計(jì)要點(diǎn),第一點(diǎn):對(duì)應(yīng)用透明、低侵入。為什么說這一點(diǎn)最重要?因?yàn)榉植际较到y(tǒng)面對(duì)的客戶是開發(fā)者,如果他們的系統(tǒng)需要花費(fèi)較大的改造才能接入你的分布式跟蹤系統(tǒng),除非你是他的老板,否則他會(huì)直接和你說:No!!沒人用是最慘的結(jié)果。那么怎么才能做到對(duì)業(yè)務(wù)系統(tǒng)最低的侵入性呢?Dapper給出的建議是在公共庫和中間件上做文章。沒錯(cuò),分布式系統(tǒng)之間的通訊靠的都是RPC、MQ等中間件系統(tǒng),即使是內(nèi)部使用的線程池或者數(shù)據(jù)庫連接池,大多也是使用經(jīng)過公司包裝公共庫,這就給服務(wù)跟蹤帶來了機(jī)會(huì),我只要對(duì)中間件和公共庫進(jìn)行改造,就幾乎可以做到全方位跟蹤,當(dāng)然,這也是有難度的;第二點(diǎn):低開銷、高穩(wěn)定。大多數(shù)應(yīng)用不愿意接入監(jiān)控系統(tǒng)的原因是怕影響線上服務(wù)器的性能,特別是那些對(duì)性能特別敏感的應(yīng)用,所以,分布式跟蹤系統(tǒng)一定要輕量級(jí),不能有太復(fù)雜的邏輯和外部依賴,甚至需要做到根據(jù)服務(wù)的流量來動(dòng)態(tài)調(diào)整采集密度。第三點(diǎn):可擴(kuò)展。隨著接入的分布式系統(tǒng)的增多,壓力也將不斷增長,分布式跟蹤系統(tǒng)是否能動(dòng)態(tài)的擴(kuò)展來支撐不斷接入的業(yè)務(wù)系統(tǒng),這也是設(shè)計(jì)時(shí)需要考慮的。可以看出,這三點(diǎn)并沒有什么特別,對(duì)于服務(wù)降級(jí)系統(tǒng)、分布式跟蹤系統(tǒng)和業(yè)務(wù)監(jiān)控系統(tǒng)等,這三點(diǎn)都是必須的。
回到主題,Zipkin的設(shè)計(jì),一般的分布式跟蹤系統(tǒng)數(shù)據(jù)流主要分為三個(gè)步驟:采集、發(fā)送和落盤分析,我們來看Zipkin官網(wǎng)給出的設(shè)計(jì)圖
這里埋怨一下,Zipkin官網(wǎng)的內(nèi)容太過簡(jiǎn)單(難道是因?yàn)閼胁艖械萌pache孵化?),也許Twitter認(rèn)為有谷歌Dapper那邊文章就足夠了吧。我們看上圖,其中的S表示的是發(fā)送跟蹤數(shù)據(jù)的客戶端SDK還是Scribe的客戶端(因?yàn)門witter內(nèi)部采用的就是Scribe來采集跟蹤數(shù)據(jù))?效果都一樣,總而言之我們看到的就是各個(gè)應(yīng)用、中間件甚至是數(shù)據(jù)庫將跟蹤數(shù)據(jù)發(fā)送到Zipkin服務(wù)器。
總體設(shè)計(jì)沒什么特別,我們看下內(nèi)部的數(shù)據(jù)模型是怎么設(shè)計(jì)的。一般的調(diào)用鏈都可以展現(xiàn)成一顆樹,比如下面的簡(jiǎn)單調(diào)用:
上圖描述的服務(wù)調(diào)用場(chǎng)景應(yīng)該是很常見也很簡(jiǎn)單的調(diào)用場(chǎng)景了,一個(gè)請(qǐng)求通過Gateway服務(wù)路由到下游的Service1,然后Service1先調(diào)用服務(wù)Service2,拿到結(jié)果后再調(diào)用服務(wù)Service3,最后組合Service2和Service3服務(wù)的結(jié)果,通過Gateway返回給用戶。我們用①②③④⑤⑥表示了RPC的順序,那么,什么是span?span直譯過來是"跨度",在谷歌的Dapper論文中表示跟蹤樹中樹節(jié)點(diǎn)引用的數(shù)據(jù)結(jié)構(gòu)體,span是跟蹤系統(tǒng)中的基本數(shù)據(jù)單元,Dapper的論文中,并沒有具體介紹span中的全部細(xì)節(jié),但在Zipkin中,每個(gè)span中一般包含如下字段:
traceId:全局跟蹤ID,用它來標(biāo)記一次完整服務(wù)調(diào)用,所以和一次服務(wù)調(diào)用相關(guān)的span中的traceId都是相同的,Zipkin將具有相同traceId的span組裝成跟蹤樹來直觀的將調(diào)用鏈路圖展現(xiàn)在我們面前。這里直接給出Zipkin官網(wǎng)中的一張Zipkin界面的圖:
id:span的id,理論上來說,span的id只要做到一個(gè)traceId下唯一就可以,比如說阿里的鷹眼系統(tǒng)巧妙用span的id來體現(xiàn)調(diào)用層次關(guān)系(例如0,0.1,0.2,0.1.1等),但Zipkin中的span的id則沒有什么實(shí)際含義。
parentId:父span的id,調(diào)用有層級(jí)關(guān)系,所以span作為調(diào)用節(jié)點(diǎn)的存儲(chǔ)結(jié)構(gòu),也有層級(jí)關(guān)系,就像圖3所示,跟蹤鏈?zhǔn)遣捎酶櫂涞男问絹碚宫F(xiàn)的,樹的根節(jié)點(diǎn)就是調(diào)用調(diào)用的頂點(diǎn),從開發(fā)者的角度來說,頂級(jí)span是從接入了Zipkin的應(yīng)用中最先接觸到服務(wù)調(diào)用的應(yīng)用中采集的。所以,頂級(jí)span是沒有parentId字段的,拿圖2所展現(xiàn)的例子來說,頂級(jí)span由Gateway來采集,Service1的span是它的子span,而Service2和Service3的span是Service1的span的子span,很顯然Service2和Service3的span是平級(jí)關(guān)系。
name:span的名稱,主要用于在界面上展示,一般是接口方法名,name的作用是讓人知道它是哪里采集的span,不然某個(gè)span耗時(shí)高我都不知道是哪個(gè)服務(wù)節(jié)點(diǎn)耗時(shí)高。
timestamp:span創(chuàng)建時(shí)的時(shí)間戳,用來記錄采集的時(shí)刻。
duration:持續(xù)時(shí)間,即span的創(chuàng)建到span完成最終的采集所經(jīng)歷的時(shí)間,除去span自己邏輯處理的時(shí)間,該時(shí)間段可以理解成對(duì)于該跟蹤埋點(diǎn)來說服務(wù)調(diào)用的總耗時(shí)。
annotations:基本標(biāo)注列表,一個(gè)標(biāo)注可以理解成span生命周期中重要時(shí)刻的數(shù)據(jù)快照,比如一個(gè)標(biāo)注中一般包含發(fā)生時(shí)刻(timestamp)、事件類型(value)、端點(diǎn)(endpoint)等信息,這里給出一個(gè)標(biāo)注的json結(jié)構(gòu):
{
"timestamp": 1476197069680000,
"value": "cs",
"endpoint": {
"serviceName": "service1",
"ipv4": "xxx.xxx.xxx.111"
}
}
那么,有哪些事件類型呢?答案是四種:cs(客戶端/消費(fèi)者發(fā)起請(qǐng)求)、cr(客戶端/消費(fèi)者接收到應(yīng)答)、sr(服務(wù)端/生產(chǎn)者接收到請(qǐng)求)和ss(服務(wù)端/生產(chǎn)者發(fā)送應(yīng)答)。可以看出,這四種事件類型的統(tǒng)計(jì)都應(yīng)該是Zipkin提供客戶端來做的,因?yàn)檫@些事件和業(yè)務(wù)無關(guān),這也是為什么跟蹤數(shù)據(jù)的采集適合放到中間件或者公共庫來做的原因。
binaryAnnotations:業(yè)務(wù)標(biāo)注列表,如果某些跟蹤埋點(diǎn)需要帶上部分業(yè)務(wù)數(shù)據(jù)(比如url地址、返回碼和異常信息等),可以將需要的數(shù)據(jù)以鍵值對(duì)的形式放入到這個(gè)字段中。
說到這里,大家對(duì)span的印象可能還是有點(diǎn)模糊不清,于是我們繼續(xù)拿圖2的服務(wù)調(diào)用來舉例,如果我們將圖2的應(yīng)用接入Zipkin,將會(huì)是下圖的效果:
這里我們看到,Gateway、Service1、Service2和Service3都在往Zipkin發(fā)送跟蹤數(shù)據(jù),你一定會(huì)感覺奇怪,Gateway作為服務(wù)調(diào)用的起點(diǎn),難道不是由Service1、Service2和Service3把各自的跟蹤數(shù)據(jù)傳回Gateway然后再由Gateway統(tǒng)計(jì)并整理好一并發(fā)往Zipkin服務(wù)端嗎?認(rèn)真想想就知道這種設(shè)計(jì)的弊端,如果一次完整的服務(wù)請(qǐng)求調(diào)用鏈路特長,比如設(shè)計(jì)上百個(gè)服務(wù)節(jié)點(diǎn)的通訊,那么將各服務(wù)節(jié)點(diǎn)的span信息傳回給頂級(jí)span和將跟蹤數(shù)據(jù)匯總并發(fā)送到Zipkin將帶來巨大的網(wǎng)絡(luò)開銷,這是不值當(dāng)?shù)模€不如將跟蹤數(shù)據(jù)組裝的任務(wù)直接交給Zipkin來做,這樣Zipkin的客戶端SDK不需要有過于復(fù)雜的邏輯,也節(jié)省了大量的網(wǎng)絡(luò)帶寬資源,可擴(kuò)展性大大提高。
需要注意的是,并不是每個(gè)span上都會(huì)完整的發(fā)生cs、cr、sr和ss這四種事件,比如圖4中Gateway上的span只會(huì)有cs和cr,因?yàn)镚ateway沒有上游應(yīng)用,Service2和Service3上的span有sr和ss,但不會(huì)有cs和cr,因?yàn)閷?duì)于此次服務(wù)調(diào)用來說,Service2和Service3并不依賴下游任何應(yīng)用服務(wù)。但對(duì)于Service1來說就復(fù)雜得多,它將產(chǎn)生三個(gè)Span,接收和應(yīng)答Gateway是一個(gè)span,調(diào)用和接收Service2是一個(gè)span,調(diào)用和接收Service3是第三個(gè)span,注意,一個(gè)span只能用于記錄兩個(gè)應(yīng)用之間的服務(wù)調(diào)用,所以不能將這三個(gè)span信息合成一個(gè)。由cs、cr、sr和ss事件的時(shí)間,可以得出很多時(shí)間數(shù)據(jù),例如:
請(qǐng)求總耗時(shí) = Gateway.cr - Gateway.cs
①的網(wǎng)絡(luò)耗時(shí) = Service1.sr - Gateway.cs
Service1的調(diào)用Service2的耗時(shí) = Service1.cr - Service1.cs (圖4中Service1節(jié)點(diǎn)上的第二個(gè)span中的cr和cs)
Service1的調(diào)用Service3的耗時(shí) = Service1.cr - Service1.cs (圖4中Service1節(jié)點(diǎn)上的第三個(gè)span中的cr和cs)
④的網(wǎng)絡(luò)耗時(shí) = Service3.sr - Service1.cs (圖4中Service1節(jié)點(diǎn)上的第三個(gè)span中的cs)
可以這樣說,如果采集到這些span,幾乎所有階段的耗時(shí)都可以計(jì)算出來。
如果要推廣Zipkin,除了Zipkin服務(wù)端要有出色的擴(kuò)展性和友好豐富的數(shù)據(jù)展示界面外,提供多種類型的客戶端SDK也是很重要的,因?yàn)楦檾?shù)據(jù)的采集都是有中間件和公共庫做的,所以SDK不應(yīng)該太過復(fù)雜,最理想的做法是官方給一些著名開發(fā)語言和中間件提供默認(rèn)的SDK實(shí)現(xiàn),目前根據(jù)Zipkin的官方說明,已經(jīng)給Go(zipkin-go-opentracing)、Java(brave)、JavaScript(zipkin-js)、Ruby(zipkin-ruby)和Scala(zipkin-finagle)提供了官方的庫,社區(qū)方面也很給力,提供了多種方案的庫實(shí)現(xiàn),詳見:http://zipkin.io/pages/existing_instrumentations.html。
分布式跟蹤系統(tǒng)(二):Zipkin的Span模型:http://manzhizhen.iteye.com/blog/2347153
最后,給出圖4四個(gè)服務(wù)采集的span數(shù)據(jù)樣例:
# Gateway的span
{
"traceId": "daaed0921874ebc3",
"id": "daaed0921874ebc3",
"name": "get",
"timestamp": 1476197067420000,
"duration": 4694000,
"annotations": [
{
"timestamp": 1476197067420000,
"value": "cs",
"endpoint": {
"serviceName": "gateway",
"ipv4": "xxx.xxx.xxx.110"
}
},
{
"timestamp": 1476197072114000,
"value": "cr",
"endpoint": {
"serviceName": "gateway",
"ipv4": "xxx.xxx.xxx.110"
}
}
],
"binaryAnnotations": [
{
"key": "http.url",
"value": "[http://localhost:8080/service1](http://localhost:8080/service1)",
"endpoint": {
"serviceName": "gateway",
"ipv4": "xxx.xxx.xxx.110"
}
}
]
}
# Service1的三個(gè)span
{
"traceId": "daaed0921874ebc3",
"id": "411d4c32c102a974",
"name": "get",
"parentId": "daaed0921874ebc3",
"timestamp": 1476197069680000,
"duration": 1168000,
"annotations": [
{
"timestamp": 1476197069680000,
"value": "cs",
"endpoint": {
"serviceName": "service1",
"ipv4": "xxx.xxx.xxx.111"
}
},
{
"timestamp": 1476197070848000,
"value": "cr",
"endpoint": {
"serviceName": "service1",
"ipv4": "xxx.xxx.xxx.111"
}
}
],
"binaryAnnotations": [
{
"key": "http.url",
"value": "[http://localhost:8089/service2](http://localhost:8089/service2)",
"endpoint": {
"serviceName": "service1",
"ipv4": "xxx.xxx.xxx.111"
}
}
]
}
{
"traceId": "daaed0921874ebc3",
"id": "7c0d7d897a858217",
"name": "get",
"parentId": "daaed0921874ebc3",
"timestamp": 1476197070850000,
"duration": 1216000,
"annotations": [
{
"timestamp": 1476197070850000,
"value": "cs",
"endpoint": {
"serviceName": "service1",
"ipv4": "xxx.xxx.xxx.111"
}
},
{
"timestamp": 1476197072066000,
"value": "cr",
"endpoint": {
"serviceName": "service1",
"ipv4": "xxx.xxx.xxx.111"
}
}
],
"binaryAnnotations": [
{
"key": "http.url",
"value": "[http://localhost:8090/service3](http://localhost:8090/service3)",
"endpoint": {
"serviceName": "service1",
"ipv4": "xxx.xxx.xxx.111"
}
}
]
}
{
"traceId": "daaed0921874ebc3",
"id": "daaed0921874ebc3",
"name": "get",
"timestamp": 1476197067623000,
"duration": 4479000,
"annotations": [
{
"timestamp": 1476197067623000,
"value": "sr",
"endpoint": {
"serviceName": "service1",
"ipv4": "xxx.xxx.xxx.111"
}
},
{
"timestamp": 1476197072102000,
"value": "ss",
"endpoint": {
"serviceName": "service1",
"ipv4": "xxx.xxx.xxx.111"
}
}
],
"binaryAnnotations": [
{
"key": "http.status_code",
"value": "200",
"endpoint": {
"serviceName": "service1",
"ipv4": "xxx.xxx.xxx.111"
}
},
{
"key": "http.url",
"value": "/service1",
"endpoint": {
"serviceName": "service1",
"ipv4": "xxx.xxx.xxx.111"
}
}
]
}
# Service2 的span
{
"traceId": "daaed0921874ebc3",
"id": "411d4c32c102a974",
"name": "get",
"parentId": "daaed0921874ebc3",
"timestamp": 1476197069806000,
"duration": 1040000,
"annotations": [
{
"timestamp": 1476197069806000,
"value": "sr",
"endpoint": {
"serviceName": "service2",
"ipv4": "xxx.xxx.xxx.112"
}
},
{
"timestamp": 1476197070846000,
"value": "ss",
"endpoint": {
"serviceName": "service2",
"ipv4": "xxx.xxx.xxx.112"
}
}
],
"binaryAnnotations": [
{
"key": "http.status_code",
"value": "200",
"endpoint": {
"serviceName": "service2",
"ipv4": "xxx.xxx.xxx.112"
}
},
{
"key": "http.url",
"value": "/service2",
"endpoint": {
"serviceName": "service2",
"ipv4": "xxx.xxx.xxx.112"
}
}
]
}
# Service3的span
{
"traceId": "daaed0921874ebc3",
"id": "7c0d7d897a858217",
"name": "get",
"parentId": "daaed0921874ebc3",
"timestamp": 1476197071011000,
"duration": 1059000,
"annotations": [
{
"timestamp": 1476197071011000,
"value": "sr",
"endpoint": {
"serviceName": "service3",
"ipv4": "xxx.xxx.xxx.113"
}
},
{
"timestamp": 1476197072070000,
"value": "ss",
"endpoint": {
"serviceName": "service3",
"ipv4": "xxx.xxx.xxx.113"
}
}
],
"binaryAnnotations": [
{
"key": "http.status_code",
"value": "200",
"endpoint": {
"serviceName": "service3",
"ipv4": "xxx.xxx.xxx.113"
}
},
{
"key": "http.url",
"value": "/service3",
"endpoint": {
"serviceName": "service3",
"ipv4": "xxx.xxx.xxx.113"
}
}
]
}