使用Envoy和Jaeger實現分散式追蹤
作者:Arvind Thangamani
譯者:馬若飛
原文: ofollow,noindex">hackernoon.com/distributed…
如果你是初次接觸服務網格和Envoy,我這裡有一篇文章可以幫助你入門。
在微服務架構中,可觀測性變得越加重要。我認為這是選擇微服務這條路的必要條件之一。我的一位前同事列出了一份非常棒的需求清單,如果你想做微服務,那麼你需要滿足提到的這些要求。
可觀測性有許多事要做:
-
監控
-
報警
-
日誌集中化
-
分散式追蹤
本文只討論Envoy下的分散式追蹤,我儘量給出一個全貌來描述分散式追蹤、OpenTracing、Envoy和Jaeger是如何整合在一起工作的。在下一篇文章中,我們將討論使用Envoy、prometheus和grafana做監控。
分散式追蹤
隨著大量的服務和請求的流轉,你需要能夠快速發現哪裡出了問題。分散式追蹤最早由谷歌的Dapper普及開來,它本質上具有在微服務的整個生命週期中追蹤請求的能力。
最簡單的實現方法是在前端代理生成一個唯一的請求id(x-request-id),並將該請求id傳遞給與其互動的所有服務。基本上可以向所有的日誌追加這一請求id。因此,如果你在kibana這樣的系統中搜索唯一id,你會看到針對該特定請求的所有相關的日誌。
這非常有用,但是它不能告訴你每個服務中請求完成的順序、是否是並行完成的或者花費了多少時間。
讓我們看看OpenTracing和Envoy如何幫助我們解決這一問題。
OpenTracing
與其只傳遞一個id(x-request-id),不如傳遞更多的資料,比如哪個服務位於請求的根級別,哪個服務是哪個服務的子服務等等。這可以幫我們找出所有的答案。標準的做法是使用OpenTracing,它是分散式追蹤的規範,和語言無關。你可以在這裡閱讀更多關於此規範的資訊。
Envoy
服務網格就像微服務的通訊層,服務之間的所有通訊都是通過網格進行的。它可以實現負載均衡、服務發現、流量轉移、速率限制、指標(metrics)收集等功能,Envoy就是這樣的一個服務網格。在我們的例子中,Envoy將幫助我們生成唯一根請求id (x-request-id),生成子請求id,並將它們傳送到Jaeger或Zipkin這樣的追蹤系統,這些系統儲存、聚合追蹤資料併為其提供視覺化的能力。
這篇文章中我們會使用Jaeger作為追蹤系統,Envoy用來生成基於zipkin或lighstep格式的追蹤資料。我們會使用zipkin的標準來相容Jaeger。
只要給我看程式碼就好
下面的圖展示了我們嘗試構建的系統全貌:
服務安裝
我們將使用docker-compose來部署Envoy。你需要向Envoy提供一份配置檔案。這裡我不打算解釋如何配置Envoy,只集中討論與追蹤相關的部分。。你可以在這裡找到更多關於配置Envoy的資訊。
前端Envoy
前端Envoy的作用是生成根請求id,你可以通過配置去實現。下面是它的配置檔案:
--- tracing: http: name: envoy.zipkin config: collector_cluster: jaeger collector_endpoint: "/api/v1/spans" admin: access_log_path: "/tmp/admin_access.log" address: socket_address: address: "127.0.0.1" port_value: 9901 static_resources: listeners: - name: "http_listener" address: socket_address: address: "0.0.0.0" port_value: 80 filter_chains: filters: - name: "envoy.http_connection_manager" config: tracing: operation_name: egress use_remote_address: true add_user_agent: true access_log: - name: envoy.file_access_log config: path: /dev/stdout format: "[ACCESS_LOG][%START_TIME%] \"%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%\" %RESPONSE_CODE% %RESPONSE_FLAGS% %BYTES_RECEIVED% %BYTES_SENT% %DURATION% %RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)% \"%REQ(X-FORWARDED-FOR)%\" \"%REQ(USER-AGENT)%\" \"%REQ(X-REQUEST-ID)%\" \"%REQ(:AUTHORITY)%\" \"%UPSTREAM_HOST%\" \"%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT%\"\n" stat_prefix: "ingress_443" codec_type: "AUTO" generate_request_id: true route_config: name: "local_route" virtual_hosts: - name: "http-route" domains: - "*" routes: - match: prefix: "/" route: cluster: "service_a" http_filters: - name: "envoy.router" clusters: - name: "service_a" connect_timeout: "0.25s" type: "strict_dns" lb_policy: "ROUND_ROBIN" hosts: - socket_address: address: "service_a_envoy" port_value: 8786 - name: jaeger connect_timeout: 0.25s type: strict_dns lb_policy: round_robin hosts: - socket_address: address: jaeger port_value: 9411複製程式碼
第1-8行啟用追蹤並配置追蹤系統和它所在的位置。
第27-28行指定流量進出的位置。
第38行指出Envoy必須生成根請求id。
第66-73行配置Jaeger追蹤系統。
所有Envoy的配置中(前端,服務a,b和c)都需要啟用追蹤和配置Jaeger地址
Service A
在我們的例子中服務A將呼叫服務B和服務C。關於分散式追蹤非常重要的一點是,儘管Envoy支援進行分散式追蹤,但也 依賴於服務把生成的Header傳遞給流出的請求 。因此,服務A將在呼叫服務B和C時轉發請求頭。服務A是一個只有一個端點(endpoint)的簡單的go服務,內部呼叫服務B和服務C。下面是我們需要傳遞的頭資訊:
req, err := http.NewRequest("GET", "http://service_a_envoy:8788/", nil) if err != nil { fmt.Printf("%s", err) } req.Header.Add("x-request-id", r.Header.Get("x-request-id")) req.Header.Add("x-b3-traceid", r.Header.Get("x-b3-traceid")) req.Header.Add("x-b3-spanid", r.Header.Get("x-b3-spanid")) req.Header.Add("x-b3-parentspanid", r.Header.Get("x-b3-parentspanid")) req.Header.Add("x-b3-sampled", r.Header.Get("x-b3-sampled")) req.Header.Add("x-b3-flags", r.Header.Get("x-b3-flags")) req.Header.Add("x-ot-span-context", r.Header.Get("x-ot-span-context")) client := &http.Client{} resp, err := client.Do(req)複製程式碼
您可能驚訝為什麼呼叫服務B時URL是 service_a_envoy
。如果你還記得我們已經討論過服務之間的所有通訊都需要通過envoy代理的話,類似的,可以在呼叫服務C時傳遞Header。
服務B和服務C
剩下的兩個服務不需要對程式碼進行任何更改,因為它們處於葉子級別。一旦這兩個服務要呼叫其他端點,則必須轉發請求追蹤頭,除此之外不需要對Envoy進行任何特殊配置。服務B和C程式碼如下:
package main import ( "fmt" "log" "net/http" ) func handler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello from service B") } func main() { http.HandleFunc("/", handler) log.Fatal(http.ListenAndServe(":8082", nil)) }複製程式碼
package main import ( "fmt" "log" "net/http" ) func handler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello from service C") } func main() { http.HandleFunc("/", handler) log.Fatal(http.ListenAndServe(":8083", nil)) }複製程式碼
所有這些完成後,如果您執行 docker-compose up
並訪問前端Envoy端點,就會生成追蹤資訊並推送到Jaeger。Jaeger有一個非常友好的UI介面來展示追蹤資訊,我們的資訊看上去像這樣:
正如你看到的,它提供了總體的時間損耗,系統各部分是時間損耗,哪個服務呼叫哪個服務,服務和服務的關係(服務b和服務c是兄弟關係)。Jaeger的進一步使用留待你自己去探索。
你可以在 這裡 找到所有的Envoy配置、程式碼和Docker compose檔案。
就是這些,謝謝,讓我知道你的反饋。
如果你在尋找Envoy的xDS服務端的話,我的同事已經搭建了 一個 。可以直接獲取(check out)。
這裡 是這一系列文章中的下一篇(使用Envoy,Prometheus和Grafana進行監控)。