Kubernetes 資源拓撲感知調度優化
作者 | 騰訊星辰算力團隊
1.背景
1.1 問題源起
近年來,隨著騰訊內部自研上云項目的不斷發展,越來越多的業務開始使用云原生方式托管自己的工作負載,容器平臺的規模因此不斷增大。以 Kubernetes 為底座的云原生技術極大推動了云原生領域的發展,已然成為各大容器平臺事實上的技術標準。在云原生場景下,為了最大化實現資源共享,單臺宿主機往往會運行多個不同用戶的計算任務。如果在宿主機內沒有進行精細化的資源隔離,在業務負載高峰時間段,多個容器往往會對資源產生激烈的競爭,可能導致程序性能的急劇下降,主要體現為:
- 資源調度時頻繁的上下文切換時間
- 頻繁的進程切換導致的 CPU 高速緩存失效
因此,在云原生場景下需要針對容器資源分配加以精細化的限制,確保在 CPU 利用率較高時,各容器之間不會產生激烈競爭從而引起性能下降。
1.2 調度場景
騰訊星辰算力平臺承載了全公司的 CPU 和 GPU 算力服務,擁有著海量多類型的計算資源。當前,平臺承載的多數重點服務偏離線場景,在業務日益增長的算力需求下,提供源源不斷的低成本資源,持續提升可用性、服務質量、調度能力,覆蓋更多的業務場景。然而,Kubernetes 原生的調度與資源綁定功能已經無法滿足復雜的算力場景,亟需對資源進行更加精細化的調度,主要體現為:
(1)Kubernetes 原生調度器無法感知節點資源拓撲信息導致 Pod 生產失敗
kube-scheduler 在調度過程中并不感知節點的資源拓撲,當 kube-scheduler 將 Pod 調度到某個節點后,kubelet 如果發現節點的資源拓撲親和性要求無法滿足時,會拒絕生產該 Pod,當通過外部控制環(如 deployment)來部署 Pod 時,則會導致 Pod 被反復創建-->調度-->生產失敗的死循環。
(2)基于離線虛擬機的混部方案導致的節點實際可用 CPU 核心數變化
面對運行在線業務的云主機平均利用率較低的現實,為充分利用空閑資源,可將離線虛擬機和在線虛擬機混合部署,解決公司離線計算需求,提升自研上云資源平均利用率。在保證離線不干擾在線業務的情況下,騰訊星辰算力基于自研內核調度器 VMF 的支持,可以將一臺機器上的閑時資源充分利用起來,生產低優先級的離線虛擬機。由于 VMF 的不公平調度策略,離線虛擬機的實際可用核心數受到在線虛擬機的影響,隨著在線業務的繁忙程度不斷變化。因此,kubelet 通過 cadvisor 在離線宿主機內部采集到的 CPU 核心數并不準確,導致了調度信息出現偏差。

cvm-architecture
(3)資源的高效利用需要更加精細化的調度粒度
kube-scheduler 的職責是為 Pod 選擇一個合適的 Node 完成一次調度。然而,想對資源進行更高效的利用,原生調度器的功能還遠遠不夠。在調度時,我們希望調度器能夠進行更細粒度的調度,比如能夠感知到 CPU 核心、GPU 拓撲、網絡拓撲等等,使得資源利用方式更加合理。
2.預備知識
2.1 cgroups 之 cpuset 子系統
cgroups 是 Linux 內核提供的一種可以限制單個進程或者多個進程所使用資源的機制,可以對 CPU、內存等資源實現精細化的控制。Linux 下的容器技術主要通過 cgroups 來實現資源控制。
在 cgroups 中,cpuset 子系統可以為 cgroups 中的進程分配獨立的 CPU 和內存節點。通過將 CPU 核心編號寫入 cpuset 子系統中的 cpuset.cpus文件中或將內存 NUMA 編號寫入 cpuset.mems文件中,可以限制一個或一組進程只使用特定的 CPU 或者內存。
幸運的是,在容器的資源限制中,我們不需要手動操作 cpuset 子系統。通過連接容器運行時(CRI)提供的接口,可以直接更新容器的資源限制。
// ContainerManager contains methods to manipulate containers managed by a
// container runtime. The methods are thread-safe.
type ContainerManager interface {
// ......
// UpdateContainerResources updates the cgroup resources for the container.
UpdateContainerResources(containerID string, resources *runtimeapi.LinuxContainerResources) error
// ......
}
2.2 NUMA 架構
非統一內存訪問架構(英語:Non-uniform memory access,簡稱 NUMA)是一種為多處理器的電腦設計的內存架構,內存訪問時間取決于內存相對于處理器的位置。在 NUMA 下,處理器訪問它自己的本地內存的速度比非本地內存(內存位于另一個處理器,或者是處理器之間共享的內存)快一些。現代多核服務器大多采用 NUMA 架構來提高硬件的可伸縮性。

