運(yùn)行時(shí)指的是程序的生命周期階段或使用特定語言來執(zhí)行程序。容器運(yùn)行時(shí)的功能與它類似——它是運(yùn)行和管理容器所需組件的軟件。這些工具可以更輕松地安全執(zhí)行和高效部署容器,是容器管理的關(guān)鍵組成部分。在容器化架構(gòu)中,容器運(yùn)行時(shí)負(fù)責(zé)從存儲(chǔ)庫加載容器鏡像、監(jiān)控本地系統(tǒng)資源、隔離系統(tǒng)資源以供容器使用以及管理容器生命周期。
容器運(yùn)行時(shí)的常見示例是 runC、containerd 和 Docker。容器運(yùn)行時(shí)主要分為三種類型——低級運(yùn)行時(shí)、高級運(yùn)行時(shí)以及沙盒或虛擬化運(yùn)行時(shí)。
在容器技術(shù)中,容器運(yùn)行時(shí)可以分為三種類型:低級運(yùn)行時(shí)、高級運(yùn)行時(shí)以及沙盒或虛擬化運(yùn)行時(shí)。
1.?低級運(yùn)行時(shí):指的是負(fù)責(zé)容器隔離和生命周期管理的基本運(yùn)行時(shí)組件。在這種運(yùn)行時(shí)中,容器是通過Linux內(nèi)核的cgroups和namespace機(jī)制進(jìn)行隔離和管理的。常見的低級運(yùn)行時(shí)包括Docker的runc、lxc等。這種運(yùn)行時(shí)通常具有輕量化和高性能的優(yōu)點(diǎn),但缺乏高級特性和管理工具。
2.?高級運(yùn)行時(shí):是在低級運(yùn)行時(shí)的基礎(chǔ)上,提供了更豐富的特性和管理工具的容器運(yùn)行時(shí)。這些特性可以包括容器網(wǎng)絡(luò)、存儲(chǔ)、監(jiān)控、鏡像傳輸、鏡像管理、鏡像API等功能,以及各種管理工具等。常見的高級運(yùn)行時(shí)包括Docker、Containerd和CRI-O等。這種運(yùn)行時(shí)通常具有更為豐富的特性和管理工具,但也帶來了更高的復(fù)雜性和資源消耗。
3.?沙盒或虛擬化運(yùn)行時(shí):是在容器運(yùn)行時(shí)中使用沙盒技術(shù)或虛擬化技術(shù)實(shí)現(xiàn)容器隔離和管理的運(yùn)行時(shí)。這種運(yùn)行時(shí)通常具有更強(qiáng)的隔離性和安全性,但也會(huì)帶來更高的性能開銷和復(fù)雜性。常見的沙盒或虛擬化運(yùn)行時(shí)包括gVisor、Kata Containers等。
總的來說,容器運(yùn)行時(shí)的不同類型具有各自的優(yōu)缺點(diǎn)和適用場景。在選擇容器運(yùn)行時(shí)時(shí),需要根據(jù)實(shí)際需求和限制進(jìn)行權(quán)衡和選擇。
低級容器運(yùn)行時(shí)
低級容器運(yùn)行時(shí)?(Low level Container Runtime),一般指按照 OCI 規(guī)范實(shí)現(xiàn)的、能夠接收可運(yùn)行文件系統(tǒng)(rootfs) 和 配置文件(config.json)并運(yùn)行隔離進(jìn)程的實(shí)現(xiàn)。
這種運(yùn)行時(shí)只負(fù)責(zé)將進(jìn)程運(yùn)行在相對隔離的資源空間里,不提供存儲(chǔ)實(shí)現(xiàn)和網(wǎng)絡(luò)實(shí)現(xiàn)。但是其他實(shí)現(xiàn)可以在系統(tǒng)中預(yù)設(shè)好相關(guān)資源,低級容器運(yùn)行時(shí)可通過 config.json 聲明加載對應(yīng)資源。
低級運(yùn)行時(shí)的特點(diǎn)是底層、輕量、靈活,限制也很明顯:
??只認(rèn)識 rootfs 和 config.json,不認(rèn)識鏡像?(下文簡稱 image),不具備鏡像存儲(chǔ)功能,也不能執(zhí)行鏡像的構(gòu)建、推送、拉取等(我們無法使用 runC, kata-runtime 處理鏡像)
??不提供網(wǎng)絡(luò)實(shí)現(xiàn),所以真正使用時(shí),往往需要利用?CNI?之類的實(shí)現(xiàn)為容器添加網(wǎng)絡(luò)
??不提供持久實(shí)現(xiàn),如果容器是有狀態(tài)應(yīng)用需要使用文件系統(tǒng)持久狀態(tài),單機(jī)環(huán)境可以掛載宿主機(jī)目錄,分布式環(huán)境可以自搭 NFS,但多數(shù)會(huì)選擇云平臺(tái)提供的?CSI?存儲(chǔ)實(shí)現(xiàn)
??與特定操作系統(tǒng)綁定無法跨平臺(tái),比如 runC 只能在 Linux 上使用;runhcs 只能在 Windows 上使用
解決了這些限制中一項(xiàng)或者多項(xiàng)的容器運(yùn)行時(shí),就叫做高級容器運(yùn)行時(shí) (High level Container Runtime)。
高級容器運(yùn)行時(shí)第一要?jiǎng)?wù)
高級容器運(yùn)行時(shí)首先要做的是打通 OCI image spec 和 runtime spec,直白來說就是高效處理 image 到 rootfs 和 config.json 的轉(zhuǎn)換。config.json 的生成比較簡單,運(yùn)行時(shí)可以結(jié)合 image config 和請求方需求直接生成;較為復(fù)雜的部分是 image 到 rootfs 的轉(zhuǎn)換,這涉及鏡像拉取、鏡像存儲(chǔ)、鏡像 layer 解壓、解壓 layer 文件系統(tǒng)(fs layer) 的存儲(chǔ)、合并 fs layer 為 rootfs。
鏡像拉取模塊先從 image registry 獲取清單(manifest)文件,處理過程不僅需要兼容 OCI image 規(guī)范,考慮到 Docker 生態(tài),也需兼容 Docker image 規(guī)范(所幸兩者區(qū)別并不大)。運(yùn)行時(shí)實(shí)現(xiàn)先從 manifest 獲取 layer list,先檢查對應(yīng) layer 在本地是否存在,如果不存在則下載對應(yīng) layer。下載的 layer tar 或者 tar.gz 一般直接存儲(chǔ)磁盤,為實(shí)現(xiàn)快速處理,需要建立索引,比如從 reference:tag (如 docker.io/library/redis:6.0.5-alpine) 到 manifest 存儲(chǔ)路徑的映射;當(dāng)然,layer 的訪問比 image 高頻,layer sha256 值到對應(yīng)存儲(chǔ)路徑也會(huì)被索引。因此 ,運(yùn)行時(shí)一般會(huì)圍繞 image 索引和 image layer 存儲(chǔ)組織獨(dú)立模塊對其他模塊提供服務(wù)。
如果要轉(zhuǎn)換 image layers 到 rootfs,就要逐層解壓 layers 為 filesystem layer(fs layer) 再做合并。這帶來了幾個(gè)問題,首先是 fs layer 同樣需要存儲(chǔ)磁盤多次復(fù)用,那么就需要有一個(gè)方式從 image 映射到對應(yīng) fs layers;接著類似 image layer,需要建立索引維系 fs layers 之間的父子關(guān)系,盡可能復(fù)用里層文件,避免重復(fù)工作;最后是層次復(fù)用帶來的煩惱,隔離進(jìn)程運(yùn)行之后會(huì)發(fā)生 rootfs 寫入,需要以某種方式避免更改發(fā)生到共享的 fs layers。
??第一個(gè)問題一般使用 image config 文件中的 diffID 解決,每解壓一層 layer,就使用上一層 fs layer id 和 本層 diffID 的拼接串做 sha256 hash,輸出結(jié)果作為本層對應(yīng)的 fs layer id(最里層 id 為其 diffID),接著建立 id 到磁盤路徑索引。因此只要通過 image manifest 文件找到 image config 文件,即可找到所有 fs layers,詳細(xì)實(shí)現(xiàn)方式見?OCI image spec layer chain id。
??第二個(gè)問題解決方式很簡單,在每個(gè) fs layer 索引存儲(chǔ)上一層 fs layer id 即可。
??第三個(gè)問題,一般通過 UnionFS 提供的 CopyOnWrite 技術(shù)解決,簡單來說,就是使用空文件夾,在鏡像對應(yīng) fs layer 最外層之上再生成一層 layer,使用 UnionFS 合并(準(zhǔn)確來說是掛載 mount)時(shí)將其聲明為 work 目錄(或者說 upper 目錄)。UnionFS 掛載出 rootfs 之后,隔離進(jìn)程所做的任何寫操作(包括刪除)都只體現(xiàn)在 work layer,而不會(huì)影響其他 fs layer。(詳細(xì)介紹可以參考?陳皓的介紹文章)
最后,高級運(yùn)行時(shí)需要充當(dāng)隔離進(jìn)程管理者角色,而一個(gè)低級運(yùn)行時(shí)(如 runC )可能同時(shí)被多個(gè)高級運(yùn)行時(shí)使用。同時(shí)試想,如果隔離進(jìn)程退出,如何以最快的方式恢復(fù)運(yùn)行?高級運(yùn)行時(shí)實(shí)現(xiàn)一般都會(huì)引入 container 抽象(或者說 container meta),meta 存儲(chǔ)了 ID、 image 信息、低級運(yùn)行時(shí)描述、OCI spec (json config)、 work layer id 以及 K-V 結(jié)構(gòu)的 label 信息。因此只要?jiǎng)?chuàng)建出 container meta,后續(xù)所有與隔離進(jìn)程相關(guān)操作,如進(jìn)程運(yùn)行、進(jìn)程信息獲取、進(jìn)程 attach、進(jìn)程日志獲取,均可通過 container ID 進(jìn)行。
containerd
containerd 是一個(gè)高度模塊化的高級運(yùn)行時(shí),所有模塊均以 RPC service 形式加載(gRPC 或者 TTRPC),所有模塊均可插拔。不同插件通過聲明互相依賴,由 containerd 核心實(shí)現(xiàn)統(tǒng)一加載,使用方可以使用 Go 語言實(shí)現(xiàn)編寫插件實(shí)現(xiàn)更豐富的功能。不過這種設(shè)計(jì)使得 containerd 擁有強(qiáng)大的跨平臺(tái)能力,并能夠作為一個(gè)組件輕松嵌入其他軟件,也帶來一個(gè)弊端,模塊之間功能互調(diào)也從簡單的函數(shù)調(diào)用,變成了更為昂貴的 RPC 調(diào)用。
注:TTRPC?是一種基于 gRPC 的改良通信協(xié)議。
containerd 架構(gòu)圖
containerd 大多功能模塊很容易與上文提到的「第一要?jiǎng)?wù)」相聯(lián)系 :
??Content,以 image layer 哈希值(一般使用 sha256 算法生成)為索引,支持快速 layer 快速查找和讀取,并支持對 layer 添加 label。索引和 label 信息存儲(chǔ)在 boltDB。
??Images,在 boltDB 中存儲(chǔ)了 reference 到 manifest layer 的映射,結(jié)合 Content 可以組織完整的 image 信息。
??Snapshot,存儲(chǔ)、處理解壓后的 fs layers 和容器 work layer,索引信息同樣存儲(chǔ)在 boltDB。Snapshot 內(nèi)置支持多種 UnionFS(如 overlay,aufs,btrfs)。
??Containers,以 container ID 為索引,在 boltDB 中存儲(chǔ)了低級運(yùn)行時(shí)描述、 snapshot 文件系統(tǒng)類型、 snapshotKey(work layer id)、image reference 等信息。
??Diff,可用于比對 image layer tar 和 fs layers 差異輸出 diffID,可以校驗(yàn) image config 中的 diffID,同樣也能比對 fs layers 之間的差異。
基于以上模塊,containerd 提供了 namespace 隔離,實(shí)現(xiàn)上是在各模塊的內(nèi)容放置于不同目錄樹,達(dá)到資源隔離效果。比如,它可以一邊服務(wù)于 Docker,一邊服務(wù) k8s kubelet,做到兩不沖突。
還有重要模塊是 Tasks (runtime.PlatformRuntime),它負(fù)責(zé)容器進(jìn)程管理和與低級運(yùn)行時(shí)打交道,對上統(tǒng)一了容器進(jìn)程運(yùn)行接口。v1 版 Tasks 只支持 Linux,1.2.0 (2018/11) 后 containerd 正式支持 Windows,新引入的 v2 版 Tasks 核心邏輯使用平臺(tái)無關(guān)代碼實(shí)現(xiàn),因此可以在 Go 語言支持的大部分平臺(tái)運(yùn)行(包括 macOS darwin/amd64)。
containerd 運(yùn)行容器,一般先從 Images 模塊觸發(fā),結(jié)合 Snapshot 模塊建立新的容器 fs layer,加上低級運(yùn)行時(shí)信息,組合成 container 結(jié)構(gòu)體。containerd 利用 container 結(jié)構(gòu)體,將之前的所有 Snapshots 轉(zhuǎn)換為 Mounts 對象(聲明了所有子文件夾的位置和掛載方式),結(jié)合低級運(yùn)行時(shí)、OCI spec、鏡像等信息在請求體中,向 Tasks 模塊提交任務(wù)請求。Tasks 模塊 Manager 根據(jù)任務(wù)低級運(yùn)行時(shí)信息(如 io.containerd.runc.v1),組合出統(tǒng)一的 containerd-shim 進(jìn)程運(yùn)行命令,通過系統(tǒng)調(diào)用啟動(dòng) shim 進(jìn)程,并同步建立與 shim 進(jìn)程的 TTRPC 通信。隨后將任務(wù)交給 shim 進(jìn)程管理。shim 進(jìn)程接到請求后,判知 Mounts 長度大于 0,則會(huì)按照 Mounts 聲明的掛載方式,使用 overlay、aufs 等聯(lián)合文件系統(tǒng)將所有子文件夾組成容器運(yùn)行需要的 rootfs,結(jié)合 OCI spec 調(diào)用低級運(yùn)行時(shí)運(yùn)行容器進(jìn)程并將結(jié)果返回給 containerd 進(jìn)程。
使用 shim 進(jìn)程管理容器進(jìn)程好處很多,containerd clash,containerd-shim 進(jìn)程和容器進(jìn)程不會(huì)受影響,containerd 恢復(fù)后只需讀取運(yùn)行目錄的 socket 文件及 pid 恢復(fù)與 shim 進(jìn)程通信即可快速還原 Tasks 信息(Unix 平臺(tái)),同一容器進(jìn)程出現(xiàn)問題,對于其他進(jìn)程來說是隔離。最重要的是,通過統(tǒng)一的 shim 接口,同一套 containerd 代碼可以同時(shí)兼容多個(gè)不同的運(yùn)行時(shí),也能同時(shí)兼容不同操作系統(tǒng)平臺(tái)。
containerd 不提供容器網(wǎng)絡(luò)和容器應(yīng)用狀態(tài)存儲(chǔ)解決方案,而是把它們留給了更高層的實(shí)現(xiàn)。
container 在其?介紹?中提到:其設(shè)計(jì)目的是成為大系統(tǒng)中的一個(gè)組件(如 Kubernetes, Docker),而非直接供用戶使用。
containerd is designed to be embedded into a larger system, rather than being used directly by developers or end-users。
下文會(huì)展示這意味著什么。
CRI-O
相比 containerd,CRI-O 的高級運(yùn)行時(shí)功能基于若干開源庫實(shí)現(xiàn),不同模塊之間為純粹 Go 語言依賴,而非通信協(xié)議:
? containers/image 庫用于 Image 下載,下載過程類似 2 階段提交。不同來源的鏡像(如 Docker, Openshift, OCI)先被統(tǒng)一為 ImageSource 通用抽象,接著被分為 3 部分進(jìn)行處理:blob 被放置在系統(tǒng)臨時(shí)文件夾,manifest 和 signature 緩存在內(nèi)存(Put*)。之后,鏡像內(nèi)容 Commit 至 containers/storage 庫。
??CRI-O 大部分業(yè)務(wù)邏輯集中在 containers/storage 之上
??LayerStore 接口統(tǒng)一處理 image layer(不包括 config layer) 和 fs layer,鏡像 Commit 存儲(chǔ)時(shí),LayerStore 先調(diào)用 fs 驅(qū)動(dòng)實(shí)現(xiàn)(如 overlay)在磁盤創(chuàng)建 fs layer 目錄并記錄層次關(guān)系,接著調(diào)用 ApplyDiff 方法,解壓內(nèi)容被存放在 layer 目錄(經(jīng)驅(qū)動(dòng)實(shí)現(xiàn)),未解壓內(nèi)容被存放在 image layer 目錄,fs layer 層次關(guān)系存儲(chǔ)在 json 文件。
??ImageStore 接口處理 image meta,包括 manifest、config 和 signature,meta 與 layer 關(guān)聯(lián)索引存儲(chǔ)在 json 文件。
??ContainerStore 接口管理 container meta,創(chuàng)建 container 的步驟和存儲(chǔ) image layer 代碼路徑近乎重合,只不過前者被限制為 read 模式,后者為 readWrite,且沒有 ApplyDiff(diff 送空),meta 與 layer 關(guān)聯(lián)索引也存儲(chǔ)在 json 文件。
containers/storage 庫 container meta 沒有 namespace 概念,但提供一個(gè) metadata 字段(string 類型)可以存儲(chǔ)任意內(nèi)容,CRI-O 便是將包括 namespace 在內(nèi)的業(yè)務(wù)信息序列化為 json string 存儲(chǔ)其中。
CRI-O 運(yùn)行容器進(jìn)程時(shí),先確保對應(yīng) image 存在(不存在則嘗試下載),隨之基于 image top layer 創(chuàng)建 UnionFS,同時(shí)生成 OCI spec config.json,之后,根據(jù)請求方提供的低級運(yùn)行時(shí)信息(RuntimeHandler),使用不同包裝實(shí)現(xiàn)操作容器進(jìn)程。
??如果 RuntimeHandler 為非 VM 類型,創(chuàng)建并委托監(jiān)視進(jìn)程?conmon?操作低級運(yùn)行時(shí)創(chuàng)建容器。之后,conmon 在特定路徑提供一個(gè)可與容器進(jìn)程通信的 socket 文件,并負(fù)責(zé)持續(xù)監(jiān)視容器進(jìn)程并負(fù)責(zé)將其 stream 寫入指定日志文件。容器進(jìn)程創(chuàng)建成功之后,CRI-O 直接與低級運(yùn)行時(shí)交互執(zhí)行 start、delete、update 等操作,或者通過 socket 文件直接與容器進(jìn)程交互。
??如果 RuntimeHandler 為 VM,則創(chuàng)建并委托 containerd-shim 進(jìn)程處理間接容器進(jìn)程(請求包含完整 rootfs,Mounts 為 空)。與非 VM 類型不同,此后所有容器進(jìn)程相關(guān)操作均通過 shim 完成。
CRI-O 架構(gòu)圖
CRI-O 依靠 CNI 插件(默認(rèn)路徑 /opt/cni/bin)為容器進(jìn)程提供網(wǎng)絡(luò)實(shí)現(xiàn)。其邏輯一般在低級運(yùn)行時(shí)創(chuàng)建完隔離進(jìn)程返回后,獲取 pid 后將對應(yīng)的 network namespace path(/proc/{pid}/ns/net)交給 CNI 處理,CNI 會(huì)根據(jù)配置會(huì)往對應(yīng) namespace 添加好網(wǎng)卡。一般地,容器進(jìn)程會(huì)在 cni 網(wǎng)橋上獲得一個(gè)獨(dú)立 IP 地址,這個(gè) IP 地址能與宿主機(jī)通信,如果 CNI 配置了 flannel 之類的 overlay 實(shí)現(xiàn),容器甚至能夠與其他主機(jī)的同一網(wǎng)段容器進(jìn)程通信,具體視配置而定。細(xì)節(jié)方面可以參考?這篇介紹。
如果指定由其管理 network namespace 生命周期(配置 manage_ns_lifecycle),則會(huì)在創(chuàng)建 sandbox 容器時(shí)采用類似?理解 OCI#給 runC 容器綁定虛擬網(wǎng)卡?的方式創(chuàng)建虛擬網(wǎng)卡,隨后通過 OCI json config 傳遞對應(yīng)路徑給低級運(yùn)行時(shí)。同樣地,當(dāng) sandbox 容器銷毀時(shí),CRI-O 會(huì)自動(dòng)回收對應(yīng) namespace 資源。這部分邏輯的網(wǎng)絡(luò)相關(guān)代碼使用 C 語言實(shí)現(xiàn),在 CRI-O 中以名為 pinns 的二進(jìn)制程序發(fā)行。
需要指出的是,CRI-O 使用文件掛載方式配置容器 hostname, dns server 等,而非 CNI 插件。
Docker
Docker 是一個(gè)大而完備的高級運(yùn)行時(shí),其用戶端核心叫做?Docker Engine,由 3 部分構(gòu)成:Docker Server (docker daemon, 簡稱 dockerd)、REST API 和 Docker cli。借助 Docker Engine 既能便捷地運(yùn)行容器進(jìn)程進(jìn)行集成開發(fā)、也能快速構(gòu)建分發(fā)鏡像。
img
如上圖所示,Docker Engine 的核心是 dockerd,既驅(qū)動(dòng)鏡像的構(gòu)建分發(fā),也為容器運(yùn)行提供成熟的持久實(shí)現(xiàn)和網(wǎng)絡(luò)實(shí)現(xiàn)。Docker cli 使用 REST API 與 dockerd 交互。
與上文其他運(yùn)行時(shí)不同,dockerd 以 image config 為核心,使用 config layer 的 sha256 hash 值索引 image 抽象,而不是 manifest。實(shí)際上,dockerd 根本不存儲(chǔ) manifest。dockerd 也不存儲(chǔ) image layers(tar, tar.gz 等),而只存儲(chǔ)解壓后的 layer fs 和一些必要的索引。
??鏡像下載時(shí),dockerd 先自 registry 獲取 manifest 文件,隨后并行下載存儲(chǔ) image layers 和 config layer。與 containers/storag 類似,image layer 解壓內(nèi)容由 fs 驅(qū)動(dòng)實(shí)現(xiàn)(如 overlay) 存儲(chǔ)至新建的子目錄中(如 /var/lib/docker/overlay2/{new-dir}),不同的是,隨后 dockerd 只是以 layer chainID 為索引,存儲(chǔ) fs new-dir、diffID、parent chainID、size 等必要信息,并不存儲(chǔ)未解壓 tar 或 tar.gz。image layers 和 config layer 均存儲(chǔ)完成后,再以 image reference 為索引,建立 reference 至 image ID 映射。作為鏡像分發(fā)模塊的一部分,dockerd 還會(huì)以 manifest layer digest 為索引,建立 digest 至 diffID 映射;以 diffID 為索引,建立 diffID 至 repository 和 digest 映射。
??鏡像推送不過是鏡像下載的逆過程。dockerd 先使用 reference 獲取 imageID(也即 image config),隨后以 imageID 為中心組織出目標(biāo) manifest,對應(yīng)的 layer fs 開始被壓縮成目標(biāo)格式(一般是 tar.gz)。layers 開始上傳時(shí),自分發(fā)模塊獲取 diffID 至 repository 和 digest 信息,發(fā)起遠(yuǎn)程請求確認(rèn)對應(yīng) layer 是否已存在,存在則跳過上傳,最終以 manifest 為中心的鏡像被分發(fā)至對應(yīng) Registry 實(shí)現(xiàn)。
Docker Engine 配套了成熟的鏡像構(gòu)建技術(shù),它使得開發(fā)者只需提供一個(gè)目錄、一份 Dockerfile,外加一行?docker build?命令即可構(gòu)建鏡像。簡單來看,鏡像構(gòu)建過程即是把應(yīng)用依賴的文件系統(tǒng)和運(yùn)行環(huán)境轉(zhuǎn)化為 image layers 和 config 的過程,構(gòu)建結(jié)果是能夠索引到構(gòu)建結(jié)果的 reference,即我們熟悉的 tag。但簡單的接口后面隱藏著非常多的考量,比如怎樣提高鏡像構(gòu)建速度,比如怎樣檢查構(gòu)建期錯(cuò)誤。我們已經(jīng)知道一份鏡像包含多份 layers,基于什么鏡像構(gòu)建新鏡像就會(huì)在之前的 layers 上構(gòu)建新 layers。實(shí)際上,dockerd 會(huì)將 Dockerfile 中的每一行命令轉(zhuǎn)化為一個(gè)構(gòu)建子步驟,每執(zhí)行一步,都可能產(chǎn)生中間鏡像和中間容器。COPY,?ADD?等文件傳輸命令一般直接產(chǎn)生中間鏡像,RUN、ENV、EXPOSE?等運(yùn)行命令會(huì)產(chǎn)生中間容器。每成功一步,該步驟產(chǎn)生的中間鏡像或者 config 就會(huì)成為下一步的基礎(chǔ),產(chǎn)生的中間容器隨之被移除,產(chǎn)生的中間鏡像會(huì)被保存供后續(xù)復(fù)用。構(gòu)建結(jié)束時(shí),最后一步產(chǎn)生的鏡像會(huì)被關(guān)聯(lián)到 tag(如果指定了)。dockerd 維護(hù)了鏡像構(gòu)建過程產(chǎn)生的 parent-child 關(guān)系,使用?docker image ls?命令羅列鏡像時(shí),沒有 tag 且存在 child 的鏡像會(huì)被過濾,如此便過濾了中間鏡像。此外,docker cli 會(huì)將中間結(jié)果輸出到控制臺(tái),這樣如果構(gòu)建出錯(cuò),用戶可以利用間鏡像和中間容器排查問題。
Docker 容器創(chuàng)建運(yùn)行相較 containerd 和 CRI-O 有更多高層的存儲(chǔ)和網(wǎng)絡(luò)抽象,如使用?-v,--volume?命令即可聲明運(yùn)行時(shí)需掛載的文件系統(tǒng),使用?-p,--publish?即可聲明 host 網(wǎng)絡(luò)至容器網(wǎng)絡(luò)映射,這些聲明信息會(huì)被持久在 docker 工作目錄下的 containers 子目錄。
執(zhí)行運(yùn)行命令之際,dockerd 首先生成容器讀寫層并通過 UnionFS 與 fs layers 一道轉(zhuǎn)化為 rootfs。接著,image config 中的環(huán)境、啟動(dòng)參數(shù)等信息被轉(zhuǎn)化為 OCI runtime spec 參數(shù)。同時(shí)類似 CRI-O,dockerd 會(huì)為容器生成一些特殊的文件,如 /etc/hosts, /etc/hostname, /etc/resolv.conf, /dev/shim 等,隨之這些特殊文件與 volume 聲明或者 mount 聲明一起作為 dockerd Mount 抽象轉(zhuǎn)化為 OCI runtime spec Mount 參數(shù)。最后,rootfs、OCI runtime spec 和低級運(yùn)行時(shí)信息通過 RPC 請求傳遞給 containerd,劇情變得和 containerd 運(yùn)行容器一致。
不難發(fā)現(xiàn),雖然持久掛載驅(qū)動(dòng)各異,但對運(yùn)行時(shí)而言,本質(zhì)都是將宿主機(jī)某類型的文件目錄映射到容器文件系統(tǒng)中。因此對于低級運(yùn)行時(shí)而言,掛載邏輯可以統(tǒng)一。dockerd 在此之上發(fā)展了豐富的持久業(yè)務(wù)層,以便于用戶使用。mount 用于直接將宿主機(jī)目錄掛載至容器文件系統(tǒng);volume 相對 bind mounts 優(yōu)勢是對應(yīng)文件持久在 dockerd 的工作目錄,由 dockerd 管理,同時(shí)具有跨平臺(tái)能力。tmpfs 則由操作系統(tǒng)提供容器讀寫層之外的臨時(shí)存儲(chǔ)能力。
dockerd 支持多種網(wǎng)絡(luò)驅(qū)動(dòng),其基礎(chǔ)抽象叫做 endpoint,可以簡單將 endpoint 理解為網(wǎng)卡背后的網(wǎng)絡(luò)資源。對于每一 endpoint,dockerd 都會(huì)通過 IPAM 實(shí)現(xiàn)在 docker0 網(wǎng)橋上分配 IP 地址,接著通過 bridge 等驅(qū)動(dòng)為容器創(chuàng)建網(wǎng)卡,如果使用?publish?參數(shù)配置了容器至宿主機(jī)的 port 映射,dockerd 會(huì)往宿主機(jī) iptable 添加對應(yīng)網(wǎng)絡(luò)規(guī)則,同時(shí)還可能會(huì)啟動(dòng) docker proxy 服務(wù) forward 流量到容器。容器的所有 endpoints 被放置在 sandbox 抽象中。準(zhǔn)備好網(wǎng)絡(luò)資源后,dockerd 調(diào)用 containerd 運(yùn)行容器時(shí),會(huì)在 OCI spec 中設(shè)置 Prestart Hook 命令,該命令包含了設(shè)置網(wǎng)絡(luò)的必要信息(容器ID,容器進(jìn)程ID,sandbox ID)。低級運(yùn)行時(shí)實(shí)現(xiàn)如 runC 會(huì)在容器進(jìn)程被創(chuàng)建但未被運(yùn)行前調(diào)用該命令,該命令最終將容器ID,容器進(jìn)程ID,sandbox ID 傳遞給 dockerd,dockerd 隨即將 sandbox 中的所有 endpoint 資源綁定到容器網(wǎng)絡(luò) namespace 中(也是 /proc/{ctr-pid}/ns/net)。
總結(jié)
上文簡述了 containerd, CRI-O 和 Docker 運(yùn)行時(shí)的基本原理和其基于低級運(yùn)行時(shí)提供的高級功能。Docker 作為提供功能最多最高層實(shí)現(xiàn),放在最后是方便漸進(jìn)式理解容器技術(shù)構(gòu)成。
實(shí)際上,目前容器生態(tài)的技術(shù)和 OCI 標(biāo)準(zhǔn),大都源自 Docker。Docker 抽離其容器管理邏輯發(fā)展出了 containerd 項(xiàng)目,并隨后使用它作為自己的低層運(yùn)行時(shí)。
libnetwork 庫?賦能了 docker (19.03) 網(wǎng)絡(luò)實(shí)現(xiàn),也演化自 Docker。
上文提到,Docker 鏡像構(gòu)建過程會(huì)產(chǎn)生中間鏡像和中間容器,這類中間產(chǎn)物提升了構(gòu)建速度,但是也帶來了使用負(fù)擔(dān)(看著莫名其妙,清理費(fèi)勁)。同時(shí),很多公司有持續(xù)、大規(guī)模構(gòu)建鏡像的需求,他們往往希望負(fù)責(zé)構(gòu)建鏡像系統(tǒng)能夠以 HTTP 或者 gRPC 的方式對其他系統(tǒng)暴露服務(wù),而 dockerd 在設(shè)計(jì)上只是一個(gè)本地服務(wù)。因此在 2017 后,dockerd 中的構(gòu)建功能逐步發(fā)展成了?buildkit 項(xiàng)目,對應(yīng)考量見?docker issuse 32925。Docker 在 18.06 版本后開始支持 buildkit,使用此種方式構(gòu)建鏡像有著相近的性能且不會(huì)產(chǎn)生中間鏡像和中間容器。
從 Docker 業(yè)務(wù)層越變越薄的情況可以看出,隨著社區(qū)對 OCI 規(guī)范的靠攏,容器技術(shù)模塊朝著越來越精細(xì)化的方向發(fā)展,同時(shí)模塊的復(fù)用程度變得越來越強(qiáng)。如果某家公司想要加強(qiáng)容器的隔離能力,只需關(guān)心如何結(jié)合操作系統(tǒng)技術(shù)實(shí)現(xiàn)低級運(yùn)行并基于 containerd 提供 shim 實(shí)現(xiàn)即可迅速將自家技術(shù)集成進(jìn) Docker 或者 Kubernetes,這樣就沒有必要把高級運(yùn)行時(shí)提供的能力再實(shí)現(xiàn)一遍。這種類比可以推廣到網(wǎng)絡(luò)、存儲(chǔ)、鏡像分發(fā)等方面。
CRI-O 項(xiàng)目初衷是嫌棄 Docker 功能太多,打算做一個(gè) Kubernetes 專用運(yùn)行時(shí),不需要鏡像構(gòu)建、不需要鏡像推送、不需要復(fù)雜的網(wǎng)絡(luò)和存儲(chǔ)。但它的業(yè)務(wù)層同樣很薄,代碼多復(fù)用社區(qū)的 containers/storage 庫和 containers/image 庫,同時(shí)會(huì)利用 containerd-shim 運(yùn)行 vm container。運(yùn)行 Linux container 情況下,純 C 的 conmon 守護(hù)進(jìn)程實(shí)現(xiàn)相較 Go 實(shí)現(xiàn)的 containerd-shim 有更少的內(nèi)存消耗。
另外兩個(gè)運(yùn)行時(shí)?PouchContainer?和?frakti?社區(qū)的日趨死寂在另一面反映了這種演進(jìn)趨勢。PouchContainer 最近一次發(fā)布還在 2019 年 1 月,frakti 是 2018 年 11 月。隨著 containerd 跨平臺(tái)能力的加強(qiáng)和其對 Kubernetes 的直接支持(2018/11 1.2.0 引入 shim-v2、CRI 插件),很多低級運(yùn)行時(shí),如 gvisor、kata-runtime,更趨向于直接提供 containerd-shim 實(shí)現(xiàn)以集成進(jìn)容器生態(tài),而不是再造一邊輪子。PouchContainer 試圖打造一個(gè)鏡像分發(fā)速度更快(利用 P2P),強(qiáng)隔離(利用 vm container、lxcfs 等),隨著 containerd 和 Docker 的演進(jìn),這些 feature 優(yōu)勢變得越來越小,開源社區(qū)對 PouchContainer 的興趣越來越弱實(shí)屬當(dāng)然。frakti 目的是打造一個(gè)支持 runV(kata-runtime 前身)的 Kubernetes 運(yùn)行時(shí),隨著 runV 和 Clear Containers 合而為 kata-containers 項(xiàng)目,而后者可利用 containerd-shim 直接集成進(jìn)生態(tài),frakti 便變得越來越無意義。
編輯:黃飛
?
評論
查看更多