Kubernetes 資源拓撲感知排程優化

騰訊雲原生發表於2022-06-25

作者

星辰算力團隊,星辰算力平臺基於深入優化雲原生統一接入和多雲排程,加固容器執行態隔離,挖掘技術增量價值,平臺承載了騰訊內部的 CPU 和異構算力服務,是騰訊內部大規模離線作業、資源統一排程平臺。

背景

問題源起

近年來,隨著騰訊內部自研上雲專案的不斷髮展,越來越多的業務開始使用雲原生方式託管自己的工作負載,容器平臺的規模因此不斷增大。以 Kubernetes 為底座的雲原生技術極大推動了雲原生領域的發展,已然成為各大容器平臺事實上的技術標準。在雲原生場景下,為了最大化實現資源共享,單臺宿主機往往會執行多個不同使用者的計算任務。如果在宿主機內沒有進行精細化的資源隔離,在業務負載高峰時間段,多個容器往往會對資源產生激烈的競爭,可能導致程式效能的急劇下降,主要體現為:

  1. 資源排程時頻繁的上下文切換時間
  2. 頻繁的程式切換導致的 CPU 快取記憶體失效

因此,在雲原生場景下需要針對容器資源分配加以精細化的限制,確保在 CPU 利用率較高時,各容器之間不會產生激烈競爭從而引起效能下降。

排程場景

騰訊星辰算力平臺承載了全公司的 CPU 和 GPU 算力服務,擁有著海量多型別的計算資源。當前,平臺承載的多數重點服務偏離線場景,在業務日益增長的算力需求下,提供源源不斷的低成本資源,持續提升可用性、服務質量、排程能力,覆蓋更多的業務場景。然而,Kubernetes 原生的排程與資源繫結功能已經無法滿足複雜的算力場景,亟需對資源進行更加精細化的排程,主要體現為:

  1. Kubernetes 原生排程器無法感知節點資源拓撲資訊導致 Pod 生產失敗

kube-scheduler 在排程過程中並不感知節點的資源拓撲,當 kube-scheduler 將 Pod 排程到某個節點後,kubelet 如果發現節點的資源拓撲親和性要求無法滿足時,會拒絕生產該 Pod,當通過外部控制環(如 deployment)來部署 Pod 時,則會導致 Pod 被反覆建立-->排程-->生產失敗的死迴圈。

  1. 基於離線虛擬機器的混部方案導致的節點實際可用 CPU 核心數變化

面對執行線上業務的雲主機平均利用率較低的現實,為充分利用空閒資源,可將離線虛擬機器和線上虛擬機器混合部署,解決公司離線計算需求,提升自研上雲資源平均利用率。在保證離線不干擾線上業務的情況下,騰訊星辰算力基於自研核心排程器 VMF 的支援,可以將一臺機器上的閒時資源充分利用起來,生產低優先順序的離線虛擬機器。由於 VMF 的不公平排程策略,離線虛擬機器的實際可用核心數受到線上虛擬機器的影響,隨著線上業務的繁忙程度不斷變化。因此,kubelet 通過 cadvisor 在離線宿主機內部採集到的 CPU 核心數並不準確,導致了排程資訊出現偏差。

  1. 資源的高效利用需要更加精細化的排程粒度

kube-scheduler 的職責是為Pod選擇一個合適的 Node 完成一次排程。然而,想對資源進行更高效的利用,原生排程器的功能還遠遠不夠。在排程時,我們希望排程器能夠進行更細粒度的排程,比如能夠感知到 CPU 核心、GPU 拓撲、網路拓撲等等,使得資源利用方式更加合理。

預備知識

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
    // ......
}

NUMA 架構

非統一記憶體訪問架構(英語:Non-uniform memory access,簡稱 NUMA)是一種為多處理器的電腦設計的記憶體架構,記憶體訪問時間取決於記憶體相對於處理器的位置。在 NUMA 下,處理器訪問它自己的本地記憶體的速度比非本地記憶體(記憶體位於另一個處理器,或者是處理器之間共享的記憶體)快一些。現代多核伺服器大多采用NUMA架構來提高硬體的可伸縮性。

