Istio調用鏈埋點原理剖析—是否真的“零修改”分享實錄(下)
正如Service Mesh的誕生是為了解決大規模分布式服務訪問的治理問題,調用鏈的出現也是為了對應於大規模的復雜的分布式系統運行中碰到的故障定位定界問題。大量的服務調用、跨進程、跨服務器,可能還會跨多個物理機房。無論是服務自身問題還是網絡環境的問題導致調用上鏈路上出現問題都比較復雜,如何定位就比單進程的一個服務打印一個異常棧來找出某個方法要困難的多。需要有一個類似的調用鏈路的跟蹤,經一次請求的邏輯規矩完整的表達出來,可以觀察到每個階段的調用關系,並能看到每個階段的耗時和調用詳細情況。
Dapper, a Large-Scale Distributed Systems Tracing Infrastructure 描述了其中的原理和一般性的機制。模型中包含的術語也很多,理解最主要的兩個即可:
Trace:一次完整的分布式調用跟蹤鏈路。
Span:跨服務的一次調用; 多個Span組合成一次Trace追蹤記錄。
上圖是Dapper論文中的經典圖示,左表示一個分布式調用關系。前端(A),兩個中間層(B和C),以及兩個後端(D和E)。用戶發起一個請求時,先到達前端,再發送兩個服務B和C。B直接應答,C服務調用後端D和E交互之後給A應答,A進而返回最終應答。要使用調用鏈跟蹤,就是給每次調用添加TraceId、SpanId這樣的跟蹤標識和時間戳。
右表示對應Span的管理關系。每個節點是一個Span,表示一個調用。至少包含Span的名、父SpanId和SpanId。節點間的連線下表示Span和父Span的關系。所有的Span屬於一個跟蹤,共用一個TraceId。從圖上可以看到對前端A的調用Span的兩個子Span分別是對B和C調用的Span,D和E兩個後端服務調用的Span則都是C的子Span。
調用鏈系統有很多實現,用的比較多的如zipkin,還有已經加入CNCF基金會並且的用的越來越多的Jaeger,滿足Opentracing語義標準的就有這麽多。
一個完整的調用鏈跟蹤系統,包括調用鏈埋點,調用鏈數據收集,調用鏈數據存儲和處理,調用鏈數據檢索(除了提供檢索的APIServer,一般還要包含一個非常酷炫的調用鏈前端)等若幹重要組件。上圖是Jaeger的一個完整實現。這裏我們僅關註與應用相關的內容,即調用鏈埋點的部分,看下在Istio中是否能做到”無侵入“的調用鏈埋點。當然在最後也會看下Istio機制下提供的不同的調用鏈數據收集方式。
Istio標準BookInfo例子
簡單期間,我們以Istio最經典的Bookinfo為例來說明。Bookinfo模擬在線書店的一個分類,顯示一本書的信息。本身是一個異構應用,幾個服務分別由不同的語言編寫的。
各個服務的模擬作用和調用關系是:
productpage :productpage 服務會調用 details 和 reviews 兩個服務,用來生成頁面。
details :這個微服務包含了書籍的信息。
reviews :這個微服務包含了書籍相關的評論。並調用 ratings 微服務。
ratings :ratings 微服務中包含了由書籍評價組成的評級信息。
調用鏈輸出
在Istio上運行這個典型例子,不用做任何的代碼修改,自帶的Zipkin上就能看到如下的調用鏈輸出。
可以看到展示給我們的調用鏈和Boookinfo這個場景設計的調用關系一致:productpage 服務會調用 details 和 reviews 兩個服務,reviews調用了ratings 微服務。除了顯示調用關系外,還顯示了每個中間調用的耗時和調用詳情。基於這個視圖,服務的運維人員比較直觀的定界到慢的或者有問題的服務,並鉆取當時的調用細節,進而定位到問題。
我們就要關註下調用鏈埋點到底是在哪裏做的,怎麽做的?
在Istio中,所有的治理邏輯的執行體都是和業務容器一起部署的Envoy這個Sidecar,不管是負載均衡、熔斷、流量路由還是安全、可觀察性的數據生成都是在Envoy上。Sidecar攔截了所有的流入和流出業務程序的流量,根據收到的規則執行執行各種動作。實際使用中一般是基於K8S提供的InitContainer機制,用於在Pod中執行一些初始化任務. InitContainer中執行了一段iptables的腳本。正是通過這些Iptables規則攔截pod中流量,並發送到Envoy上。Envoy攔截到Inbound和Outbound的流量會分別作不同操作,執行上面配置的操作,另外再把請求往下發,對於Outbound就是根據服務發現找到對應的目標服務後端上;對於Inbound流量則直接發到本地的服務實例上。
我們今天的重點是看下攔截到流量後Sidecar在調用鏈埋點怎麽做的。
Istio調用鏈埋點邏輯
Envoy的埋點規則和在其他服務調用方和被調用方的對應埋點邏輯沒有太大差別。
Inbound流量:對於經過Sidecar流入應用程序的流量,如果經過Sidecar時Header中沒有任何跟蹤相關的信息,則會在創建一個根Span,TraceId就是這個SpanId,然後再將請求傳遞給業務容器的服務;如果請求中包含Trace相關的信息,則Sidecar從中提取Trace的上下文信息並發給應用程序。
Outbound流量:對於經過Sidecar流出的流量,如果經過Sidecar時Header中沒有任何跟蹤相關的信息,則會創建根Span,並將該跟Span相關上下文信息放在請求頭中傳遞給下一個調用的服務;當存在Trace信息時,Sidecar從Header中提取Span相關信息,並基於這個Span創建子Span,並將新的Span信息加在請求頭中傳遞。
特別是Outbound部分的調用鏈埋點邏輯,通過一段偽代碼描述如圖:
調用鏈詳細解析
如圖是對前面Zipkin上輸出的一個Trace一個透視圖,觀察下每個調用的細節。可以看到每個階段四個服務與部署在它旁邊上的Sidecar是怎麽配合的。在圖上只標記了Sidecar生成的Span主要信息。
因為Sidecar 處理 Inbound和Outbound的邏輯有所不同,在圖上表也分開兩個框圖分開表達。如productpage,接收外部請求是一個處理,給details發出請求是一個處理,給reviews發出請求是另外一個處理,因此圍繞productpage這個app有三個黑色的處理塊,其實是一個Sidecar在做事。
同時,為了不使的圖上箭頭太多,最終的Response都沒有表達出來,其實圖上每個請求的箭頭都有一個反方向的Response。在服務發起方的Sidecar會收到Response時,會記錄一個CR(client Received)表示收到響應的時間並計算整個Span的持續時間。
下面通過解析下具體數據來找出埋點邏輯:
首先從調用入口的Gateway開始,Gateway作為一個獨立部署在一個pod中的Envoy進程,當有請求過來時,它會將請求轉給入口服務productpage。Gateway這個Envoy在發出請求時裏面沒有Trace信息,會生成一個根Span:SpanId和TraceId都是f79a31352fe7cae9,因為是第一個調用鏈上的第一個Span,也就是一般說的根Span,所有ParentId為空,在這個時候會記錄CS(Client Send);
請求從入口Gateway這個Envoy進入productpage的app業務進程其Inbound流量被productpage Pod內的Envoy攔截,Envoy處理請求頭中帶著Trace信息,記錄SR(Server Received),並將請求發送給productpage業務容器處理,productpage在處理請求的業務方法中在接受調用的參數時,除了接受一般的業務參數外,同時解析請求中的調用鏈Header信息,並把Header中的Trace信息傳遞給了調用的Details和Reviews的微服務。
從productpage出去的請求到達reviews服務前,其Oubtbound流量又一次通過同Pod的Envoy,Envoy埋點邏輯檢查Header中包含了Trace相關信息,在將請求發出前會做客戶端的調用鏈埋點,即以當前Span為parent Span,生成一個子Span:新的SpanId cb4c86fb667f3114,TraceId保持一致9a31352fe7cae9,ParentId就是上個Span的Id: f79a31352fe7cae9。
從prodcutepage到review的請求經過productpage的Sidecar走LB後,發給一個review的實例。請求在到達Review業務容器前,同樣也被Review的Envoy攔截,Envoy檢查從Header中解析出Trace信息存在,則發送Trace信息給reviews。reviews處理請求的服務端代碼中同樣接收和解析出這些包含Trace的Header信息,發送給下一個Ratings服務。
在這裏我們只是理了一遍請求從入口Gateway,訪問productpage服務,再訪問reviews服務的流程。可以看到期間每個訪問階段,對服務的Inbound和Outbound流量都會被Envoy攔截並執行對應的調用鏈埋點邏輯。圖示的Reviews訪問Ratings和productpage訪問Details邏輯與以上類似,這裏不做復述。
以上過程也印證了前面我們提出的Envoy的埋點邏輯。可以看到過程中除了Envoy再處理Inbound和Outbound流量時要執行對應的埋點邏輯外。每一步的調用要串起來,應用程序其實做了些事情。就是在將請求發給下一個服務時,需要將調用鏈相關的信息同樣傳下去,盡管這些Trace和Span的標識並不是它生成的。這樣在出流量的proxy向下一跳服務發起請求前才能判斷並生成子Span並和原Span進行關聯,進而形成一個完整的調用鏈。否則,如果在應用容器未處理Header中的Trace,則Sidecar在處理請求時會創建根Span,最終會形成若幹個割裂的Span,並不能被關聯到一個Trace上,就會出現我們開始提到的問題。
不斷被問到兩個問題來試圖說明這個業務代碼配合修改來實現調用鏈邏輯可能不必要:問題一、既然傳入的請求上已經帶了這些Header信息了,直接往下一直傳不就好了嗎?Sidecar請求APP的時候帶著這些Header,APP請求Sidecar時也帶著這些Header不就完了嗎?問題二、既然TraceId和SpanId是Sidecar生成的,為什麽要再費勁讓App收到請求的時候解析下,發出請求時候再帶著發出來傳回給Sidecar呢?
回答問題一,只需理解一點,這裏的App業務代碼是處理請求不是轉發請求,即圖上左邊的Request to Productpage 到prodcutpage中請求就截止了,要怎麽處理完全是productpage的服務接口的內容了,可以是調用本地處理邏輯直接返回,也可以是如示例中的場景構造新的請求調用其他的服務。右邊的Request from productpage 完全是服務構造的發出的另外一個請求;
回答問題二,需要理解當前Envoy是獨立的Listener來處理Inbound和Outbound的請求。Inbound只會處理入的流量並將流量轉發到本地的服務實例上。而Outbound就是根據服務發現找到對應的目標服務後端上。除了實在一個進程裏外兩個之間可以說沒有任何關系。 另外如問題一描述,因為到Outbound已經是一個新構造的請求了,使得想維護一個map來記錄這些Trace信息這種方案也變得不可行。
這樣基於一個例子來打開看一個調用鏈的主要過程就介紹到這裏。附加productpage訪問reviews的Span詳細,刪減掉一些數據只保留主要信息大致是這樣:
Productpage的Proxy上報了個CS,CR, reviews的那個proxy上報了個SS,SR。分別表示Productpage作為client什麽時候發出請求,什麽時候最終收到請求,reviews的proxy什麽時候收到了客戶端的請求,什麽時候發出了response。另外還包括這次訪問的其他信息如Response Code、Response Size等。
根據前面的分析我們可以得到結論:埋點邏輯是在Sidecar代理中完成,應用程序不用處理復雜的埋點邏輯,但應用程序需要配合在請求頭上傳遞生成的Trace相關信息。
服務代碼修改示例
前面通過一個典型例子詳細解析了Istio的調用鏈埋點過程中Envoy作為Sidecar和應用程序的配合關系。分析的結論是調用鏈埋點由Envoy來執行,但是業務程序要有適當修改。下面抽取服務代碼來印證下。
Python寫的 productpage在服務端處理請求時,先從Request中提取接收到的Header。然後再構造請求調用details獲取服務接口,並將Header轉發出去。
首先處理productpage請問的rest方法中從Request中提取Trace相關的Header。
然後重新構造一個請求發出去,請求reviews服務接口。可以看到請求中包含收到的Header。
reviews服務中Java的Rest代碼類似,在服務端接收請求時,除了接收Request中的業務參數外,還要提取Header信息,調用Ratings服務時再傳遞下去。其他的productpage調用details,reviews調用ratings邏輯類似。
當然這裏只是個demo,示意下要在那個位置修改代碼。實際項目中我們不會這樣在每個業務方法上作這樣的修改,這樣對代碼的侵入,甚至說汙染太嚴重。根據語言的特點會盡力把這段邏輯提取成一段通用邏輯。
Istio調用鏈數據收集:by Envoy
一個完整的埋點過程,除了inject、extract這種處理Span信息,創建Span外,還要將Span report到一個調用鏈的服務端,進行存儲並支持檢索。在Isito中這些都是在Envoy這個Sidecar中處理,業務程序不用關心。在proxy自動註入到業務pod時,會自動刷這個後端地址.
即Envoy會連接zipkin的服務端上報調用鏈數據,這些業務容器完全不用關心。當然這個調 用鏈收集的後端地址配置成jaeger也是ok的,因為Jaeger在接收數據是兼容zipkin格式的。
Istio調用鏈數據收集:by Mixer
除了直接從Envoy上報調用鏈到zipkin後端外,Istio提供了Mixer這個統一的面板來對接不同的後端來收集遙測數據,當然Trace數據也可以采用同樣的方式。
即如TraceSpan中描述,創建一個TraceSpan的模板,來描述從mixer的一次訪問中提取哪些數據,可以看到Trace相關的幾個ID從請求的Header中提取。除了基礎數據外,基於Mixer和kubernetes的繼承能力,有些對象的元數據,如Pod上的相關信息Mixr可以補充,背後其實是Mixer連了kubeapiserver獲取對應的pod資源,從而較之直接從Envoy上收集的原始數據,可以有更多的業務上的擴張,如namespace、cluster等信息APM數據要用到,但是Envoy本身不會生成,通過這種方式就可以從Kubernetes中自動補充完整,非常方便。
這也是Istio的核心組件Mixer在可觀察性上的一個優秀實踐。
Istio官方說明更新
最近一直在和社區溝通,督促在更顯著的位置明確的告訴使用者用Istio作治理並不是所有場景下都不需要修改代碼,比如調用鏈,雖然用戶不用業務代碼埋點,但還是需要修改些代碼。尤其是避免首頁“without any change”對大家的誤導。得到回應是1.1中社區首頁what-is-istio已經修改了這部分說明,不再是1.0中說without any changes in service code,而是改為with few or no code changes in service code。提示大家在使用Isito進行調用鏈埋點時,應用程序需要進行適當的修改。當然了解了其中原理,做起來也不會太麻煩。
改了個程度輕一點的否定詞,很少幾乎不用修改,還是基本不用改的意思。這也是社區一貫的觀點。
結合對Istio調用鏈的原理的分析和一個典型例子中細節字段、流程包括代碼的額解析,再加上和社區溝通的觀點。得到以下結論:
Istio的絕大多數治理能力都是在Sidecar而非應用程序中實現,因此是非侵入的;
Istio的調用鏈埋點邏輯也是在Sidecar代理中完成,對應用程序非侵入,但應用程序需做適當的修改,即配合在請求頭上傳遞生成的Trace相關信息。
華為雲Istio服務網格公測中
在騰訊的場子上只講幹貨的技術,盡量少做廣告。在這裏只是用一頁PPT來簡單介紹下華為雲當前正在公測的Istio服務網格服務。
華為雲容器引擎CCE的深度集成,一鍵啟用後,即可享受Istio服務網格的全部治理能力;基於應用運行的全景視圖配置管理熔斷、故障註入、負載均衡等多種智能流量治理功能;內置金絲雀、A/B Testing典型灰度發布流程灰度版本一鍵部署,流量切換一鍵生效;配置式基於流量比例、請求內容灰度策略配置,一站式健康、性能、流量監控,實現灰度發布過程量化、智能化、可視化;集成華為雲APM,使用調用鏈、應用拓撲等多種手段對應用運行進行透視、診斷和管理。
華為雲Istio社區貢獻
華為作為CNCF 基金會的初創會員、白金會員,CNCF / Kubernetes TOC 成員。在Kubernetes社區貢獻國內第一,全球第三,全球貢獻3000+ PR,先後貢獻了集群聯邦、高級調度策略、IPVS負載均衡,容器存儲快照等重要項目。
隨著Istio項目的深入產品化,團隊也積極投入到Istio的社區貢獻。當前社區貢獻國內第一,全球第三。Approver 3席,Member 6席,Contributor 若幹。
通過Pilot agent轉發實現HTTP協議的健康檢查: 針對mTLS enabled環境,傳統的kubernetes http健康檢查不能工作,實現 sidecar轉發功能,以及injector的自動註入。
Istioctl debug功能增強:針對istioctl缺失查詢sidecar中endpoint的能力,增加proxy-config endpoint、proxy-status endpoint命令,提高debug效率。
HTTPRetry API增強: 增加HTTPRetry 配置項RetryOn策略,可以通過此控制sidecar重試。
MCP 配置實現: Pilot支持mesh configuration, 可以與galley等多個實現了MCP協議的server端交互獲取配置。以此解耦後端註冊中心。
Pilot CPU異常問題解決:1.0.0-snapshot.0 pilot free 狀態CPU 利用率超過20%,降低到1%以下。
Pilot 服務數據下發優化:緩存service,避免每次push時進行重復的轉換。
Pilot服務實例查詢優化:根據label selector查詢endpoints(涵蓋95%以上的場景),避免遍歷所有namespace的endpoints。
Pilot 數據Push性能優化:將原有的串行順序推送配置,更新為並行push,降低配置下發時延。
Istio調用鏈埋點原理剖析—是否真的“零修改”分享實錄(下)