作者介绍:王成,腾讯云研发工程师,Kubernetes member,从事数据库产品容器化、资源管控等工作,关注 Kubernetes、Go、云原生领域。
文章目录
- 1 概述
- 2 从 Docker 说起
- 2.1 Docker Engine
- 2.2 OCI
- 2.3 runc
- 3 CRI
- 3.1 dockershim
- 3.2 CRI shim
- 3.3 RuntimeClass
- 4 Kubelet 启动
- 5 Pod 创建/删除
- 6 Container 创建/删除
- 7 CRI RPC 接口
- 8 小结
1
概述
进入 K8s 的世界,会发现有很多方便扩展的 Interface,包括 CRI、CSI、CNI 等,将这些接口抽象出来,是为了更好的提供开放、扩展、规范等能力。
K8s CRI(Container Runtime Interface)是 K8s 定义的一组与容器运行时进行交互的接口,用于将 K8s 平台与特定的容器运行时实现解耦。CRI 在 Kubernetes 1.5 中引入,并充当 kubelet 和容器运行时之间的桥梁。目前实现了 CRI spec 的 Runtime 有 Docker Engine、containerd、CRI-O、Mirantis Container Runtime(Docker 企业版)等。
2020 年,K8s 宣布弃用 dockershim,标志着容器运行时正式向 CRI 切换,一方面是为了将 kubelet 核心主干代码与 Runtime 相关代码解耦,便于更好的维护;另一方面则是为了便于生态圈按 CRI spec 实现自己的运行时插件,提供个性化的运行时扩展能力,以满足对更多 Runtime 的支持,提高 K8s 生态的开放性和扩展性。
本文将从 Docker Engine、Kubelet 启动、Pod 创建/删除、Container 创建/删除、CRI RPC 调用等核心流程,对 CRI 实现机制进行了解析。
流程概览如下:
本文及后续相关文章都基于 K8s v1.23
2
从 Docker 说起
2.1
Docker Engine
Docker Engine 是用来运行和管理容器的核心软件。通常人们会简单地将其代指为 Docker 或 Docker 平台。
Docker Engine 主要的组件构成:Docker 客户端(Docker Client)、Docker 守护进程(Docker daemon)、Docker APIs、containerd 以及 runc,它们共同负责容器的创建和运行。
2.2
OCI
OCI(Open Container Initiative,开放容器计划),是在 2015 年由 Docker、CoreOS 等公司共同成立的项目,并由 Linux 基金会进行管理,致力于 container runtime 标准的制定和 runc 的开发等工作。
所谓 container runtime,主要负责的是容器的生命周期的管理。OCI 主要分为容器运行时规范(runtime-spec)和镜像规范(image-spec)两部分,runtime-spec 标准对容器的创建、删除、查看、状态等操作进行了定义,image-spec 对镜像格式、打包(Bundle)、存储等进行了定义。
2.3
runc
runc,是由 Docker 贡献的对于 OCI 标准的一个参考实现,是一个可以用于创建和运行容器的 CLI(command-line interface)工具。runc 直接与容器所依赖的 Cgroup/OS 等进行交互,负责为容器配置 Cgroup/namespace 等启动容器所需的环境,创建启动容器的相关进程。
为了兼容 OCI 标准,Docker 也做了架构调整。将容器运行时相关的程序从 Docker daemon 剥离出来,形成了containerd。containerd 向 Docker 提供运行容器的 API,二者通过 gRPC 进行交互。containerd 最后会通过 runc 来实际运行容器。
3
CRI
CRI(Container Runtime Interface,容器运行时接口)是 K8s 定义的一组与容器运行时进行交互的接口,用于将 K8s 平台与特定的容器实现解耦。在 K8s 早期的版本中,对于容器环境的支持是通过 Dockershim(hard code)方式直接调用 Docker API 的,后来为了支持更多的容器运行时和更精简的容器运行时,K8s 在遵循 OCI 基础上提出了CRI。
3.1
dockershim
dockershim 是 Kubernetes 的一个组件,主要目的是为了通过 CRI 操作 Docker。Kubernetes 在创建之初便采用 Docker 作为它的默认容器进行时,后续代码当中包含了很多对 Docker 相关的操作逻辑。后期 Kubernetes 为了能够做解耦,兼容更多的容器进行时,将操作 Docker 相关逻辑整体独立起来组成了 dockershim。
2020 年,K8s 宣布弃用 dockershim,标志着容器运行时正式向 CRI 切换,以满足对更多 Runtime 的支持,提高 K8s 生态的开放性和扩展性。
3.2
CRI shim
当前实现了 CRI 的 remote shim 有如下:
- containerd:由 Docker 公司创建,并且在 2017 年捐赠给了 CNCF,2019 年毕业。
- CRI-O:基于 OCI 规范的作为 CRI 和 OCI 之间的一座桥梁。
- Docker Engine:Docker 运行时的支持,由 cri-dockerd 进行实现。
- Mirantis Container Runtime:Docker 企业版(Enterprise Edition)运行时的支持,由 Mirantis Container Runtime(MCR)进行实现。
CRI shim 小结如下:
3.3
RuntimeClass
RuntimeClass 是 v1.12 引入的新 API 对象,用来支持多个容器运行时,可通过 Pod 字段直接指定。 定义一个 RuntimeClass 如下,对应的 CRI handler 即为目标容器运行时,比如 containerd、crio:
代码语言:javascript复制apiVersion: node.k8s.io/v1 # RuntimeClass is defined in the node.k8s.io API groupkind: RuntimeClassmetadata: name: myclass # The name the RuntimeClass will be referenced by # RuntimeClass is a non-namespaced resourcehandler: myconfiguration # The name of the corresponding CRI configuration
在 Pod 中直接指定对应的 runtimeClassName 即可:
代码语言:javascript复制apiVersion: v1kind: Podmetadata: name: mypodspec: runtimeClassName: myclass # ...
4
Kubelet 启动
kubelet 在 Node 节点上负责 Pod 的创建、销毁、监控上报等核心流程,通过 Cobra 命令行解析参数启动二进制可执行文件。
启动入口如下:
代码语言:javascript复制// kubernetes/cmd/kubelet/kubelet.gofunc main() { command := app.NewKubeletCommand() // kubelet uses a config file and does its own special // parsing of flags and that config file. It initializes // logging after it is done with that. Therefore it does // not use cli.Run like other, simpler commands. code := run(command) os.Exit(code)}
接着,一路往下进行初始化:
cmd -> Run -> PreInitRuntimeService -> RunKubelet -> createAndInitKubelet -> startKubelet -> Run
其中 PreInitRuntimeService 会进一步初始化 CRI shim,分别初始化 RuntimeService、ImageService 对容器运行时和镜像生命周期进行管理;然后启动 gRPC CRI server 监听 client 请求,进行具体的操作如 PodSandbox、Container 创建与删除。
kubelet 启动后,会负责:
- Pod、Volume 事件的监听,创建/删除对应的 Pod/Volume;
- Node 资源监控与更新、Pod 健康探测(Probe)及上报;
- 当 Node 资源紧张时,还负责 Pod Preemption(抢占)与 Eviction(驱逐);
- Metrics 监控采集、Image/Container GC 工作等;
其中,Pod 事件流程图请看上面概述中的流程概览图。
5
Pod 创建/删除
K8s 中 Pod 的调谐采用 channel 生产者-消费者模型实现,具体通过 PLEG(Pod Lifecycle Event Generator)进行 Pod 生命周期事件管理。
代码语言:javascript复制// kubernetes/pkg/kubelet/pleg/pleg.go// 通过 PLEG 进行 Pod 生命周期事件管理type PodLifecycleEventGenerator interface { Start() // 通过 relist 获取所有 Pods 并计算事件类型 Watch() chan *PodLifecycleEvent // 监听 eventChannel,传递给下游消费者 Healthy() (bool, error)}
Pod 事件生产者(producer)- 相关代码:
代码语言:javascript复制// kubernetes/pkg/kubelet/pleg/generic.go// 生产者:获取所有 Pods 列表,计算出对应的事件类型,进行 Syncfunc (g *GenericPLEG) relist() { klog.V(5).InfoS("GenericPLEG: Relisting") ... // 获取当前所有 Pods 列表 podList, err := g.runtime.GetPods(true) if err != nil { klog.ErrorS(err, "GenericPLEG: Unable to retrieve pods") return }
for pid := range g.podRecords { allContainers := getContainersFromPods(oldPod, pod) for _, container := range allContainers {
// 计算事件类型:running/exited/unknown/non-existent events := computeEvents(oldPod, pod, &container.ID) for _, e := range events { updateEvents(eventsByPodID, e) } } }
// 遍历所有事件 for pid, events := range eventsByPodID { for i := range events { // Filter out events that are not reliable and no other components use yet. if events[i].Type == ContainerChanged { continue } select { case g.eventChannel <- events[i]: // 生产者:发送到事件 channel,对应监听的 goroutine 会消费 default: metrics.PLEGDiscardEvents.Inc() klog.ErrorS(nil, "Event channel is full, discard this relist() cycle event") } } } ...}
Pod 事件消费者(Consumer)- 相关代码:
代码语言:javascript复制// kubernetes/pkg/kubelet/kubelet.go// 消费者:根据 channel 获取的各类事件,进行 Pod Syncfunc (kl *Kubelet) syncLoopIteration(configCh <-chan kubetypes.PodUpdate, handler SyncHandler, syncCh <-chan time.Time, housekeepingCh <-chan time.Time, plegCh <-chan *pleg.PodLifecycleEvent) bool { select { ... // 消费者:监听 plegCh 的事件 case e := <-plegCh: if e.Type == pleg.ContainerStarted { // 更新容器的最后启动时间 kl.lastContainerStartedTime.Add(e.ID, time.Now()) } if isSyncPodWorthy(e) { if pod, ok := kl.podManager.GetPodByUID(e.ID); ok { klog.V(2).InfoS("SyncLoop (PLEG): event for pod", "pod", klog.KObj(pod), "event", e)
// 进行相关 Pod 事件的 Sync handler.HandlePodSyncs([]*v1.Pod{pod}) } else { // If the pod no longer exists, ignore the event. klog.V(4).InfoS("SyncLoop (PLEG): pod does not exist, ignore irrelevant event", "event", e) } }
// 容器销毁事件处理:清除 Pod 内相关 Container if e.Type == pleg.ContainerDied { if containerID, ok := e.Data.(string); ok { kl.cleanUpContainersInPod(e.ID, containerID) } } ... } return true}
当 kubelet 监听到 Pod 事件时,进行对应 Pod 的创建或删除,流程如下:
kubelet -> Run -> syncLoop -> SyncPodCreate/Kill -> UpdatePod -> syncPod/syncTerminatingPod -> dockershim gRPC -> Pod running/teminate
6
Container 创建/删除
当 Pod-Sandbox 创建出来以后,首先会创建基础容器(infra-container,也叫 pause 容器),通过 CNI 机制 配置 Pod 网络环境,创建临时数据目录(存放 container logs),为下一步 Container 创建做好相关准备工作。
Container 目前分为三种类型:
- Ephemeral Container:临时容器,用于 debug 及排障所需的一次性容器。
- Init Container:初始化容器,在正常业务容器之前、按序启动,一般做一些准备工作。
- Regular Container:普通业务容器,应用使用的主容器。
创建 Container 的过程主要有:
PullImage -> CreateContainer -> StartContainer -> PostStartHook -> Container running
当 Pod 内所有 Container 启动并正常运行起来后,Pod-phase 更新为 Running,表示 Pod 正常运行。
Pod-Container 创建流程小结如下:
7
CRI RPC 接口
CRI 标准规范接口,包含了 ImageService、RuntimeService 两方面的接口:
- ImageService:管理镜像的查询、拉取、删除、统计等操作;
- RuntimeService:管理 PodSandbox 和容器的生命周期,包括查询、创建、启动、删除、统计等操作,另外还提供版本(Version)、执行命令(Exec)、端口转发(PortForward)等接口能力;
可以看到,用户只要按照 CRI RPC 接口规范进行具体实现,就可以实现自己的 Runtime 插件,提高了 K8s 生态的高扩展性与灵活性。
8
小结
本文通过分析 K8s 中 Kubelet 启动、Pod 创建/删除、Container 创建/删除、CRI RPC 调用等核心流程,对 K8s CRI 实现机制进行了解析。通过源码、图文方式说明了相关流程逻辑,以期更好的理解 K8s CRI 实现细节。
K8s CRI 经历了从 in-tree Dockershim 到 CRI remote-shim(out-of-tree)的 迁移,一方面是为了将 kubelet 核心主干代码与 Runtime 相关代码解耦,便于更好的维护;另一方面则是为了便于生态圈按 CRI spec 实现自己的运行时插件,提供个性化的运行时扩展能力,以期达到容器生态圈的开放共赢。
参考资料
- Docker Engine:https://docs.docker.com/engine/
- Kubernetes 源码:https://github.com/kubernetes/kubernetes
- OCI spec 及 runc:https://github.com/opencontainers
- Container runtimes:https://kubernetes.io/docs/setup/production-environment/container-runtimes/
- CRI RPC:https://github.com/kubernetes/cri-api/blob/master/pkg/apis/runtime/v1/api.proto
- xinkun 的博客:https://www.cnblogs.com/xuxinkun/p/8036832.html
腾源会是腾讯云成立的汇聚开源项目、开源爱好者、开源领导者的开放社区,致力于帮助开源项目健康成长、开源爱好者能交流协助、开源领导者能发挥领袖价值,让全球开源生态变得更加繁荣。
欢迎关注「腾源会」公众号,期待你的「在看」