在docker/k8s時(shí)代,經(jīng)常聽(tīng)到CRI, OCI,containerd和各種shim等名詞,看完本篇博文,您會(huì)有個(gè)徹底的理解。
典型的K8S Runtime架構(gòu)
從最常見(jiàn)的Docker說(shuō)起,kubelet和Docker的集成方案圖如下:
當(dāng)kubelet要?jiǎng)?chuàng)建一個(gè)容器時(shí),需要以下幾步:
Kubelet 通過(guò) CRI 接口(gRPC)調(diào)用 dockershim,請(qǐng)求創(chuàng)建一個(gè)容器。CRI 即容器運(yùn)行時(shí)接口(Container Runtime Interface),這一步中,Kubelet 可以視作一個(gè)簡(jiǎn)單的 CRI Client,而 dockershim 就是接收請(qǐng)求的 Server。目前 dockershim 的代碼其實(shí)是內(nèi)嵌在 Kubelet 中的,所以接收調(diào)用的湊巧就是 Kubelet 進(jìn)程;
dockershim 收到請(qǐng)求后,轉(zhuǎn)化成 Docker Daemon 能聽(tīng)懂的請(qǐng)求,發(fā)到 Docker Daemon 上請(qǐng)求創(chuàng)建一個(gè)容器。
Docker Daemon 早在 1.12 版本中就已經(jīng)將針對(duì)容器的操作移到另一個(gè)守護(hù)進(jìn)程——containerd 中了,因此 Docker Daemon 仍然不能幫我們創(chuàng)建容器,而是要請(qǐng)求 containerd 創(chuàng)建一個(gè)容器;
containerd 收到請(qǐng)求后,并不會(huì)自己直接去操作容器,而是創(chuàng)建一個(gè)叫做 containerd-shim 的進(jìn)程,讓 containerd-shim 去操作容器。這是因?yàn)槿萜鬟M(jìn)程需要一個(gè)父進(jìn)程來(lái)做諸如收集狀態(tài),維持 stdin 等 fd 打開(kāi)等工作。而假如這個(gè)父進(jìn)程就是 containerd,那每次 containerd 掛掉或升級(jí),整個(gè)宿主機(jī)上所有的容器都得退出了。而引入了 containerd-shim 就規(guī)避了這個(gè)問(wèn)題(containerd 和 shim 并不是父子進(jìn)程關(guān)系);
我們知道創(chuàng)建容器需要做一些設(shè)置 namespaces 和 cgroups,掛載 root filesystem 等等操作,而這些事該怎么做已經(jīng)有了公開(kāi)的規(guī)范了,那就是 OCI(Open Container Initiative,開(kāi)放容器標(biāo)準(zhǔn))。它的一個(gè)參考實(shí)現(xiàn)叫做 runC。于是,containerd-shim 在這一步需要調(diào)用 runC 這個(gè)命令行工具,來(lái)啟動(dòng)容器;
runC 啟動(dòng)完容器后本身會(huì)直接退出,containerd-shim 則會(huì)成為容器進(jìn)程的父進(jìn)程,負(fù)責(zé)收集容器進(jìn)程的狀態(tài),上報(bào)給 containerd,并在容器中 pid 為 1 的進(jìn)程退出后接管容器中的子進(jìn)程進(jìn)行清理,確保不會(huì)出現(xiàn)僵尸進(jìn)程。
這個(gè)過(guò)程乍一看像是在搞我們:Docker Daemon 和 dockershim 看上去就是兩個(gè)不干活躺在中間劃水的啊,Kubelet 為啥不直接調(diào)用 containerd 呢?
當(dāng)然可以,先看下現(xiàn)在的架構(gòu)為什么如此繁雜。
容器歷史小敘
早期的k8s runtime架構(gòu),遠(yuǎn)沒(méi)這么復(fù)雜,kubelet創(chuàng)建容器,直接調(diào)用docker daemon,docker daemon自己調(diào)用libcontainer就把容器運(yùn)行起來(lái)。
但往往,事情不會(huì)如此簡(jiǎn)單,一系列政治斗爭(zhēng)開(kāi)始了,先是大佬們認(rèn)為運(yùn)行時(shí)標(biāo)準(zhǔn)不能被 Docker 一家公司控制,于是就攛掇著搞了開(kāi)放容器標(biāo)準(zhǔn) OCI。Docker 則把 libcontainer 封裝了一下,變成 runC 捐獻(xiàn)出來(lái)作為 OCI 的參考實(shí)現(xiàn)。
再接下來(lái)就是 rkt(coreos推出的,類(lèi)似docker) 想從 Docker 那邊分一杯羹,希望 Kubernetes 原生支持 rkt 作為運(yùn)行時(shí),而且 PR 還真的合進(jìn)去了。維護(hù)過(guò)一塊業(yè)務(wù)同時(shí)接兩個(gè)需求方的讀者老爺應(yīng)該都知道類(lèi)似的事情有多坑,Kubernetes 中負(fù)責(zé)維護(hù) kubelet 的小組 sig-node 也是被狠狠坑了一把。
大家一看這么搞可不行,今天能有 rkt,明天就能有更多幺蛾子出來(lái),這么搞下去我們小組也不用干活了,整天搞兼容性的 bug 就夠嗆。于是乎,Kubernetes 1.5 推出了 CRI 機(jī)制,即容器運(yùn)行時(shí)接口(Container Runtime Interface),Kubernetes 告訴大家,你們想做 Runtime 可以啊,我們也資瓷歡迎,實(shí)現(xiàn)這個(gè)接口就成,成功反客為主。
不過(guò) CRI 本身只是 Kubernetes 推的一個(gè)標(biāo)準(zhǔn),當(dāng)時(shí)的 Kubernetes 尚未達(dá)到如今這般武林盟主的地位,容器運(yùn)行時(shí)當(dāng)然不能說(shuō)我跟 Kubernetes 綁死了只提供 CRI 接口,于是就有了 shim(墊片)這個(gè)說(shuō)法,一個(gè) shim 的職責(zé)就是作為 Adapter 將各種容器運(yùn)行時(shí)本身的接口適配到 Kubernetes 的 CRI 接口上。
接下來(lái)就是 Docker 要搞 Swarm 進(jìn)軍 PaaS 市場(chǎng),于是做了個(gè)架構(gòu)切分,把容器操作都移動(dòng)到一個(gè)單獨(dú)的 Daemon 進(jìn)程 containerd 中去,讓 Docker Daemon 專(zhuān)門(mén)負(fù)責(zé)上層的封裝編排。可惜 Swarm 在 Kubernetes 面前實(shí)在是不夠打,慘敗之后 Docker 公司就把 containerd 項(xiàng)目捐給 CNCF 縮回去安心搞 Docker 企業(yè)版了。
最后就是我們?cè)谏弦粡垐D里看到的這一坨東西了,盡管現(xiàn)在已經(jīng)有 CRI-O,containerd-plugin 這樣更精簡(jiǎn)輕量的 Runtime 架構(gòu),dockershim 這一套作為經(jīng)受了最多生產(chǎn)環(huán)境考驗(yàn)的方案,迄今為止仍是 Kubernetes 默認(rèn)的 Runtime 實(shí)現(xiàn)。
OCI, CRI
OCI(開(kāi)放容器標(biāo)準(zhǔn)),規(guī)定了2點(diǎn):
容器鏡像要長(zhǎng)啥樣,即 ImageSpec。里面的大致規(guī)定就是你這個(gè)東西需要是一個(gè)壓縮了的文件夾,文件夾里以 xxx 結(jié)構(gòu)放 xxx 文件;
容器要需要能接收哪些指令,這些指令的行為是什么,即 RuntimeSpec。這里面的大致內(nèi)容就是“容器”要能夠執(zhí)行 “create”,“start”,“stop”,“delete” 這些命令,并且行為要規(guī)范。
runC 為啥叫參考實(shí)現(xiàn)呢,就是它能按照標(biāo)準(zhǔn)將符合標(biāo)準(zhǔn)的容器鏡像運(yùn)行起來(lái),標(biāo)準(zhǔn)的好處就是方便搞創(chuàng)新,反正只要我符合標(biāo)準(zhǔn),生態(tài)圈里的其它工具都能和我一起愉快地工作(……當(dāng)然 OCI 這個(gè)標(biāo)準(zhǔn)本身制定得不怎么樣,真正工程上還是要做一些 adapter 的),那我的鏡像就可以用任意的工具去構(gòu)建,我的“容器”就不一定非要用 namespace 和 cgroups 來(lái)做隔離。這就讓各種虛擬化容器可以更好地參與到游戲當(dāng)中,我們暫且不表。
而 CRI 更簡(jiǎn)單,單純是一組 gRPC 接口,掃一眼 kubelet/apis/cri/services.go 就能歸納出幾套核心接口:
一套針對(duì)容器操作的接口,包括創(chuàng)建,啟停容器等等;
一套針對(duì)鏡像操作的接口,包括拉取鏡像刪除鏡像等;
一套針對(duì) PodSandbox(容器沙箱環(huán)境)的操作接口,我們之后再說(shuō)。
現(xiàn)在我們可以找到很多符合 OCI 標(biāo)準(zhǔn)或兼容了 CRI 接口的項(xiàng)目,而這些項(xiàng)目就大體構(gòu)成了整個(gè) Kuberentes 的 Runtime 生態(tài):
OCI Compatible:runC,Kata(以及它的前身 runV 和 Clear Containers),gVisor。其它比較偏門(mén)的還有 Rust 寫(xiě)的 railcar
CRI Compatible:Docker(借助 dockershim),containerd(借助 CRI-containerd),CRI-O,F(xiàn)rakti,etc
OCI, CRI 確實(shí)不是一個(gè)好名字,在這篇文章的語(yǔ)境中更準(zhǔn)確的說(shuō)法:cri-runtime 和 oci-runtime。通過(guò)這個(gè)粗略的分類(lèi),我們其實(shí)可以總結(jié)出整個(gè) Runtime 架構(gòu)萬(wàn)變不離其宗的三層抽象:
Orchestration API -> Container API(cri-runtime) -> Kernel API(oci-runtime)
根據(jù)這個(gè)思路,我們就很容易理解下面這兩種東西:
各種更為精簡(jiǎn)的 cri-runtime(反正就是要干掉 Docker)
各種“強(qiáng)隔離”容器方案
Containerd和CRI-O
上一節(jié)看到現(xiàn)在的 Runtime 實(shí)在是有點(diǎn)復(fù)雜了,而復(fù)雜是萬(wàn)惡之源(其實(shí)本質(zhì)上就是想干掉 Docker),于是就有了直接拿 containerd 做 oci-runtime 的方案。當(dāng)然,除了 Kubernetes 之外,containerd 還要接諸如 Swarm 等調(diào)度系統(tǒng),因此它不會(huì)去直接實(shí)現(xiàn) CRI,這個(gè)適配工作當(dāng)然就要交給一個(gè) shim 了。
containerd 1.0 中,對(duì) CRI 的適配通過(guò)一個(gè)單獨(dú)的進(jìn)程 CRI-containerd 來(lái)完成:
containerd 1.1 中做的又更漂亮一點(diǎn),砍掉了 CRI-containerd 這個(gè)進(jìn)程,直接把適配邏輯作為插件放進(jìn)了 containerd 主進(jìn)程中:
但在 containerd 做這些事情前,社區(qū)就已經(jīng)有了一個(gè)更為專(zhuān)注的 cri-runtime:CRI-O,它非常純粹,就是兼容 CRI 和 OCI,做一個(gè) Kubernetes 專(zhuān)用的運(yùn)行時(shí):
其中 conmon 就對(duì)應(yīng) containerd-shim,大體意圖是一樣的。
CRI-O 和(直接調(diào)用)containerd 的方案比起默認(rèn)的 dockershim 確實(shí)簡(jiǎn)潔很多,但沒(méi)啥生產(chǎn)環(huán)境的驗(yàn)證案例,我所知道的僅僅是 containerd 在 GKE 上是 beta 狀態(tài)。因此假如你對(duì) Docker 沒(méi)有特殊的政治恨意,大可不必把 dockershim 這套換掉。
強(qiáng)隔離容器:Kata,gVisor,F(xiàn)irecracker
一直以來(lái),K8S都難以實(shí)現(xiàn)真正的多租戶。
理想來(lái)說(shuō),平臺(tái)的各個(gè)租戶(tenant)之間應(yīng)該無(wú)法感受到彼此的存在,表現(xiàn)得就像每個(gè)租戶獨(dú)占這整個(gè)平臺(tái)一樣。具體來(lái)說(shuō),我不能看到其它租戶的資源,我的資源跑滿了不能影響其它租戶的資源使用,我也無(wú)法從網(wǎng)絡(luò)或內(nèi)核上攻擊其它租戶。
Kubernetes 當(dāng)然做不到,其中最大的兩個(gè)原因是:
kube-apiserver 是整個(gè)集群中的單例,并且沒(méi)有多租戶概念
默認(rèn)的 oci-runtime 是 runC,而 runC 啟動(dòng)的容器是共享內(nèi)核的
對(duì)于第二個(gè)問(wèn)題,一個(gè)典型的解決方案就是提供一個(gè)新的 OCI 實(shí)現(xiàn),用 VM 來(lái)跑容器,實(shí)現(xiàn)內(nèi)核上的硬隔離。runV 和 Clear Containers 都是這個(gè)思路。因?yàn)檫@兩個(gè)項(xiàng)目做得事情是很類(lèi)似,后來(lái)就合并成了一個(gè)項(xiàng)目 Kata Container。Kata 的一張圖很好地解釋了基于虛擬機(jī)的容器與基于 namespaces 和 cgroups 的容器間的區(qū)別:
當(dāng)然,沒(méi)有系統(tǒng)是完全安全的,假如 hypervisor 存在漏洞,那么用戶仍有可能攻破隔離。但所有的事情都要對(duì)比而言,在共享內(nèi)核的情況下,暴露的攻擊面是非常大的,做安全隔離的難度就像在美利堅(jiān)和墨西哥之間修 The Great Wall,而當(dāng)內(nèi)核隔離之后,只要守住 hypervisor 這道關(guān)子就后顧無(wú)虞了。
嗯,一個(gè) VM 里跑一個(gè)容器,聽(tīng)上去隔離性很不錯(cuò),但不是說(shuō)虛擬機(jī)又笨重又不好管理才切換到容器的嗎,怎么又要走回去了?
Kata 告訴你,虛擬機(jī)沒(méi)那么邪惡,只是以前沒(méi)玩好:
不好管理是因?yàn)闆](méi)有遵循“不可變基礎(chǔ)設(shè)施”,大家都去虛擬機(jī)上這摸摸那碰碰,這臺(tái)裝 Java 8 那臺(tái)裝 Java 6,Admin 是要 angry 的。Kata 則支持 OCI 鏡像,完全可以用上 Dockerfile + 鏡像,讓不好管理成為了過(guò)去時(shí);
笨重是因?yàn)橹耙摂M化整個(gè)系統(tǒng),現(xiàn)在我們只著眼于虛擬化應(yīng)用,那就可以裁剪掉很多功能,把 VM 做得很輕量,因此即便用虛擬機(jī)來(lái)做容器,Kata 還是可以將容器啟動(dòng)時(shí)間壓縮得非常短,啟動(dòng)后在內(nèi)存上和 IO 上的 overhead 也盡可能去優(yōu)化。
不過(guò)話說(shuō)回來(lái),Kubernetes 上的調(diào)度單位是 Pod,是容器組啊,Kata 這樣一個(gè)虛擬機(jī)里一個(gè)容器,同一個(gè) Pod 間的容器還怎么做 namespace 的共享?
這就要說(shuō)回我們前面講到的 CRI 中針對(duì) PodSandbox(容器沙箱環(huán)境)的操作接口了。第一節(jié)中,我們刻意簡(jiǎn)化了場(chǎng)景,只考慮創(chuàng)建一個(gè)容器,而沒(méi)有討論創(chuàng)建一個(gè) Pod。大家都知道,真正啟動(dòng) Pod 里定義的容器之前,kubelet 會(huì)先啟動(dòng)一個(gè) infra 容器,并執(zhí)行 /pause 讓 infra 容器的主進(jìn)程永遠(yuǎn)掛起。這個(gè)容器存在的目的就是維持住整個(gè) Pod 的各種 namespace,真正的業(yè)務(wù)容器只要加入 infra 容器的 network 等 namespace 就能實(shí)現(xiàn)對(duì)應(yīng) namespace 的共享。而 infra 容器創(chuàng)造的這個(gè)共享環(huán)境則被抽象為 PodSandbox。每次 kubelet 在創(chuàng)建 Pod 時(shí),就會(huì)先調(diào)用 CRI 的 RunPodSandbox 接口啟動(dòng)一個(gè)沙箱環(huán)境,再調(diào)用 CreateContainer 在沙箱中創(chuàng)建容器。
這里就已經(jīng)說(shuō)出答案了,對(duì)于 Kata Container 而言,只要在 RunPodSandbox 調(diào)用中創(chuàng)建一個(gè) VM,之后再往 VM 中添加容器就可以了。最后運(yùn)行 Pod 的樣子就是這樣的:
說(shuō)完了 Kata,其實(shí) gVisor 和 Firecracker 都不言自明了,大體上都是類(lèi)似的,只是:
gVisor 并不會(huì)去創(chuàng)建一個(gè)完整的 VM,而是實(shí)現(xiàn)了一個(gè)叫 “Sentry” 的用戶態(tài)進(jìn)程來(lái)處理容器的 syscall,而攔截 syscall 并重定向到 Sentry 的過(guò)程則由 KVM 或 ptrace 實(shí)現(xiàn)。
Firecracker 稱自己為 microVM,即輕量級(jí)虛擬機(jī),它本身還是基于 KVM 的,不過(guò) KVM 通常使用 QEMU 來(lái)虛擬化除 CPU 和內(nèi)存外的資源,比如 IO 設(shè)備,網(wǎng)絡(luò)設(shè)備。Firecracker 則使用 rust 實(shí)現(xiàn)了最精簡(jiǎn)的設(shè)備虛擬化,為的就是壓榨虛擬化的開(kāi)銷(xiāo),越輕量越好。
責(zé)任編輯:xj
原文標(biāo)題:淺析 k8s 容器運(yùn)行時(shí)演進(jìn)
文章出處:【微信公眾號(hào):Linux愛(ài)好者】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
-
容器
+關(guān)注
關(guān)注
0文章
495瀏覽量
22060 -
CRI
+關(guān)注
關(guān)注
1文章
16瀏覽量
12234 -
Docker
+關(guān)注
關(guān)注
0文章
457瀏覽量
11847
原文標(biāo)題:淺析 k8s 容器運(yùn)行時(shí)演進(jìn)
文章出處:【微信號(hào):LinuxHub,微信公眾號(hào):Linux愛(ài)好者】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論