容器中的 Shim 到底是個(gè)什么鬼?

Kubernetes 1.20 版開始廢除了對(duì) dockershim 的支持,改用 Containerd[1] 作為默認(rèn)的容器運(yùn)行時(shí)。本文將介紹 Containerd 中的 "shim" 接口。
每一個(gè) Containerd 或 Docker 容器都有一個(gè)相應(yīng)的 "shim" 守護(hù)進(jìn)程,這個(gè)守護(hù)進(jìn)程會(huì)提供一個(gè) API,Containerd 使用該 API 來管理容器基本的生命周期(啟動(dòng)/停止),在容器中執(zhí)行新的進(jìn)程、調(diào)整 TTY 的大小以及與特定平臺(tái)相關(guān)的其他操作。shim 還有一個(gè)作用是向 Containerd 報(bào)告容器的退出狀態(tài),在容器退出狀態(tài)被 Containerd 收集之前,shim 會(huì)一直存在。這一點(diǎn)和僵尸進(jìn)程很像,僵尸進(jìn)程在被父進(jìn)程回收之前會(huì)一直存在,只不過僵尸進(jìn)程不會(huì)占用資源,而 shim 會(huì)占用資源。
shim 將 Containerd 進(jìn)程從容器的生命周期中分離出來,具體的做法是 runc 在創(chuàng)建和運(yùn)行容器之后退出,并將 shim 作為容器的父進(jìn)程,即使 Containerd 進(jìn)程掛掉或者重啟,也不會(huì)對(duì)容器造成任何影響。這樣做的好處很明顯,你可以高枕無憂地升級(jí)或者重啟 Containerd,不會(huì)對(duì)運(yùn)行中的容器產(chǎn)生任何影響。Docker 的 --live-restore[2] 特征也實(shí)現(xiàn)了類似的功能。
Containerd 支持哪些 shim?
Containerd 目前官方支持的 shim 清單:
io.containerd.runtime.v1.linux
io.containerd.runtime.v1.linux 是最原始的 shim API 和實(shí)現(xiàn)的 v1 版本,在 Containerd 1.0 之前被設(shè)計(jì)出來。該 shim 使用 runc 來執(zhí)行容器,并且只支持 cgroup v1。目前 v1 版 shim API 已被廢棄,并將于 Containerd 2.0 被刪除。
io.containerd.runc.v1
io.containerd.runc.v1 與 io.containerd.runtime.v1.linux 的實(shí)現(xiàn)類似,唯一的區(qū)別是它使用了 v2 版本 shim API。該 shim 仍然只支持 cgroup v1。
io.containerd.runc.v2
該 shim 與 v1 采用了完全不同的實(shí)現(xiàn),并且使用了 v2 版本 shim API,同時(shí)支持 cgroup v1 和 v2。該 shim 進(jìn)程以運(yùn)行多個(gè)容器,用于 Kubernetes 的 CRI 實(shí)現(xiàn),可以在一個(gè) Pod 中運(yùn)行多個(gè)容器。
io.containerd.runhcs.v1
這是 Windows 平臺(tái)的 shim,使用 Window 的 HCSv2 API 來管理容器。
當(dāng)然,除了官方正式支持的 shim 之外,任何人都可以編寫自己的 shim,并讓 Containerd 調(diào)用該 shim。Containerd 在調(diào)用時(shí)會(huì)將 shim 的名稱解析為二進(jìn)制文件,并在 $PATH 中查找這個(gè)二進(jìn)制文件。例如 io.containerd.runc.v2 會(huì)被解析成二進(jìn)制文件 containerd-shim-runc-v2,io.containerd.runhcs.v1 會(huì)被解析成二進(jìn)制文件 containerd-shim-runhcs-v1.exe。客戶端在創(chuàng)建容器時(shí)可以指定使用哪個(gè) shim,如果不指定就使用默認(rèn)的 shim。
下面是一個(gè)示例,用來指定將要使用的 shim:
package?main
import?(
????"context"
????"github.com/containerd/containerd"
????"github.com/containerd/containerd/namespaces"
????"github.com/containerd/containerd/oci"
????v1opts?"github.com/containerd/containerd/pkg/runtimeoptions/v1"
)
func?main()?{
????ctx?:=?namespaces.WithNamespace(context.TODO(),?"default")
????//?Create?containerd?client
????client,?err?:=?containerd.New("/run/containerd/containerd.sock")
????if?err?!=?nil?{
????????panic(err)
????}
????//?Get?the?image?ref?to?create?the?container?for
????img,?err?:=?client.GetImage(ctx,?"docker.io/library/busybox:latest")
????if?err?!=?nil?{
????????panic(err)
????}
????//?set?options?we?will?pass?to?the?shim?(not?really?setting?anything?here,?but?we?could)
????var?opts?v1opts.Options
????//?Create?a?container?object?in?containerd
????cntr,?err?:=?client.NewContainer(ctx,?"myContainer",
????????//?All?the?basic?things?needed?to?create?the?container
????????containerd.WithSnapshotter("overlayfs"),
????????containerd.WithNewSnapshot("myContainer-snapshot",?img),
????????containerd.WithImage(img),
????????containerd.WithNewSpec(oci.WithImageConfig(img)),
????????//?Set?the?option?for?the?shim?we?want
????????containerd.WithRuntime("io.containerd.runc.v1",?&opts),
????)
????if?err?!=?nil?{
????????panic(err)
????}
????//?cleanup
????cntr.Delete(ctx)
}
??注意:
WithRuntime將interface{}作為第二個(gè)參數(shù),可以傳遞任何類型給 shim。只要確保你的 shim 能夠識(shí)別這個(gè)類型的數(shù)據(jù),并在 typeurl 包中注冊(cè)這個(gè)類型,以便它能被正確編碼。
每個(gè) shim 都有自己支持的一組配置選項(xiàng),可以單獨(dú)針對(duì)每個(gè)容器進(jìn)行配置。例如 io.containerd.runc.v2 可以將容器的 stdout/stderr 轉(zhuǎn)發(fā)到一個(gè)單獨(dú)的進(jìn)程,為 shim 的運(yùn)行設(shè)置自定義的 cgroup 等等。你可以創(chuàng)建自定義的 shim,在容器運(yùn)行時(shí)添加自定義的選項(xiàng)。總的來說,shim 的 API 包含了 RPC 和一些二進(jìn)制調(diào)用用于創(chuàng)建/刪除 shim,以及到 Containerd 進(jìn)程的反向通道。
如果你想實(shí)現(xiàn)自己的 shim,下面是相關(guān)參考資料:
(v2) shim RPC API 的詳細(xì)定義[3] 實(shí)現(xiàn) shim 二進(jìn)制和RPC API的輔助工具[4] shim 的使用方式[5]
你只需要實(shí)現(xiàn)一個(gè)接口,shim.Run 會(huì)處理剩下的事情。shim 需要重點(diǎn)關(guān)注的是內(nèi)存使用,因?yàn)槊總€(gè)容器都有一個(gè) shim 進(jìn)程,隨著容器數(shù)量的增加,shim 的內(nèi)存使用會(huì)急劇上升。shim 的 API 是在 protobuf 中定義的,看起來有點(diǎn)像 gRPC 的 API,但實(shí)際上 shim 使用的是一個(gè)叫做 ttrpc[6] 的自定義協(xié)議,與 gRPC 并不兼容。ttrpc 是一個(gè)原 RPC 協(xié)議,專為降低內(nèi)存使用而設(shè)計(jì)。
創(chuàng)建容器的 RPC 調(diào)用流程
Containerd 中有一個(gè) container 對(duì)象,當(dāng)你創(chuàng)建一個(gè) container 對(duì)象,只是創(chuàng)建了一些與容器相關(guān)的數(shù)據(jù),并將這些數(shù)據(jù)存儲(chǔ)到本地?cái)?shù)據(jù)庫(kù)中,并不會(huì)在系統(tǒng)中啟動(dòng)任何容器。container 對(duì)象創(chuàng)建成功后,客戶端會(huì)從 container 對(duì)象中創(chuàng)建一個(gè) task,接下來是調(diào)用 shim API。
以下是 RPC 調(diào)用的總體流程:
客戶端調(diào)用
container.NewTask(…),containerd 根據(jù)指定或默認(rèn)的運(yùn)行時(shí)名稱解析 shim 二進(jìn)制文件,例如:io.containerd.runc.v2->containerd-shim-runc-v2。containerd 通過 start 命令啟動(dòng) shim 二進(jìn)制文件,并加上一些額外的參數(shù),用于定義命名空間、OCI bundle 路徑、調(diào)試模式、返回給 containerd 的 unix socket 路徑等。在這一步調(diào)用中,當(dāng)前工作目錄設(shè)置為 shim 的工作路徑。
此時(shí),新創(chuàng)建的 shim 進(jìn)程會(huì)向
stdout寫一個(gè)連接字符串,以允許 containerd 連接到 shim ,進(jìn)行 API 調(diào)用。一旦連接字符串初始化完成,shim 開始監(jiān)聽之后,start 命令就會(huì)返回。containerd 使用 shim start 命令返回的連接字符串,打開一個(gè)與 shim API 的連接。
containerd 使用 OCI bundle 路徑和其他選項(xiàng),調(diào)用 Create shim RPC。這一步會(huì)創(chuàng)建所有必要的 沙箱,并返回沙箱進(jìn)程的 pid。以 runc 為例,我們使用
runc create --pid-file=命令創(chuàng)建容器,runc 會(huì)分叉出一個(gè)新進(jìn)程(runc init)用來設(shè)置沙箱,然后等待調(diào)用runc start,所有這些都準(zhǔn)備好后,runc create 命令就會(huì)返回結(jié)果。在 runc create 返回結(jié)果之前,runc 會(huì)將 runc-init 進(jìn)程的 pid 寫入定義的 pid 文件中,客戶端可以使用這個(gè) pid 來做一些操作,比如在沙箱中設(shè)置網(wǎng)絡(luò)(網(wǎng)絡(luò)命名空間可以在/proc/中設(shè)置)。/ns/net create 調(diào)用還會(huì)提供一個(gè)掛載列表以構(gòu)建 rootfs,還包含 checkpoint 信息。
下一步客戶端調(diào)用
task.Wait,觸發(fā) containerd 調(diào)用 shim ?WaitAPI。這是一個(gè)持久化的請(qǐng)求,只有在容器退出后才會(huì)返回。到這一步仍然不會(huì)啟動(dòng)容器。客戶端繼續(xù)調(diào)用
task.Start,觸發(fā) containerd 調(diào)用 Start shim RPC。這一步才會(huì)真正啟動(dòng)容器,并返回容器進(jìn)程的 pid。這一步,客戶端就可以針對(duì) task 進(jìn)行一些額外的調(diào)用請(qǐng)求。例如,如果 task 包含 TTY,會(huì)請(qǐng)求
task.ResizePTY,或者請(qǐng)求task.Kill來發(fā)送一個(gè)信號(hào)等等。task.Exec比較特殊,它會(huì)調(diào)用 shim Exec RPC,但并沒有在容器中執(zhí)行某個(gè)進(jìn)程,只是在 shim 中注冊(cè)了 exec,后面會(huì)使用 exec ID 來調(diào)用 shimStartRPC。在容器或 exec 進(jìn)程退出后,containerd 將會(huì)調(diào)用 shim
DeleteRPC,清理 exec 進(jìn)程或容器的所有資源。例如,對(duì)于runc shim, 這一步會(huì)調(diào)用 runc delete。containerd 調(diào)用
ShutdownRPC,此時(shí) shim 將會(huì)退出。
shim 的另一個(gè)重要部分是將容器的生命周期事件返回給 containerd ,包括:TaskCreate TaskStart TaskDelete TaskExit, TaskOOM, TaskExecAdded, TaskExecStarted, TaskPaused, TaskResumed, TaskCheckpointed。可參考 task 的詳細(xì)定義[7]。
總結(jié)
Containerd 通過 shim 為底層的容器運(yùn)行時(shí)提供了可插拔能力。雖然這不是使用 Containerd 管理容器的唯一手段,但目前內(nèi)置的 TaskService 使用了該方式,Kubernetes 通過調(diào)用 CRI 來創(chuàng)建 Pod 也是使用的 shim。由此可見 shim 這種方式很受歡迎,它不但增強(qiáng)了 Containerd 的擴(kuò)展能力,以支持更多平臺(tái)和基于虛擬機(jī)的運(yùn)行時(shí)(firecracker[8], kata[9]),而且允許嘗試其他 shim 實(shí)現(xiàn)(systemd[10])。
引用鏈接
Containerd: https://containerd.io/
[2]--live-restore: https://docs.docker.com/config/containers/live-restore/
[3](v2) shim RPC API 的詳細(xì)定義: https://github.com/containerd/containerd/blob/v1.5.8/runtime/v2/task/shim.proto
[4]實(shí)現(xiàn) shim 二進(jìn)制和RPC API的輔助工具: https://github.com/containerd/containerd/blob/89370122089d9cba9875f468db525f03eaf61e96/runtime/v2/shim/shim.go#L181-L194
[5]shim 的使用方式: https://github.com/containerd/containerd/blob/v1.5.8/cmd/containerd-shim-runc-v2/main.go
[6]ttrpc: https://github.com/containerd/ttrpc
[7]task 的詳細(xì)定義: https://github.com/containerd/containerd/blob/v1.5.6/api/events/task.proto
[8]firecracker: https://github.com/firecracker-microvm/firecracker-containerd/tree/main/runtime
[9]kata: https://github.com/kata-containers/kata-containers/tree/2.3.0/src/runtime
[10]systemd: https://github.com/cpuguy83/containerd-shim-systemd-v1
原文鏈接:https://container42.com/2022/01/10/shim-shiminey-shim-shiminey/


你可能還喜歡
點(diǎn)擊下方圖片即可閱讀

云原生是一種信仰???
關(guān)注公眾號(hào)
后臺(tái)回復(fù)?k8s?獲取史上最方便快捷的 Kubernetes 高可用部署工具,只需一條命令,連 ssh 都不需要!


點(diǎn)擊?"閱讀原文"?獲取更好的閱讀體驗(yàn)!
發(fā)現(xiàn)朋友圈變“安靜”了嗎?


