導讀:在(zài)學習(xí)Containerd之前,我們需要去(qù)了解Docker與Kubernetes這兩個使用Containerd最(zuì)多的技術,也需(xū)要明白什麽是容器(qì),什麽是容器運行時,以(yǐ)及裏麵涉及的組(zǔ)件,這(zhè)些(xiē)組件是用來幹什麽的,及容器領域的概念,如libcontainer、runc、OCI、CRI、shim等。
在 Linux 內核中,容器不(bú)是一類對象。容器本質上由幾個底層的內核原(yuán)語組成:namespace(允許你跟誰交談),cgroup(允許使用的資源量),和 LSM(Linux 安全模塊 —— 允許你做(zuò)的事情)。這些(xiē)湊在一起能夠為我們的進程設置安全、隔離(lí)和可計量的執行環境。
每次創建隔離(lí)進程時,都不需要手動隔離、自定義命名空間等,把這些組件捆(kǔn)綁(bǎng)在一起(qǐ),我們稱之為容器。但是每次(cì)手動執行(háng)所(suǒ)有的操作將(jiāng)很麻煩,因此出(chū)現了容器運行時工具,它能將這些部分組合成一個隔離的、安全的執行環境變得很容易,讓我們能以重複的方式部署。
容器運行時是掌控容器運行的整個生命(mìng)周期,以docker為例,其主要提供(gòng)功能如下:
這些功能均(jun1)可由小的組件單獨實現,因此容器運行時是運行和管理容器運行所需要的組件。
隨著容器運行時的發展,Docker公司與CoreOS和Google共同創建了OCI(開放容器標準),並提供了兩種規範:
運行(háng)時(shí)規(guī)範:該規範目標是定義容器的配置、執行環境和生命周期
鏡像規範:該規範的目標創建可互操(cāo)作的工具,用於構建(jiàn)、傳輸(shū)和準備要運行的容器鏡像
在runc作為了OCI的一種實現參考之後,各種運行時工具和庫也(yě)慢慢出現。而(ér)根(gēn)據這些(xiē)運行時的功能不同,比如有的隻運行容器(runc,lxc),有的還(hái)可以對鏡像進行管理(Containerd,cri-o),因此通俗的分為(wéi)高級運行時(high-level)和(hé)低(dī)級運行時(shí)(low-level)。
低級運行時:側重於運行容器,為容(róng)器設(shè)置namespace和cgroup
高級運行時:包含更多上層功能,如為開發(fā)人員提(tí)供API,鏡像存(cún)儲管理等(děng)
Docker是第一個流行的(de)容器技術,最初Docker使用的(de)是LXC(0.7版本之前)但是隔離的層次不(bú)完善,後來Docker開發了libcontainer(0.7~1.10版本),最後演變為runc和Containerd(Docker被逼無奈將libcontainer捐獻出(chū)來改名為runc)
從1.11版本之後,Docker容器運行開始(shǐ)通過集成Containerd和runc等多個組件(jiàn)完成。現在的架構中,Containerd負責容器的生命周期管理,提供(gòng)了在一個節點上執行容(róng)器和管理鏡像的最小功能(néng)集,並向上為Docker Daemon提供grpc接(jiē)口。

當請求創建(jiàn)一個容器時(shí),Docker Daemon並不會直接去創建,而且請求containerd創(chuàng)建容器,containerd在收到請求後,也(yě)不會去直接操作(zuò)容器,而是創建containerd-shim的進程去(qù)操作容器(因為需要(yào)一個父進程去做狀態收集、維(wéi)持stdin、stdout、stderr打開等工作,如果父進程是contaienrd,當(dāng)containerd掛掉(diào)時(shí),整個宿(xiǔ)主機的容器都會退出(chū)),而(ér)containerd-shim會去調用runc來啟動容器,runc在啟動完容器後會直接退出(chū),此時containerd-shim成為容器(qì)的(de)父進程,負責收集容器進程的狀態上報給containerd,並在容器中 pid 為 1 的進程(chéng)退出後接管(guǎn)容器中的子(zǐ)進程進行清,確保不會出現僵屍進程。
runc創建容器則是根據上述的OCI去做操作,例如namespaces、cgroups的配置,以及掛載root文件係統等操作。
Docker 將容器操作都遷移到 containerd 中去是因為當時做 Swarm,想要進軍 PaaS 市場,做了(le)這個架構切分,讓 Docker Daemon 專門去負責上層的封裝(zhuāng)編排,當然後(hòu)麵的結果我(wǒ)們知道(dào) Swarm 在 Kubernetes 麵前是慘敗,然後 Docker 公司就把 containerd 項目捐獻給(gěi)了 CNCF 基(jī)金(jīn)會,這個也是現在的 Docker 架構。
2014年Kubernetes誕生,由於當時Docker很(hěn)流行,因此很自然(rán)的選擇了Docker,在CRI出現(xiàn)之前,Kubelet通過內(nèi)嵌的dockershim操作(zuò)Docker API來操作(zuò)容器,進而達到一個麵向終態的效果。

