容器運行時概覽

要把進程運行在容器中,還需要有便捷的 SDK 或命令來調用 Linux 的系統功能,從而創建出容器。容器的運行時(runtime)就是運行和管理容器進程、鏡像的工具。

作者:趙慧慧, 中國移動雲能力中心軟件開發工程師,專注於雲原生、Istio、微服務等領域。

01

容器運行時分類

Docker 屬於容器技術早期的發展項目,也是目前最廣泛的容器引擎技術。當然,隨着容器生態圈的日益繁榮,業界慢慢也出現了其他各種運行時工具,如 containerd、rkt、Kata Container、CRI-O 等。這些工具提供的功能不盡相同,有些只有容器運行的功能,有些除運行容器外還提供了容器鏡像的管理功能。根據容器運行時提供功能,可以講容器運行時分爲低層運行時高層運行時

低層運行時主要負責與宿主機操作系統打交道,根據指定的容器鏡像在宿主機上運行容器的進程,並對容器的整個生命週期進行管理。而這個低層運行時,正是負責執行我們前面講解過的設置容器 Namespace、Cgroups 等基礎操作的組件。常見的低層運行時種類有:

Ø runc:傳統的運行時,基於 Linux Namespace 和 Cgroups 技術實現,代表實現 Docker

Ø runv:基於虛擬機管理程序的運行時,通過虛擬化 guest kernel,將容器和主機隔離開來,使得其邊界更加清晰,代表實現是 Kata Container 和 Firecracker

Ø runsc:runc + safety ,通過攔截應用程序的所有系統調用,提供安全隔離的輕量級容器運行時沙箱,代表實現是谷歌的 gVisor

高層運行時主要負責鏡像的管理、轉化等工作,爲容器的運行做前提準備。主流的高層運行時主要 containerd 和 CRI-O。

高層運行時與低層運行時各司其職,容器運行時一般先由高層運行時將容器鏡像下載下來,並解壓轉換爲容器運行需要的操作系統文件,再由低層運行時啓動和管理容器。

兩者之間的關係如下:

02

Kubernetes 容器運行時

前面的兩部分,我們介紹了容器運行的原理及常見的容器運行時工具,Kubernetes 作爲容器編排工具會對容器進行調度和管理。那 Kubernetes 如何啓動容器的?

Kubernetes 早期是利用 Docker 作爲容器運行時管理工具的,在 1.6 版本之前 Kubernetes 將 Docker 默認爲自己的運行時工具,通過直接調用 Docker 的 API 來創建和管理容器。在 Docker 項目盛行不久,CoreOS 推出了 rkt 運行時工具,Kubernetes 又添加了對 rkt 的支持。但隨着容器技術的蓬勃發展,越來越多的運行時工具出現,提供對所有運行時工具的支持,顯然是一項龐大的工程;而且直接將運行時的集成內置於 Kubernetes,兩者緊密結合,對 Kubernetes 代碼本身也是一種負擔,每更新一次重要的功能,Kubernetes 都需要考慮對所有容器運行時的兼容適配。

爲了打破這種尷尬的局面,Kubernetes 將對容器的操作抽象爲一個接口,將接口作爲 kubelet 與運行時工具之間的橋樑,kubelet 通過發送接口請求對容器進行啓動和管理,各個容器工具通過實現這個接口即可接入 Kubernetes。這個統一的容器操作接口,就是容器運行時接口 (Container Runtime Interface, CRI)。

我們來具體看下 CRI 的工作流程,上圖可以看到,CRI 主要有 gRPC client、gRPC Server 和具體的容器運行時工具三個組件。其中 kubelet 作爲 gRPC 的客戶端來調用 CRI 接口;CRI shim 作爲 gRPC 服務端來響應 CRI 請求,負責將 CRI 請求的內容轉換爲具體的容器運行時 API,在 kubelet 和運行時之間充當翻譯的角色。具體的容器創建邏輯是,Kubernetes 在通過調度指定一個具體的節點運行 pod,該節點的 Kubelet 在接到 pod 創建請求後,調用一個叫作 GenericRuntime 的通用組件來發起創建 Pod 的 CRI 請求給 CRI shim;CRI shim 監聽一個端口來響應 kubelet, 在收到 CRI 請求後,將其轉化爲具體的容器運行時指令,並調用相應的容器運行時來創建 pod。

