1. 程式人生 > >idou老師教你學Istio 08: 調用鏈埋點是否真的“零修改”?

idou老師教你學Istio 08: 調用鏈埋點是否真的“零修改”?

容器 inbound 基金 傳遞 outbound 高級 號稱 檢查 mail

本文將結合一個具體例子中的細節詳細描述Istio調用鏈的原理和使用方式。並基於Istio中埋點的原理解釋來說明:為了輸出一個質量良好的調用鏈,業務程序需根據自身特點做適當的修改,即並非官方一直在說的完全無侵入的做各種治理。另外還會描述Istio當前版本中收集調用鏈數據可以通過Envoy和Mixer兩種不同的方式。

Istio一直強調其無侵入的服務治理,服務運行可觀察性。即用戶完全無需修改代碼,就可以通過和業務容器一起部署的proxy來執行服務治理和與性能數據的收集。原文是這樣描述的:

Istio makes it easy to create a network of deployed services with load balancing, service-to-service authentication, monitoring, and more, without any changes in service code. You add Istio support to services by deploying a special sidecar proxy throughout your environment that intercepts all network communication between microservices, then configure and manage Istio using its control plane functionality。

調用鏈的埋點是一個比起來記錄日誌,報個metric或者告警要復雜的多,根本原因是要能將在多個點上收集的關於一次調用的多個中間請求過程關聯起來形成一個鏈。Dapper, a Large-Scale Distributed Systems Tracing Infrastructure 描述了其中的原理和一般性的機制,還是挺復雜的。也有很多實現,用的比較多的如zipkin,和已經在CNCF基金會的用的越來越多的Jaeger,滿足Opentracing語義標準的就有這麽多。

在Istio中大段的埋點邏輯在Sidecar中已經提供,業務代碼不用調用以上這些埋點方式來創建trace,維護span等這些復雜邏輯,但是為了能真正連接成一個完整的鏈路,業務代碼還是需要做適當修改。我們來分析下細節為什麽號稱不用修改代碼就能搞定治理、監控等高級功能的Sidecar為什麽在調用鏈埋點的時候需要改應用代碼。

調用詳細

服務調用關系

簡單期間,我們以Istio最經典的Bookinfo為例來說明。Bookinfo的4個為服務的調用關系是這樣:

技術分享圖片

調用鏈輸出

從前端入口gateway那個envoy上進行一次調用,到四個不同語言開發的服務間完成調用,一次調用輸出的調用鏈是這樣:

技術分享圖片

簡單看下bookinfo 中的代碼,能看到並沒有任何創建維護span這種埋點的邏輯,想也是,對於python、java、ruby、nodejs四種不同的語言采用不同的埋點的庫在來實現類似的埋點邏輯也是非常頭痛的一件事情。那我們看到這個調用鏈信息是怎麽輸出的?答案當然是應用邊上的sidecar Envoy,Envoy對於調用鏈相關設計參照這裏。sidecar攔截應用程序所有的進和出的網絡流量,跟蹤到所有的網絡請求,像Service mesh的設計理念中其他的路由策略、負載均衡等治理一樣,只要攔截到流量Sidecar也可以實現埋點的邏輯。

埋點邏輯

對於經過sidecar流入應用程序的流量,如例子中流入roductpage, details、reviews和ratings的流量,如果經過Sidecar時header中沒有任何跟蹤相關的信息,則會在創建一個span,Traceid就是這個spanId,然後在將請求傳遞給通pod的業務服務;如果請求中包含trace相關的信息,則sidecar從走回歸提取trace的上下文信息並發給應用程序。

技術分享圖片

對於經過sidecar流出的流量,如例子中gateway調用productpage,或者productpage調用鏈details和reviews的請求。如果經過sidecar時header中沒有任何跟蹤相關的信息,則會創建根span,並將該跟span相關上下文信息放在請求頭中傳遞給下一個調用的服務,當然調用前會被目標服務的sidecar攔截掉執行上面流入的邏輯;當存在trace信息時,sidecar從header中提取span相關信息,並基於這個span創建子span,並將新的span信息加在請求頭中傳遞。

以上是bookinfo一個實際的調用中在proxy上生成的span主要信息。可以看到,對於每個app訪問都經過Sidecar代理,inbound的流量和outbound的流量都通過Sidecar。圖上為了清楚表達每個將對Sidecar的每個處理都分開表示,如productpage,接收外部請求是一個處理,給details發出請求是一個處理,給reviews發出請求是另外一個處理,因此圍繞Productpage這個app有三個黑色的處理塊,其實是一個Sidecar。為了不使的圖上太淩亂,最終的Response都沒有表示。其實圖上每個請求的箭頭都有一個反方向的response,在服務發起方的Sidecar會收到response時,會記錄一個CR(client received)表示收到響應的時間並計算整個span的持續時間。

解析下具體數據,結合實際調用中生成的數據來看下前面proxy埋點的邏輯會更清楚些。

1.從gateway開始,gateway作為一個獨立部署在一個pod中的envoy進程,當有請求過來時,它會將請求轉給入口的微服務productpage。Gateway這個Envoy在發出請求時裏面沒有trace信息,會生成一個根span:spanid和traceid都是f79a31352fe7cae9,parentid為空,並記錄CS時間,即Client Send;

