從零開始入門 K8s | 理解 RuntimeClass 與使用多容器

longmanma發表於2021-09-09

圖片描述

作者 | 賈之光 阿里巴巴高階開發工程師

一、RuntimeClass 需求來源

容器執行時的演進過程

我們首先了解一下容器執行時的演進過程,整個過程大致分為三個階段:

圖片描述

  • 第一個階段:2014 年 6 月

Kubernetes 正式開源,Docker 是當時唯一的、也是預設的容器執行時;

  • 第二個階段:Kubernetes v1.3

rkt 合入 Kubernetes 主幹,成為了第二個容器執行時。

  • 第三個階段:Kubernetes v.15

與此同時,越來越多的容器執行時也想接入到 Kubernetes 中。如果還是按 rkt 和 Docker 一樣內建支援的話,會給 Kubernetes 的程式碼維護和質量保障帶來嚴重挑戰。

社群也意識到了這一點,所以在 1.5 版本時推出了 CRI,它的全稱是 Container Runtime Interface。這樣做的好處是:實現了執行時和 Kubernetes 的解耦,社群不必再為各種執行時做適配工作,也不用擔心執行時和 Kubernetes 迭代週期不一致所帶來的版本維護問題。比較典型的,比如 containerd 中的 cri-plugin 就實現了 CRI、kata-containers、gVisor 這樣的容器執行時只需要對接 containerd 就可以了。

隨著越來越多的容器執行時的出現,不同的容器執行時也有不同的需求場景,於是就有了多容器執行時的需求。但是,如何來執行多容器執行時還需要解決以下幾個問題:

  • 叢集裡有哪些可用的容器執行時?
  • 如何為 Pod 選擇合適的容器執行時?
  • 如何讓 Pod 排程到裝有指定容器執行時的節點上?
  • 容器執行時在執行容器時會產生有一些業務執行以外的額外開銷,這種「額外開銷」需要怎麼統計?

RuntimeClass 的工作流程

為了解決上述提到的問題,社群推出了 RuntimeClass。它其實在 Kubernetes v1.12 中就已被引入,不過最初是以 CRD 的形式引入的。v1.14 之後,它又作為一種內建叢集資源物件 RuntimeClas 被引入進來。v1.16 又在 v1.14 的基礎上擴充了 Scheduling 和 Overhead 的能力。

圖片描述

下面以 v1.16 版本為例,講解一下 RuntimeClass 的工作流程。如上圖所示,左側是它的工作流程圖,右側是一個 YAML 檔案。

YAML 檔案包含兩個部分:上部分負責建立一個名字叫 runv 的 RuntimeClass 物件,下部分負責建立一個 Pod,該Pod 透過 spec.runtimeClassName 引用了 runv 這個 RuntimeClass。

RuntimeClass 物件中比較核心的是 handler,它表示一個接收建立容器請求的程式,同時也對應一個容器執行時。比如示例中的 Pod 最終會被 runv 容器執行時建立容器;scheduling 決定 Pod 最終會被排程到哪些節點上。

結合左圖來說明一下 RuntimeClass 的工作流程:

  1. K8s-master 接收到建立 Pod 的請求;
  2. 方格部分表示三種型別的節點。每個節點上都有 Label 標識當前節點支援的容器執行時,節點內會有一個或多個 handler,每個 handler 對應一種容器執行時。比如第二個方格表示節點內有支援 runc 和 runv 兩種容器執行時的 handler;第三個方格表示節點內有支援 runhcs 容器執行時的 handler;
  3. 根據 scheduling.nodeSelector, Pod 最終會排程到中間方格節點上,並最終由 runv handler 來建立 Pod。

二、RuntimeClass 功能介紹

RuntimeClass 的結構體定義

圖片描述

我們還是以 Kubernetes v1.16 版本中的 RuntimeClass 為例。首先介紹一下 RuntimeClass 的結構體定義。