因此,任何容器運行時如果想接入 Kubernetes,都需要實現一個自己的 CRI shim,來實現 CRI 接口規範。那麼 CRI 有哪些接口需要實現呢?查看 Kubernetes 代碼可以發現,它定義了下圖所示兩類接口:RuntimeService 和 ImageService。RuntimeService 定義了跟容器相關的操作,如創建、啓動、刪除容器等。ImageService 主要定義了容器鏡像相關的操作,如拉取鏡像、刪除鏡像等。

ImageService 的操作比較簡單,就是拉取、刪除、查看鏡像狀態及獲取鏡像列表這幾個操作。下面着重介紹下 RuntimeService。

從上圖可以看到,RuntimeService 除了有 container 的管理接口外,還包含 PodSandbox 相關的管理接口和 exec、attach 等與容器交互的接口。

顧名思義,PodSandbox 這個概念對應的是 Kubernetes 裏的 Pod,它描述了 Kubernetes 裏的 Pod 與容器運行相關的屬性或者信息,如 HostName、CgroupParent 等。設計這個的初衷是因爲 Pod 裏所有容器的資源和環境信息是共享的,但是不同的容器運行時實現共享的機制不同,如 Docker 中 Pod 會是一個 Linux 命名空間,各容器網絡信息的共享通過創建 pause 容器的方法來實現,而 Kata Containers 則直接將 pod 具化爲一個輕量級的虛擬機。將這個邏輯抽象爲 PodSandbox 接口,可以讓不同的容器運行時在 pod 實現上自由發揮,自己解釋和實現 pod 的的邏輯。

Exec、Attach 和 PortForward 是三個和容器進行數據交互的接口,由於交互數據需要長鏈接來傳輸,稱這些接口爲 Streaming API。CRI shim 依賴一套獨立的 Streaming Server 機制來實現客戶端與容器的交互需求。長連接比較消耗網絡資源,爲了避免因長連接給 kubelet 節點帶來網絡流量瓶頸,CRI 要求容器運行時啓動一個對應請求的單獨的流服務器,讓客戶端直接與流服務器進行連同交互。

上圖所示,kubectl exec 命令實現過程如下:

1. 客戶端發送 kubectl exec 命令給 apiserver;

2. apiserver 調用 kubelet 的 Exec API;

3. kubelet 調用 CRI 的 Exec 接口(具體的執行者爲實現該接口的 CRI Shim );

4. CRI Shim 向 kubelet 返回 Streaming Server 的地址和端口;

5. kubelet 以 redirect 的方式返回給 apiserver

6. apiserver 通過重定向來向 Streaming Server 發起真正的 /exec 請求,與它建立長連接,完成 Exec 的請求和響應。

以上是 CRI 的設計及工作原理。

概括來講,kubelet 在引入 CRI 之後,主要的架構如下圖所示。其中 Generic Runtime Manager 負責發送容器創建、刪除等 CRI 請求,Container Runtime Interface(CRI) 負責定義 CRI 接口規範,具體的 CRI 實現可分爲兩種:kubelet 內置的 dockershim 和遠端的 CRI shim。其中 dockershim 是 Kubernetes 自己實現的適配 Docker 接口的 CRI 接口實現,主要用來將 CRI 請求裏的內容組裝成 Docker API 請求發給 Docker Daemon;遠端的 CRI shim 主要是用來匹配其他的容器運行時工具到 kubelet。CRI shim 主要負責響應 kubelect 發送的 CRI 請求,並將請求轉化爲具體的運行時命令發送給具體的運行時(如 runc、kata 等);Stream Server 用來響應客戶端與容器的交互,除此之外,CRI 還提供接入 CNI 的能力以實現 pod 網絡的共享。常用的遠端 CRI 的實現有 CRI-Containerd、CRI-O 等。

從上圖可以看出,Kubernetes 把 docker shim 內置在了官方的代碼庫中,將 Docker 設計爲 Kubernetes 默認的容器運行時工具。但是官方在 Kubernetes 1.20 版本的更新日誌中聲明已經廢用對 Docker 的支持,並將在未來的版本中將其刪除。在 Kubernetes 1.24 版本中,dockershim 代碼也如期被刪除,替換爲 containerd 作爲其默認運行時。

