字節跳動開源 Kelemetry:面向 Kubernetes 控制面的全局追蹤系統

Kelemetry是字節跳動開發的用于Kubernetes控制平面的追蹤系統,它從全局視角串聯起多個 Kubernetes 組件的行為,追蹤單個 Kubernetes 對象的完整生命周期以及不同對象之間的相互影響。通過可視化 K8s 系統內的事件鏈路,它使得 Kubernetes 系統更容易觀測、更容易理解、更容易 Debug。

背景
在傳統的分布式追蹤中,“追蹤”通常對應于用戶請求期間的內部調用。特別是,當用戶請求到達時,追蹤會從根跨度開始,然后每個內部RPC調用會啟動一個新的子跨度。由于父跨度的持續時間通常是其子跨度的超集,追蹤可以直觀地以樹形或火焰圖的形式觀察,其中層次結構表示組件之間的依賴關系。
與傳統的RPC系統相反,Kubernetes API是異步和聲明式的。為了執行操作,組件會更新apiserver上對象的規范(期望狀態),然后其他組件會不斷嘗試自我糾正以達到期望的狀態。例如,當我們將ReplicaSet從3個副本擴展到5個副本時,我們會將spec.replicas字段更新為5,rs controller會觀察到此更改,并不斷創建新的pod對象,直到總數達到5個。當kubelet觀察到其管理的節點創建了一個pod時,它會在其節點上生成與pod中的規范匹配的容器。
在此過程中,我們從未直接調用過rs controller,rs controller也從未直接調用過kubelet。這意味著我們無法觀察到組件之間的直接因果關系。如果在過程中刪除了原始的3個pod中的一個,副本集控制器將與兩個新的pod一起創建一個不同的pod,我們無法將此創建與ReplicaSet的擴展或pod的刪除關聯起來。因此,由于“追蹤”或“跨度”的定義模糊不清,傳統的基于跨度的分布式追蹤模型在Kubernetes中幾乎不適用。
過去,各個組件一直在實現自己的內部追蹤,通常每個“reconcile”對應一個追蹤(例如,kubelet追蹤只追蹤處理單個pod創建/更新的同步操作)。然而,沒有單一的追蹤能夠解釋整個流程,這導致了可觀察性的孤立島,因為只有觀察多個reconcile才能理解許多面向用戶的行為;例如,擴展ReplicaSet的過程只能通過觀察副本集控制器處理ReplicaSet更新或pod就緒更新的多個reconcile來推斷。
為解決可觀察性數據孤島的問題,Kelemetry以組件無關、非侵入性的方式,收集并連接來自不同組件的信號,并以追蹤的形式展示相關數據。
設計
將對象作為跨度
為了連接不同組件的可觀察性數據,Kelemetry采用了一種不同的方法,受到kspan項目的啟發,與將單個操作作為根跨度的嘗試不同,這里為對象本身創建一個跨度,而每個在對象上發生的事件都是一個子跨度。此外,各個對象通過它們的擁有關系連接在一起,使得子對象的跨度成為父對象的子跨度。因此,我們得到了兩個維度:樹形層次結構表示對象層次結構和事件范圍,而時間線表示事件順序,通常與因果關系一致。
例如,當我們創建一個單pod部署時,deployment controller、rs controller和kubelet之間的交互可以使用審計日志和事件的數據在單個追蹤中顯示:

