容器运行时
容器运行时是什么
容器运行时(Container Runtime)是容器化技术中的关键组件之一,它负责运行容器。容器运行时管理容器的生命周期,包括创建、启动、停止以及销毁容器等操作。
容器运行时种类
docker:这可能是最为人所知的容器运行时。Docker 通过简化容器的创建和管理,使得容器技术变得更加易用。
containerd:是从 Docker 中分离出来的一个项目,可以作为一个底层容器运行时,现在它成了Kubernete 容器运行时更好的选择。containerd 是一个高级的容器运行时,它强调简单性、稳定性和可移植性,可以直接被用于Docker和其他系统。containerd 是真正管控容器的一个进程,执行容器的时候用的是 runc。
runc:runc:是一个轻量级的工具,它是containerd的一部分,用于根据OCI(Open Container Initiative)标准创建和运行容器。runc 直接在操作系统上运行容器,是containerd和CRI-O等更高级运行时的底层运行时。
CRI-O:也是CNCF的一个项目,它是一个轻量级的容器运行时,专门用于Kubernetes。CRI-O 直接实现了Kubernetes的容器运行时接口(CRI),不依赖于Docker或其他容器引擎。
什么是OCI
OCI 是一个开放的容器标准,定义了容器的运行时规范(runtime specification)和映像规范(image specification)。Docker 以及其他一些容器平台实际上使用的都是 runc 或其他兼容 OCI 规范的工具来创建和运行容器。
什么是CRI
CRI本质上就是 Kubernetes 定义的一组与容器运行时进行交互的接口,所以只要实现了这套接口的容器运行时都可以对接到 Kubernetes 平台上来。不过 Kubernetes 推出 CRI 这套标准的时候还没有现在的统治地位,所以有一些容器运行时可能不会自身就去实现 CRI 接口,于是就有了 shim(垫片), 一个 shim 的职责就是作为适配器将各种容器运行时本身的接口适配到 Kubernetes 的 CRI 接口上,其中 dockershim 就是 Kubernetes 对接 Docker 到 CRI 接口上的一个垫片实现。
Kubernetes最新版本(v1.24+)宣布弃用 dockershim,移除了对Docker作为容器运行时的原生支持,但这并不意味着完全不支持Docker。
Kubernetes仍然支持使用Docker镜像。所有现有的Docker镜像可以直接用在k8s集群中,不需要修改。
Kubernetes已经全面拥抱容器运行时接口(CRI)。最新版本的kubelet将通过CRI管理容器,而不是直接通过Docker API。
用户可以继续使用Docker作为CRI运行时。只需要安装并配置Docker的CRI插件,即docker-containerd插件,kubelet就可以通过CRI控制Docker。
推荐的容器运行时是containerd。它功能丰富,性能好,资源消耗少。docker-containerd确保它可以与Docker镜像兼容。
切换到containerd后,节点上仍可以同时运行Docker引擎,用于构建镜像等场景。
Kubernetes移除Docker的依赖可以使其支持更多类型的容器运行时,提高其可移植性。
docker容器运行时
在 Docker 中,当你运行一个容器时(例如使用 docker run 命令),Docker 会调用 runc(或 Docker 的默认运行时)来实际创建和启动容器。runc 是一个底层的工具,对于大多数 Docker 用户来说,通常不需要直接与 runc 交互。
当我们要创建一个容器的时候,现在 Docker Daemon 并不能直接帮我们创建了,而是请求 containerd 来创建一个容器,containerd 收到请求后,也并不会直接去操作容器,而是创建一个叫做 containerd-shim 的进程,让这个进程去操作容器,我们指定容器进程是需要一个父进程来做状态收集、维持 stdin 等 fd 打开等工作的,假如这个父进程就是 containerd,那如果 containerd 挂掉的话,整个宿主机上所有的容器都得退出了,而引入 containerd-shim 这个垫片就可以来规避这个问题了。
然后创建容器需要做一些 namespaces 和 cgroups 的配置,以及挂载 root 文件系统等操作,这些操作其实已经有了标准的规范,那就是 OCI(开放容器标准),runc 就是它的一个参考实现(Docker 被逼无耐将 libcontainer 捐献出来改名为 runc 的),这个标准其实就是一个文档,主要规定了容器镜像的结构、以及容器需要接收哪些操作指令,比如 create、start、stop、delete 等这些命令。runc 就可以按照这个 OCI 文档来创建一个符合规范的容器,既然是标准肯定就有其他 OCI 实现,比如 Kata、gVisor 这些容器运行时都是符合 OCI 标准的。
所以真正启动容器是通过 containerd-shim 去调用 runc 来启动容器的,runc 启动完容器后本身会直接退出,containerd-shim 则会成为容器进程的父进程,负责收集容器进程的状态,上报给 containerd, 并在容器中 pid 为 1 的进程退出后接管容器中的子进程进行清理,确保不会出现僵尸进程。
现在如果我们使用的是 Docker 的话,当我们在 Kubernetes 中创建一个 Pod 的时候,首先就是 kubelet 通过 CRI 接口调用 dockershim,请求创建一个容器,kubelet 可以视作一个简单的 CRI Client, 而 dockershim 就是接收请求的 Server,不过他们都是在 kubelet 内置的。
dockershim 收到请求后,转化成 Docker Daemon 能识别的请求,发到 Docker Daemon 上请求创建一个容器,请求到了 Docker Daemon 后续就是 Docker 创建容器的流程了,去调用 containerd,然后创建 containerd-shim 进程,通过该进程去调用 runc 去真正创建容器。
其实我们仔细观察也不难发现使用 Docker 的话其实是调用链比较长的,真正容器相关的操作其实 containerd 就完全足够了,Docker 太过于复杂笨重了,当然 Docker 深受欢迎的很大一个原因就是提供了很多对用户操作比较友好的功能,但是对于 Kubernetes 来说压根不需要这些功能,因为都是通过接口去操作容器的,所以自然也就可以将容器运行时切换到 containerd 来。
在 k8s v1.24 以后需要额外安装 cri-dockerd, k8s 才能够正常识别到 Docker。