那 Kubernetes 爲何要拋棄 Docker 轉而使用 containerd,其中的緣由是什麼?

這話要從頭說起,Docker 最初是一個單體引擎,主要負責容器鏡像的製作、上傳、拉取及容器的運行及管理。隨着容器技術的繁榮發展,爲了促進容器技術相關的規範生成和 Docker 自身項目的發展,Docker 將單體引擎拆分爲三部分,分別爲 runC、containerd 和 dockerd,其中 runC 主要負責容器的運行和生命週期的管理(即前述的低層運行時)、containerd 主要負責容器鏡像的下載和解壓等鏡像管理功能(即前述的高層運行時)、dockerd 主要負責提供鏡像製作、上傳等功能同時提供容器存儲和網絡的映射功能,同時也是 Docker 服務器端的守護進程,用來響應 Docker 客戶端(命令行 CLI 工具)發來的各種容器、鏡像管理的任務。Docker 公司將 runC 捐獻給了 OCI,將 containerd 捐獻給了 CNCF,剩下的 dockerd 作爲 Docker 運行時由 Docker 公司自己維護。

如前所述,Kubernetes 在引入 CRI 之後,kubelet 需要通過 CRI shim 去調用具體的容器運行時工具,由於早期 Kubernetes 對 Docker 的支持是內置的,因此官方自己實現了 dockershim,通過 dockershim 去訪問 dockerd。

由於 dockershim 的維護出現了問題,官方廢棄了對 Docker 的支持,使用 containerd 爲默認運行時。那我們知道,kubelet 需要一個 CRI shim 作爲中間件去調用運行時,那 kubelet 在拋棄了 dockershim 之後又是怎麼訪問 containerd 的呢?答案是 containerd 自己集成了 CRI shim,提供了一個 CRI 插件來實現 shim 的功能,這樣 kubelet 就可以直接訪問 containerd。

由此可以看到,廢棄 dockershim 之前 kubelet 其實也是使用 containerd 作爲高層運行時,只是中間通過了 dockershim 和 dockerd 兩步轉發;在將 dockershim 移除之後,kubelet 越****過 docker 門戶直接訪問了 containerd,這明顯的輕量化了調用過程,大大加快了 kubelet 調用運行時的速度。

03

安全容器運行時

Kubernetes 目前作爲企業級容器平臺,企業生產最重要的是安全。前面我們講過,Docker 容器通過 Linux Namespace 和 Cgroups 實現容器之間的資源限制和隔離,在實際運行中各容器資源(網絡、存儲、計算)仍由宿主機直接提供,這就可能出現某個容器進程奪取整個宿主機控制權的問題,在安全問題上存在一定的隱患。於是,Kata Container 和 gVisor 等安全容器運行時應用而生。

Kata Container 來源於 Intel Clear Containers 和 Hyper runV 項目的合併,它使用傳統的虛擬化技術,通過虛擬硬件模擬出了一臺 “小虛擬機”,然後再這臺小虛擬機中安裝了一個裁剪後的 Linux 內核來實現容器建的隔離。gVisor 由谷歌公司發佈,它通過爲容器進程配置一個用 Go 語言實現的、在用戶態實現的、極小的 “獨立內核”,通過這個內核控制容器進程向宿主機發起有限可控的系統調用。

那 Kubernetes 如何集成這些安全運行時呢?下面以 Kata Container 爲例,介紹安全容器運行時如何集成到 Kubernetes 中對各種資源進行管控。

Kata Container 支持 OCI 運行時規範,可以以插件形式嵌入到 Docker 中作爲低層的容器運行時;也支持 Kubernetes 的 CRI 接口,可以與 CRI-O 和 containerd 集成。由於目前 Kubernetes 默認的運行時是 containerd,下面主要講解 Kata Container 如何與 containerd 集成以及集成後 Kubernetes 如何使用 Kata Container 創建負載。

由於 Kata Container 使用虛擬化技術實現,首先需要集成的 Kubernetes 環境支持 Intel VT-x technology、ARM Hyp mode、IBM Power Systems 或 IBM Z manframes 四種中的任意一種 CPU 虛擬化技術