2.請求從入口gateway這個envoy進入productpage前先講過productpage pod內的envoy,envoy處理請求頭中帶著trace信息,則記錄SR,Server received,並將請求發送給Productpage業務容器處理,productpage在處理請求的業務方法中需要接收這些header中的trace信息,然後再調用Details和Reviews的微服務。

Python寫的 productpage在服務端處理請求時,先從request中提取接收到的header。然後再調用details獲取details服務時,將header轉發出去。

app.route(‘/productpage‘)
def front():
product_id = 0 # TODO: replacedefault value
headers = getForwardHeaders(request)

detailsStatus, details = getProductDetails(product_id, headers)
reviewsStatus, reviews = getProductReviews(product_id, headers)
return…
可以看到就是提取幾個trace相關的header kv

def getForwardHeaders(request):
headers = {}
incoming_headers = [ ‘x-request-id‘,
‘x-b3-traceid‘,
‘x-b3-spanid‘,
‘x-b3-parentspanid‘,
‘x-b3-sampled‘,
‘x-b3-flags‘,
‘x-ot-span-context‘
]

for ihdr in incoming_headers:
val = request.headers.get(ihdr)
if val is not None:
headers[ihdr] = val
return headers
其實就是重新構造一個請求發出去,可以看到請求中包含收到的header。

def getProductReviews(product_id, headers):
url = reviews[‘name‘] + "/" + reviews[‘endpoint‘] + "/" + str(product_id)
res = requests.get(url, headers=headers, timeout=3.0)
3.從ProductPage出的請求去請求Reviews服務前,又一次通過同Pod的envoy,envoy埋點邏輯檢查header中包含了trace相關信息,在將請求發出前會做客戶端的調用鏈埋點,即以當前span為parent span,生成一個子span:即traceid:保持一致9a31352fe7cae9, spanid重新生成cb4c86fb667f3114,parentid就是上個span: f79a31352fe7cae9。

4.請求在到達Review業務容器前,先經過Review的Envoy,從Header中解析出trace信息存在,則發送Trace信息給Reviews。Reviews處理請求的服務端代碼中需要解析出這些包含trace的Header信息。

reviews服務中java的rest代碼如下:

@GET@Path("/reviews/{productId}")
br/>@Path("/reviews/{productId}")
@HeaderParam("end-user") String user,
@HeaderParam("x-request-id") String xreq,
@HeaderParam("x-b3-traceid") String xtraceid,
@HeaderParam("x-b3-spanid") String xspanid,
@HeaderParam("x-b3-parentspanid") String xparentspanid,
@HeaderParam("x-b3-sampled") String xsampled,
@HeaderParam("x-b3-flags") String xflags,
@HeaderParam("x-ot-span-context") String xotspan)
即在服務端接收請求的時候也同樣會提取header。調用Ratings服務時再傳遞下去。其他的productpage調用Details,Reviews調用Ratings邏輯類似。不再復述。

以一實際調用的例子了解以上調用過程的細節,可以看到Envoy在處理inbound和outbound時的埋點邏輯,更重要的是看到了在這個過程中應用程序需要配合做的事情。即需要接收trace相關的header並在請求時發送出去,這樣在出流量的proxy向下一跳服務發起請求前才能判斷並生成子span並和原span進行關聯,進而形成一個完整的調用鏈。否則,如果在應用容器未處理Header中的trace,則Sidecar在處理outbound的請求時會創建根span,最終會形成若幹個割裂的span,並不能被關聯到一個trace上。

即雖然Istio一直是講服務治理和服務的可觀察性對業務代碼0侵入。但是要獲得一個質量良好的調用鏈,應用程序還是要配合做些事情。在官方的distributed-tracing 中這部分有描述:“盡管proxy可以自動生成span,但是應用程序需要在類似HTTP Header的地方傳遞這些span的信息,這樣這些span才能被正確的鏈接成一個trace。因此要求應用程序必須要收集和傳遞這些trace相關的header並傳遞出去”

? x-request-id

? x-b3-traceid

? x-b3-spanid

? x-b3-parentspanid

? x-b3-sampled

? x-b3-flags

? x-ot-span-context

調用鏈階段span解析:

前端gateway訪問productpage的proxy的這個span大致是這樣:

"traceId": "f79a31352fe7cae9",
"id": "f79a31352fe7cae9",
"name": "productpage-route",
"timestamp": 1536132571838202,
"duration": 77474,
"annotations": [
{
"timestamp": 1536132571838202,
"value": "cs",
"endpoint": {
"serviceName": "istio-ingressgateway",
"ipv4": "172.16.0.28"
}
},
{
"timestamp": 1536132571839226,
"value": "sr",
"endpoint": {
"serviceName": "productpage",
"ipv4": "172.16.0.33"
}
},
{
"timestamp": 1536132571914652,
"value": "ss",
"endpoint": {
"serviceName": "productpage",
"ipv4": "172.16.0.33"
}
},
{
"timestamp": 1536132571915676,
"value": "cr",
"endpoint": {
"serviceName": "istio-ingressgateway",
"ipv4": "172.16.0.28"
}
}
],
gateway上報了個cs,cr, productpage的那個proxy上報了個ss,sr。