從圖中可以看出,每個 NUMA Node 有獨立的 CPU 核心、L3 cache 和記憶體,NUMA Node 之間相互連線。相同 NUMA Node 上的 CPU 可以共享 L3 cache,同時訪問本 NUMA Node 上的記憶體速度更快,跨 NUMA Node 訪問記憶體會更慢。因此,我們應當為 CPU 密集型應用分配同一個 NUMA Node 的 CPU 核心,確保程式的區域性效能得到充分滿足。

Kubernetes 排程框架

Kubernetes 自 v1.19 開始正式穩定支援排程框架,排程框架是面向 Kubernetes 排程器的一種外掛架構,它為現有的排程器新增了一組新的“外掛”API,外掛會被編譯到排程器之中。這為我們自定義排程器帶來了福音。我們可以無需修改 kube-scheduler 的原始碼,通過實現不同的排程外掛,將外掛程式碼與 kube-scheduler 編譯為同一個可執行檔案中,從而開發出自定義的擴充套件排程器。這樣的靈活性擴充套件方便我們開發與配置各類排程器外掛,同時無需修改 kube-scheduler 的原始碼的方式使得擴充套件排程器可以快速更改依賴,更新到最新的社群版本。

排程器的主要擴充套件點如上圖所示。我們擴充套件的排程器主要關心以下幾個步驟:

  1. PreFilterFilter

這兩個外掛用於過濾出不能執行該Pod的節點,如果任何Filter外掛將節點標記為不可行,該節點都不會進入候選集合,繼續後面的排程流程。

  1. PreScoreScoreNormalizeScore

這三個外掛用於對通過過濾階段的節點進行排序,排程器將為每個節點呼叫每個評分外掛,最終評分最高的節點將會作為最終排程結果被選中。

  1. ReserveUnreserve

這個外掛用於在Pod真正被繫結到節點之前,對資源做一些預留工作,保證排程的一致性。如果繫結失敗則通過 Unreserve 來釋放預留的資源。

  1. Bind

這個外掛用於將 Pod 繫結到節點上。預設的繫結外掛只是為節點指定 spec.nodeName來完成排程,如果我們需要擴充套件排程器,加上其他的排程結果資訊,就需要禁用預設 Bind 外掛,替換為自定義的 Bind 外掛。

國內外技術研究現狀

目前 Kubernetes 社群、Volcano 開源社群均有關於拓撲感知相關的解決方案,各方案有部分相同之處,但各自都有侷限性,無法滿足星辰算力的複雜場景。

Kubernetes 社群

Kubernetes 社群 scheduling 興趣小組針對拓撲感知排程也有過一套解決方案,這個方案主要是由 RedHat 來主導,通過scheduler-pluginsnode-feature-discovery配合實現了考慮節點拓撲的排程方法。社群的方法僅僅考慮節點是否能夠在滿足 kubelet 配置要求的情況下,完成排程節點篩選和打分,並不會執行綁核,綁核操作仍然交給 kubelet 來完成,相關提案在這裡。具體實現方案如下:

  1. 節點上的 nfd-topology-updater 通過 gRPC 上報節點拓撲到 nfd-master 中(週期60s)。
  2. nfd-master 更新節點拓撲與分配情況到 CR 中(NodeResourceTopology)。
  3. 擴充套件 kube-scheduler,進行排程時考慮 NodeTopology。
  4. 節點 kubelet 完成綁核工作。