集成及使用過程如下:

1、安裝 Kata Container(以 centos 爲例)

source /etc/os-release
sudo yum -y install yum-utils
export BRANCH='stable-1.10'
ARCH=$(arch)
BRANCH="${BRANCH:-master}"
sudo -E yum-config-manager --add-repo "http://download.opensuse.org/repositories/home:/katacontainers:/releases:/${ARCH}:/${BRANCH}/CentOS_${VERSION_ID}/home:katacontainers:releases:${ARCH}:${BRANCH}.repo"
sudo -E yum -y install kata-runtime kata-proxy kata-shim

安裝完成之後,執行命令 kata-runtime kata-check 檢查系統是否支持運行 kata runtime,下面的輸出表示運行環境支持 Kata Containers 。

1. [root@node1 ~]# kata-runtime kata-check
2.System is capable of running Kata Containers
3.System can currently create Kata Containers

2、修改 Kubernetes 啓動參數

1)修改 kubelet 啓動參數

1. mkdir -p  /etc/systemd/system/kubelet.service.d/
2.cat << EOF | sudo tee  /etc/systemd/system/kubelet.service.d/0-containerd.conf
3.[Service]                                                 
4.Environment="KUBELET_EXTRA_ARGS=--container-runtime=remote --runtime-request-timeout=15m --container-runtime-endpoint=unix:///run/containerd/containerd.sock"
5.EOF

2)重啓 containerd 和 kubelet

1. systemctl daemon-reload
2.systemctl start containerd
3.systemctl restart kubelet

3、Kubernetes 配置使用 Kata Container

配置了 Kata Container 之後,我們就可以在 Kubernetes 集羣中使用 Kata Container 進行容器管理了。如上所述,目前 containerd 中存在兩個低層運行時,分別是默認的 runC 和新接入的 Kata Container。那我們該如何告訴 Kubernetes 哪些負載需要使用 Kata Container 呢?根據不同的版本,Kata 提供了不同的方式:

Ø 使用 Kubernetes 的 RuntimeClass(推薦)

Ø 使用 Kubernetes 的 untrusted_workload_runtime

1)使用 RuntimClass

這種方式對相關組件版本有要求:

Kata Containers v1.5.0 or above (including 1.5.0-rc)
Containerd v1.2.0 or above
Kubernetes v1.12.0 or above

a)、修改 containerd 的配置文件

在 /etc/containerd/config.toml 配置文件中 [plugins."io.containerd.grpc.v1.cri".containerd.runtimes] 下增加 kata 運行時配置:

 [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.kata] #這裏最後的這個kata 將被作爲 RuntimeClass handler 關鍵字
  runtime_type = "io.containerd.kata.v2"
  [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.katacli]
    runtime_type = "io.containerd.runc.v1"
    [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.katacli.options]
      NoPivotRoot = false
      NoNewKeyring = false
      ShimCgroup = ""
      IoUid = 0
      IoGid = 0
      BinaryName = "/usr/bin/kata-runtime"
      Root = ""
      CriuPath = ""
      SystemdCgroup = false

需要注意的是,[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.kata] 中的 kata 將被作爲 RuntimeClass handler 關鍵字。

b)、重啓 containerd

systemctl daemon-reload
systemctl restart containerd

c)、使用 Kata Container

ü 準備 RuntimeClass,並用該 RuntimeClass 創建 Pod 聲明文件

runtime.yaml
apiVersion: node.k8s.io/v1beta1  # RuntimeClass is defined in the node.k8s.io API group
kind: RuntimeClass
metadata:
  name: kata  
handler: kata  # 這裏與containerd配置文件中的 [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.{handler}] 匹配
pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: kata-nginx
spec:
  runtimeClassName: kata
  containers:
    - name: nginx
      image: nginx
      ports:
      - containerPort: 80

ü 執行 yaml,創建 RuntimeClass 和 Pod

[root@node1 zhh]# kubectl apply -f runtime.yaml
runtimeclass.node.k8s.io/kata created
[root@node1 zhh]# kubectl get runtimeclass
NAME   HANDLER   AGE
kata   kata      7s
[root@node1 zhh]# kubectl apply -f pod.yaml
7pod/kata-nginx created