一個 RuntimeClass 物件代表了一個容器執行時,它的結構體中主要包含 Handler、Overhead、Scheduling 三個欄位。

  • 在之前的例子中我們也提到過 Handler,它表示一個接收建立容器請求的程式,同時也對應一個容器執行時;
  • Overhead 是 v1.16 中才引入的一個新的欄位,它表示 Pod 中的業務執行所需資源以外的額外開銷;
  • 第三個欄位Scheduling 也是在 v1.16 中被引入的,該 Scheduling 配置會被自動注入到 Pod 的 nodeSelector 中。

RuntimeClass 資源定義例子

圖片描述
圖片描述

在 Pod 中引用 RuntimeClass 的用法非常簡單,只要在 runtimeClassName 欄位中配置好 RuntimeClass 的名字,就可以把這個 RuntimeClass 引入進來。

Scheduling 結構體的定義

顧名思義,Scheduling 表示排程,但這裡的排程不是說 RuntimeClass 物件本身的排程,而是會影響到引用了 RuntimeClass 的 Pod 的排程。

圖片描述

Scheduling 中包含了兩個欄位,NodeSelector 和 Tolerations。這兩個和 Pod 本身所包含的 NodeSelector 和 Tolerations 是極為相似的。

NodeSelector 代表的是支援該 RuntimeClass 的節點上應該有的 label 列表。一個 Pod 引用了該 RuntimeClass 後,RuntimeClass admission 會把該 label 列表與 Pod 中的 label 列表做一次合併。如果這兩個 label 中有衝突的,會被 admission 拒絕。這裡的衝突是指它們的 key 相同,但是 value 不相同,這種情況就會被 admission 拒絕。另外需要注意的是,RuntimeClass 並不會自動為 Node 設定 label,需要使用者在使用前提前設定好。

Tolerations 表示 RuntimeClass 的容忍列表。一個 Pod 引用該 RuntimeClass 之後,admission 也會把 toleration 列表與 Pod 中的 toleration 列表做一個合併。如果這兩處的 Toleration 有相同的容忍配置,就會將其合併成一個。

為什麼引入 Pod Overhead?

圖片描述

上圖左邊是一個 Docker Pod,右邊是一個 Kata Pod。我們知道,Docker Pod 除了傳統的 container 容器之外,還有一個 pause 容器,但我們在計算它的容器開銷的時候會忽略 pause 容器。對於 Kata Pod,除了 container 容器之外,kata-agent, pause, guest-kernel 這些開銷都是沒有被統計進來的。像這些開銷,多的時候甚至能超過 100MB,這些開銷我們是沒法忽略的。

這就是我們引入 Pod Overhead 的初衷。它的結構體定義如下:

圖片描述

它的定義非常簡單,只有一個欄位 PodFixed。它這裡面也是一個對映,它的 key 是一個 ResourceName,value 是一個 Quantity。每一個 Quantity 代表的是一個資源的使用量。因此 PodFixed 就代表了各種資源的佔用量,比如 CPU、記憶體的佔用量,都可以透過 PodFixed 進行設定。

Pod Overhead 的使用場景與限制

Pod Overhead 的使用場景主要有三處:

  • Pod 排程

在沒有引入 Overhead 之前,只要一個節點的資源可用量大於等於 Pod 的 requests 時,這個 Pod 就可以被排程到這個節點上。引入 Overhead 之後,只有節點的資源可用量大於等於 Overhead 加上 requests 的值時才能被排程上來。

  • ResourceQuota

它是一個 namespace 級別的資源配額。假設我們有這樣一個 namespace,它的記憶體使用量是 1G,我們有一個 requests 等於 500 的 Pod,那麼這個 namespace 之下,最多可以排程兩個這樣的 Pod。而如果我們為這兩個 Pod 增添了 200MB 的 Overhead 之後,這個 namespace 下就最多隻可排程一個這樣的 Pod。

  • Kubelet Pod 驅逐

引入 Overhead 之後,Overhead 就會被統計到節點的已使用資源中,從而增加已使用資源的佔比,最終會影響到 Kubelet Pod 的驅逐。

