Containerd深度剖析-runtime篇


技術(shù)深度|簡單

需求簡介
注: Container runtime統(tǒng)稱為容器運行時
在Docker時代,關(guān)于容器運行時術(shù)語的定義是非常明確的,其為運行和管理容器的軟件。但隨著Docker涵蓋的內(nèi)容日益增多,以及多種容器編排工具的引入,該定義變得日益模糊了。
當(dāng)你運行一個Docker容器時,一般的步驟是:
下載鏡像
將鏡像解壓成一個bundle,即將各層文件平鋪到一個單一的文件系統(tǒng)中。
運行容器
最初的規(guī)范規(guī)定,只有運行容器的部分定義為容器運行時,但一般用戶,將上述三個步驟都默認(rèn)為容器運行時所必須的能力,從而讓容器運行時的定義成為一個令人困惑的話題。
當(dāng)人們想到容器運行時,可能會想到一連串的相關(guān)概念;runc、runv、lxc、lmctfy、Docker(containerd)、rkt、cri-o。每一個都是基于不同的場景而實現(xiàn)的,均實現(xiàn)了不同的功能。如containerd和cri-o,實際均可使用runc來運行容器,但其實現(xiàn)了如鏡像管理、容器API等功能,可以將這些看作是比runc具備的更高級的功能。
可以發(fā)現(xiàn),容器運行時是相當(dāng)復(fù)雜的。每個運行時都涵蓋了從低級到高級的不同部分,如下圖所示。

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

低級容器運行時
低級運行時的功能有限,通常執(zhí)行運行容器的低級任務(wù)。大多數(shù)開發(fā)者日常工作中不會使用到。其一般指按照 OCI 規(guī)范、能夠接收可運行roofs文件系統(tǒng)和配置文件并運行隔離進(jìn)程的實現(xiàn)。這種運行時只負(fù)責(zé)將進(jìn)程運行在相對隔離的資源空間里,不提供存儲實現(xiàn)和網(wǎng)絡(luò)實現(xiàn)。但是其他實現(xiàn)可以在系統(tǒng)中預(yù)設(shè)好相關(guān)資源,低級容器運行時可通過 config.json 聲明加載對應(yīng)資源。低級運行時的特點是底層、輕量,限制也很一目了然:
只認(rèn)識 rootfs 和 config.json,沒有其他鏡像能力
不提供網(wǎng)絡(luò)實現(xiàn)
不提供持久實現(xiàn)
無法跨平臺等
低級運行時demo
通過以root方式使用Linux cgcreate、cgset、cgexec、chroot和unshare命令來實現(xiàn)簡單容器。
首先,以busybox容器鏡像作為基礎(chǔ),設(shè)置一個根文件系統(tǒng)。然后,創(chuàng)建一個臨時目錄,并將busybox解壓到該目錄中。
CID=$(docker create busybox)ROOTFS=$(mktemp -d)docker export $CID | tar -xf - -C $ROOTFS
緊接著創(chuàng)建uuid,并對內(nèi)存和CPU設(shè)置限制。內(nèi)存限制是以字節(jié)為單位設(shè)置的。在這里,將內(nèi)存限制設(shè)置為100MB。
$ UUID=$(uuidgen)$ cgcreate -g cpu,memory:$UUID$ cgset -r memory.limit_in_bytes=100000000 $UUID$ cgset -r cpu.shares=512 $UUID
例如,如果我們想把我們的容器限制在兩個cpu core上,可以設(shè)定一秒鐘的周期和兩秒鐘的配額(1s=1,000,000us),這將允許進(jìn)程在一秒鐘的時間內(nèi)使用兩個cpu core。
cgset -r cpu.cfs_period_us=1000000 $UUIDcgset -r cpu.cfs_quota_us=2000000 $UUID
接下來在容器中執(zhí)行命令。
$ 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
最后,刪除前面創(chuàng)建的cgroup和臨時目錄。
cgdelete -r -g cpu,memory:$UUIDrm -r $ROOTFS
低級運行時demo
為了更好地理解低級容器運行時,以下列舉了幾個低級運行時代表,各自實現(xiàn)了不同的功能。
runC
runC是目前使用最廣泛的容器運行時。它最初是集成在Docker的內(nèi)部,后來作為一個單獨的工具,并以公共庫的方式提取出來。
在2015 年,在 Linux 基金會的支持下有了 Open Container Initiative (OCI)(就是負(fù)責(zé)制定容器標(biāo)準(zhǔn)的組織),Docker 將自己容器格式和運行時 runC 捐給了 OCI。OCI 在此基礎(chǔ)上制定了 2 個標(biāo)準(zhǔn):運行時標(biāo)準(zhǔn) Runtime Specification (runtime-spec) 和 鏡像標(biāo)準(zhǔn) Image Specification (image-spec) ,下面通過示例,簡要介紹一下 runC。
首先創(chuàng)建根文件系統(tǒng)。這里我們將再次使用busybox。
mkdir rootfsdocker export $(docker create busybox) | tar -xf - -C rootfs
接下來創(chuàng)建一個config.json文件
runc spec這個命令為容器創(chuàng)建一個模板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": {...
默認(rèn)情況下,它在根文件系統(tǒng)位于./rootfs的目錄下運行命令。
$ sudo runc run mycontainerid/ # echo "Hello from in a container"Hello from in a container
rkt(已廢棄)
rkt是一個同時具有低級和高級功能的運行時。例如,很像Docker,rkt允許你構(gòu)建容器鏡像,獲取和管理本地存儲庫中的容器鏡像,并通過一個命令運行它們。
runV
runv 是 OCF 基于管理程序的(Hypervisor-based )運行時 Runtime.runV 兼容 OCF。作為虛擬容器運行時引擎的runV已被淘汰。runV團隊與英特爾一起在OpenInfra Foundation中創(chuàng)建了Kata Containers項目
youki
Rust是時下最流行的編程語言,而容器開發(fā)也是一個時興的應(yīng)用領(lǐng)域。將兩者結(jié)合使用Rust來做容器開發(fā)是一個值得嘗鮮的體驗。youki是使用Rust的實現(xiàn)OCI運行時規(guī)范,類似于runc。

