Containerd 深度剖析 - runtime 篇

雖然容器領域的創業隨着 CoreOS、Docker 的賣身,而逐漸歸於平寂,但隨着 Rust 語言的興起,Firecracker、youki 項目在容器領域泛起漣漪,對於雲原生從業者來說,面試等場景中或多或少都會談論到容器一些的歷史與技術背景。

文|ianlewis

編輯|zouyee

技術深度|簡單

需求簡介

注: Container runtime 統稱爲容器運行時

Docker 時代,關於容器運行時術語的定義是非常明確的,其爲運行和管理容器的軟件。但隨着 Docker 涵蓋的內容日益增多,以及多種容器編排工具的引入,該定義變得日益模糊了。

當你運行一個 Docker 容器時,一般的步驟是:

最初的規範規定,只有運行容器的部分定義爲容器運行時,但一般用戶,將上述三個步驟都默認爲容器運行時所必須的能力,從而讓容器運行時的定義成爲一個令人困惑的話題。

當人們想到容器運行時,可能會想到一連串的相關概念;runc、runv、lxc、lmctfy、Docker(containerd)、rkt、cri-o。每一個都是基於不同的場景而實現的,均實現了不同的功能。如 containerd 和 cri-o,實際均可使用 runc 來運行容器,但其實現瞭如鏡像管理、容器 API 等功能,可以將這些看作是比 runc 具備的更高級的功能。

可以發現,容器運行時是相當複雜的。每個運行時都涵蓋了從低級到高級的不同部分,如下圖所示。

根據功能範圍劃分,將其分爲低級容器運行時 (Low level Container Runtime) 和高級容器運行時 (High level Container Runtime),其中只關注容器的本身運行通常稱爲低級容器運行時 (Low level Container Runtime)。支持更多高級功能的運行時,如鏡像管理及一些 gRPC/Web APIs,通常被稱爲 高級容器運行時 (High level Container Runtime)。需要注意的是,低級運行時和高級運行時有本質區別,各自解決的問題也不同。

低級容器運行時

低級運行時的功能有限,通常執行運行容器的低級任務。大多數開發者日常工作中不會使用到。其一般指按照 OCI 規範、能夠接收可運行 roofs 文件系統和配置文件並運行隔離進程的實現。這種運行時只負責將進程運行在相對隔離的資源空間裏,不提供存儲實現和網絡實現。但是其他實現可以在系統中預設好相關資源,低級容器運行時可通過 config.json 聲明加載對應資源。低級運行時的特點是底層、輕量,限制也很一目瞭然:

低級運行時 demo

通過以 root 方式使用 Linux cgcreate、cgset、cgexec、chroot 和 unshare 命令來實現簡單容器。

首先,以 busybox 容器鏡像作爲基礎,設置一個根文件系統。然後,創建一個臨時目錄,並將 busybox 解壓到該目錄中。

$ CID=$(docker create busybox)
$ ROOTFS=$(mktemp -d)
$ docker export $CID | tar -xf - -C $ROOTFS

緊接着創建 uuid,並對內存和 CPU 設置限制。內存限制是以字節爲單位設置的。在這裏,將內存限制設置爲 100MB。

$ UUID=$(uuidgen)
$ cgcreate -g cpu,memory:$UUID
$ cgset -r memory.limit_in_bytes=100000000 $UUID
$ cgset -r cpu.shares=512 $UUID

例如,如果我們想把我們的容器限制在兩個 cpu core 上,可以設定一秒鐘的週期和兩秒鐘的配額(1s=1,000,000us),這將允許進程在一秒鐘的時間內使用兩個 cpu core。

$ cgset -r cpu.cfs_period_us=1000000 $UUID
$ cgset -r cpu.cfs_quota_us=2000000 $UUID

接下來在容器中執行命令。

$ cgexec -g cpu,memory:$UUID \
>     unshare -uinpUrf --mount-proc \
>     sh -c "/bin/hostname $UUID && chroot $ROOTFS /bin/sh"
/ # echo "Hello from in a container"
Hello from in a container
/ # exit

最後,刪除前面創建的 cgroup 和臨時目錄。

$ cgdelete -r -g cpu,memory:$UUID
$ rm -r $ROOTFS

低級運行時 demo

爲了更好地理解低級容器運行時,以下列舉了幾個低級運行時代表,各自實現了不同的功能。

runC

runC 是目前使用最廣泛的容器運行時。它最初是集成在 Docker 的內部,後來作爲一個單獨的工具,並以公共庫的方式提取出來。