以上是 Pod Overhead 的使用場景。除此之外,Pod Overhead 還有一些使用限制和注意事項:

  • Pod Overhead 最終會永久注入到 Pod 內並且不可手動更改。即便是將 RuntimeClass 刪除或者更新,Pod Overhead 依然存在並且有效;
  • Pod Overhead 只能由 RuntimeClass admission 自動注入(至少目前是這樣的),不可手動新增或更改。如果這麼做,會被拒絕;
  • HPA 和 VPA 是基於容器級別指標資料做聚合,Pod Overhead 不會對它們造成影響。

三、多容器執行時示例

圖片描述

目前阿里雲 ACK 安全沙箱容器已經支援了多容器執行時,我們以上圖所示環境為例來說明一下多容器執行時是怎麼工作的。

如上圖所示有兩個 Pod,左側是一個 runc 的 Pod,對應的 RuntimeClass 是 runc,右側是一個 runv 的Pod,引用的 RuntimeClass 是 runv。對應的請求已用不同的顏色標識了出來,藍色的代表是 runc 的,紅色的代表是 runv 的。圖中下半部分,其中比較核心的部分是 containerd,在 containerd 中可以配置多個容器執行時,最終上面的請求也會到達這裡進行請求的轉發。

我們先來看一下 runc 的請求,它先到達 kube-apiserver,然後 kube-apiserver 請求轉發給 kubelet,最終 kubelet 將請求發至 cri-plugin(它是一個實現了 CRI 的外掛),cri-plugin 在 containerd 的配置檔案中查詢 runc 對應的 Handler,最終查到是透過 Shim API runtime v1 請求 containerd-shim,然後由它建立對應的容器。這是 runc 的流程。

runv 的流程與 runc 的流程類似。也是先將請求到達 kube-apiserver,然後再到達 kubelet,再把請求到達 cri-plugin,cri-plugin 最終還回去匹配 containerd 的配置檔案,最終會找到透過 Shim API runtime v2 去建立 containerd-shim-kata-v2,然後由它建立一個 Kata Pod。

下面我們再看一下 containerd 的具體配置。

圖片描述

containerd 預設放在 這個位置下。比較核心的配置是在 plugins.cri.containerd 目錄下。其中 runtimes 的配置都有相同的字首 plugins.cri.containerd.runtimes,後面有 runc, runv 兩種 RuntimeClass。這裡面的 runc 和 runv 和前面 RuntimeClass 物件中 Handler 的名字是相對應的。除此之外,還有一個比較特殊的配置 plugins.cri.containerd.runtimes.default_runtime,它的意思是說,如果一個 Pod 沒有指定 RuntimeClass,但是被排程到當前節點的話,那麼就預設使用 runc 容器執行時。

下面的例子是建立 runc 和 runv 這兩個 RuntimeClass 物件,我們可以透過 kubectl get runtimeclass 看到當前所有可用的容器執行時。

圖片描述

下圖從左至右分別是一個 runc 和 runv 的 Pod,比較核心的地方就是在 runtimeClassName 欄位中分別引用了 runc 和 runv 的容器執行時。

圖片描述

最終將 Pod 建立起來之後,我們可以透過 kubectl 命令來檢視各個 Pod 容器的執行狀態以及 Pod 所使用的容器執行時。我們可以看到現在叢集中有兩個 Pod:一個是 runc-pod,另一個是 runv-pod,分別引用的是 runc 和 runv 的 RuntimeClass,並且它們的狀態都是 Running。

圖片描述

四、本文總結

本文的主要內容就到此為止了,這裡為大家簡單總結一下:

  • RuntimeClass 是 Kubernetes 一種內建的叢集資源,主要用來解決多個容器執行時混用的問題;
  • RuntimeClass 中配置 Scheduling 可以讓 Pod 自動排程到執行了指定容器執行時的節點上。但前提是需要使用者提前為這些 Node 設定好 label;
  • RuntimeClass 中配置 Overhead,可以把 Pod 中業務執行所需以外的開銷統計進來,讓排程、ResourceQuota、Kubelet Pod 驅逐等行為更準確。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2730/viewspace-2825202/,如需轉載,請註明出處,否則將追究法律責任。

相關文章