在學習kubernetes的過程中,我們會遇到CRI、CNI、CSI、OCI 等術語,本文試圖先通過分析k8s目前默認的一種容器運行時架構,來幫助我們更好理解k8s 運行時背后設計邏輯。進而引出CRI、OCI的提出背景。
一、k8s 架構
我們在構建k8s集群的時候首先需要搭建master節點、其次需要創建node節點並將node節點加入到k8s集群中。當我們構建好k8s集群后,我們可以通過kubectl create -f nginx.yml 命令的方式來創建應用對應的pod。當我們執行命令
后,命令會提交給API server,它會解析yml文件,並將其以API對象的形式存到 etcd里。這時master組件中的Controller Manager會通過控制循環的方式來做編排工作,創建應用所需要的Pod。Scheduler 會 watch etcd中新Pod 的變化。如
果他發現有一個新的Pod 出現,Scheduler會運行調度算法,通過調度算法最終選擇出最佳的Node節點,並將這個Node節點的名字寫到pod對象的NodeName字段上面,這一步就是所謂的Bind Pod to Node(下圖的標注),然后把bind的結果寫回到etcd。
其次,當我們在構建k8s集群的時候,默認每個節點上都會初始化創建一個kubelet進程,kubelet進程的會watch etcd中的pod的變化,當kubelet進程watch到pod的bind的更新操作,並且bind的節點是本節點時,它會接管接下來的
所做的事情,如鏡像下載,容器創建等。
二、k8s 默認容器運行時架構
接下來將通過k8s默認集成的容器運行時架構來看kublete如何創建一個容器(如下圖所示)。
1. kubelet 通過 CRI(Container Runtime Interface) 接口(gRPC) 調用 dockershim, 請求創建一個容器, 這一步中, Kubelet 可以視作一個簡單的 CRI Client, 而 dockershim 就是接收請求的 Server.
2. dockershim 收到請求后, 通過適配的方式,適配成 Docker Daemon 的請求格式, 發到 Docker Daemon 上請求創建一個容器。在docker 1.12后版本中,docker daemon被拆分成dockerd和containerd,containerd負責操作容器。
3. dockerd收到請求后, 調用containerd進程去創建一個容器。
4. containerd 收到請求后, 並不會自己直接去操作容器, 而是創建一個叫做 containerd-shim 的進程, 讓 containerd-shim 去操作容器. 創建containered-shim的目的主要有:
1)讓containerd-shim做諸如收集狀態, 維持 stdin 等 fd 打開等工作.
2)允許容器運行時(runC)啟動容器后退出,不必為每個容器一直運行一個容器運行時runC。
3)即使在 containerd 和 dockerd 都掛掉的情況下,容器的標准 IO 和其它的文件描述符也都是可用的。
4)向 containerd 報告容器的退出狀態
5)在不中斷容器運行的情況下升級或重啟 dockerd
5. 而containerd-shim 在這一步需要調用 runC 這個命令行工具, 來啟動容器,runC是OCI(Open Container Initiative, 開放容器標准) 的一個參考實現。主要用來設置 namespaces 和 cgroups, 掛載 root filesystem等操作。
6.runC
啟動完容器后本身會直接退出, containerd-shim 則會成為容器進程的父進程, 負責收集容器進程的狀態, 上報給 containerd, 並在容器中 pid 為 1 的進程退出后接管容器中的子進程進行清理, 確保不會出現僵屍進程(關閉進程描述符等)。
三、容器與容器編排背景簡述
從k8s的容器運行時可以看出,kubelet啟動容器的過程經過了很長的一段調用鏈路。這個是由於在容器及編排領域各大廠商與docker之間的競爭以及docker公司為了搶占paas領域市場,對架構做出的一系列調整。其實 k8s 最開始
的運行時架構鏈路調用沒有這么復雜: kubelet 想要創建容器直接通過 docker api 調用 Docker Daemon,Docker Daemon 調 libcontainer 這個庫來啟動容器。為了防止docker壟斷以及受控docker運行時, 各大廠商於是就聯合起來制定出開
放容器標准OCI(Open Containers Initiative).大家可以基於這個標准開發自己的容器運行時。Docker公司則把 libcontainer做了一層封裝, 變成 runC 捐獻給CNCF作為 OCI 的參考實現.
接下來就是 Docker 要搞 Swarm 進軍 PaaS 市場, 於是做了個架構切分, 把容器操作都移動到一個單獨的 Daemon 進程 containerd 中去, 讓 Docker Daemon 專門負責上層的封裝編排. 最終swarm敗給了k8s, 於是
Docker 公司就把 containerd 捐給 CNCF ,專注於搞 Docker 企業版了.
與此同時,容器領域,core os公司推出了個rkt容器運行時。希望 k8s 原生支持 rkt 作為運行時, 由於core os與google的關系,最終rkt運行時的支持在2016年也被合並進kubelet主干代碼里. 這樣做后反而給k8s中負責維護 kubelet 的小
組 SIG-Node帶來了更大的負擔,每一次kubelet的更新都要維護docker和rkt兩部分代碼。與此同時,隨着虛擬化技術強隔離容器技術runV(Kata Containers前身,后與intel clear container 合並)的逐漸成熟。k8s上游對虛擬化容器的支持很
快被提上了日程。為了從集成每一種運行時都要維護一份代碼中解放出來,k8s SIG-Node工作組決定對容器的操作統一地抽象成一個接口,這樣kubelet只需要跟這個接口
打交道,而具體地容器運行時,他們只需要實現該接口,並對kubelet暴露gRPC服務即可。這個統一地抽象地接口就是k8s中俗稱的 CRI。
四、CRI(容器運行時接口)
CRI 基於 gRPC 定義了 RuntimeService 和 ImageService 等兩個 gRPC 服務,分別用於容器運行時和鏡像的管理。如下所示:
// 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.
// TODO: what must the runtime do 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) {}
// 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) {}
}
具體容器運行時則需要實現 CRI 定義的接口(即 gRPC server,通常稱為 CRI shim)。容器運行時在啟動 gRPC server 時需要監聽在本地的 Unix Socket (Windows 使用 tcp 格式)。
五、 容器運行時實現
除了上面介紹的默認的容器運行時的實現,目前容器運行時主要有:
- cri-o:同時兼容OCI和CRI的容器運行時
- cri-containerd:基於Containerd的Kubernetes CRI 實現
- rkt:由CoreOS主推的用來跟docker抗衡的容器運行時
- frakti:基於hypervisor的CRI
- Clear Containers:由Intel推出的同時兼容OCI和CRI的容器運行時
- Kata Containers:符合OCI規范同時兼容CRI
- gVisor:由谷歌推出的容器運行時沙箱(Experimental)
參考文檔:
https://github.com/kubernetes/cri-api/blob/master/pkg/apis/runtime/v1alpha2/api.proto
https://feisky.gitbooks.io/kubernetes/plugins/CRI.html
https://aleiwu.com/post/cncf-runtime-landscape/
https://www.infoq.cn/article/r*ikOvovTHhADAWw1Hb1