Appdash,用Go實現的分布式系統跟蹤神器
六 17
bigwhite 技術志 appdash,Baidu,dapper,distributedsystem,eBay,github,Go,Google,http,Opensource,sourcegraph,span,taobao,Trace,推ter,zipkin, 云計算,亞馬遜,分布式系統,基礎設施,開源,性能優化,線程局部存儲,跟蹤系統 No Comments
在“云”盛行的今天, 分布式系統 已不是什么新鮮的玩意兒。用腳也能想得出來:Google、baidu、淘寶、亞馬遜、推ter等IT巨頭 背后的巨型計算平臺都是分布式系統了,甚至就連一個簡單的微信公眾號應用的后端也都分布式了,即便僅有幾臺機器而已。分布式讓系統富有彈性,面 對紛繁變化的需求,可以伸縮自如。但分布式系統也給開發以及運維人員帶來了難題:如何監控和優化分布式系統的行為。
以google為例,想象一下,用戶通過瀏覽器發起一個搜索請求,Google后端可能會有成百上千臺機器、多種編程語言實現的幾十個、上百個應 用服務開始忙碌起來,一起計算請求的返回結果。一旦這個過程中某一個環節出現問題/bug,那么查找和定位起來是相當困難的,于是乎分布式系統跟 蹤系統出爐了。Google在2010年發表了著名論文《 Dapper, a Large-Scale Distributed Systems Tracing Infrastructure 》(中文版在 這里 )。Dapper是google內部使用的一個分布式系統跟蹤基礎設施,與之前的一些跟蹤系統相比,Dapper以低消耗、對應用透明以及良好的擴展性著 稱。并且 Google Dapper更傾向于性能數據方面的收集和調查,可以輔助開發人員和運維人員發現分布式系統的性能瓶頸并著手優化。Dapper出現后,各大巨頭開始跟 風,比如推ter的 Zipkin (開源)、淘寶的“鷹眼”、eBay的Centralized Activity Logging (CAL)等,它們基本上都是參考google的dapper論文設計和實現的。
而本文將要介紹的 Appdash 則是 sourcegraph 開源的一款用 Go 實現的分布式系統跟蹤工具套件,它同樣是以google的 dapper為原型設計和實現的,目前用于sourcegraph平臺的性能跟蹤和監控。
一、原理
Appdash實現了Google dapper中的四個主要概念:
【Span】
Span指的是一個服務調用的跨度,在實現中用SpanId標識。根服務調用者的Span為根span(root span),在根級別進行的下一級服務調用Span的Parent Span為root span。以此類推,服務調用鏈構成了一棵tree,整個tree構成了一個Trace。
Appdash中SpanId由三部分組成: TraceID/SpanID/parentSpanID ,例如: 34c31a18026f61df/aab2a63e86ac0166/592043d0a5871aaf 。TraceID用于唯一標識一次Trace。traceid在申請RootSpanID時自動分配。
在上面原理圖中,我們也可以看到一次Trace過程中SpanID的情況。圖中調用鏈大致是:
frontservice:
call serviceA
call serviceB
call serviceB1
… …
call serviceN
對應服務調用的Span的樹形結構如下:
frontservice: SpanId = xxxxx/nnnn1,該span為root span:traceid=xxxxx, spanid=nnnn1,parent span id為空。
serviceA: SpanId = xxxxx/nnnn2/nnnn1,該span為child span:traceid=xxxxx, spanid=nnnn2,parent span id為root span id:nnnn1。
serviceB: SpanId = xxxxx/nnnn3/nnnn1,該span為child span:traceid=xxxxx, spanid=nnnn3,parent span id為root span id:nnnn1。
… …
serviceN: SpanId = xxxxx/nnnnm/nnnn1,該span為child span:traceid=xxxxx, spanid=nnnnm,parent span id為root span id:nnnn1。
serviceB1: SpanId = xxxxx/nnnn3-1/nnnn3,該span為serviceB的child span,traceid=xxxxx, spanid=nnnn3-1,parent span id為serviceB的spanid:nnnn3
【Event】
個人理解在Appdash中Event是服務調用跟蹤信息的wrapper。最終我們在Appdash UI上看到的信息,都是由event承載的并且發給Appdash Server的信息。在Appdash中,你可以顯式使用event埋點,吐出跟蹤信息,也可以使用Appdash封裝好的包接口,比如 httptrace.Transport等發送調用跟蹤信息,這些包的底層實現也是基于event的。event在傳輸前會被encoding為 Annotation的形式。
【Recorder】
在Appdash中,Recorder是用來發送event給Appdash的Collector的,每個Recorder會與一個特定的span相關聯。
【Collector】
從Recorder那接收Annotation(即encoded event)。通常一個appdash server會運行一個Collector,監聽某個跟蹤信息收集端口,將收到的信息存儲在Store中。
二、安裝
appdash是開源的,通過go get即可得到源碼并安裝example:
go get - u sourcegraph . com / sourcegraph / appdash / cmd /…
appdash自帶一個example,在examples/cmd/webapp下面。執行webapp,你會看到如下結果:
$webapp
2015/06/17 13:14:55 Appdash web UI running on HTTP :8700
[negroni] listening on :8699
這是一個集appdash server, frontservice, fakebackendservice于一身的example,其大致結構如下圖:
通過瀏覽器打開:localhost:8700頁面,你會看到appdash server的UI,通過該UI你可以看到所有Trace的全貌。
訪問http://localhost:8699/,你就觸發了一次Trace。在appdash server ui下可以看到如下畫面:
從頁面上展示的信息可以看出,該webapp在處理用戶request時共進行了三次服務調用,三次調用的耗時分別為:201ms,202ms, 218ms,共耗時632ms。
一個更復雜的例子在cmd/appdash下面,后面的應用實例也是根據這個改造出來的,這里就不細說了。
三、應用實例
這里根據cmd/appdash改造出一個應用appdash的例子,例子的結構如下圖:
例子大致分為三部分:
appdash — 實現了一個appdash server, 該server帶有一個collector,用于收集跟蹤信息,收集后的信息存儲在一個memstore中;appdash server提供ui,ui從memstore提取信息并展示在ui上供operator查看。
backendservices — 實現兩個模擬的后端服務,供frontservice調用。
frontservice — 服務調用的起始端,當用戶訪問系統時觸發一次跟蹤。
<p> 先從backendservice這個簡單的demo service說起,backendservice下有兩個service: ServiceA和ServiceB,兩個service幾乎一模一樣,我們看一個就ok了: </p>
<p> //appdash_examples/backendservices/serviceA.go <br />
package main </p>
<p> import ( <br />
"fmt"
"net/http"
"time"
) </p>
<p> func handleRequest(w http.ResponseWriter, r *http.Request) { <br />
var err error
if err = r.ParseForm(); err != nil {
fmt.Println("Http parse form err:", err)
return
}
fmt.Println("SpanId =", r.Header.Get("Span-Id")) </p>
<p> time.Sleep(time.Millisecond * 101) <br />
w.Write([]byte("service1 ok"))
} </p>
<p> func main() { <br />
http.HandleFunc("/", handleRequest)
http.ListenAndServe(":6601", nil)
} </p>
<div>
<p> 這是一個"hello world"級別的web server。值得注意的只有兩點: </p>
<p> 1、在handleRequest中我們故意Sleep 101ms,用來模擬服務的耗時。 </p>
<p> 2、打印出request頭中的"Span-Id"選項值,用于跟蹤Span-Id的分配情況。 </p>
</div>
<p> 接下來我們來看appdash server。appdash server = collector +store +ui。 </p>
<div>
//appdash.go var c Server
</div>
<p> func init() { <br />
c = Server{
CollectorAddr: ":3001",
HTTPAddr: ":3000",
}
} </p>
<p> type Server struct { <br />
CollectorAddr string
HTTPAddr string
} </p>
<p> func main() { <br />
var (
memStore = appdash.NewMemoryStore()
Store = appdash.Store(memStore)
Queryer = memStore
) </p>
<p> app := traceapp.New(nil) <br />
app.Store = Store
app.Queryer = Queryer </p>
<p> var h http.Handler = app <br />
var l net.Listener
var proto string
var err error
l, err = net.Listen("tcp", c.CollectorAddr)
if err != nil {
log.Fatal(err)
}
proto = "plaintext TCP (no security)"
log.Printf("appdash collector listening on %s (%s)",
c.CollectorAddr, proto)
cs := appdash.NewServer(l, appdash.NewLocalCollector(Store))
go cs.Start() </p>
<p> log.Printf("appdash HTTP server listening on %s", c.HTTPAddr) <br />
err = http.ListenAndServe(c.HTTPAddr, h)
if err != nil {
fmt.Println("listenandserver listen err:", err)
}
} </p>
<p> appdash中的Store是用來存儲收集到的跟蹤結果的,Store是Collector接口的超集,這個例子中,直接利用 memstore(實現了 Collector接口)作為local collector,利用store的Collect方法收集trace數據。UI側則從store中讀取結果展示給用戶。 </p>
<p> 最后我們說說:frontservice。frontservice是Trace的觸發起點。當用戶訪問8080端口時,frontservice調用兩個backend service: </p>
<p> //frontservice.go <br />
func handleRequest(w http.ResponseWriter, r *http.Request) {
var result string
span := appdash. NewRootSpanID ()
fmt.Println("span is ", span)
collector := appdash.NewRemoteCollector(":3001") </p>
<p> httpClient := &http.Client{ <br />
Transport: &httptrace.Transport{
Recorder: appdash.NewRecorder(span, collector),
SetName: true,
},
} </p>
<p> //Service A <br />
resp, err := httpClient.Get("http://localhost:6601")
if err != nil {
log.Println("access serviceA err:", err)
} else {
log.Println("access serviceA ok")
resp.Body.Close()
result += "access serviceA ok\n"
} </p>
<p> //Service B <br />
resp, err = httpClient.Get("http://localhost:6602")
if err != nil {
log.Println("access serviceB err:", err)
return
} else {
log.Println("access serviceB ok")
resp.Body.Close()
result += "access serviceB ok\n"
}
w.Write([]byte(result))
} </p>
<p> func main() { <br />
http.HandleFunc("/", handleRequest)
http.ListenAndServe(":8080", nil)
} </p>
<p> 從代碼看,處理每個請求時都會分配一個root span,同時traceid也隨之分配出來。例子中沒有直接使用Recorder埋點發送event,而是利用了appdash封裝好的 httptrace.Transport,在初始化httpClient時,將transport實例與span和一個remoteCollector想 關聯。后續每次調用httpClient進行Get/Post操作時,底層代碼會自動調用httptrace.Transport的RoundTrip方 法,后者在Request header上添加"Span-Id"參數,并調用Recorder的Event方法將跟蹤信息發給RemoteCollector: </p>
<p> //appdash/httptrace/client.go <br />
func (t Transport) RoundTrip(req http.Request) (*http.Response, error) {
var transport http.RoundTripper
if t.Transport != nil {
transport = t.Transport
} else {
transport = http.DefaultTransport
} </p>
<p> … … <br />
req = cloneRequest(req) </p>
<p> child := t.Recorder.Child() <br />
if t.SetName {
child.Name(req.URL.Host)
}
SetSpanIDHeader(req.Header, child.SpanID) </p>
<p> e := NewClientEvent(req) <br />
e.ClientSend = time.Now() </p>
<p> // Make the HTTP request. <br />
resp, err := transport.RoundTrip(req) </p>
<p> e.ClientRecv = time.Now() <br />
if err == nil {
e.Response = responseInfo(resp)
} else {
e.Response.StatusCode = -1
}
child.Event(e) </p>
<p> return resp, err <br />
} </p>
<p> 這種方法在一定程度上實現了trace對應用的透明性。 </p>
<p> 你也可以顯式的在代碼中調用Recorder的Event的方法將trace信息發送給Collector,下面是一個fake SQLEvent的跟蹤發送: </p>
<p> // SQL event <br />
traceRec := appdash.NewRecorder(span, collector)
traceRec.Name("sqlevent example") </p>
<p> // A random length for the trace. <br />
length := time.Duration(rand.Intn(1000)) time.Millisecond
startTime := time.Now().Add(-time.Duration(rand.Intn(100)) time.Minute)
traceRec.Event(&sqltrace.SQLEvent{
ClientSend: startTime,
ClientRecv: startTime.Add(length),
SQL: "SELECT * FROM table_name;",
Tag: fmt.Sprintf("fakeTag%d", rand.Intn(10)),
}) </p>
<p> 不過這種顯式埋點需要程序配合做一些改造。 </p>
<p> 四、小結 </p>
<p> 目前Appdash的資料甚少,似乎只是其東家sourcegraph在production環境有應用。在github.com上受到的關注度也不算高。 </p>
<p> appdash是參考google dapper實現的,但目前來看appdash只是實現了“形”,也許稱為神器有些言過其實^_^。 </p>
<p> 首先,dapper強調對應用透明,并使用了Thread LocalStorage。appdash實現了底層的recorder+event機制,上層通過httptrace、sqltrace做了封裝,以降 低對應用代碼的侵入性。但從上面的應用來看,透明性還有很大提高空間。 </p>
<p> 其次,appdash的性能數據、擴展方案sourcegraph并沒有給出明確說明。 </p>
<p> 不過作為用go實現的第一個分布式系統跟蹤工具,appdash還是值得肯定的。在小規模分布式系統中應用對于系統行為的優化還是會有很大幫助的。 </p>
<p> BTW,上述例子的完整源碼在 <a href="/misc/goto?guid=4958877440241489040" rel="nofollow,noindex">這里</a> 可以下載到。 </p>
<p> ? 2015,bigwhite. 版權所有. </p>
</div>
</div>