追蹤通常用于追蹤持續幾秒鐘的短暫請求,所以追蹤存儲實現可能不支持具有長生命周期或包含太多跨度的追蹤;包含過多跨度的追蹤可能導致某些存儲后端的性能問題。因此,我們通過將每個事件分到其所屬的半小時時間段中,將每個追蹤的持續時間限制為30分鐘。例如,發生在12:56的事件將被分組到12:30-13:00的對象跨度中。
我們使用分布式KV存儲來存儲(集群、資源類型、命名空間、名稱、字段、半小時時間戳)到相應對象創建的追蹤/跨度ID的映射,以確保每個對象只創建一個追蹤。
審計日志收集
Kelemetry的主要數據源之一是apiserver的審計日志。審計日志提供了關于每個控制器操作的豐富信息,包括發起操作的客戶端、涉及的對象、從接收請求到完成的準確持續時間等。在Kubernetes架構中,每個對象的更改會觸發其相關的控制器進行協調,并導致后續對象的更改,因此觀察與對象更改相關的審計日志有助于理解一系列事件中控制器之間的交互。
Kubernetes apiserver的審計日志以兩種不同的方式暴露:日志文件和webhook。一些云提供商實現了自己的審計日志收集方式,而在社區中配置審計日志收集的與廠商無關的方法進展甚微。為了簡化自助提供的集群的部署過程,Kelemetry提供了一個審計webhook,用于接收原生的審計信息,也暴露了插件API以實現從特定廠商的消息隊列中消費審計日志。
Event 收集
當Kubernetes控制器處理對象時,它們會發出與對象關聯的“event”。當用戶運行kubectl describe命令時,這些event會顯示出來,通常提供了控制器處理過程的更友好的描述。例如,當調度器無法調度一個pod時,它會發出一個FailToSchedulePod事件,其中包含詳細的消息:
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命令優化,它們并不保留每個原始事件,而是存儲了最后一次記錄事件的時間戳和次數。另一方面,Kelemetry使用Kubernetes中的對象列表觀察API檢索事件,而該API僅公開event對象的最新版本。為了避免重復事件,Kelemetry使用了幾種啟發式方法來“猜測”是否應將event報告為一個跨度:
- 持久化處理的最后一個event的時間戳,并在重啟后忽略該時間戳之前的事件。雖然事件的接收順序不一定有保證(由于客戶端時鐘偏差、控制器 — apiserver — etcd往返的不一致延遲等原因),但這種延遲相對較小,可以消除由于控制器重啟導致的大多數重復。
- 驗證event的resourceVersion是否發生了變化,避免由于重列導致的重復event。
將對象狀態與審計日志關聯
在研究審計日志進行故障排除時,我們最想知道的是“此請求改變了什么”,而不是“誰發起了此請求”,尤其是當各個組件的語義不清楚時。Kelemetry運行一個控制器來監視對象的創建、更新和刪除事件,并在接收到審計事件時將其與審計跨度關聯起來。當Kubernetes對象被更新時,它的resourceVersion字段會更新為一個新的唯一值。這個值可以用來關聯更新對應的審計日志。Kelemetry把對象每個resourceVersion的diff和快照緩存在分布式KV存儲中,以便稍后從審計消費者中鏈接,從而使每個審計日志跨度包含控制器更改的字段。
追蹤resourceVersion還有助于識別控制器之間的409沖突。當客戶端傳遞UPDATE請求的resourceVersion過舊,且其他請求是將resourceVersion更改時,就會發生沖突請求。Kelemetry能夠將具有相同舊資源版本的多個審計日志組合在一起,以顯示與其后續沖突相關的審計請求作為相關的子跨度。
為了確保無縫可用性,該控制器使用多主選舉機制,允許控制器的多個副本同時監視同一集群,以確保在控制器重新啟動時不會丟失任何事件。

前端追蹤轉換
在傳統的追蹤中,跨度總是在同一個進程(通常是同一個函數)中開始和結束。因此,OTLP 等追蹤協議不支持在跨度完成后對其進行修改。不幸的是,Kelemetry 不是這種情況,因為對象不是運行中的函數,并且沒有專門用于啟動或停止其跨度的進程。相反,Kelemetry 在創建后立即確定對象跨度,并將其他數據寫入子跨度, 是以每個審計日志和事件都是一個子跨度而不是對象跨度上的日志。
然而,由于審計日志的結束時間/持續時間通常沒有什么價值,因此追蹤視圖非常丑陋且空間效率低下:

為了提高用戶體驗,Kelemetry 攔截在 Jaeger 查詢前端和存儲后端之間,將存儲后端結果返回給查詢前端之前,對存儲后端結果執行自定義轉換流水線。
Kelemetry 目前支持 4 種轉換流水線:
- tree:服務名/操作名等字段名簡化后的原始trace樹
- timeline:修剪所有嵌套的偽跨度,將所有事件跨度放在根跨度下,有效地提供審計日志
- tracing:非對象跨度被展平為相關對象的跨度日志

- 分組:在追蹤管道輸出之上,為每個數據源(審計/事件)創建一個新的偽跨度。當多個組件將它們的跨度發送到 Kelemetry 時,組件所有者可以專注于自己組件的日志并輕松地交叉檢查其他組件的日志。
用戶可以在追蹤搜索時通過設置“service name”來選擇轉換流水線。中間存儲插件為每個追蹤搜索結果生成一個新的“CacheID”,并將其與實際 TraceID 和轉換管道一起存儲到緩存 KV 中。當用戶查看時,他們傳遞CacheID,CacheID 由中間存儲插件轉換為實際TraceID,并執行與 CacheID 關聯的轉換管道。

