1. 程式人生 > >分散式跟蹤系統(一):Zipkin的背景和設計

分散式跟蹤系統(一):Zipkin的背景和設計

       2010年穀歌發表了其內部使用的分散式跟蹤系統Dapper的論文(http://static.googleusercontent.com/media/research.google.com/zh-CN//archive/papers/dapper-2010-1.pdf,譯文地址:http://bigbully.github.io/Dapper-translation/),講述了Dapper在谷歌內部兩年的演變和設計、運維經驗,Twitter也根據該論文開發了自己的分散式跟蹤系統Zipkin,並將其開源,但不知為啥沒有貢獻給Apache。其實還有很多的分散式跟蹤系統,比如Apache的HTrace,阿里的鷹眼Tracing、京東的Hydra、新浪的Watchman等。

      大型網際網路公司為什麼需要分散式跟蹤系統?為了支撐日益增長的龐大業務量,我們會把服務進行整合、拆分,使我們的服務不僅能通過叢集部署抵擋流量的衝擊,又能根據業務在其上進行靈活的擴充套件。一次請求少則經過三四次服務呼叫完成,多則跨越幾十個甚至是上百個服務節點。如何動態展示服務的鏈路?如何分析服務鏈路的瓶頸並對其進行調優?如何快速進行服務鏈路的故障發現?這就是服務跟蹤系統存在的目的和意義。

     即使作為分散式系統的開發者,也很難清楚的說出某個服務的呼叫鏈路,況且服務呼叫鏈路還是動態變化的,這時候只能咬咬牙翻程式碼了。接下來,我們看看Zipkin是如何做到這一點的。在這之前,我們先來簡單討論一下分散式跟蹤系統的設計要點,第一點:對應用透明、低侵入

。為什麼說這一點最重要?因為分散式系統面對的客戶是開發者,如果他們的系統需要花費較大的改造才能接入你的分散式跟蹤系統,除非你是他的老闆,否則他會直接和你說:No!!沒人用是最慘的結果。那麼怎麼才能做到對業務系統最低的侵入性呢?Dapper給出的建議是在公共庫和中介軟體上做文章。沒錯,分散式系統之間的通訊靠的都是RPC、MQ等中介軟體系統,即使是內部使用的執行緒池或者資料庫連線池,大多也是使用經過公司包裝公共庫,這就給服務跟蹤帶來了機會,我只要對中介軟體和公共庫進行改造,就幾乎可以做到全方位跟蹤,當然,這也是有難度的;第二點:低開銷、高穩定。大多數應用不願意接入監控系統的原因是怕影響線上伺服器的效能,特別是那些對效能特別敏感的應用,所以,分散式跟蹤系統一定要輕量級,不能有太複雜的邏輯和外部依賴,甚至需要做到根據服務的流量來動態調整採集密度。第三點:可擴充套件
。隨著接入的分散式系統的增多,壓力也將不斷增長,分散式跟蹤系統是否能動態的擴充套件來支撐不斷接入的業務系統,這也是設計時需要考慮的。可以看出,這三點並沒有什麼特別,對於服務降級系統、分散式跟蹤系統和業務監控系統等,這三點都是必須的。

     回到主題,Zipkin的設計,一般的分散式跟蹤系統資料流主要分為三個步驟:採集、傳送和落盤分析,我們來看Zipkin官網給出的設計圖:

 

Zipkin結構(圖1)

      這裡埋怨一下,Zipkin官網的內容太過簡單(難道是因為懶才懶得去Apache孵化?),也許Twitter認為有谷歌Dapper那邊文章就足夠了吧。我們看上圖,其中的S表示的是傳送跟蹤資料的客戶端SDK還是Scribe的客戶端(因為Twitter內部採用的就是Scribe來採集跟蹤資料)?效果都一樣,總而言之我們看到的就是各個應用、中介軟體甚至是資料庫將跟蹤資料傳送到Zipkin伺服器。

       總體設計沒什麼特別,我們看下內部的資料模型是怎麼設計的。一般的呼叫鏈都可以展現成一顆樹,比如下面的簡單呼叫:


簡單的服務呼叫(圖2)

        上圖描述的服務呼叫場景應該是很常見也很簡單的呼叫場景了,一個請求通過Gateway服務路由到下游的Service1,然後Service1先呼叫服務Service2,拿到結果後再呼叫服務Service3,最後組合Service2和Service3服務的結果,通過Gateway返回給使用者。我們用①②③④⑤⑥表示了RPC的順序,那麼,什麼是span?span直譯過來是"跨度",在谷歌的Dapper論文中表示跟蹤樹中樹節點引用的資料結構體,span是跟蹤系統中的基本資料單元,Dapper的論文中,並沒有具體介紹span中的全部細節,但在Zipkin中,每個span中一般包含如下欄位:

traceId:全域性跟蹤ID,用它來標記一次完整服務呼叫,所以和一次服務呼叫相關的span中的traceId都是相同的,Zipkin將具有相同traceId的span組裝成跟蹤樹來直觀的將呼叫鏈路圖展現在我們面前。這裡直接給出Zipkin官網中的一張Zipkin介面的圖:


Zipkin介面展現的跟蹤樹(圖3)

id:span的id,理論上來說,span的id只要做到一個traceId下唯一就可以,比如說阿里的鷹眼系統巧妙用span的id來體現呼叫層次關係(例如0,0.1,0.2,0.1.1等),但Zipkin中的span的id則沒有什麼實際含義。

parentId:父span的id,呼叫有層級關係,所以span作為呼叫節點的儲存結構,也有層級關係,就像圖3所示,跟蹤鏈是採用跟蹤樹的形式來展現的,樹的根節點就是呼叫呼叫的頂點,從開發者的角度來說,頂級span是從接入了Zipkin的應用中最先接觸到服務呼叫的應用中採集的。所以,頂級span是沒有parentId欄位的,拿圖2所展現的例子來說,頂級span由Gateway來採集,Service1的span是它的子span,而Service2和Service3的span是Service1的span的子span,很顯然Service2和Service3的span是平級關係。

name:span的名稱,主要用於在介面上展示,一般是介面方法名,name的作用是讓人知道它是哪裡採集的span,不然某個span耗時高我都不知道是哪個服務節點耗時高。

timestamp:span建立時的時間戳,用來記錄採集的時刻。

duration:持續時間,即span的建立到span完成最終的採集所經歷的時間,除去span自己邏輯處理的時間,該時間段可以理解成對於該跟蹤埋點來說服務呼叫的總耗時。

annotations:基本標註列表,一個標註可以理解成span生命週期中重要時刻的資料快照,比如一個標註中一般包含發生時刻(timestamp)、事件型別(value)、端點(endpoint)等資訊,這裡給出一個標註的json結構:

{

            "timestamp":1476197069680000,

            "value": "cs",

            "endpoint": {

                "serviceName": "service1",

                "ipv4": "xxx.xxx.xxx.111"

            }

 }

那麼,有哪些事件型別呢?答案是四種:cs(客戶端/消費者發起請求)、cr(客戶端/消費者接收到應答)、sr(服務端/生產者接收到請求)和ss(服務端/生產者傳送應答。可以看出,這四種事件型別的統計都應該是Zipkin提供客戶端來做的,因為這些事件和業務無關,這也是為什麼跟蹤資料的採集適合放到中介軟體或者公共庫來做的原因。

binaryAnnotations:業務標註列表,如果某些跟蹤埋點需要帶上部分業務資料(比如url地址、返回碼和異常資訊等),可以將需要的資料以鍵值對的形式放入到這個欄位中。

說到這裡,大家對span的印象可能還是有點模糊不清,於是我們繼續拿圖2的服務呼叫來舉例,如果我們將圖2的應用接入Zipkin,將會是下圖的效果:


接入Zipkin後(圖4)

         這裡我們看到,Gateway、Service1、Service2和Service3都在往Zipkin傳送跟蹤資料,你一定會感覺奇怪,Gateway作為服務呼叫的起點,難道不是由Service1、Service2和Service3把各自的跟蹤資料傳回Gateway然後再由Gateway統計並整理好一併發往Zipkin服務端嗎?認真想想就知道這種設計的弊端,如果一次完整的服務請求呼叫鏈路特長,比如設計上百個服務節點的通訊,那麼將各服務節點的span資訊傳回給頂級span和將跟蹤資料彙總併發送到Zipkin將帶來巨大的網路開銷,這是不值當的,還不如將跟蹤資料組裝的任務直接交給Zipkin來做,這樣Zipkin的客戶端SDK不需要有過於複雜的邏輯,也節省了大量的網路頻寬資源,可擴充套件性大大提高。

       需要注意的是,並不是每個span上都會完整的發生cs、cr、sr和ss這四種事件,比如圖4中Gateway上的span只會有cs和cr,因為Gateway沒有上游應用,Service2和Service3上的span有sr和ss,但不會有cs和cr,因為對於此次服務呼叫來說,Service2和Service3並不依賴下游任何應用服務。但對於Service1來說就複雜得多,它將產生三個Span,接收和應答Gateway是一個span,呼叫和接收Service2是一個span,呼叫和接收Service3是第三個span,注意,一個span只能用於記錄兩個應用之間的服務呼叫,所以不能將這三個span資訊合成一個。由cs、cr、sr和ss事件的時間,可以得出很多時間資料,例如:

請求總耗時 =Gateway.cr - Gateway.cs

①的網路耗時 = Service1.sr - Gateway.cs

Service1的呼叫Service2的耗時 = Service1.cr - Service1.cs (圖4中Service1節點上的第二個span中的cr和cs)

Service1的呼叫Service3的耗時 = Service1.cr - Service1.cs (圖4中Service1節點上的第三個span中的cr和cs)

④的網路耗時 = Service3.sr - Service1.cs (圖4中Service1節點上的第三個span中的cs)

可以這樣說,如果採集到這些span,幾乎所有階段的耗時都可以計算出來

        如果要推廣Zipkin,除了Zipkin服務端要有出色的擴充套件性和友好豐富的資料展示介面外,提供多種型別的客戶端SDK也是很重要的,因為跟蹤資料的採集都是有中介軟體和公共庫做的,所以SDK不應該太過複雜,最理想的做法是官方給一些著名開發語言和中介軟體提供預設的SDK實現,目前根據Zipkin的官方說明,已經給Go(zipkin-go-opentracing)、Java(brave)、JavaScript(zipkin-js)、Ruby(zipkin-ruby)和Scala(zipkin-finagle)提供了官方的庫,社群方面也很給力,提供了多種方案的庫實現,詳見:http://zipkin.io/pages/existing_instrumentations.html。

 http://blog.csdn.net/manzhizhen/article/details/53865368

       最後,給出圖4四個服務採集的span資料樣例:

# 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",
            "endpoint": {
                "serviceName": "gateway",
                "ipv4": "xxx.xxx.xxx.110"
            }
        }
    ]
}


# Service1的三個span
{
    "traceId": "daaed0921874ebc3",
    "id": "daaed0921874def7",
    "name": "get",
    "parentId": "daaed0921874ebc3",
    "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"
            }
        }
    ]
}
{
    "traceId": "daaed0921874ebc3",
    "id": "411d4c32c102a974",
    "name": "get",
    "parentId": "daaed0921874def7",
    "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",
            "endpoint": {
                "serviceName": "service1",
                "ipv4": "xxx.xxx.xxx.111"
            }
        }
    ]
}


{
    "traceId": "daaed0921874ebc3",
    "id": "7c0d7d897a858217",
    "name": "get",
    "parentId": "daaed0921874def7",
    "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",
            "endpoint": {
                "serviceName": "service1",
                "ipv4": "xxx.xxx.xxx.111"
            }
        }
    ]
}






# Service2 的span
{
    "traceId": "daaed0921874ebc3",
    "id": "411d4c32c102a888",
    "name": "get",
    "parentId": "411d4c32c102a974",
    "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": "7c0d7d897a858982",
    "name": "get",
    "parentId": "7c0d7d897a858217",
    "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"
            }
        }
    ]
}