而隨著Docker將(jiāng)Containerd開源出以及更多的容器運行(háng)時出來,Kubernetes為了精簡和支持更多的容(róng)器(qì)運行時,Google和Redhat推出了CRI標準,用於Kubernetes平台和容器運行時解耦CRI(容器運行時接口)。
CRI本質上是Kubernetes定義的一(yī)組與容器運行時進行交互的(de)接口(kǒu),因此容器運行時隻要實現了CRI,就可以對接到Kubernetes平台中。但是當時Kubernetes的地位不高,所以一(yī)些容器運行時不會去實現CRI接口,於(yú)是就(jiù)出現(xiàn)了shim,shim的職責是作為適(shì)配器將各種容器運(yùn)行時本身的接口適配到 Kubernetes 的(de) CRI 接口上,上(shàng)圖的dockershim就(jiù)是Kubernetes對接Docker到CRI接(jiē)口的實現。

在(zài)引入CRI後,Kubelet的架構如圖所(suǒ)示:

通過觀察分析能夠發現,Kubernetes使用Docker的調用鏈比較長,而Docker的一些功能對於Kubernetes來說又不需要,所以自然的將容器運行時切換到Containerd。切換到Containerd後取消掉了中間環節,但操作體(tǐ)驗和以前一樣,在Containerd1.0時,對CRI的適配是通過一個單獨的CRI-Containerd實現(因為最開始containerd還會去適配(pèi)其他(tā)係統(tǒng),所以沒有(yǒu)直接實現CRI)。到了Containerd1.1版本(běn)後就去掉了CRI-Containerd,直接把適配邏輯作為插件集成到Containerd主進程中,變得更加簡潔。


CRI的接口主要分為兩類:
ImageService:鏡像相(xiàng)關(guān)的操作
RuntimeService:容器和Sandbox運行時管理(lǐ)
RuntimeService 中 CRI 設計的一個重要原則,就是確保這個接口本身,隻關注容器,不關注(zhù) Pod,這麽做是因為:
Pod 是 Kubernetes 的編排概念,而不(bú)是容(róng)器運行時(shí)的概念。所以,我們就不能假設所有下層(céng)容器項目,都能夠暴露(lù)出可以直接映射為 Pod 的 API;
如果 CRI 裏引入了關於 Pod 的概念,那麽接下來隻要 Pod API 對象的字段(duàn)發生變化,那(nà)麽(me) CRI 就很(hěn)有可能需要變更。而在 Kubernetes 開發的前期,Pod 對象的變(biàn)化(huà)還是比較頻繁的,但(dàn)對於 CRI 這樣的標準接口來(lái)說,這個變更頻率就有點麻煩了。