numa-architecture
從圖中可以看出,每個 NUMA Node 有獨立的 CPU 核心、L3 cache 和內存,NUMA Node 之間相互連接。相同 NUMA Node 上的 CPU 可以共享 L3 cache,同時訪問本 NUMA Node 上的內存速度更快,跨 NUMA Node 訪問內存會更慢。因此,我們應當為 CPU 密集型應用分配同一個 NUMA Node 的 CPU 核心,確保程序的局部性能得到充分滿足。
2.3 Kubernetes 調度框架
Kubernetes 自 v1.19 開始正式穩定支持調度框架,調度框架是面向 Kubernetes 調度器的一種插件架構,它為現有的調度器添加了一組新的“插件”API,插件會被編譯到調度器之中。這為我們自定義調度器帶來了福音。我們可以無需修改 kube-scheduler 的源代碼,通過實現不同的調度插件,將插件代碼與 kube-scheduler 編譯為同一個可執行文件中,從而開發出自定義的擴展調度器。這樣的靈活性擴展方便我們開發與配置各類調度器插件,同時無需修改 kube-scheduler 的源代碼的方式使得擴展調度器可以快速更改依賴,更新到最新的社區版本。

scheduling-framework-extensions
調度器的主要擴展點如上圖所示。我們擴展的調度器主要關心以下幾個步驟:
(1)PreFilter 和 Filter
這兩個插件用于過濾出不能運行該 Pod 的節點,如果任何 Filter 插件將節點標記為不可行,該節點都不會進入候選集合,繼續后面的調度流程。
(2)PreScore、Score 和 NormalizeScore
這三個插件用于對通過過濾階段的節點進行排序,調度器將為每個節點調用每個評分插件,最終評分最高的節點將會作為最終調度結果被選中。
(3)Reserve 和 Unreserve
這個插件用于在 Pod 真正被綁定到節點之前,對資源做一些預留工作,保證調度的一致性。如果綁定失敗則通過 Unreserve 來釋放預留的資源。
(4)Bind
這個插件用于將 Pod 綁定到節點上。默認的綁定插件只是為節點指定 spec.nodeName 來完成調度,如果我們需要擴展調度器,加上其他的調度結果信息,就需要禁用默認 Bind 插件,替換為自定義的 Bind 插件。
3.國內外技術研究現狀
目前 Kubernetes 社區、Volcano 開源社區均有關于拓撲感知相關的解決方案,各方案有部分相同之處,但各自都有局限性,無法滿足星辰算力的復雜場景。
3.1 Kubernetes 社區
Kubernetes 社區 scheduling 興趣小組針對拓撲感知調度也有過一套解決方案,這個方案主要是由 RedHat 來主導,通過scheduler-plugins和node-feature-discovery配合實現了考慮節點拓撲的調度方法。社區的方法僅僅考慮節點是否能夠在滿足 kubelet 配置要求的情況下,完成調度節點篩選和打分,并不會執行綁核,綁核操作仍然交給 kubelet 來完成,相關提案在這里。具體實現方案如下:
- 節點上的 nfd-topology-updater 通過 gRPC 上報節點拓撲到 nfd-master 中(周期 60s)。
- nfd-master 更新節點拓撲與分配情況到 CR 中(NodeResourceTopology)。
- 擴展 kube-scheduler,進行調度時考慮 NodeTopology。
- 節點 kubelet 完成綁核工作。
該方案存在較多的問題,不能解決生產實踐中的需求:
- 具體核心分配依賴 kubelet 完成,因此調度器只會考慮資源拓撲信息,并不會選擇拓撲,調度器沒有資源預留。這導致了節點調度與拓撲調度不在同一個環節,會引起數據不一致問題。
- 由于具體核心分配依賴 kubelet 完成,所以已調度 Pod 的拓撲信息需要依靠 nfd-worker 每隔 60s 匯報一次,導致拓撲發現過慢因此使得數據不一致問題更加嚴重,參見這里。
- 沒有區分需要拓撲親和的 pod 和普通的 pod,容易造成開啟拓撲功能的節點高優資源浪費。
3.2 Volcano 社區
Volcano 是在 Kubernetes 上運行高性能工作負載的容器批量計算引擎,隸屬于 CNCF 孵化項目。在 v1.4.0-Beta 版本中進行了增強,發布了有關 NUMA 感知的特性。與 Kubernetes 社區 scheduling 興趣小組的實現方式類似,真正的綁核并未單獨實現,直接采用的是 kubelet 自帶的功能。具體實現方案如下:
- resource-exporter 是部署在每個節點上的 DaemonSet,負責節點的拓撲信息采集,并將節點信息寫入 CR中(Numatopology)。
- Volcano 根據節點的 Numatopology,在調度 Pod 時進行 NUMA 調度感知。
- 節點 kubelet 完成綁核工作。
該方案存在的問題基本與 Kubernetes 社區 scheduling 興趣小組的實現方式類似,具體核心分配依賴 kubelet
完成。雖然調度器盡力保持與 kubelet 一致,但因為無法做資源預留,仍然會出現不一致的問題,在高并發場景下尤其明顯。
3.3 小結
基于國內外研究現狀的結果進行分析,開源社區在節點資源綁定方面還是希望交給 kubelet,調度器盡量保證與 kubelet 的一致,可以理解這比較符合社區的方向。因此,目前各個方案的典型實現都不完美,無法滿足騰訊星辰算力的要求,在復雜的生產環境中我們需要一套更加穩健、擴展性更好的方案。因此,我們決定從各個方案的架構優點出發,探索出一套更加強大的、貼合騰訊星辰算力實際場景的資源精細化調度增強方案。
4.問題分析
4.1 離線虛擬機節點實際可用 CPU 核心數變化
從 1.2 節中我們可以知道,騰訊星辰算力使用了基于離線虛擬機的混部方案,節點實際的 CPU 可用核心數會收到在線業務的峰值影響從而變化。因此,kubelet 通過 cadvisor 在離線宿主機內部采集到的 CPU 核心數并不準確,這個數值是一個固定值。因此,針對離線資源我們需要調度器通過其他的方式來獲取節點的實際算力。