ü 驗證是否正確創建:通過 kata-runtime list 可以查看創建出來的 container

[root@node1 zhh]# kata-runtime list
ID                                                                 PID         STATUS      BUNDLE                                                                                                                  CREATED                          OWNER
a5abf7227bf3e1868ff590e207d4f755ff6c879d821274a6ae25ae33839f5933   -1          running     /run/containerd/io.containerd.runtime.v2.task/k8s.io/a5abf7227bf3e1868ff590e207d4f755ff6c879d821274a6ae25ae33839f5933   2022-10-12T11:30:44.156957259Z   #0
[root@node1 zhh]# kubectl get pod
NAME                                READY   STATUS              RESTARTS   AGE
kata-nginx                          0/1     ContainerCreating   0          33s
nginx-deployment-746fbb99df-lmjcv   1/1     Running             1          18h

2) 使用 untrusted_workload_runtime

對於不符合上述版本要求的環境,可以使用 untrusted_workload_runtime 的方式,該方式對版本無要求。

a)、修改 containerd 的配置文件

在 /etc/containerd/config.toml 配置文件中新增 untrusted_workload_runtime 運行時配置:

 [plugins."io.containerd.grpc.v1.cri".containerd.untrusted_workload_runtime]
 runtime_type = "io.containerd.kata.v2"

對於不支持 Runtime V2 (Shim API) 的早期版本的 Kata Containers 和 containd,可以使用以下替代配置:

[plugins."io.containerd.grpc.v1.cri".containerd.untrusted_workload_runtime]
  # runtime_type is the runtime type to use in containerd e.g. io.containerd.runtime.v1.linux
  runtime_type = "io.containerd.runtime.v1.linux"
  # runtime_engine is the name of the runtime engine used by containerd.
  runtime_engine = "/usr/bin/kata-runtime"

b)、重啓 containerd

systemctl daemon-reload & systemctl restart containerd

c)、使用 Kata Container

untrusted_workload_runtime 使用 annotations 告訴 Kubernetes 集羣哪些負載需要使用 kata-runtime。因此需要使用 kata-runtime 的資源,只需要在 annotations 中聲明即可。如:

apiVersion: v1
kind: Pod
metadata:
  name: nginx-untrusted
  annotations:
    io.kubernetes.cri.untrusted-workload: "true"
spec:
  nodeName: k8s3
  containers:
  - name: nginx
    image: nginx

驗證方式 Pod 是否被正確創建方法同 RuntimClass。

當然,如果想直接把 Kata Container 作爲 Kubernetes 默認的運行時也是可以的。我們知道 Kubernetes 默認的低層運行時是 runC,如果想把 Kata Container 設置爲默認的低層運行時,在 containerd 的配置文件中設置 default_runtime,重啓 containerd(systemctl daemon-reload & systemctl restart containerd)後,創建的負載就自動使用 Kata Container 來進行管理了。

  [plugins."io.containerd.grpc.v1.cri".containerd]
    [plugins."io.containerd.grpc.v1.cri".containerd.default_runtime]
      runtime_type = "io.containerd.kata.v2"

對於不支持 Runtime V2 (Shim API) 的早期版本的 Kata Containers 和 containerd,可以使用以下替代配置:

 [plugins."io.containerd.grpc.v1.cri".containerd]
    [plugins."io.containerd.grpc.v1.cri".containerd.default_runtime]
      runtime_type = "io.containerd.runtime.v1.linux"
      runtime_engine = "/usr/bin/kata-runtime"

參考文獻:

1. https://www.bilibili.com/video/BV15Q4y1B7yk?spm_id_from=333.337.search-card.all.click

2.https://github.com/kata-containers/documentation/blob/master/how-to/containerd-kata.md

3. https://blog.csdn.net/m2l0zgssvc7r69efdtj/article/details/110297314

4. https://zhuanlan.zhihu.com/p/102171749

5. https://blog.csdn.net/u013533380/article/details/115682900

6. https://zhuanlan.zhihu.com/p/438351320

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/QNdWjb5roamFTsdIMsuepQ