雖然 CRI 裏還是有一組叫(jiào)做 RunPodSandbox 的接口。但是,這個 PodSandbox,對應的並不是 Kubernetes 裏的 Pod API 對象,而隻(zhī)是抽取了 Pod 裏的一(yī)部分與容器運行時相關的字段,比如 HostName、DnsConfig、CgroupParent 等。所以(yǐ)說,PodSandbox 這個(gè)接口描述的其實是 Kubernetes 將(jiāng) Pod 這個概(gài)念映射到容器運行時層麵(miàn)所(suǒ)需(xū)要的字段,或者(zhě)說是一個 Pod 對(duì)象的子集。而創建、管(guǎn)理 Pod 的邏輯則放(fàng)置在(zài) kubernetes 中,而不是(shì) CRI 要實現的接口中。
隨著 CRI 方案的發展,以(yǐ)及(jí)其他(tā)容器運行時對 CRI 的支持越來越(yuè)完善,Kubernetes 社區在2020年7月份就(jiù)開始著手移除 dockershim 方案了(le),現在的移除計劃是在 1.20 版本(běn)中將 kubelet 中內置的 dockershim 代碼分離,將內置的 dockershim 標(biāo)記為維護模式,當然(rán)這個時候仍然還可以使用 dockershim,目標是在 1.24 版本(běn)發布沒有 dockershim 的版本(代碼還在(zài),但是(shì)要(yào)默(mò)認(rèn)支持(chí)開箱即用的 docker 需要自(zì)己構建 kubelet,會(huì)在某個寬(kuān)限期(qī)過後從 kubelet 中刪除(chú)內置的 dockershim 代碼)。
目前,CRI領域(yù)有兩個主要的參(cān)與者,一個是Docker的高級運行時Containerd,一個是RedHat專(zhuān)門為Kubernetes設(shè)計(jì)的運行時CRI-O。
當容器運行時的標準(zhǔn)被提出以後,RedHat的一些(xiē)人開(kāi)始想他們可以構(gòu)建一個更(gèng)簡單的運行時,而且這個運行時僅僅為Kubernetes所用。這樣就有了skunkworks項目,最後定名為 CRI-O, 它實現(xiàn)了一個(gè)最小(xiǎo)的CRI接口,旨(zhǐ)在(zài)充當CRI和支持的OCI運行時的輕量級橋梁。

Containerd 是一個工業級標(biāo)準的容器運行時,它強調簡單性、健壯性和可移植性,可以在宿主機(jī)中管理(lǐ)完(wán)整(zhěng)的容器生命周期(qī):容器鏡像的傳輸和存儲、容(róng)器的執(zhí)行和管理、存儲和網絡等,主要有以下功能:
管理容器的生(shēng)命周期(從(cóng)創建容器到銷毀容(róng)器)
存儲管理(管理(lǐ)鏡像及(jí)容器數據(jù)的存儲)
調用 runc 運行容器(與 runc 等容器(qì)運行時交互(hù))
管理容器(qì)網絡接口(kǒu)及網(wǎng)絡(CNI)
Containerd在Docker或者Kunernetes中都是使用最多的運行時,同時也是我(wǒ)們(men)環境中接觸最多的,因此後續著重學(xué)習Containerd。
Containerd 可用作 Linux 和 Windows 的守護程(chéng)序,它管理其主機係(xì)統完整的容器生命周期,從鏡像傳輸和存儲到容器執行和監測(cè),再(zài)到底層存(cún)儲到網絡附件(jiàn)等等。

為了(le)解耦,Containerd 將係統劃分(fèn)成了不同的組件,每個組件都由一個或(huò)多(duō)個模塊協作完成(Core 部(bù)分),每一種類型的模塊(kuài)都以插件(jiàn)的形(xíng)式集成到 Containerd 中,而且插件之間是相互依(yī)賴的,例如,上圖中的每一個長虛線的方框(kuàng)都表示一種類型的插件,包(bāo)括 Service Plugin、Metadata Plugin、GC Plugin、Runtime Plugin 等,其中 Service Plugin 又會依賴 Metadata Plugin、GC Plugin 和(hé) Runtime Plugin。每一個小方框都表示一(yī)個細分的插(chā)件,例如 Metadata Plugin 依賴 Containers Plugin、Content Plugin 等。比如:
Content Plugin: 提供對(duì)鏡像(xiàng)中可尋址內容的訪(fǎng)問,所有不可變的內容都被存(cún)儲在這裏
Snapshot Plugin: 用來管理容器(qì)鏡(jìng)像(xiàng)的文件係統快照,鏡像(xiàng)中的每一層都(dōu)會被(bèi)解壓成文件係統快照(zhào),類似於 Docker 中的 graphdriver
總體來(lái)看(kàn) Containerd 可以分為三個大(dà)塊:
Metadata 管理(lǐ)鏡像和容器的元數(shù)據(jù)