cvm
目前調度和綁核都不能到離線虛擬機的實際算力,導致任務綁定到在線干擾比較嚴重的 NUMA node,資源競爭非常嚴重使得任務的性能下降。

cvm-2
幸運的是,我們在物理機上可以采集到離線虛擬機每個 NUMA node 上實際可用的 CPU 資源比例,通過折損公式計算出離線虛擬機的實際算力。接下來就只需要讓調度器在調度時能夠感知到 CPU 拓撲以及實際算力,從而進行分配。
4.2 精細化調度需要更強的靈活性
通過 kubelet 自帶的 cpumanager 進行綁核總是會對該節點上的所有 Pod 均生效。只要 Pod 滿足 Guaranteed 的 QoS 條件,且 CPU 請求值為整數,都會進行綁核。然而,有些 Pod 并不是高負載類型卻獨占 CPU,這種方式的方式很容易造成開啟拓撲功能的節點高優資源浪費。
同時,對于不同資源類型的節點,其拓撲感知的要求也是不一樣的。例如,星辰算力的資源池中也含有較多碎片虛擬機,這部分節點不是混部方式生產出來的,相比而言資源穩定,但是規格很小(如 8 核 CVM,每個 NUMA Node 有 4 核)。由于大多數任務規格都會超過 4 核,這類資源就在使用過程中就可以跨 NUMA Node 進行分配,否則很難匹配。
因此,拓撲感知調度需要更強的靈活性,適應各種核心分配與拓撲感知場景。
4.3 調度方案需要更強的擴展性
調度器在抽象拓撲資源時,需要考慮擴展性。對于今后可能會需要調度的擴展資源,如各類異構資源的調度,也能夠在這套方案中輕松使用,而不僅僅是 cgroups 子系統中含有的資源。
4.4 避免超線程帶來的 CPU 競爭問題
在 CPU 核心競爭較為激烈時,超線程可能會帶來更差的性能。更加理想的分配方式是將一個邏輯核分配給高負載應用,另一個邏輯核分配給不繁忙的應用,或者是將兩個峰谷時刻相反的應用分配到同一個物理核心上。同時,我們避免將同一個應用分配到同一個物理核心的兩個邏輯核心上,這樣很可能造成 CPU 競爭問題。
5.解決方案
為了充分解決上述問題,并考慮到未來的擴展性,我們設計了一套精細化調度的方案,命名為 cassini。整套解決方案包含三個組件和一個 CRD,共同配合完成資源精細化調度的工作。
注:cassini這個名稱來源于知名的土星探測器卡西尼-惠更斯號,對土星進行了精準無誤的探測,借此名來象征更加精準的拓撲發現與調度。
5.1 總體架構

