【云原生•容器】Docker架构剖析,它还是之前认识的那个Docker吗?
CRI
从上文《Docker架构剖析,它还是之前认识的那个Docker吗?(上)》可以看到OCI规范提出目的是Google等科技大厂将容器运行时和镜像的实现从 Docker 项目中完全剥离出来,避免火热的容器技术被垄断在这家初出茅庐的Docker公司手里。后来Google又顺势退出kubernetes项目,在容器技术基本被Docker垄断,Docker技术基本成为容器生态的事实标准情况下,Google要在容器上层应用平台层展开竞争,并通过这一系列操作后,Docker实力被大大削弱,Kubernetes上位成为新一代霸主。
Kubernetes专注的是上层应用平台层,底层还是需要通过依赖其它容器运行时才能管理容器。在 Kubernetes 早期,由于 Docker 风头正盛,所以 Kubernetes 选择通过直接调用 Docker API 来管理容器,这也导致只支持Docker一种容器运行时:
早期 Kubernetes 完全依赖且绑定 Docker,由于当时 Docker 太流行了,所以也没有过多考虑够日后使用其它容器引擎的可能性。后来随着容器技术的发展,出现了很多其它容器运行时,比如 CoreOS 推出的开源容器引擎 Rocket(简称 rkt),rkt 出现之后,Kubernetes 用类似强绑定 Docker 的方式又实现了对 rkt 容器引擎的支持。
随着容器技术的蓬勃发展,越来越多运行时出现,如果继续使用与 Docker 类似强绑定的方式,Kubernetes 的工作量将无比庞大。Kubernetes 需要重新考虑对所有容器运行时的兼容适配问题了。为了让 Kubernetes 隔离各个容器引擎之间的差异,从而支持更多的容器运行时,而不仅仅是和 Docker 绑定,Google 于是联合 Red Hat 一起在 Kubernetes 1.5 推出了 CRI 标准。CRI 的全称为 Container Runtime Interface,也就是容器运行时接口,它是 Kubernetes 定义的一组与容器运行时进行交互的接口,只要你实现了这套接口,就可以对接到 Kubernetes 平台上来。
❝
在kubernetes中,CRI扮演了kubelet和container runtime的通信桥梁。也因为CRI的存在,container runtime和kubelet解耦,就有了多种选择,比如: docker、 CRI-O、containerd、frakti等等。
不过 CRI 本身只是 Kubernetes 推的一个标准,当时的 Kubernetes 推出 CRI 这套标准的时候还没有现在的统治地位,容器运行时当然不能说我跟 Kubernetes 绑死了只提供 CRI 接口,所以有一些容器运行时可能不会自身就去实现 CRI 接口,于是就有了 shim(垫片)
。 一个 shim 的职责就是作为适配器将各种容器运行时本身的接口适配到 Kubernetes 的 CRI 接口上,dockershim 就是 Kubernetes 将 Docker 适配到 CRI 接口的一个实现,并且由 Kubernetes 自身开发维护:
当 Kubelet 想要创建一个容器时, 需要下面几步:
- Kubelet 通过 CRI 接口(gRPC) 调用 dockershim, 请求创建一个容器,目前 dockershim 的代码其实是内嵌在 Kubelet 中的。
- dockershim 收到请求后, 解析转化成符合 Docker daemon 的接口调用, 发到 Docker daemon 请求创建一个容器。
- Docker daemon 早在 1.12 版本中就已经将针对容器的操作拆分到 containerd 项目中了, 因此 Docker daemon 仍然不能帮我们创建容器, 而是要请求 containerd 创建一个容器。
- containerd 收到请求后, 并不会自己直接去操作容器, 而是创建一个叫做 containerd-shim 的进程, 让 containerd-shim 去操作容器。这是因为容器进程需要一个父进程来做诸如收集状态、维持 stdin 等 fd 打开等工作,而假如这个父进程就是 containerd, 那每次 containerd 挂掉或升级, 整个宿主机上所有的容器都得退出了,而引入了 containerd-shim 就规避了这个问题。
- 我们知道创建容器需要做一些设置 namespaces 和 cgroups, 挂载 root filesystem 等等操作, 而这些事该怎么做已经有了公开的规范了, 那就是 OCI 规范,它的一个参考实现叫做 RunC。于是, containerd-shim 在这一步需要调用
runc
这个命令行工具, 来创建容器、启动容器。 runc
启动完容器后本身会直接退出, containerd-shim 则会成为容器进程的父进程, 负责收集容器进程的状态, 上报给 containerd, 并在容器中 pid 为 1 的进程退出后接管容器中的子进程进行清理, 确保不会出现僵尸进程。
从这个创建过程中看出,Kubernetes 管理容器的链路太长,通过 Containerd 和 RunC 等项目已经将容器核心依赖从 Docker 项目中拆分出来了,那么 Kubernetes 是否可以绕过 Docker 直接与 Containerd 通信呢?答案当然是肯定的。从 Containerd 1.0 开始,Containerd 开发了 CRI-Containerd,可以直接与 Containerd 通信,从而取代了 dockershim。因此,在 Kubernetes 1.20 版本发布的时候提到未来会弃用dockershim,而在 Kubernetes 1.24 版本发布时正式弃用。
到了 Containerd 1.1 版本,Containerd 又进一步将 CRI-Containerd 直接以插件的形式集成到了 Containerd 主进程中,也就是说 Containerd 已经原生支持 CRI 接口了,这使得调用链路更加简洁。至此,kubelet完成了最终的 CRI 架构的演进,这也是目前 Kubernetes 默认的容器运行方案:
从上述可以看出,Docker是如何从最开始和 Kubernetes 强绑定,再到通过CRI标准进行解耦,并通过dockershim进行接口适配,再到最后被废弃的过程,了解了这一步步演变历程你对整个容器生态有了更加深入的认识。从架构上看,Docker 由于没有实现 CRI 标准而被 Kubernetes 废弃这是一个必然的结果,因为经过Containerd、RunC等项目从Docker中分离出来形成一个个独立的开源项目,Docker中容器核心技术已被掏空,如今容器已不再与Docker紧密耦合,Docker也不再代表容器技术的发展。
因此,弃用 Docker 对 Kubernetes 来说影响不大,原有的 Docker 镜像和容器依然会正常运行,唯一的变化是 Kubernetes 绕过了 Docker,直接和 Containerd 对接。自此,Kubernetes 与 Docker 彻底"分道扬镳",在可预见的未来,Docker最终将走下历史的舞台,而取而代之的,恰是Docker自己开发的Containerd。
❝
containerd 项目中 CRI 标准定义位于 third_party/k8s.io/cri-api/pkg/apis/runtime/v1alpha2/api.proto,主要定义了两类接口 ImageService 和 RuntimeService,分别对应镜像和容器相关操作接口。
架构说明
我们再来看下整个架构:
架构说明:
- Client:对于docker、kubelet还是ctr(Containerd CLI)来说,它们都是Containerd容器运行时客户端,通过gRPC协议和Containerd进程交互。
- Containerd:是一个工业级标准的容器运行时,是一个实现 CRI 标准规范的容器运行时,它强调简单性、健壮性和可移植性,从Docker 1.11版本进行架构重构时拆分出来,后来捐赠给 CNCF 社区成为开源项目。docker 对容器的管理和操作基本都是通过 containerd 完成的,containerd 的主要功能有:容器生命周期管理、日志管理、镜像管理、存储管理、容器网络接口及网络管理。
- containerd-shim:容器垫片,它是containerd的一个子组件,当用户通过containerd创建一个容器时,containerd-shim会被调用来实际创建和运行容器。每启动一个容器contianerd进程都会拉起一个新的 containerd-shim 进程,containerd-shim进程开启ttrpc服务,containerd通过ttrpc协议调用containerd-shim接口,网络上也是采用的unix本地套接字方式,套接字文件:/var/run/containerd/s/[containerdID]。containerd-shim是一个轻量级的组件,它的设计目标是尽可能简单和可靠,它可以与不同的容器运行时(如runc)配合使用,并且可以通过插件机制进行功能扩展。containerd-shim进程作用如下:
- shim 一个作用是向 Containerd 报告容器的退出状态,在容器退出状态被 Containerd 收集之前,shim 会一直存在。这一点和僵尸进程很像,僵尸进程在被父进程回收之前会一直存在,只不过僵尸进程不会占用资源,而 shim 会占用资源。
- shim 将 Containerd 进程从容器的生命周期中分离出来,具体的做法是 runc 在创建和运行容器之后退出,并将 shim 作为容器的父进程,即使 Containerd 进程挂掉或者重启,也不会对容器造成任何影响。这样做的好处很明显,你可以高枕无忧地升级或者重启 dockerd、Containerd等进程,不会对运行中的容器产生任何影响。
- shim 的另一个重要部分是将容器的生命周期事件返回给 containerd ,包括:
TaskCreate
TaskStart
TaskDelete
TaskExit
,TaskOOM
,TaskExecAdded
,TaskExecStarted
,TaskPaused
,TaskResumed
,TaskCheckpointed
。
❝
task的详细定义参考:https://github.com/containerd/containerd/blob/v1.5.6/api/events/task.proto
- RunC:Docker 公司按照 OCI 标准规范编写实现的容器运行时,其前身是 libcontainer 项目演化而来,根据 OCI(开放容器组织)的标准来创建和运行容器,实现了容器启停、资源隔离等功能。