高級容器運行時
高級運行時負(fù)責(zé)容器鏡像的傳輸和管理,解壓鏡像,并傳遞給低級運行時來運行容器。通常情況下,高級運行時提供一個守護(hù)程序和一個API,遠(yuǎn)程應(yīng)用程序可以使用它來運行容器并監(jiān)控它們,它們位于低層運行時或其他高級運行時之上。
高層運行時也會提供一些看似很低級的功能。例如,管理網(wǎng)絡(luò)命名空間,并允許容器加入另一個容器的網(wǎng)絡(luò)命名空間。
這里有一個類似邏輯分層圖,可以幫助理解這些組件是如何結(jié)合在一起工作的。

高級運行時代表
Docker
Docker是最早的開源容器運行時之一。它是由平臺即服務(wù)的公司dotCloud開發(fā)的,用于在容器中運行用戶的應(yīng)用。
Docker是一個容器運行時,包含了構(gòu)建、打包、共享和運行容器。Docker基于C/S架構(gòu)實現(xiàn),最初是由一個守護(hù)程序dockerd和docker客戶端應(yīng)用程序組成。守護(hù)程序提供了構(gòu)建容器、管理鏡像和運行容器的大部分邏輯,以及一些API。命令行客戶端可以用來發(fā)送命令和從守護(hù)進(jìn)程中獲取信息。
它是第一個流行開來的運行時間,毫不過分的說,Docker對容器的推廣做出了巨大的貢獻(xiàn)。
Docker最初實現(xiàn)了高級和低級的運行時功能,但這些功能后來被分解成單獨的項目,如runc和containerd,以前Docker的架構(gòu)如下圖所示,現(xiàn)有架構(gòu)中,docker-containerd變成了containerd,docker-runc變成了runc。

dockerd提供了諸如構(gòu)建鏡像的功能,而dockerd使用containerd來提供諸如鏡像管理和運行容器的功能。例如,Docker的構(gòu)建步驟實際上只是一些邏輯,它解釋Docker文件,使用containerd在容器中運行必要的命令,并將產(chǎn)生的容器文件系統(tǒng)保存為一個鏡像。
Containerd
containerd是從Docker中分離出來的高級運行時。containerd實現(xiàn)了下載鏡像、管理鏡像和運行鏡像中的容器。當(dāng)需要運行一個容器時,它會將鏡像解壓到一個OCI運行時bundle中,并向runc發(fā)送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允許你構(gòu)建容器鏡像,獲取和管理本地存儲庫中的容器鏡像,并通過一個命令運行它們。