該方案存在較多的問題,不能解決生產實踐中的需求:

  1. 具體核心分配依賴 kubelet 完成,因此排程器只會考慮資源拓撲資訊,並不會選擇拓撲,排程器沒有資源預留。這導致了節點排程與拓撲排程不在同一個環節,會引起資料不一致問題。
  2. 由於具體核心分配依賴 kubelet 完成,所以已排程 Pod 的拓撲資訊需要依靠 nfd-worker 每隔 60s 彙報一次,導致拓撲發現過慢因此使得資料不一致問題更加嚴重,參見這裡
  3. 沒有區分需要拓撲親和的 pod 和普通的 pod,容易造成開啟拓撲功能的節點高優資源浪費。

Volcano 社群

Volcano 是在 Kubernetes 上執行高效能工作負載的容器批量計算引擎,隸屬於 CNCF 孵化專案。在v1.4.0-Beta 版本中進行了增強,釋出了有關 NUMA 感知的特性。與 Kubernetes 社群 scheduling 興趣小組的實現方式類似,真正的綁核並未單獨實現,直接採用的是 kubelet 自帶的功能。具體實現方案如下:

  1. resource-exporter 是部署在每個節點上的 DaemonSet,負責節點的拓撲資訊採集,並將節點資訊寫入 CR 中(Numatopology)。
  2. Volcano 根據節點的 Numatopology,在排程 Pod 時進行 NUMA 排程感知。
  3. 節點 kubelet 完成綁核工作。

該方案存在的問題基本與 Kubernetes 社群 scheduling 興趣小組的實現方式類似,具體核心分配依賴 kubelet 完成。雖然排程器盡力保持與 kubelet 一致,但因為無法做資源預留,仍然會出現不一致的問題,在高併發場景下尤其明顯。

小結

基於國內外研究現狀的結果進行分析,開源社群在節點資源繫結方面還是希望交給 kubelet,排程器儘量保證與 kubelet 的一致,可以理解這比較符合社群的方向。因此,目前各個方案的典型實現都不完美,無法滿足騰訊星辰算力的要求,在複雜的生產環境中我們需要一套更加穩健、擴充套件性更好的方案。因此,我們決定從各個方案的架構優點出發,探索出一套更加強大的、貼合騰訊星辰算力實際場景的資源精細化排程增強方案。

問題分析

離線虛擬機器節點實際可用 CPU 核心數變化

從1.2節中我們可以知道,騰訊星辰算力使用了基於離線虛擬機器的混部方案,節點實際的 CPU 可用核心數會收到線上業務的峰值影響從而變化。因此,kubelet 通過 cadvisor 在離線宿主機內部採集到的 CPU 核心數並不準確,這個數值是一個固定值。因此,針對離線資源我們需要排程器通過其他的方式來獲取節點的實際算力。

目前排程和綁核都不能到離線虛擬機器的實際算力,導致任務繫結到線上干擾比較嚴重的 NUMA node,資源競爭非常嚴重使得任務的效能下降。

幸運的是,我們在物理機上可以採集到離線虛擬機器每個 NUMA node 上實際可用的 CPU 資源比例,通過折損公式計算出離線虛擬機器的實際算力。接下來就只需要讓排程器在排程時能夠感知到 CPU 拓撲以及實際算力,從而進行分配。

精細化排程需要更強的靈活性

通過 kubelet 自帶的 cpumanager進行綁核總是會對該節點上的所有 Pod 均生效。只要 Pod 滿足 Guaranteed 的 QoS 條件,且 CPU 請求值為整數,都會進行綁核。然而,有些 Pod 並不是高負載型別卻獨佔 CPU,這種方式的方式很容易造成開啟拓撲功能的節點高優資源浪費。

同時,對於不同資源型別的節點,其拓撲感知的要求也是不一樣的。例如,星辰算力的資源池中也含有較多碎片虛擬機器,這部分節點不是混部方式生產出來的,相比而言資源穩定,但是規格很小(如8核 CVM,每個 NUMA Node 有4核)。由於大多數任務規格都會超過4核,這類資源就在使用過程中就可以跨 NUMA Node 進行分配,否則很難匹配。