分別表示gateway作為client什麽時候發出請求,什麽時候最終受到請求,productpage的proxy什麽時候收到了客戶端的請求,什麽時候發出了response。

Reviews的span如下:

"traceId": "f79a31352fe7cae9",
"id": "cb4c86fb667f3114",
"name": "reviews-route",
"parentId": "f79a31352fe7cae9",
"timestamp": 1536132571847838,
"duration": 64849,
Details的span如下:

"traceId": "f79a31352fe7cae9",
"id": "951a4487642c0966",
"name": "details-route",
"parentId": "f79a31352fe7cae9",
"timestamp": 1536132571842677,
"duration": 2944,
可以看到productpage這個微服務和detail和reviews這兩個服務的調用。

一個細節就是traceid就是第一個productpage span的id,所以第一個span也稱為根span,而後面兩個review和details的span的parentid是前一個productpage的span的id。

Ratings 服務的span信息如下:可以看到traceid保持一樣,parentid就是reviews的spanid。

"traceId": "f79a31352fe7cae9",
"id": "5aac176b61ec8d84",
"name": "ratings-route",
"parentId": "cb4c86fb667f3114",
"timestamp": 1536132571889086,
"duration": 1449,
當然在Jaeger裏上報會是這個樣子:

技術分享圖片

根據一個實際的例子理解原理後會發現,應用程序要修改代碼根本原因就是調用發起方,在Isito裏其實就是Sidecar在處理outbound的時生成span的邏輯,而這個埋點的代碼和業務代碼不在一個進程裏,沒法使用進程內的一些類似ThreadLocal的方式(threadlocal在golang中也已經不支持了,推薦顯式的通過Context傳遞),只能顯式的在進程間傳遞這些信息。這也能理解為什麽Istio的官方文檔中告訴我們為了能把每個階段的調用,即span,串成一個串,即完整的調用鏈,你需要修你的代碼來傳遞點東西。

當然實例中只是對代碼侵入最少的方式,就是只在協議頭上機械的forward這幾個trace相關的header,如果需要更多的控制,如在在span上加特定的tag,或者在應用代碼中代碼中根據需要構造一個span,可以使用opentracing的StartSpanFromContext 或者SetTag等方法。

調用鏈數據上報

Envoy上報

一個完整的埋點過程,除了inject、extract這種處理span信息,創建span外,還要將span report到一個調用鏈的服務端,進行存儲並支持檢索。在Isito中這些都是在Envoy這個sidecar中處理,業務程序不用關心。在proxy自動註入到業務pod時,會自動刷這個後端地址。如:

技術分享圖片

即envoy會連接zipkin的服務端上報調用鏈數據,這些業務容器完全不用關心。當然這個調用鏈收集的後端地址配置成jaeger也是ok的,因為Jaeger在接收數據是兼容zipkin格式的。

Mixers上報

除了直接從Envoy上報調用鏈到zipkin後端外,和其他的Metric等遙測數據一樣通過Mixer這個統一面板來收集也是可行的。

即如tracespan中描述,創建一個tracespan的模板,來描述從mixer的一次訪問中提取哪些數據,可以看到trace相關的幾個ID從請求的header中提取,而訪問的很多元數據有些從訪問中提取,有些根據需要從pod中提取(背後去訪問了kubeapiserver的pod資源)

apiVersion: "config.istio.io/v1alpha2"
kind: tracespan
metadata:
name: default
namespace: istio-system
spec:
traceId: request.headers["x-b3-traceid"]
spanId: request.headers["x-b3-spanid"] | ""
parentSpanId: request.headers["x-b3-parentspanid"] | ""
spanName: request.path | "/"
startTime: request.time
endTime: response.time
clientSpan: (context.reporter.local | true) == false
rewriteClientSpanId: false
spanTags:
http.method: request.method | ""
http.status_code: response.code | 200
http.url: request.path | ""
request.size: request.size | 0
response.size: response.size | 0
source.ip: source.ip | ip("0.0.0.0")
source.service: source.service | ""
source.user: source.user | ""
source.version: source.labels["version"] | ""
最後

在這個文章發出前,一直在和社區溝通,督促在更明晰的位置告訴大家用Istio的調用鏈需要修改些代碼,而不是只在一個旮旯的位置一小段描述。得到回應是1.1中社區首頁第一頁what-is-istio/已經修改了這部分說明,不再是1.0中說without any changes in service code,而是改為with few or no code changes in service code。提示大家在使用Isito進行調用鏈埋點時,應用程序需要進行適當的修改。當然了解了其中原理,做起來也不會太麻煩。

參照

https://thenewstack.io/distributed-tracing-istio-and-your-applications/

https://github.com/istio/old_mixer_repo/issues/797

http://www.idouba.net/opentracing-serverside-tracing/

idou老師教你學Istio 08: 調用鏈埋點是否真的“零修改”?