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