因此,拓撲感知排程需要更強的靈活性,適應各種核心分配與拓撲感知場景。

排程方案需要更強的擴充套件性

排程器在抽象拓撲資源時,需要考慮擴充套件性。對於今後可能會需要排程的擴充套件資源,如各類異構資源的排程,也能夠在這套方案中輕鬆使用,而不僅僅是 cgroups 子系統中含有的資源。

避免超執行緒帶來的 CPU 競爭問題

在 CPU 核心競爭較為激烈時,超執行緒可能會帶來更差的效能。更加理想的分配方式是將一個邏輯核分配給高負載應用,另一個邏輯核分配給不繁忙的應用,或者是將兩個峰谷時刻相反的應用分配到同一個物理核心上。同時,我們避免將同一個應用分配到同一個物理核心的兩個邏輯核心上,這樣很可能造成 CPU 競爭問題。

解決方案

為了充分解決上述問題,並考慮到未來的擴充套件性,我們設計了一套精細化排程的方案,命名為 cassini。整套解決方案包含三個元件和一個 CRD,共同配合完成資源精細化排程的工作。

注:cassini這個名稱來源於知名的土星探測器卡西尼-惠更斯號,對土星進行了精準無誤的探測,藉此名來象徵更加精準的拓撲發現與排程。

總體架構

各模組職責如下:

  • cassini-worker:負責收集節點資源拓撲、執行資源繫結工作,作為 DaemonSet在每個節點上執行。
  • 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又承擔了執行者的職責,負責落實排程器的資源拓撲排程結果。

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 實際可用算力。

排程器設計

擴充套件排程器在原生排程器基礎上擴充套件了新的外掛,大體如下所示:

  1. Filter:讀取 NRT 資源,根據每個拓撲內的實際可用算力以及 Pod 拓撲感知要求來篩選節點並選擇拓撲。
  2. Score:根據 Zone 個數打分來打分,Zone 越多分越低(跨 Zone 會帶來效能損失)。
  3. Reserve:在真正繫結前做資源預留,避免資料不一致,kube-scheduler 的 cache 中也有類似的 assume 功能。
  4. Bind:禁用預設的 Bind 外掛,在 Bind 時加入 Zone 的選擇結果,附加在 annotations 中。

通過 TopologyMatch外掛使得排程器在排程時能夠考慮節點拓撲資訊並進行拓撲分配,並通過 Bind 外掛將結果附加在 annotations 中。

值得一提的是,這裡還額外實現了關於節點電力排程等更多維度排程的排程器外掛。

master設計

cassini-master是中控元件,從外部來採集一些節點上無法採集的資源資訊。我們從物理上採集到離線虛擬機器的實際可用算力,由 cassini-master負責將這類結果附加到對應節點的 NRT 資源中。該元件將統一資源收集的功能進行了剝離,方便更新與擴充套件。

worker設計

cassini-worker是一個較為複雜的元件,作為 DaemonSet 在每個節點上執行。它的職責分兩部分:

  1. 採集節點上的拓撲資源。
  2. 執行排程器的拓撲排程結果。

資源採集

資源拓撲採集主要是通過從 /sys/devices下采集系統相關的硬體資訊,並建立或更新到 NRT 資源中。該元件會 watch 節點 kubelet 的配置資訊並上報,讓排程器感知到節點的 kubelet 的綁核策略、預留資源等資訊。

由於硬體資訊幾乎不變化,預設會較長時間採集一次並更新。但 watch 配置的事件是實時的,一旦 kubelet 配置後會立刻感知到,方便排程器根據節點的配置進行不同的決策。

拓撲排程結果執行

拓撲排程結果執行是通過週期性地 reconcile 來完成制定容器的拓撲分配。

  1. 獲取 Pod 資訊