Containerd被設(shè)計成可以很容易的嵌入到更大的係統中,例如Docker使用containerd運行容器,Kubernetes通過CRI使用containerd管理單個 節點上的容器 除(chú)了編程方式使用外,它還可以(yǐ)通過命令行(háng)使用,但不像docker全(quán)麵,主要用於調試和學習目的,主要有:
ctr Containerd依據自身開發的命令行工具
nerdctl 與docker命令行風格兼容的命令行工具
crictl K8S根據CRI規範定義的命令行工具

Containerd通過暴露的gRPC API給外部管理容器,而Containerd中主要提供的API有:
其他(tā)還包括events、diffs等,具(jù)體見containerd的gRPC API

Docker、ctr、nerdctl都是通過Containerd提供的API進行容器的管理,Kubernetes、crictl則是(shì)通過CRI接口實現。
分配一個新的讀寫快(kuài)照(snapshot),使得容器可以存儲持久化(huà)數據(為(wéi)容器創(chuàng)建新快照時,需(xū)要提供快照ID以及容器使用(yòng)的鏡像)
創建一個Container對象(xiàng),用於分配(pèi)數據
創建一個Task,用於實際的運行容器(當Task已創(chuàng)建時(shí),意味(wèi)著命名空間(jiān)、根文件係(xì)統和各種容器級別的設置已被初始化,但(dàn)容器定義的進程尚未啟(qǐ)動)
在啟(qǐ)動(dòng)Task之前需要等待Task創建成功,然後再調用Start去啟動(dòng)Task

調用CRI插件,通過RuntimeService創建Pod
CRI調用CNI接口創建和配置Pod的(de)網絡命令空間(jiān)
CRI調用(yòng)Containerd內部接口創建特殊的pause容(róng)器,並將(jiāng)該容器放入Pod的cgroups和namespace中(使用不同的容器運行時,PodSandbox的實現方式(shì)也不一樣,比如使用kata作為runtime,PodSandbox被實現為一個虛擬(nǐ)機;而使用runc作為(wéi)runtime,PodSandbox就是一個獨立的namespace和cgroups)
調用CRI插件(jiàn),通過ImageServie拉取應用容器鏡像
如果節點上(shàng)不存在鏡像,則使用Contianerd拉取鏡像
調用CRI插件,使用RuntimeService創建和(hé)啟動應用容器
CRI調用Containerd內(nèi)部接口創建容器(qì),放到Pod的cgroups和namespace中(zhōng)

Containerd創建任務流(liú)程
上述說的創建容器流(liú)程和創建Pod流程都是調用Containerd內部接口的邏輯,實際的過(guò)程由Containerd啟(qǐ)動Containerd-shim進程調用runc創建容器,具體步(bù)驟如(rú)下:
Containerd調(diào)用Containerd-shim start 啟(qǐ)動用於創建runc的Containerd-shim,這樣Containerd-shim就與Containerd脫離了關係,重啟Containerd也不會影響(xiǎng)Containerd-shim進程(chéng)
通過ttrpc調用Containerd-shim的Newtask方法,之後調用runc create
再通過ttrpc調(diào)用Containerd-shim的Start方法,之後調用runc start啟動pause容器
以同樣的方式啟動Pod中(zhōng)定義(yì)的container


1. Containerd 被(bèi)設計成(chéng)嵌入到一個更(gèng)大的係統中,而不是直接由開發人員或終端用戶(hù)使用
2. Docker有網絡功能模塊,比(bǐ)如它會創(chuàng)建 docker0 網橋,所以在使用 docker 時可以直接實現端口映射等功能,而這些網絡能力都是 Docker Daemon 實現的。但是Containerd 中不包(bāo)含相應的網絡功能,想要(yào)啟動的容器有網絡能力,需要額外安裝 CNI 相(xiàng)關的工具(jù)和插件(bridge、flannel 等)
*Containerd一(yī)切皆插件
本文通過引入Docker和Kubernetes的發(fā)展介紹容器(qì)、容(róng)器運行時,將容(róng)器領域c常見的概念OCI、CRI、shim、runc,containerd串聯起來,能夠幫組我們進一步理解Docker和Kubernetes背後是怎(zěn)麽(me)創建容器的,以(yǐ)及Containerd的實際運行原理。
https://github.com/containerd/containerd
https://github.com/kubernetes
https://github.com/moby/moby
https://github.com/kubernetes/enhancements/tree/master/keps/sig-node/2221-remove-dockershim
一文(wén)搞懂容器運行時
containerd shim原理深入解讀