solution
各模塊職責如下:
- cassini-master:從外部系統負責收集節點特性(如節點的 offline_capacity,節點電力情況),作為 controller 采用
Deployment 方式運行。 - scheduler-plugins:新增調度插件的擴展調度器替換原生調度器,在節點綁定的同時還會分配拓撲調度結果,作為靜態 Pod 在每個 master 節點上運行。
調度整體流程如下:
(1)cassini-worker啟動,收集節點上的拓撲資源信息。
(2)創建或更新 NodeResourceTopology(NRT)類型的 CR 資源,用于記錄節點拓撲信息。
(3)讀取 kubelet 的 cpu_manager_state 文件,將已有容器的 kubelet 綁核結果 patch 到 Pod annotations 中。
(4)cassini-master 根據外部系統獲取到的信息來更新對應節點的 NRT 對象。
(5)擴展調度器 scheduler-plugins 執行 Pod 調度,根據 NRT 對象感知到節點的拓撲信息,調度 Pod 時將拓撲調度結構寫到 Pod annotations 中。
(6)節點 kubelet 監聽并準備啟動 Pod。
(7)節點 kubelet 調用容器運行時接口啟動容器。
(8)cassini-worker 周期性地訪問 kubelet 的 10250 端口來 List 節點上的 Pod 并從 Pod annotations 中獲取調度器的拓撲調度結果。
(9)cassini-worker 調用容器運行時接口來更改容器的綁核結果。
整體可以看出,cassini-worker 在節點上收集更詳細的資源拓撲信息,cassini-master 從外部系統集中獲取節點資源的附加信息。scheduler-plugins 擴展了原生調度器,以這些附加信息作為決策依據來進行更加精細化的調度,并將結果寫到 Pod annotations 中。最終,cassini-worker 又承擔了執行者的職責,負責落實調度器的資源拓撲調度結果。
5.2 API 設計
NodeResourceTopology(NRT)是用于抽象化描述節點資源拓撲信息的 Kubernetes CRD,這里主要參考了 Kubernetes
社區 scheduling 興趣小組的設計。每一個 Zone 用于描述一個抽象的拓撲區域,ZoneType 來描述其類型,ResourceInfo 來描述 Zone 內的資源總量。
// Zone represents a resource topology zone, e.g. socket, node, die or core.
type Zone struct {
// Name represents the zone name.
// +required
Name string `json:"name" protobuf:"bytes,1,opt,name=name"`
// Type represents the zone type.
// +kubebuilder:validation:Enum=Node;Socket;Core
// +required
Type ZoneType `json:"type" protobuf:"bytes,2,opt,name=type"`
// Parent represents the name of parent zone.
// +optional
Parent string `json:"parent,omitempty" protobuf:"bytes,3,opt,name=parent"`
// Costs represents the cost between different zones.
// +optional
Costs CostList `json:"costs,omitempty" protobuf:"bytes,4,rep,name=costs"`
// Attributes represents zone attributes if any.
// +optional
Attributes map[string]string `json:"attributes,omitempty" protobuf:"bytes,5,rep,name=attributes"`
// Resources represents the resource info of the zone.
// +optional
Resources *ResourceInfo `json:"resources,omitempty" protobuf:"bytes,6,rep,name=resources"`
}
注意到,為了更強的擴展性,每個 Zone 內加上了一個 Attributes 來描述 Zone 上的自定義屬性。如 4.1 節中所示,我們將采集到的離線虛擬機實際算力寫入到 Attributes 字段中,來描述每個 NUMA Node 實際可用算力。
5.3 調度器設計

