1. Kubernetes支持的容器运行时
Kubernetes 支持任何符合 CRI 标准的容器运行时。在 1.23 版本之前,常用的容器运行时有三种:docker、containerd、cri-o。
- Docker
Docker 守护进程是不符合 CRI 标准的。为了支持 Docker 作为容器运行时,kubelet 内置了一个 dockershim 模块,kubelet 通过 CRI 调用 dockershim,再由它转换请求,调用 Docker 守护进程,而 1.24 版本将要移除的就是这个模块。此模式下创建容器时的调用过程如下:
- Kubelet 通过 CRI 调用 dockershim
- dockershim 转换请求,调用 docker 守护进程
- docker 调用 containerd
- containerd 创建 containerd-shim 进程,再由 containerd-shim 调用 runC 完成容器创建。 最终容器由containerd-shim 管理,容器内所有进程都是 containerd-shim 的子进程。
- Containerd
Containerd 是从 docker 守护进程中独立出来的容器运行时,最终也要通过 runC 运行容器。
在 CRI 标准被提出后,为了兼容 CRI,减少调用开销,containerd 开发了一个守护进程,叫 CRI-containerd。原先调用链 kubelet -> dockershim -> dockerd -> containerd被简化成为 kubelet -> CRI-containerd -> containerd。后来,containerd 干脆将 CRI-containerd以 CRI 插件形式内建在项目中,直接通过方法调用,进一步将调用链简化为 kubelet -> containerd。
- cri-o
CRI 标准被提出后,红帽按照 CRI 开发的一个轻量级容器运行时,是 CRI 标准的最小实现。此模式下kubelet直接调用 cri-o,再由 cri-o 调用 runC 完成容器创建和管理,调用链比较简洁。
2. 容器运行时介绍——Docker(dockershim)
由于 Docker 当时的江湖地位很高,Kubernetes 是直接内置了 dockershim 在 kubelet 中的,所以如果你使用的是 Docker 这种容器运行时的话是不需要单独去安装配置适配器之类的,当然这个举动似乎也麻痹了 Docker 公司。
kubelet和Docker的集成方案图如下:
当kubelet要创建一个容器时,需要以下几步:
- Kubelet 通过 CRI 接口(gRPC)调用 dockershim,请求创建一个容器。CRI 即容器运行时接口(Container Runtime Interface),这一步中,Kubelet 可以视作一个简单的 CRI Client,而 dockershim 就是接收请求的 Server。目前 dockershim 的代码其实是内嵌在 Kubelet 中的,所以接收调用的凑巧就是 Kubelet 进程;
- dockershim 收到请求后,转化成 Docker Daemon 能听懂的请求,发到 Docker Daemon 上请求创建一个容器。
- Docker Daemon 早在 12 版本中就已经将针对容器的操作移到另一个守护进程——containerd 中了,因此 Docker Daemon 仍然不能帮我们创建容器,而是要请求 containerd 创建一个容器;
- containerd 收到请求后,并不会自己直接去操作容器,而是创建一个叫做 containerd-shim 的进程,让 containerd-shim 去操作容器。这是因为容器进程需要一个父进程来做诸如收集状态,维持 stdin 等 fd 打开等工作。而假如这个父进程就是 containerd,那每次 containerd 挂掉或升级,整个宿主机上所有的容器都得退出了。而引入了 containerd-shim 就规避了这个问题(containerd 和 shim 并不是父子进程关系);
- 我们知道创建容器需要做一些设置 namespaces 和 cgroups,挂载 root filesystem 等等操作,而这些事该怎么做已经有了公开的规范了,那就是 OCI(Open Container Initiative,开放容器标准)。它的一个参考实现叫做 runC。于是,containerd-shim 在这一步需要调用 runC 这个命令行工具,来启动容器;
- runC 启动完容器后本身会直接退出,containerd-shim 则会成为容器进程的父进程,负责收集容器进程的状态,上报给 containerd,并在容器中 pid 为 1 的进程退出后接管容器中的子进程进行清理,确保不会出现僵尸进程。
但是随着 CRI 方案的发展,以及其他容器运行时对 CRI 的支持越来越完善,Kubernetes 社区在2020年7月份就开始着手移除 dockershim 方案了:https://github.com/kubernetes/enhancements/tree/master/keps/sig-node/2221-remove-dockershim,移除计划是在 1.20 版本中将 kubelet 中内置的 dockershim 代码分离,将内置的 dockershim 标记为维护模式(代码还在,但是要默认支持开箱即用的 docker 需要自己构建 kubelet,会在1.24过后从 kubelet 中删除内置的 dockershim 代码),当然这个时候仍然还可以使用 dockershim,目标是在1.24 版本发布没有 dockershim 的版本。
2022年05月,Kubernetes 1.24正式发布,比较引人注目的就是在这个版本中正式将dockershim 组件从 kubelet 中删除。从这个版本开始,用户使用Kubernetes时需要优先选择containerd 或 CRI-O作为容器运行时。Docker 和其他容器运行时将一视同仁,不会单独对待内置支持,如果希望继续依赖 Docker Engine 作为容器运行时,需要cri-dockerd组件(由Mirantis提供,于2019年收购Docker Enterprise部门。即将 dockershim 的功能单独提取出来独立维护一个 cri-dockerd ,就类似于 containerd 1.0 版本中提供的 cri-containerd,当然还有一种办法就是 Docker 官方社区将 CRI 接口内置到 Dockerd 中去实现)。
此外,由于Docker Image已经成为了各类容器运行时使用的标准镜像格式,未来很长一段时间,开发阶段使用Docker,生产环境中使用的Containerd等其他容器运行时,可能会成为一种普遍的现象。
3. 容器运行时介绍——Containerd
CRI是k8s定义的一套与容器运行时进行交互的接口。Containerd就是docker为了适应这个标准而开发的CRI实现,但它已经是CNCF的了,不再属于docker了。
很早之前的 Docker Engine 中就有了 containerd,只不过现在是将 containerd 从 Docker Engine 里分离出来,作为一个独立的开源项目,目标是提供一个更加开放、稳定的容器运行基础设施。分离出来的 containerd 将具有更多的功能,涵盖整个容器运行时管理的所有需求,提供更强大的支持。
Containerd 是一个工业级标准的容器运行时,它强调简单性、健壮性和可移植性,dockerd实际真实调用的还是containerd的api接口,containerd是dockerd和runC之间的一个中间交流组件。containerd 可以负责干下面这些事情:
- 管理容器的生命周期(从创建容器到销毁容器)
- 拉取/推送容器镜像
- 存储管理(管理镜像及容器数据的存储)
- 调用 runc 运行容器(与 runc 等容器运行时交互)
- 管理容器网络接口及网络
3.1 CRI-Containerd介绍(Containerd 1.0版本所提供)
其实我们仔细观察也不难发现使用 Docker 的话其实是调用链比较长的,真正容器相关的操作其实 containerd 就完全足够了,Docker 太过于复杂笨重了,当然 Docker 深受欢迎的很大一个原因就是提供了很多对用户操作比较友好的功能,但是对于 Kubernetes 来说压根不需要这些功能,因为都是通过接口去操作容器的,所以自然也就可以将容器运行时切换到 containerd 来。
从上图可以看出在 containerd 1.0 中,对 CRI 的适配是通过一个单独的 CRI-Containerd 进程来完成的,这是因为最开始 containerd 还会去适配其他的系统(比如 swarm),所以没有直接实现 CRI,所以这个对接工作就交给 CRI-Containerd 这个 shim 了。然后到了 containerd 1.1 版本后就去掉了 CRI-Containerd 这个 shim,直接把适配逻辑作为插件的方式集成到了 containerd 主进程中,现在这样的调用就更加简洁了。
3.2 Containerd(Containerd 1.1版本开始)
如上所述,到了 containerd 1.1 版本后就去掉了 CRI-Containerd 这个 shim,直接把适配逻辑作为插件的方式集成到了 containerd 主进程中,就只剩一个containerd。
切换到 containerd 可以消除掉中间环节,操作体验也和以前一样,但是由于直接用容器运行时调度容器,所以它们对 Docker 来说是不可见的。因此,你以前用来检查这些容器的 Docker 工具就不能使用了。不能再使用 docker ps 或 docker inspect 命令来获取容器信息。由于不能列出容器,因此也不能获取日志、停止容器,甚至不能通过 docker exec 在容器中执行命令。当然仍然可以下载镜像,或者用 docker build 命令构建镜像,但用 Docker 构建、下载的镜像,对于容器运行时和 Kubernetes,均不可见。为了在 Kubernetes 中使用,需要把镜像推送到镜像仓库中去。
containerd 可用作 Linux 和 Windows 的守护程序,它管理其主机系统完整的容器生命周期,从镜像传输和存储到容器执行和监测,再到底层存储到网络附件等等。
上图是 containerd 官方提供的架构图,可以看出 containerd 采用的也是 C/S 架构,服务端通过 unix domain socket 暴露低层的 gRPC API 接口出去,客户端通过这些 API 管理节点上的容器,每个 containerd 只负责一台机器。
为了解耦,containerd 将系统划分成了不同的组件,每个组件都由一个或多个模块协作完成(Core 部分),每一种类型的模块都以插件的形式集成到 Containerd 中,而且插件之间是相互依赖的,例如,上图中的每一个长虚线的方框都表示一种类型的插件,包括 Service Plugin、Metadata Plugin、GC Plugin、Runtime Plugin 等,其中 Service Plugin 又会依赖 Metadata Plugin、GC Plugin 和 Runtime Plugin。每一个小方框都表示一个细分的插件,例如 Metadata Plugin 依赖 Containers Plugin、Content Plugin 等。比如:
- Content Plugin: 提供对镜像中可寻址内容的访问,所有不可变的内容都被存储在这里。
- Snapshot Plugin: 用来管理容器镜像的文件系统快照,镜像中的每一层都会被解压成文件系统快照,类似于 Docker 中的 graphdriver。
总体来看 containerd 可以分为三个大块:Storage、Metadata 和 Runtime。
4. 容器运行时介绍——CRI-O
Kubernetes 社区也做了一个专门用于 Kubernetes 的 CRI 运行时 CRI-O,直接兼容 CRI 和 OCI 规范。当然,除了containerd其他厂商也可以基于CRI-O做一些事情,同样实现了CRI接口。这样就可以无缝接入到k8s中,比如redhat的OpenShift,就选用的CRI-O。但对于容器的真正调度,其实还是OCI负责的,CRI只是个中转站而已。比如,Podman,原来就是CRI-O项目的一部分,现在它可以直接操作runc来启动容器。
这个方案和 containerd 的方案显然比默认的 dockershim 简洁很多,不过由于大部分用户都比较习惯使用 Docker,所以大家还是更喜欢使用 dockershim 方案。