容器运行时是一个负责运行和管理容器的软件。它负责创建、启动、停止和监控容器。它也负责为容器提供和宿主机器隔离的运行环境,包括文件系统、网络堆栈、进程空间等。
有多种类型的容器运行时,包括:
- Docker:这可能是最为人所知的容器运行时。Docker 通过简化容器的创建和管理,使得容器技术变得更加易用。
- containerd:这是一个开源的容器运行时,是 Docker 的一个组件,但也可以独立于 Docker 运行。containerd 用于管理容器的完整生命周期,包括镜像传输和存储、容器执行和监控、低级存储和网络接口。
- runc:这是一个符合 Open Container Initiative(OCI)规范的轻量级容器运行时。Docker 和 containerd 使用 runc 来运行容器。
- CRI-O:这是一个实现了 Kubernetes Container Runtime Interface(CRI)的轻量级容器运行时。CRI-O 专门针对 Kubernetes 设计,使得 Kubernetes 可以直接运行 OCI 兼容的容器。
runc
是一个命令行工具,用于根据 OCI(Open Container Initiative)规范创建和运行容器。OCI 是一个开放的容器标准,定义了容器的运行时规范(runtime specification)和映像规范(image specification)。Docker 以及其他一些容器平台实际上使用的都是 runc
或其他兼容 OCI 规范的工具来创建和运行容器。
runc
的主要职责包括:
- 创建、启动和监控容器的进程
- 对容器进程进行资源隔离(如 CPU、内存、I/O 等)
- 管理容器的生命周期
在 Docker 中,当你运行一个容器时(例如使用 docker run
命令),Docker 会调用 runc
(或 Docker 的默认运行时)来实际创建和启动容器。runc
是一个底层的工具,对于大多数 Docker 用户来说,通常不需要直接与 runc
交互。
从 Docker 1.11 版本开始,Docker 容器运行就不是简单通过 Docker Daemon 来启动了,而是通过集成 containerd、runc 等多个组件来完成的。虽然 Docker Daemon 守护进程模块在不停的重构,但是基本功能和定位没有太大的变化,一直都是 CS 架构,守护进程负责和 Docker Client 端交互,并管理 Docker 镜像和容器。现在的架构中组件 containerd 就会负责集群节点上容器的生命周期管理,并向上为 Docker Daemon 提供 gRPC 接口。
当我们要创建一个容器的时候,现在 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 的进程退出后接管容器中的子进程进行清理,确保不会出现僵尸进程。
CRI
(Container Runtime Interface 容器运行时接口)本质上就是 Kubernetes 定义的一组与容器运行时进行交互的接口,所以只要实现了这套接口的容器运行时都可以对接到 Kubernetes 平台上来。不过 Kubernetes 推出 CRI 这套标准的时候还没有现在的统治地位,所以有一些容器运行时可能不会自身就去实现 CRI 接口,于是就有了 shim(垫片)
, 一个 shim 的职责就是作为适配器将各种容器运行时本身的接口适配到 Kubernetes 的 CRI 接口上,其中 dockershim
就是 Kubernetes 对接 Docker 到 CRI 接口上的一个垫片实现。
Kubelet 通过 gRPC 框架与容器运行时或 shim 进行通信,其中 kubelet 作为客户端,CRI shim(也可能是容器运行时本身)作为服务器。
现在如果我们使用的是 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 来。
切换到 containerd 可以消除掉中间环节,操作体验也和以前一样,但是由于直接用容器运行时调度容器,所以它们对 Docker 来说是不可见的。 因此,你以前用来检查这些容器的 Docker 工具就不能使用了。
你不能再使用 docker ps
或 docker inspect
命令来获取容器信息。由于不能列出容器,因此也不能获取日志、停止容器,甚至不能通过 docker exec
在容器中执行命令。
当然我们仍然可以下载镜像,或者用 docker build
命令构建镜像,但用 Docker 构建、下载的镜像,对于容器运行时和 Kubernetes,均不可见。为了在 Kubernetes 中使用,需要把镜像推送到镜像仓库中去。
从上图可以看出在 containerd 1.0 中,对 CRI 的适配是通过一个单独的 CRI-Containerd
进程来完成的,这是因为最开始 containerd 还会去适配其他的系统(比如 swarm),所以没有直接实现 CRI,所以这个对接工作就交给 CRI-Containerd
这个 shim 了。
然后到了 containerd 1.1 版本后就去掉了 CRI-Containerd
这个 shim,直接把适配逻辑作为插件的方式集成到了 containerd 主进程中,现在这样的调用就更加简洁了。
与此同时 Kubernetes 社区也做了一个专门用于 Kubernetes 的 CRI 运行时 CRI-O,直接兼容 CRI 和 OCI 规范。
Containerd
我们知道很早之前的 Docker Engine 中就有了 containerd,只不过现在是将 containerd 从 Docker Engine 里分离出来,作为一个独立的开源项目,目标是提供一个更加开放、稳定的容器运行基础设施。分离出来的 containerd 将具有更多的功能,涵盖整个容器运行时管理的所有需求,提供更强大的支持。
containerd 是一个工业级标准的容器运行时,它强调简单性、健壮性和可移植性,containerd 可以负责干下面这些事情:
- 管理容器的生命周期(从创建容器到销毁容器)
- 拉取 / 推送容器镜像
- 存储管理(管理镜像及容器数据的存储)
- 调用 runc 运行容器(与 runc 等容器运行时交互)
- 管理容器网络接口及网络
容器运行时环境还是使用大家熟知的 Docker,只是在 k8s v1.24 以后需要额外安装 cri-dockerd, k8s 才能够正常识别到 Docker。这里也可以使用其它容器运行时工具,比如 containerd, CRI-O 等可以根据个人喜好使用,只是截至目前 Docker 在国内占的比重依然可以说是一枝独秀