Kubernetes CRI - 容器運行時接口解析
本文轉載自微信公眾號「運維開發故事」,作者沒有文案的夏老師。轉載本文請聯系運維開發故事公眾號。
kubelet 的組件
kubelet 本身,也是按照“控制器”模式來工作的。它實際的工作原理,可以用如下所示的一幅示意圖來表示清楚。
- Kubelet Server 對外提供 API,供 kube-apiserver、metrics-server 等服務調用。比如 kubectl exec 時需要通過 Kubelet API /exec/{token} 與容器進行交互;
- Container Manager 管理容器的各種資源,比如 CGroups、QoS、cpuset、device 等;
- Volume Manager 管理容器的存儲卷,比如格式化磁盤、掛載到 Node 本地、最后再將掛載路徑傳給容器;
- Eviction 負責容器的驅逐,比如在資源不足時驅逐優先級低的容器,保證高優先級容器的運行;
- cAdvisor 負責為容器提供 Metrics;
- Metrics 和 stats 提供容器和節點的度量數據,比如 metrics-server 通過 /stats/summary 提取的度量數據是 HPA 自動擴展的依據;
- Generic Runtime Manager 是容器運行時的管理者,負責與CRI 交互,完成容器和鏡像的管理;
CRI中定義了容器和鏡像的服務的接口,因為容器運行時與鏡像的生命周期是彼此隔離的,因此需要定義兩個服務。該接口使用Protocol Buffer,基于gRPC,在Kubernetes v1.10+版本中是在pkg/kubelet/apis/cri/runtime/v1alpha2的api.proto中定義的。
CRI架構
Kubernetes 中的容器運行時組成
按照不同的功能可以分為四個部分:
(1)kubelet 中容器運行時的管理,kubeGenericRuntimeManager,它管理與 CRI shim 通信的客戶端,完成容器和鏡像的管理(代碼位置:pkg/kubelet/kuberuntime/kuberuntime_manager.go);
(2)容器運行時接口 CRI,包括了容器運行時客戶端接口與容器運行時服務端接口;
(3)CRI shim 客戶端,kubelet 持有,用于與 CRI shim 服務端進行通信;
(4)CRI shim 服務端,即具體的容器運行時實現,包括 kubelet 內置的 dockershim (代碼位置:pkg/kubelet/dockershim)以及外部的容器運行時remote。如 cri-containerd(用于支持容器引擎containerd)、rktlet(用于支持容器引擎rkt)等。
更普遍的場景,就是你需要在每臺宿主機上單獨安裝一個負責響應 CRI 的組件。這個組件,一般被稱作 CRI shim。顧名思義,CRI shim 的工作,就是扮演 kubelet 與容器項目之間的“墊片”(shim)。所以它的作用非常單一,那就是實現 CRI 規定的每個接口,然后把具體的 CRI 請求“翻譯”成對后端容器項目的請求或者操作。如下所示
CRI gRPC Server的具體實現
Container Runtime實現了CRI gRPC Server,包括RuntimeService和ImageService。該gRPC Server需要監聽本地的Unix socket,而kubelet則作為gRPC Client運行。CRI 接口包括 RuntimeService 和 ImageService 兩個服務,這兩個服務可以在一個 gRPC server 中實現,也可以分開成兩個獨立服務。目前社區的很多運行時都是將其在一個 gRPC server 里面實現。這其中包含了兩個gRPC服務:
看一下源碼,Kubernetes 1.20中的CRI接口在api.proto中的定義如下:
- // Runtime service defines the public APIs for remote container runtimes
- service RuntimeService {
- // Version returns the runtime name, runtime version, and runtime API version.
- rpc Version(VersionRequest) returns (VersionResponse) {}
- // RunPodSandbox creates and starts a pod-level sandbox. Runtimes must ensure
- // the sandbox is in the ready state on success.
- rpc RunPodSandbox(RunPodSandboxRequest) returns (RunPodSandboxResponse) {}
- // StopPodSandbox stops any running process that is part of the sandbox and
- // reclaims network resources (e.g., IP addresses) allocated to the sandbox.
- // If there are any running containers in the sandbox, they must be forcibly
- // terminated.
- // This call is idempotent, and must not return an error if all relevant
- // resources have already been reclaimed. kubelet will call StopPodSandbox
- // at least once before calling RemovePodSandbox. It will also attempt to
- // reclaim resources eagerly, as soon as a sandbox is not needed. Hence,
- // multiple StopPodSandbox calls are expected.
- rpc StopPodSandbox(StopPodSandboxRequest) returns (StopPodSandboxResponse) {}
- // RemovePodSandbox removes the sandbox. If there are any running containers
- // in the sandbox, they must be forcibly terminated and removed.
- // This call is idempotent, and must not return an error if the sandbox has
- // already been removed.
- rpc RemovePodSandbox(RemovePodSandboxRequest) returns (RemovePodSandboxResponse) {}
- // PodSandboxStatus returns the status of the PodSandbox. If the PodSandbox is not
- // present, returns an error.
- rpc PodSandboxStatus(PodSandboxStatusRequest) returns (PodSandboxStatusResponse) {}
- // ListPodSandbox returns a list of PodSandboxes.
- rpc ListPodSandbox(ListPodSandboxRequest) returns (ListPodSandboxResponse) {}
- // CreateContainer creates a new container in specified PodSandbox
- rpc CreateContainer(CreateContainerRequest) returns (CreateContainerResponse) {}
- // StartContainer starts the container.
- rpc StartContainer(StartContainerRequest) returns (StartContainerResponse) {}
- // StopContainer stops a running container with a grace period (i.e., timeout).
- // This call is idempotent, and must not return an error if the container has
- // already been stopped.
- // The runtime must forcibly kill the container after the grace period is
- // reached.
- rpc StopContainer(StopContainerRequest) returns (StopContainerResponse) {}
- // RemoveContainer removes the container. If the container is running, the
- // container must be forcibly removed.
- // This call is idempotent, and must not return an error if the container has
- // already been removed.
- rpc RemoveContainer(RemoveContainerRequest) returns (RemoveContainerResponse) {}
- // ListContainers lists all containers by filters.
- rpc ListContainers(ListContainersRequest) returns (ListContainersResponse) {}
- // ContainerStatus returns status of the container. If the container is not
- // present, returns an error.
- rpc ContainerStatus(ContainerStatusRequest) returns (ContainerStatusResponse) {}
- // UpdateContainerResources updates ContainerConfig of the container.
- rpc UpdateContainerResources(UpdateContainerResourcesRequest) returns (UpdateContainerResourcesResponse) {}
- // ReopenContainerLog asks runtime to reopen the stdout/stderr log file
- // for the container. This is often called after the log file has been
- // rotated. If the container is not running, container runtime can choose
- // to either create a new log file and return nil, or return an error.
- // Once it returns error, new container log file MUST NOT be created.
- rpc ReopenContainerLog(ReopenContainerLogRequest) returns (ReopenContainerLogResponse) {}
- // ExecSync runs a command in a container synchronously.
- rpc ExecSync(ExecSyncRequest) returns (ExecSyncResponse) {}
- // Exec prepares a streaming endpoint to execute a command in the container.
- rpc Exec(ExecRequest) returns (ExecResponse) {}
- // Attach prepares a streaming endpoint to attach to a running container.
- rpc Attach(AttachRequest) returns (AttachResponse) {}
- // PortForward prepares a streaming endpoint to forward ports from a PodSandbox.
- rpc PortForward(PortForwardRequest) returns (PortForwardResponse) {}
- // ContainerStats returns stats of the container. If the container does not
- // exist, the call returns an error.
- rpc ContainerStats(ContainerStatsRequest) returns (ContainerStatsResponse) {}
- // ListContainerStats returns stats of all running containers.
- rpc ListContainerStats(ListContainerStatsRequest) returns (ListContainerStatsResponse) {}
- // PodSandboxStats returns stats of the pod. If the pod sandbox does not
- // exist, the call returns an error.
- rpc PodSandboxStats(PodSandboxStatsRequest) returns (PodSandboxStatsResponse) {}
- // ListPodSandboxStats returns stats of the pods matching a filter.
- rpc ListPodSandboxStats(ListPodSandboxStatsRequest) returns (ListPodSandboxStatsResponse) {}
- // UpdateRuntimeConfig updates the runtime configuration based on the given request.
- rpc UpdateRuntimeConfig(UpdateRuntimeConfigRequest) returns (UpdateRuntimeConfigResponse) {}
- // Status returns the status of the runtime.
- rpc Status(StatusRequest) returns (StatusResponse) {}
- }
- // ImageService defines the public APIs for managing images.
- service ImageService {
- // ListImages lists existing images.
- rpc ListImages(ListImagesRequest) returns (ListImagesResponse) {}
- // ImageStatus returns the status of the image. If the image is not
- // present, returns a response with ImageStatusResponse.Image set to
- // nil.
- rpc ImageStatus(ImageStatusRequest) returns (ImageStatusResponse) {}
- // PullImage pulls an image with authentication config.
- rpc PullImage(PullImageRequest) returns (PullImageResponse) {}
- // RemoveImage removes the image.
- // This call is idempotent, and must not return an error if the image has
- // already been removed.
- rpc RemoveImage(RemoveImageRequest) returns (RemoveImageResponse) {}
- // ImageFSInfo returns information of the filesystem that is used to store images.
- rpc ImageFsInfo(ImageFsInfoRequest) returns (ImageFsInfoResponse) {}
- }
RuntimeService
RuntimeService 則提供了更多的接口,按照功能可以劃分為四組:
- PodSandbox 的管理接口:PodSandbox 是對 Kubernete Pod 的抽象,用來給容器提供一個隔離的環境(比如掛載到相同的 CGroup 下面),并提供網絡等共享的命名空間。PodSandbox 通常對應到一個 Pause 容器或者一臺虛擬機;
- Container 的管理接口:在指定的 PodSandbox 中創建、啟動、停止和刪除容器;
- Streaming API 接口:包括 Exec、Attach 和 PortForward 等三個和容器進行數據交互的接口,這三個接口返回的是運行時 Streaming Server 的 URL,而不是直接跟容器交互;
狀態接口:包括查詢 API 版本和查詢運行時狀態。
ImageService
管理鏡像的 ImageService 提供了 5 個接口:
- 查詢鏡像列表;
- 拉取鏡像到本地;
- 查詢鏡像狀態;
- 刪除本地鏡像;
- 查詢鏡像占用空間等。
這些都很容易映射到 Docker API 或者CRI上面。
CRI相關初始化
跟容器最相關的一個 Manager 是 Generic Runtime Manager,就是一個通用的運行時管理器。我們可以看到目前 dockershim 還是存在于 Kubelet 的代碼中的,它是當前性能最穩定的一個容器運行時的實現。remote 指的就是 CRI 接口。CRI 接口主要包含兩個部分:
- 一個是 CRI Server,即通用的比如說創建、刪除容器這樣的接口;
- 另外一個是流式數據的接口 Streaming Server,比如 exec、port-forward 這些流式數據的接口。
CNI(容器網絡接口)也是在 CRI 進行操作的,因為我們在創建 Pod 的時候需要同時創建網絡資源然后注入到 Pod 中。接下來就是我們的容器和鏡像。我們通過具體的容器創建引擎來創建一個具體的容器。kubelet中CRI相關初始化邏輯如下:
(1)當kubelet選用dockershim作為容器運行時,則初始化并啟動容器運行時服務端dockershim(初始化dockershim過程中也會初始化網絡插件CNI)。
- 如果是外部外部容器運行時的時候,需要在每臺宿主機上單獨安裝一個負責響應 CRI 的組件。這個組件就是CRI shim,需要包含網絡插件CNI。比如支持containerd的CRI-Containerd的shim。到了 containerd 1.1 版本后就去掉了 CRI-Containerd 這個 shim,直接把適配邏輯作為插件的方式集成到了 containerd 主進程中,所以我們現在可以直接使用--container-runtime-endpoint=unix:///run/containerd/containerd.sock這個套接字,就可以無縫切換的containerd。
(2)初始化容器運行時CRI shim客戶端(用于調用CRI shim服務端:內置的容器運行時dockershim或remote容器運行時);
(3)初始化Generic Runtime Manager,用于容器運行時的管理。初始化完成后,后續kubelet對容器以及鏡像的相關操作都會通過該結構體持有的CRI shim客戶端,與CRI shim服務端進行通信來完成。
下面來簡單分析幾個比較重要的CRI相關啟動參數:(1)--container-runtime:指定kubelet要使用的容器運行時,可選值docker、remote、rkt (deprecated),默認值為docker,即使用kubelet內置的容器運行時dockershim。當需要使用外部容器運行時,該參數配置為remote,并設置--container-runtime-endpoint參數值為監聽的 unix socket位置。(2)--runtime-cgroups:容器運行時使用的cgroups,可選值。(3)--docker-endpoint:docker暴露服務的socket地址,默認值為unix:///var/run/docker.sock,該參數配置當且僅當--container-runtime參數值為docker時有效。(4)--pod-infra-container-image:pod sandbox的鏡像地址,默認值為k8s.gcr.io/pause:3.5,該參數配置當且僅當--container-runtime參數值為docker時有效。(5)--image-pull-progress-deadline:容器鏡像拉取超時時間,默認值為1分鐘,該參數配置當且僅當--container-runtime參數值為docker時有效。(6)--experimental-dockershim:設置為true時,啟用dockershim模式,只啟動dockershim,默認值為false,該參數配置當且僅當--container-runtime參數值為docker時有效。(7)--experimental-dockershim-root-directory:dockershim根目錄,默認值為/var/lib/dockershim,該參數配置當且僅當--container-runtime參數值為docker時有效。(8)--container-runtime-endpoint:容器運行時的endpoint,linux中默認值為unix:///var/run/dockershim.sock,注意與上面的--docker-endpoint區分開來。
- unix:///var/run/dockershim.sock
- unix:///run/containerd/containerd.sock,即使用本地的containerd作為容器運行時。
- 默認是unix:///var/run/dockershim.sock,即默認使用本地的docker作為容器運行時。
(簡單介紹一下socket通信之Unix domain socket:Unix domain socket 又叫 IPC(inter-process communication 進程間通信。用于實現同一主機上的進程間通信。socket 原本是為網絡通訊設計的,但后來在 socket 的框架上發展出一種 IPC 機制,就是 UNIX domain socket。雖然網絡 socket 也可用于同一臺主機的進程間通訊(通過 loopback 地址 127.0.0.1),但是 UNIX domain socket 用于 IPC 更有效率:不需要經過網絡協議棧,不需要打包拆包、計算校驗和、維護序號和應答等,只是將應用層數據從一個進程拷貝到另一個進程。這是因為,IPC 機制本質上是可靠的通訊,而網絡協議是為不可靠的通訊設計的。)
(9)--image-service-endpoint:鏡像服務的endpoint,linux中默認值為unix:///var/run/dockershim.sock。
當前支持的CRI后端
我們最初在使用Kubernetes時通常會默認使用Docker作為容器運行時,其實從Kubernetes 1.5開始已經開始支持CRI,目前是處于Alpha版本,通過CRI接口可以指定使用其它容器運行時作為Pod的后端,docker、containerd、CRI-O、Frakti、pouch,它們銜接Kubelet與運行時方式對比如下:
棄用 docker 后到底會產生什么影響
正常的 K8s 用戶不會有任何影響
生產環境中高版本的集群只需要把運行時從 docker 切換到 containerd即可。containerd 是 docker 中的一個底層組件,主要負責維護容器的生命周期,跟隨 docker 經歷了長期考驗。同時 2019年初就從 CNCF 畢業,可以單獨作為容器運行時用在集群中。到了 containerd 1.1 版本后就去掉了 CRI-Containerd 這個 shim,直接把適配邏輯作為插件的方式集成到了 containerd 主進程中,所以我們現在可以直接使用--container-runtime-endpoint=unix:///run/containerd/containerd.sock這個套接字,就可以無縫切換的containerd。因此把 runtime 從 docker 轉換到 containerd 是一個基本無痛的過程。
- 開發環境中通過docker build構建出來的鏡像依然可以在集群中使用鏡像一直是容器生態的一大優勢,雖然人們總是把鏡像稱之為“docker鏡像”,但鏡像早就成為了一種規范了。具體規范可以參考image-spec。在任何地方只要構建出符合 Image Spec 的鏡像,就可以拿到其他符合 Image Spec 的容器運行時上運行。如果你是一名開發/運維人員,你依舊可以繼續使用 Docker 來構建鏡像,以相同的方式將鏡像推送到 Registry,并且將這些鏡像部署到你的 Kubernetes 中;如果你是運行和操作集群的用戶,你只需要將 Docker 切換成你需要的containerd 容器運行時即可。
- 在 Pod 中使用 DinD(Docker in Docker)的用戶會受到影響
有些使用者會把 docker 的 socket (/run/docker.sock)掛載到 Pod 中,并在 Pod 中調用 docker 的 api 構建鏡像或創建編譯容器等,官方在這里的建議是使用 Kaniko、Img 或 Buildah。
2.我們可以通過把 docker daemon 作為 DaemonSet 或者給想要使用 docker 的 Pod 添加一個 docker daemon 的 sidecar 的方式在任意運行時中使用 DinD 的方案。
3.同一集群中docker 節點與 containerd 節點共存,通過按節點標簽調度,保 證這類業務調度到 docker 節點沒有通過上述方案。
預告
后期會圍繞runc,shim等探索容器的底層實現與管理API的暴露。敬請期待!!!!
reference
https://feisky.xyz/posts/kubernetes-container-runtime/https://jimmysong.io/kubernetes-handbook/concepts/cri.htmlhttps://github.com/kubernetes/enhancements/tree/master/keps/sig-node/2221-remove-dockershimhttps://kubernetes.io/zh/docs/setup/production-environment/container-runtimes/https://www.qikqiak.com/post/containerd-usage/https://kubernetes.io/zh/blog/2020/12/02/dockershim-faq/https://github.com/containerdhttps://www.zhihu.com/question/324124344