容器中的 Shim 到底是個什么鬼?

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


你可能還喜歡
點擊下方圖片即可閱讀

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


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