Kubernetes CRI
CRI在Kubernetes 1.5中引入,作為kubelet和容器運行時之間的橋梁。社區(qū)希望Kubernetes集成的高級容器運行時實現(xiàn)CRI。該運行時處理鏡像的管理,支持Kubernetes pods,并管理容器,因此根據(jù)高級運行時的定義,支持CRI的運行時必須是一個高級運行時。低級別的運行時并不具備上述功能。
為了進(jìn)一步了解CRI,可以看看整個Kubernetes架構(gòu)。kubelet代表工作節(jié)點,位于Kubernetes集群的每個節(jié)點上,kubelet負(fù)責(zé)管理其節(jié)點的工作負(fù)載。當(dāng)需要運行工作負(fù)載時,kubelet通過CRI與運行時進(jìn)行通信。由此可以看出,CRI只是一個抽象層,允許切換不同的容器運行時。

CRI規(guī)范
CRI定義了gRPC API,該規(guī)范定義在Kubernetes倉庫中cri-api目錄中。CRI定義了幾個遠(yuǎn)程程序調(diào)用(RPC)和消息類型。這些RPC用于管理工作負(fù)載等內(nèi)容,如 "拉取鏡像"(ImageService.PullImage)、"創(chuàng)建pod"(RuntimeService.RunPodSandbox)、"創(chuàng)建容器"(RuntimeService.CreateContainer)、"啟動容器"(RuntimeService.StartContainer)、"停止容器"(RuntimeService.StopContainer)等操作。
例如,通過CRI啟動一個新的Pod(篇幅有限,進(jìn)行了一些簡化工作)。RunPodSandbox和CreateContainer RPCs在其響應(yīng)中返回ID,在后續(xù)請求中使用。
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運行時交互,可以用它來調(diào)試和測試CRI的相關(guān)實現(xiàn)。
cat <runtime-endpoint: unix:///run/containerd/containerd.sockEOF
或者通過命令行指定:
crictl --runtime-endpoint unix:///run/containerd/containerd.sock …關(guān)于crictl的使用參見官網(wǎng)。
支持CRI的運行時
Containerd
containerd應(yīng)該是目前最流行的CRI運行時。它以插件的方式實現(xiàn)CRI,默認(rèn)是啟用的。它默認(rèn)在unix套接字上監(jiān)聽消息。
從1.2版本開始,它通過 runtime handler來支持多種低級運行時。運行時處理程序是通過CRI中的字段傳遞,根據(jù)該運行時處理程序,containerd運行shim的應(yīng)用程序來啟動容器。這可以用來運行 runc及其他的低級運行時的容器,如 gVisor、Kata Containers等。在Kubernetes API中通過RuntimeClass進(jìn)行運行時配置。
下圖是Containerd的發(fā)展史。


Docker
docker-shim是K8s社區(qū)第一個被開發(fā)的,作為kubelet和Docker之間的shim。隨著Docker將其許多功能分解到containerd中,現(xiàn)在通過containerd支持CRI。當(dāng)現(xiàn)代版本的Docker被安裝時,containerd也一起被安裝,CRI直接與containerd對話,隨著docker-shim正式廢棄,是時候考慮相關(guān)遷移的工作了,K8s在這方面做了大量的工作,具體可參看官方文檔。
CRI-O
cri-o是一個輕量級的CRI運行時,它支持OCI,并提供鏡像的管理、容器進(jìn)程管理、監(jiān)控日志及資源隔離等工作。
cri-o的通信地址默認(rèn)是在/var/run/crio/crio.sock。


下圖為CRI插件的演變史。

由于筆者時間、視野、認(rèn)知有限,本文難免出現(xiàn)錯誤、疏漏等問題,期待各位讀者朋友、業(yè)界專家指正交流。
參考文獻(xiàn)
1.https://blog.mobyproject.org/where-are-containerds-graph-drivers-145fc9b7255