在 2015 年,在 Linux 基金會的支持下有了 Open Container Initiative (OCI)(就是負責制定容器標準的組織),Docker 將自己容器格式和運行時 runC 捐給了 OCI。OCI 在此基礎上制定了 2 個標準:運行時標準 Runtime Specification (runtime-spec) 和 鏡像標準 Image Specification (image-spec) ,下面通過示例,簡要介紹一下 runC。

首先創建根文件系統。這裏我們將再次使用 busybox。

$ mkdir rootfs
$ docker export $(docker create busybox) | tar -xf - -C rootfs

接下來創建一個 config.json 文件

$ runc spec

這個命令爲容器創建一個模板 config.json。

$ cat config.json
{
        "ociVersion": "1.0.2",
        "process": {
                "terminal": true,
                "user": {
                        "uid": 0,
                        "gid": 0
                },
                "args": [
                        "sh"
                ],
                "env": [
                        "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                        "TERM=xterm"
                ],
                "cwd": "/",
                "capabilities": {
...

默認情況下,它在根文件系統位於./rootfs 的目錄下運行命令。

$ sudo runc run mycontainerid
/ # echo "Hello from in a container"
Hello from in a container

rkt(已廢棄)

rkt 是一個同時具有低級和高級功能的運行時。例如,很像 Docker,rkt 允許你構建容器鏡像,獲取和管理本地存儲庫中的容器鏡像,並通過一個命令運行它們。

runV

runv 是 OCF 基於管理程序的(Hypervisor-based )運行時 Runtime.runV 兼容 OCF。作爲虛擬容器運行時引擎的 runV 已被淘汰。runV 團隊與英特爾一起在 OpenInfra Foundation 中創建了 Kata Containers 項目

youki

Rust 是時下最流行的編程語言,而容器開發也是一個時興的應用領域。將兩者結合使用 Rust 來做容器開發是一個值得嚐鮮的體驗。youki 是使用 Rust 的實現 OCI 運行時規範,類似於 runc。

高級容器運行時

高級運行時負責容器鏡像的傳輸和管理,解壓鏡像,並傳遞給低級運行時來運行容器。通常情況下,高級運行時提供一個守護程序和一個 API,遠程應用程序可以使用它來運行容器並監控它們,它們位於低層運行時或其他高級運行時之上。

高層運行時也會提供一些看似很低級的功能。例如,管理網絡命名空間,並允許容器加入另一個容器的網絡命名空間。

這裏有一個類似邏輯分層圖,可以幫助理解這些組件是如何結合在一起工作的。

高級運行時代表

Docker

Docker 是最早的開源容器運行時之一。它是由平臺即服務的公司 dotCloud 開發的,用於在容器中運行用戶的應用。

Docker 是一個容器運行時,包含了構建、打包、共享和運行容器。Docker 基於 C/S 架構實現,最初是由一個守護程序 dockerd 和 docker 客戶端應用程序組成。守護程序提供了構建容器、管理鏡像和運行容器的大部分邏輯,以及一些 API。命令行客戶端可以用來發送命令和從守護進程中獲取信息。

它是第一個流行開來的運行時間,毫不過分的說,Docker 對容器的推廣做出了巨大的貢獻。

Docker 最初實現了高級和低級的運行時功能,但這些功能後來被分解成單獨的項目,如 runc 和 containerd,以前 Docker 的架構如下圖所示,現有架構中,docker-containerd 變成了 containerd,docker-runc 變成了 runc。

dockerd 提供了諸如構建鏡像的功能,而 dockerd 使用 containerd 來提供諸如鏡像管理和運行容器的功能。例如,Docker 的構建步驟實際上只是一些邏輯,它解釋 Docker 文件,使用 containerd 在容器中運行必要的命令,並將產生的容器文件系統保存爲一個鏡像。

Containerd

containerd 是從 Docker 中分離出來的高級運行時。containerd 實現了下載鏡像、管理鏡像和運行鏡像中的容器。當需要運行一個容器時,它會將鏡像解壓到一個 OCI 運行時 bundle 中,並向 runc 發送 init 以運行它。

Containerd 還提供了 API,可以用來與它交互。containerd 的命令行客戶端是 ctr 和 nerdctl。

可以通過 ctr 拉取一個容器鏡像。

$ sudo ctr images pull docker.io/library/redis:latest

列出所有的鏡像:

$ sudo ctr images list

運行容器:

$ sudo ctr container create docker.io/library/redis:latest redis

列出運行容器:

$ sudo ctr container list

停止容器:

$ sudo ctr container delete redis

這些命令類似於用戶與 Docker 的互動方式。

rkt(已廢棄)

rkt 是一個同時具有低級和高級功能的運行時。例如,很像 Docker,rkt 允許你構建容器鏡像,獲取和管理本地存儲庫中的容器鏡像,並通過一個命令運行它們。

Kubernetes CRI

CRI 在 Kubernetes 1.5 中引入,作爲 kubelet 和容器運行時之間的橋樑。社區希望 Kubernetes 集成的高級容器運行時實現 CRI。該運行時處理鏡像的管理,支持 Kubernetes pods,並管理容器,因此根據高級運行時的定義,支持 CRI 的運行時必須是一個高級運行時。低級別的運行時並不具備上述功能。

爲了進一步瞭解 CRI,可以看看整個 Kubernetes 架構。kubelet 代表工作節點,位於 Kubernetes 集羣的每個節點上,kubelet 負責管理其節點的工作負載。當需要運行工作負載時,kubelet 通過 CRI 與運行時進行通信。由此可以看出,CRI 只是一個抽象層,允許切換不同的容器運行時。

CRI 規範

CRI 定義了 gRPC API,該規範定義在 Kubernetes 倉庫中 cri-api 目錄中。CRI 定義了幾個遠程程序調用(RPC)和消息類型。這些 RPC 用於管理工作負載等內容,如 "拉取鏡像"(ImageService.PullImage)、"創建 pod"(RuntimeService.RunPodSandbox)、"創建容器"(RuntimeService.CreateContainer)、"啓動容器"(RuntimeService.StartContainer)、"停止容器"(RuntimeService.StopContainer)等操作。

例如,通過 CRI 啓動一個新的 Pod(篇幅有限,進行了一些簡化工作)。RunPodSandbox 和 CreateContainer RPCs 在其響應中返回 ID,在後續請求中使用。

ImageService.PullImage({image: "image1"})
ImageService.PullImage({image: "image2"})
podID = RuntimeService.RunPodSandbox({name: "mypod"})
id1 = RuntimeService.CreateContainer({
    pod: podID,
    name: "container1",
    image: "image1",
})
id2 = RuntimeService.CreateContainer({
    pod: podID,
    name: "container2",
    image: "image2",
})
RuntimeService.StartContainer({id: id1})
RuntimeService.StartContainer({id: id2})

可以直接使用 crictl 工具與 CRI 運行時交互,可以用它來調試和測試 CRI 的相關實現。

cat <<EOF | sudo tee /etc/crictl.yaml
runtime-endpoint: unix:///run/containerd/containerd.sock
EOF

或者通過命令行指定:

crictl --runtime-endpoint unix:///run/containerd/containerd.sock …

關於 crictl 的使用參見官網。

支持 CRI 的運行時

Containerd

containerd 應該是目前最流行的 CRI 運行時。它以插件的方式實現 CRI,默認是啓用的。它默認在 unix 套接字上監聽消息。

從 1.2 版本開始,它通過 runtime handler 來支持多種低級運行時。運行時處理程序是通過 CRI 中的字段傳遞,根據該運行時處理程序,containerd 運行 shim 的應用程序來啓動容器。這可以用來運行 runc 及其他的低級運行時的容器,如 gVisor、Kata Containers 等。在 Kubernetes API 中通過 RuntimeClass 進行運行時配置。

下圖是 Containerd 的發展史。

Docker

docker-shim 是 K8s 社區第一個被開發的,作爲 kubelet 和 Docker 之間的 shim。隨着 Docker 將其許多功能分解到 containerd 中,現在通過 containerd 支持 CRI。當現代版本的 Docker 被安裝時,containerd 也一起被安裝,CRI 直接與 containerd 對話,隨着 docker-shim 正式廢棄,是時候考慮相關遷移的工作了,K8s 在這方面做了大量的工作,具體可參看官方文檔。

CRI-O

cri-o 是一個輕量級的 CRI 運行時,它支持 OCI,並提供鏡像的管理、容器進程管理、監控日誌及資源隔離等工作。

cri-o 的通信地址默認是在 / var/run/crio/crio.sock。

下圖爲 CRI 插件的演變史。

由於筆者時間、視野、認知有限,本文難免出現錯誤、疏漏等問題,期待各位讀者朋友、業界專家指正交流。

參考文獻

1.https://blog.mobyproject.org/where-are-containerds-graph-drivers-145fc9b7255

2.https://insujang.github.io/2019-10-31/container-runtime/

3.https://github.com/cri-o/cri-o

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