為了防止每個節點的 cassini-worker都 watch kube-apiserver 造成 kube-apiserver 的壓力,cassini-worker改用週期性訪問 kubelet 的10250埠的方式,來 List 節點上的Pod 並從 Pod annotations 中獲取排程器的拓撲排程結果。同時,從 status 中還可以獲取到每個容器的 ID 與狀態,為拓撲資源的分配建立了條件。

  1. 記錄 kubelet 的 CPU 繫結資訊

在 kubelet 開啟 CPU 核心繫結時,擴充套件排程器將會跳過所有的 TopologyMatch外掛。此時 Pod annotations 中不會包含拓撲排程結果。在 kubelet 為 Pod 完成CPU核心繫結後,會將結果記錄在 cpu_manager_state檔案中,cassini-worker讀取該檔案,並將 kubelet 的繫結結果 patch 到 Pod annotations 中,供後續排程做判斷。

  1. 記錄CPU繫結資訊

根據 cpu_manager_state檔案,以及從 annotations 中獲取的 Pod 的拓撲排程結果,生成自己的 cassini_cpu_manager_state檔案,該檔案記錄了節點上所有 Pod 的核心繫結結果。

  1. 執行拓撲分配

根據 cassini_cpu_manager_state檔案,呼叫容器執行時介面,完成最終的容器核心繫結工作。

優化結果

根據上述精細化排程方案,我們對一些線上的任務進行了測試。此前,使用者反饋任務排程到一些節點後計算效能較差,且由於 steal_time升高被頻繁驅逐。在替換為拓撲感知排程的解決方案後,由於拓撲感知排程可以細粒度地感知到每個NUMA節點的離線實際算力(offline_capacity),任務會被排程到合適的 NUMA 節點上,測試任務的訓練速度可提升至原來的3倍,與業務在高優 CVM 的耗時相當,且訓練速度較為穩定,資源得到更合理地利用。

同時,在使用原生排程器的情況下,排程器無法感知離線虛擬機器的實際算力。當任務排程到某個節點上後,該節點 steal_time會因此升高,任務無法忍受這樣的繁忙節點就會由驅逐器發起 Pod 的驅逐。在此情況下,如果採用原生排程器,將會引起反覆驅逐然後反覆排程的情況,導致 SLA收到較大影響。經過本文所述的解決方案後,可將 CPU 搶佔的驅逐率大大下降至物理機水平。

總結與展望

本文從實際業務痛點出發,首先簡單介紹了騰訊星辰算力的業務場景與精細化排程相關的各類背景知識,然後充分調研國內外研究現狀,發現目前已有的各種解決方案都存在侷限性。最後通過痛點問題分析後給出了相應的解決方案。經過優化後,資源得到更合理地利用,原有測試任務的訓練速度能提升至原來的3倍,CPU 搶佔的驅逐率大大降低至物理機水平。

未來,精細化排程將會覆蓋更多的場景,包括在 GPU 虛擬化技術下對 GPU 整卡、碎卡的排程,支援高效能網路架構的排程,電力資源的排程,支援超售場景,配合核心排程共同完成等等。

關於我們

更多關於雲原生的案例和知識,可關注同名【騰訊雲原生】公眾號~

福利:

①公眾號後臺回覆【手冊】,可獲得《騰訊雲原生路線圖手冊》&《騰訊雲原生最佳實踐》~

②公眾號後臺回覆【系列】,可獲得《15個系列100+篇超實用雲原生原創乾貨合集》,包含Kubernetes 降本增效、K8s 效能優化實踐、最佳實踐等系列。

③公眾號後臺回覆【白皮書】,可獲得《騰訊雲容器安全白皮書》&《降本之源-雲原生成本管理白皮書v1.0》

④公眾號後臺回覆【光速入門】,可獲得騰訊雲專家5萬字精華教程,光速入門Prometheus和Grafana。

【騰訊雲原生】雲說新品、雲研新術、雲遊新活、雲賞資訊,掃碼關注同名公眾號,及時獲取更多幹貨!!

相關文章