突破時長限制
如上所述,追蹤不能無限增長,因為它可能會導致某些存儲后端出現問題。相反,我們每 30 分鐘開始一個新的追蹤。這會導致用戶體驗混亂,因為在 12:28 開始滾動的部署追蹤會在 12:30 突然終止,用戶必須在 12:30 手動跳轉到下一個追蹤才能繼續查看追蹤 . 為了避免這種認知開銷,Kelemetry 存儲插件在搜索追蹤時識別具有相同對象標簽的跨度,并將它們與相同的緩存 ID 以及用戶指定的搜索時間范圍一起存儲。在渲染 span 時,所有相關的軌跡都合并在一起,具有相同對象標簽的對象 span 被刪除重復,它們的子對象被合并。軌跡搜索時間范圍成為軌跡的剪切范圍,將對象組的完整故事顯示為單個軌跡。
多集群支持
可以部署 Kelemetry 來監視來自多個集群的事件。在字節跳動,Kelemetry 每天創建 80 億個跨度(不包括偽跨度)(使用多 raft 緩存后端而不是 etcd)。對象可以鏈接到來自不同集群的父對象,以啟用對跨集群組件的追蹤。
未來增強
采用自定義追蹤源
為了真正連接K8S生態系統中的所有觀測點,審計和事件并不足夠全面。Kelemetry將從現有組件收集追蹤,并將其集成到Kelemetry追蹤系統中,以提供對整個系統的統一和專業化視圖。
批量分析
通過Kelemetry的聚合追蹤,回答諸如“從部署升級到首次拉取鏡像的進展需要多長時間”等問題變得更加容易,但我們仍然缺乏在大規模上聚合這些指標以提供整體性能洞察的能力。通過每隔半小時分析Kelemetry的追蹤輸出,我們可以識別一系列跨度中的模式,并將其關聯為不同的場景。
使用案例
1. replicaset controller 異常
用戶報告,一個 deployment 不斷創建新的 Pod。我們可以通過deployment名稱快速查找其 Kelemetry 追蹤,分析replicaset與其創建的 Pod 之間的關系。

從追蹤可見,幾個關鍵點:
- Replicaset-controller 發出
SuccessfulCreate事件,表示 Pod 創建請求成功返回,并在replicaset reconcile中得到了replicaset controller的確認。 - 沒有replicaset狀態更新事件,這意味著replicaset controller中的 Pod reconcile未能更新replicaset狀態或未觀察到這些 Pod。
此外,查看其中一個 Pod 的追蹤:

- Replicaset controller 在 Pod 創建后再也沒有與該 Pod 進行交互,甚至沒有失敗的更新請求。
因此,我們可以得出結論,replicaset controller中的 Pod 緩存很可能與 apiserver 上的實際 Pod 存儲不一致,我們應該考慮 pod informer 的性能或一致性問題。如果沒有 Kelemetry,定位此問題將涉及查看多個 apiserver 實例的各個 Pod 的審計日志。
2.浮動的 minReadySeconds
用戶發現deployment的滾動更新非常緩慢,從14:00到18:00花費了幾個小時。如不使用Kelemetry,通過使用 kubectl 查找對象,發現 minReadySeconds 字段設置為 10,所以長時間的滾動更新時間是不符合預期的。kube-controller-manager 的日志顯示,在一個小時后 Pod 才變為 Ready 狀態

進一步查看 kube-controller-manager 的日志后發現,在某個時刻 minReadySeconds 的值為 3600。

使用 Kelemetry 進行調試,我們可以直接通過deployment名稱查找追蹤,并發現federation組件增加了 minReadySeconds 的值。

后來,deployment controller將該值恢復為 10。

因此,我們可以得出結論,問題是由用戶在滾動更新過程中臨時注入的較大 minReadySeconds 值引起的。通過檢視對象 diff ,可以輕松識別由非預期中間狀態引起的問題。
嘗試Kelemetry
Kelemetry已在GitHub上開源:https://github.com/kubewharf/kelemetry
按照 docs/QUICK_START.md 快速入門指南試試Kelemetry如何與您的組件進行交互,或者如果您不想設置一個集群,可以查看從GitHub CI流水線構建的在線預覽:https://kubewharf.io/kelemetry/trace-deployment/
加入我們
火山引擎云原生團隊火山引擎云原生團隊主要負責火山引擎公有云及私有化場景中 PaaS 類產品體系的構建,結合字節跳動多年的云原生技術棧經驗和最佳實踐沉淀,幫助企業加速數字化轉型和創新。產品包括容器服務、鏡像倉庫、分布式云原生平臺、函數服務、服務網格、持續交付、可觀測服務等。

