plugin擴展調度器在原生調度器基礎上擴展了新的插件,大體如下所示:
- Filter:讀取 NRT 資源,根據每個拓撲內的實際可用算力以及 Pod 拓撲感知要求來篩選節點并選擇拓撲。
- Score:根據 Zone 個數打分來打分,Zone 越多分越低(跨 Zone 會帶來性能損失)。
- Reserve:在真正綁定前做資源預留,避免數據不一致,kube-scheduler 的 cache 中也有類似的 assume 功能。
- Bind:禁用默認的 Bind 插件,在 Bind 時加入 Zone 的選擇結果,附加在 annotations 中。
通過 TopologyMatch 插件使得調度器在調度時能夠考慮節點拓撲信息并進行拓撲分配,并通過 Bind 插件將結果附加在 annotations 中。
值得一提的是,這里還額外實現了關于節點電力調度等更多維度調度的調度器插件,本篇中不作展開討論,感興趣的同學可以私戳我了解。
5.4 master 設計
cassini-master 是中控組件,從外部來采集一些節點上無法采集的資源信息。我們從物理上采集到離線虛擬機的實際可用算力,由 cassini-master 負責將這類結果附加到對應節點的 NRT 資源中。該組件將統一資源收集的功能進行了剝離,方便更新與擴展。
5.5 worker 設計
cassini-worker 是一個較為復雜的組件,作為 DaemonSet 在每個節點上運行。它的職責分兩部分:
(1)采集節點上的拓撲資源。(2)執行調度器的拓撲調度結果。
5.5.1 資源采集
資源拓撲采集主要是通過從 /sys/devices下采集系統相關的硬件信息,并創建或更新到 NRT 資源中。該組件會 watch 節點 kubelet 的配置信息并上報,讓調度器感知到節點的 kubelet 的綁核策略、預留資源等信息。
由于硬件信息幾乎不變化,默認會較長時間采集一次并更新。但 watch 配置的事件是實時的,一旦 kubelet 配置后會立刻感知到,方便調度器根據節點的配置進行不同的決策。
5.5.2 拓撲調度結果執行
拓撲調度結果執行是通過周期性地 reconcile 來完成制定容器的拓撲分配。
(1)獲取 Pod 信息
為了防止每個節點的 cassini-worker都 watch kube-apiserver 造成 kube-apiserver 的壓力,cassini-worker改用周期性訪問 kubelet 的 10250 端口的方式,來 List 節點上的 Pod 并從 Pod annotations 中獲取調度器的拓撲調度結果。同時,從 status 中還可以獲取到每個容器的 ID 與狀態,為拓撲資源的分配創建了條件。
(2)記錄 kubelet 的 CPU 綁定信息
在 kubelet 開啟 CPU 核心綁定時,擴展調度器將會跳過所有的 TopologyMatch插件。此時 Pod annotations 中不會包含拓撲調度結果。在 kubelet 為 Pod 完成 CPU 核心綁定后,會將結果記錄在 cpu_manager_state文件中,cassini-worker 讀取該文件,并將 kubelet 的綁定結果 patch 到 Pod annotations 中,供后續調度做判斷。
(3)記錄 CPU 綁定信息
根據 cpu_manager_state文件,以及從 annotations 中獲取的 Pod 的拓撲調度結果,生成自己的 cassini_cpu_manager_state 文件,該文件記錄了節點上所有 Pod 的核心綁定結果。
(4)執行拓撲分配
根據 cassini_cpu_manager_state 文件,調用容器運行時接口,完成最終的容器核心綁定工作。
6.優化結果
根據上述精細化調度方案,我們對一些線上的任務進行了測試。此前,用戶反饋任務調度到一些節點后計算性能較差,且由于 steal_time升高被頻繁驅逐。在替換為拓撲感知調度的解決方案后,由于拓撲感知調度可以細粒度地感知到每個 NUMA 節點的離線實際算力(offline_capacity),任務會被調度到合適的 NUMA 節點上,測試任務的訓練速度可提升至原來的 3 倍,與業務在高優 CVM 的耗時相當,且訓練速度較為穩定,資源得到更合理地利用。
同時,在使用原生調度器的情況下,調度器無法感知離線虛擬機的實際算力。當任務調度到某個節點上后,該節點
steal_time會因此升高,任務無法忍受這樣的繁忙節點就會由驅逐器發起 Pod 的驅逐。在此情況下,如果采用原生調度器,將會引起反復驅逐然后反復調度的情況,導致 SLA 收到較大影響。經過本文所述的解決方案后,可將 CPU 搶占的驅逐率大大下降至物理機水平。
7.總結與展望
本文從實際業務痛點出發,首先簡單介紹了騰訊星辰算力的業務場景與精細化調度相關的各類背景知識,然后充分調研國內外研究現狀,發現目前已有的各種解決方案都存在局限性。最后通過痛點問題分析后給出了相應的解決方案。經過優化后,資源得到更合理地利用,原有測試任務的訓練速度能提升至原來的 3 倍,CPU 搶占的驅逐率大大降低至物理機水平。
未來,精細化調度將會覆蓋更多的場景,包括在 GPU 虛擬化技術下對 GPU 整卡、碎卡的調度,支持高性能網絡架構的調度,電力資源的調度,支持超售場景,配合內核調度共同完成等等。































