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