字節(jié)跳動(dòng)開(kāi)源 Kelemetry:面向 Kubernetes 控制面的全局追蹤系統(tǒng)
Kelemetry是字節(jié)跳動(dòng)開(kāi)發(fā)的用于Kubernetes控制平面的追蹤系統(tǒng),它從全局視角串聯(lián)起多個(gè) Kubernetes 組件的行為,追蹤單個(gè) Kubernetes 對(duì)象的完整生命周期以及不同對(duì)象之間的相互影響。通過(guò)可視化 K8s 系統(tǒng)內(nèi)的事件鏈路,它使得 Kubernetes 系統(tǒng)更容易觀測(cè)、更容易理解、更容易 Debug。
背景
在傳統(tǒng)的分布式追蹤中,“追蹤”通常對(duì)應(yīng)于用戶請(qǐng)求期間的內(nèi)部調(diào)用。特別是,當(dāng)用戶請(qǐng)求到達(dá)時(shí),追蹤會(huì)從根跨度開(kāi)始,然后每個(gè)內(nèi)部RPC調(diào)用會(huì)啟動(dòng)一個(gè)新的子跨度。由于父跨度的持續(xù)時(shí)間通常是其子跨度的超集,追蹤可以直觀地以樹形或火焰圖的形式觀察,其中層次結(jié)構(gòu)表示組件之間的依賴關(guān)系。
與傳統(tǒng)的RPC系統(tǒng)相反,Kubernetes API是異步和聲明式的。為了執(zhí)行操作,組件會(huì)更新apiserver上對(duì)象的規(guī)范(期望狀態(tài)),然后其他組件會(huì)不斷嘗試自我糾正以達(dá)到期望的狀態(tài)。例如,當(dāng)我們將ReplicaSet從3個(gè)副本擴(kuò)展到5個(gè)副本時(shí),我們會(huì)將spec.replicas字段更新為5,rs controller會(huì)觀察到此更改,并不斷創(chuàng)建新的pod對(duì)象,直到總數(shù)達(dá)到5個(gè)。當(dāng)kubelet觀察到其管理的節(jié)點(diǎn)創(chuàng)建了一個(gè)pod時(shí),它會(huì)在其節(jié)點(diǎn)上生成與pod中的規(guī)范匹配的容器。
在此過(guò)程中,我們從未直接調(diào)用過(guò)rs controller,rs controller也從未直接調(diào)用過(guò)kubelet。這意味著我們無(wú)法觀察到組件之間的直接因果關(guān)系。如果在過(guò)程中刪除了原始的3個(gè)pod中的一個(gè),副本集控制器將與兩個(gè)新的pod一起創(chuàng)建一個(gè)不同的pod,我們無(wú)法將此創(chuàng)建與ReplicaSet的擴(kuò)展或pod的刪除關(guān)聯(lián)起來(lái)。因此,由于“追蹤”或“跨度”的定義模糊不清,傳統(tǒng)的基于跨度的分布式追蹤模型在Kubernetes中幾乎不適用。
過(guò)去,各個(gè)組件一直在實(shí)現(xiàn)自己的內(nèi)部追蹤,通常每個(gè)“reconcile”對(duì)應(yīng)一個(gè)追蹤(例如,kubelet追蹤只追蹤處理單個(gè)pod創(chuàng)建/更新的同步操作)。然而,沒(méi)有單一的追蹤能夠解釋整個(gè)流程,這導(dǎo)致了可觀察性的孤立島,因?yàn)橹挥杏^察多個(gè)reconcile才能理解許多面向用戶的行為;例如,擴(kuò)展ReplicaSet的過(guò)程只能通過(guò)觀察副本集控制器處理ReplicaSet更新或pod就緒更新的多個(gè)reconcile來(lái)推斷。
為解決可觀察性數(shù)據(jù)孤島的問(wèn)題,Kelemetry以組件無(wú)關(guān)、非侵入性的方式,收集并連接來(lái)自不同組件的信號(hào),并以追蹤的形式展示相關(guān)數(shù)據(jù)。
設(shè)計(jì)
將對(duì)象作為跨度
為了連接不同組件的可觀察性數(shù)據(jù),Kelemetry采用了一種不同的方法,受到kspan項(xiàng)目的啟發(fā),與將單個(gè)操作作為根跨度的嘗試不同,這里為對(duì)象本身創(chuàng)建一個(gè)跨度,而每個(gè)在對(duì)象上發(fā)生的事件都是一個(gè)子跨度。此外,各個(gè)對(duì)象通過(guò)它們的擁有關(guān)系連接在一起,使得子對(duì)象的跨度成為父對(duì)象的子跨度。因此,我們得到了兩個(gè)維度:樹形層次結(jié)構(gòu)表示對(duì)象層次結(jié)構(gòu)和事件范圍,而時(shí)間線表示事件順序,通常與因果關(guān)系一致。
例如,當(dāng)我們創(chuàng)建一個(gè)單pod部署時(shí),deployment controller、rs controller和kubelet之間的交互可以使用審計(jì)日志和事件的數(shù)據(jù)在單個(gè)追蹤中顯示:
追蹤通常用于追蹤持續(xù)幾秒鐘的短暫請(qǐng)求,所以追蹤存儲(chǔ)實(shí)現(xiàn)可能不支持具有長(zhǎng)生命周期或包含太多跨度的追蹤;包含過(guò)多跨度的追蹤可能導(dǎo)致某些存儲(chǔ)后端的性能問(wèn)題。因此,我們通過(guò)將每個(gè)事件分到其所屬的半小時(shí)時(shí)間段中,將每個(gè)追蹤的持續(xù)時(shí)間限制為30分鐘。例如,發(fā)生在12:56的事件將被分組到12:30-13:00的對(duì)象跨度中。
我們使用分布式KV存儲(chǔ)來(lái)存儲(chǔ)(集群、資源類型、命名空間、名稱、字段、半小時(shí)時(shí)間戳)到相應(yīng)對(duì)象創(chuàng)建的追蹤/跨度ID的映射,以確保每個(gè)對(duì)象只創(chuàng)建一個(gè)追蹤。
審計(jì)日志收集
Kelemetry的主要數(shù)據(jù)源之一是apiserver的審計(jì)日志。審計(jì)日志提供了關(guān)于每個(gè)控制器操作的豐富信息,包括發(fā)起操作的客戶端、涉及的對(duì)象、從接收請(qǐng)求到完成的準(zhǔn)確持續(xù)時(shí)間等。在Kubernetes架構(gòu)中,每個(gè)對(duì)象的更改會(huì)觸發(fā)其相關(guān)的控制器進(jìn)行協(xié)調(diào),并導(dǎo)致后續(xù)對(duì)象的更改,因此觀察與對(duì)象更改相關(guān)的審計(jì)日志有助于理解一系列事件中控制器之間的交互。
Kubernetes apiserver的審計(jì)日志以兩種不同的方式暴露:日志文件和webhook。一些云提供商實(shí)現(xiàn)了自己的審計(jì)日志收集方式,而在社區(qū)中配置審計(jì)日志收集的與廠商無(wú)關(guān)的方法進(jìn)展甚微。為了簡(jiǎn)化自助提供的集群的部署過(guò)程,Kelemetry提供了一個(gè)審計(jì)webhook,用于接收原生的審計(jì)信息,也暴露了插件API以實(shí)現(xiàn)從特定廠商的消息隊(duì)列中消費(fèi)審計(jì)日志。
Event 收集
當(dāng)Kubernetes控制器處理對(duì)象時(shí),它們會(huì)發(fā)出與對(duì)象關(guān)聯(lián)的“event”。當(dāng)用戶運(yùn)行kubectl describe命令時(shí),這些event會(huì)顯示出來(lái),通常提供了控制器處理過(guò)程的更友好的描述。例如,當(dāng)調(diào)度器無(wú)法調(diào)度一個(gè)pod時(shí),它會(huì)發(fā)出一個(gè)FailToSchedulePod事件,其中包含詳細(xì)的消息:
0/4022 nodes are available to run pod xxxxx: 1072 Insufficient memory, 1819 Insufficient cpu, 1930 node(s) didn't match node selector, 71 node(s) had taint {xxxxx}, that the pod didn't tolerate.
由于event針對(duì)用于kubectl describe命令優(yōu)化,它們并不保留每個(gè)原始事件,而是存儲(chǔ)了最后一次記錄事件的時(shí)間戳和次數(shù)。另一方面,Kelemetry使用Kubernetes中的對(duì)象列表觀察API檢索事件,而該API僅公開(kāi)event對(duì)象的最新版本。為了避免重復(fù)事件,Kelemetry使用了幾種啟發(fā)式方法來(lái)“猜測(cè)”是否應(yīng)將event報(bào)告為一個(gè)跨度:
- 持久化處理的最后一個(gè)event的時(shí)間戳,并在重啟后忽略該時(shí)間戳之前的事件。雖然事件的接收順序不一定有保證(由于客戶端時(shí)鐘偏差、控制器 — apiserver — etcd往返的不一致延遲等原因),但這種延遲相對(duì)較小,可以消除由于控制器重啟導(dǎo)致的大多數(shù)重復(fù)。
- 驗(yàn)證event的resourceVersion是否發(fā)生了變化,避免由于重列導(dǎo)致的重復(fù)event。
將對(duì)象狀態(tài)與審計(jì)日志關(guān)聯(lián)
在研究審計(jì)日志進(jìn)行故障排除時(shí),我們最想知道的是“此請(qǐng)求改變了什么”,而不是“誰(shuí)發(fā)起了此請(qǐng)求”,尤其是當(dāng)各個(gè)組件的語(yǔ)義不清楚時(shí)。Kelemetry運(yùn)行一個(gè)控制器來(lái)監(jiān)視對(duì)象的創(chuàng)建、更新和刪除事件,并在接收到審計(jì)事件時(shí)將其與審計(jì)跨度關(guān)聯(lián)起來(lái)。當(dāng)Kubernetes對(duì)象被更新時(shí),它的resourceVersion字段會(huì)更新為一個(gè)新的唯一值。這個(gè)值可以用來(lái)關(guān)聯(lián)更新對(duì)應(yīng)的審計(jì)日志。Kelemetry把對(duì)象每個(gè)resourceVersion的diff和快照緩存在分布式KV存儲(chǔ)中,以便稍后從審計(jì)消費(fèi)者中鏈接,從而使每個(gè)審計(jì)日志跨度包含控制器更改的字段。
追蹤resourceVersion還有助于識(shí)別控制器之間的409沖突。當(dāng)客戶端傳遞UPDATE請(qǐng)求的resourceVersion過(guò)舊,且其他請(qǐng)求是將resourceVersion更改時(shí),就會(huì)發(fā)生沖突請(qǐng)求。Kelemetry能夠?qū)⒕哂邢嗤f資源版本的多個(gè)審計(jì)日志組合在一起,以顯示與其后續(xù)沖突相關(guān)的審計(jì)請(qǐng)求作為相關(guān)的子跨度。
為了確保無(wú)縫可用性,該控制器使用多主選舉機(jī)制,允許控制器的多個(gè)副本同時(shí)監(jiān)視同一集群,以確保在控制器重新啟動(dòng)時(shí)不會(huì)丟失任何事件。
前端追蹤轉(zhuǎn)換
在傳統(tǒng)的追蹤中,跨度總是在同一個(gè)進(jìn)程(通常是同一個(gè)函數(shù))中開(kāi)始和結(jié)束。因此,OTLP 等追蹤協(xié)議不支持在跨度完成后對(duì)其進(jìn)行修改。不幸的是,Kelemetry 不是這種情況,因?yàn)閷?duì)象不是運(yùn)行中的函數(shù),并且沒(méi)有專門用于啟動(dòng)或停止其跨度的進(jìn)程。相反,Kelemetry 在創(chuàng)建后立即確定對(duì)象跨度,并將其他數(shù)據(jù)寫入子跨度, 是以每個(gè)審計(jì)日志和事件都是一個(gè)子跨度而不是對(duì)象跨度上的日志。
然而,由于審計(jì)日志的結(jié)束時(shí)間/持續(xù)時(shí)間通常沒(méi)有什么價(jià)值,因此追蹤視圖非常丑陋且空間效率低下:
為了提高用戶體驗(yàn),Kelemetry 攔截在 Jaeger 查詢前端和存儲(chǔ)后端之間,將存儲(chǔ)后端結(jié)果返回給查詢前端之前,對(duì)存儲(chǔ)后端結(jié)果執(zhí)行自定義轉(zhuǎn)換流水線。
Kelemetry 目前支持 4 種轉(zhuǎn)換流水線:
- tree:服務(wù)名/操作名等字段名簡(jiǎn)化后的原始trace樹
- timeline:修剪所有嵌套的偽跨度,將所有事件跨度放在根跨度下,有效地提供審計(jì)日志
- tracing:非對(duì)象跨度被展平為相關(guān)對(duì)象的跨度日志
- 分組:在追蹤管道輸出之上,為每個(gè)數(shù)據(jù)源(審計(jì)/事件)創(chuàng)建一個(gè)新的偽跨度。當(dāng)多個(gè)組件將它們的跨度發(fā)送到 Kelemetry 時(shí),組件所有者可以專注于自己組件的日志并輕松地交叉檢查其他組件的日志。
用戶可以在追蹤搜索時(shí)通過(guò)設(shè)置“service name”來(lái)選擇轉(zhuǎn)換流水線。中間存儲(chǔ)插件為每個(gè)追蹤搜索結(jié)果生成一個(gè)新的“CacheID”,并將其與實(shí)際 TraceID 和轉(zhuǎn)換管道一起存儲(chǔ)到緩存 KV 中。當(dāng)用戶查看時(shí),他們傳遞CacheID,CacheID 由中間存儲(chǔ)插件轉(zhuǎn)換為實(shí)際TraceID,并執(zhí)行與 CacheID 關(guān)聯(lián)的轉(zhuǎn)換管道。
突破時(shí)長(zhǎng)限制
如上所述,追蹤不能無(wú)限增長(zhǎng),因?yàn)樗赡軙?huì)導(dǎo)致某些存儲(chǔ)后端出現(xiàn)問(wèn)題。相反,我們每 30 分鐘開(kāi)始一個(gè)新的追蹤。這會(huì)導(dǎo)致用戶體驗(yàn)混亂,因?yàn)樵?12:28 開(kāi)始滾動(dòng)的部署追蹤會(huì)在 12:30 突然終止,用戶必須在 12:30 手動(dòng)跳轉(zhuǎn)到下一個(gè)追蹤才能繼續(xù)查看追蹤 . 為了避免這種認(rèn)知開(kāi)銷,Kelemetry 存儲(chǔ)插件在搜索追蹤時(shí)識(shí)別具有相同對(duì)象標(biāo)簽的跨度,并將它們與相同的緩存 ID 以及用戶指定的搜索時(shí)間范圍一起存儲(chǔ)。在渲染 span 時(shí),所有相關(guān)的軌跡都合并在一起,具有相同對(duì)象標(biāo)簽的對(duì)象 span 被刪除重復(fù),它們的子對(duì)象被合并。軌跡搜索時(shí)間范圍成為軌跡的剪切范圍,將對(duì)象組的完整故事顯示為單個(gè)軌跡。
多集群支持
可以部署 Kelemetry 來(lái)監(jiān)視來(lái)自多個(gè)集群的事件。在字節(jié)跳動(dòng),Kelemetry 每天創(chuàng)建 80 億個(gè)跨度(不包括偽跨度)(使用多 raft 緩存后端而不是 etcd)。對(duì)象可以鏈接到來(lái)自不同集群的父對(duì)象,以啟用對(duì)跨集群組件的追蹤。
未來(lái)增強(qiáng)
采用自定義追蹤源
為了真正連接K8S生態(tài)系統(tǒng)中的所有觀測(cè)點(diǎn),審計(jì)和事件并不足夠全面。Kelemetry將從現(xiàn)有組件收集追蹤,并將其集成到Kelemetry追蹤系統(tǒng)中,以提供對(duì)整個(gè)系統(tǒng)的統(tǒng)一和專業(yè)化視圖。
批量分析
通過(guò)Kelemetry的聚合追蹤,回答諸如“從部署升級(jí)到首次拉取鏡像的進(jìn)展需要多長(zhǎng)時(shí)間”等問(wèn)題變得更加容易,但我們?nèi)匀蝗狈υ诖笠?guī)模上聚合這些指標(biāo)以提供整體性能洞察的能力。通過(guò)每隔半小時(shí)分析Kelemetry的追蹤輸出,我們可以識(shí)別一系列跨度中的模式,并將其關(guān)聯(lián)為不同的場(chǎng)景。
使用案例
1. replicaset controller 異常
用戶報(bào)告,一個(gè) deployment 不斷創(chuàng)建新的 Pod。我們可以通過(guò)deployment名稱快速查找其 Kelemetry 追蹤,分析replicaset與其創(chuàng)建的 Pod 之間的關(guān)系。
從追蹤可見(jiàn),幾個(gè)關(guān)鍵點(diǎn):
- Replicaset-controller 發(fā)出
SuccessfulCreate
事件,表示 Pod 創(chuàng)建請(qǐng)求成功返回,并在replicaset reconcile中得到了replicaset controller的確認(rèn)。 - 沒(méi)有replicaset狀態(tài)更新事件,這意味著replicaset controller中的 Pod reconcile未能更新replicaset狀態(tài)或未觀察到這些 Pod。
此外,查看其中一個(gè) Pod 的追蹤:
- Replicaset controller 在 Pod 創(chuàng)建后再也沒(méi)有與該 Pod 進(jìn)行交互,甚至沒(méi)有失敗的更新請(qǐng)求。
因此,我們可以得出結(jié)論,replicaset controller中的 Pod 緩存很可能與 apiserver 上的實(shí)際 Pod 存儲(chǔ)不一致,我們應(yīng)該考慮 pod informer 的性能或一致性問(wèn)題。如果沒(méi)有 Kelemetry,定位此問(wèn)題將涉及查看多個(gè) apiserver 實(shí)例的各個(gè) Pod 的審計(jì)日志。
2.浮動(dòng)的 minReadySeconds
用戶發(fā)現(xiàn)deployment的滾動(dòng)更新非常緩慢,從14:00到18:00花費(fèi)了幾個(gè)小時(shí)。如不使用Kelemetry,通過(guò)使用 kubectl 查找對(duì)象,發(fā)現(xiàn) minReadySeconds 字段設(shè)置為 10,所以長(zhǎng)時(shí)間的滾動(dòng)更新時(shí)間是不符合預(yù)期的。kube-controller-manager 的日志顯示,在一個(gè)小時(shí)后 Pod 才變?yōu)? Ready 狀態(tài)
進(jìn)一步查看 kube-controller-manager 的日志后發(fā)現(xiàn),在某個(gè)時(shí)刻 minReadySeconds 的值為 3600。
使用 Kelemetry 進(jìn)行調(diào)試,我們可以直接通過(guò)deployment名稱查找追蹤,并發(fā)現(xiàn)federation組件增加了 minReadySeconds 的值。
后來(lái),deployment controller將該值恢復(fù)為 10。
因此,我們可以得出結(jié)論,問(wèn)題是由用戶在滾動(dòng)更新過(guò)程中臨時(shí)注入的較大 minReadySeconds 值引起的。通過(guò)檢視對(duì)象 diff ,可以輕松識(shí)別由非預(yù)期中間狀態(tài)引起的問(wèn)題。
嘗試Kelemetry
Kelemetry已在GitHub上開(kāi)源:https://github.com/kubewharf/kelemetry
按照 docs/QUICK_START.md 快速入門指南試試Kelemetry如何與您的組件進(jìn)行交互,或者如果您不想設(shè)置一個(gè)集群,可以查看從GitHub CI流水線構(gòu)建的在線預(yù)覽:https://kubewharf.io/kelemetry/trace-deployment/
加入我們
火山引擎云原生團(tuán)隊(duì)火山引擎云原生團(tuán)隊(duì)主要負(fù)責(zé)火山引擎公有云及私有化場(chǎng)景中 PaaS 類產(chǎn)品體系的構(gòu)建,結(jié)合字節(jié)跳動(dòng)多年的云原生技術(shù)棧經(jīng)驗(yàn)和最佳實(shí)踐沉淀,幫助企業(yè)加速數(shù)字化轉(zhuǎn)型和創(chuàng)新。產(chǎn)品包括容器服務(wù)、鏡像倉(cāng)庫(kù)、分布式云原生平臺(tái)、函數(shù)服務(wù)、服務(wù)網(wǎng)格、持續(xù)交付、可觀測(cè)服務(wù)等。