K8s根本甩不掉Docker,原因一说就懂
题图摄于北京前门
上个月 Kubernetes 1.20 beta 版的发布记录(release note)里面声明了 kubelet 的 dockershim 模块已经过时了(deprecated),最快将在 1.23 版本中移除,即大约是一年之后。
这本来是个很普通的消息,没想到上周突然冒出了一批抢眼球的文章,说什么 Kubernetes 终于“甩掉”了 Docker ,一时间这条消息被炒得沸沸扬扬。不明就里的用户被吓得战战兢兢,不知所措。
这个 dockershim 其实是 Kubernetes 早期生长的一颗乳牙而已,现在“恒牙”已经长结实了,乳牙自然脱落就好。所以说,移去 dockershim 只是项目发展的必然结果,对用户影响微乎其微,不必多虑。
下面是一个简单的示意图,根据笔者在《Harbor权威指南》一书中的插图略微修改而来。Kubernetes 的 kubelet 可以支持多种符合 CRI 规范的运行时(runtime),例如 containerd 和 CRI-O。
而用户熟悉的 Docker(图中的 dockerd)不符合 CRI 规范,因此当年 kubelet 内置了一个模块 dockershim,用来对 Docker 进行 CRI 接口的适配。经过几年的发展,CRI 的运行时已经很成熟了,用户在 Kubernetes 中可以直接使用 containerd或者 CRI-O ,无需再通过 dockershim – dockerd – containerd 绕一圈(图中红色箭头),既费时又费力的。由此可见,dockershim 就是那颗已完成历史使命的乳牙而已,无足轻重了。
至于说 Kubernetes 彻底 “甩掉”了 Docker,也只是耸人听闻罢了。在可见的将来,Kubernetes 都无法真正摆脱 Docker 的影响。
先说说容器运行时,符合 CRI 标准的 containerd,以及底层的 runC,都是从Docker 项目中分拆出来的,蕴含了挥之不去的 Docker 印记。
此外,Docker 最精华的部分并不是容器运行时。因为容器的运行时归根到底仅仅是 Linux 内核功能的调用而已,Docker 的容器运行时是可以被替代的。
Docker 最具革命性的创新,是应用程序的封装方式,即容器镜像的格式定义。笔者在 2015 年文章中就旗帜鲜明地指出,Docker的核心价值是容器镜像。容器镜像是真正改变世界的技术,这个观点至今仍然未变。Kubernetes 上跑的容器,离不开 Docker 镜像的使用。
截至2020年初,Docker Hub 中的镜像累计下载了 1300 亿次,用户创建了约 600 万个容器镜像库。 -- 摘自《Harbor权威指南》Docker 镜像格式已是实际上的标准, OCI 的镜像规范是以 Docker 镜像格式为蓝本制定的,在大多数情况下两者是兼容的。开发者平时用到的“Docker”,除了可以运行容器之外,还有一个重要的功能就是构建容器镜像(例如 docker build),是上图中 dockerd 提供的主要功能之一。这部分面向开发者的功能在运行环境中确实用处不大,是 dockershim 被移除的原因之一。
因为镜像的格式已经标准化了,除了 Docker 以外,其他工具也可以构建镜像,如红帽的 Podman 等,但这些工具万变不离其宗,依然(必须)使用 Docker 开创的镜像格式标准。
Docker 公司有个著名的口号:“Build, Ship and Run”,翻译过来就是三个动词:“构建、传送和运行”,简练地描绘出了应用开发的精髓,其中隐含的意思是:构建镜像、传送镜像和运行镜像,一切皆以镜像为中心。OCI 组织对应有三个规范,分别与上述三个动词对应,即镜像规范(构建)、运行时规范(运行)和正在制定的分发规范(传送)。镜像是容器应用的关键技术,围绕镜像的一系列管理工作将是实际运维中的重要组成部分,这也是我们当初创建 Harbor 开源项目所希望解决的问题。
Kubernetes 还将在较长时间内使用 Docker 创立的技术和规范。为帮助读者理解,下面摘录《Harbor权威指南》第1章的部分内容,介绍各种容器运行时之间的关系。本公众号后续文章将给大家解释容器镜像的各种原理,请关注本号的文章更新。
Harbor 是原创于中国、广泛应用于全球的云原生开源项目,主要的维护者和贡献者均来自中国。《Harbor权威指南》是第一本全面介绍 Harbor 云原生制品仓库的书籍,由 Harbor 项目维护者和贡献者编写。双十二期间在京东、当当等网站半价优惠中。
容器的运行时 (《Harbor权威指南》节选)
Linux提供了命名空间和控制组两大系统功能,它们是容器的基础。但是,要把进程运行在容器中,还需要有便捷的 SDK 或命令来调用 Linux 的系统功能,从而创建出容器。容器的运行时(runtime)就是容器进程运行和管理的工具。容器运行时分为低层运行时和高层运行时,功能各有侧重。低层运行时主要负责运行容器,可在给定的容器文件系统上运行容器的进程;高层运行时则主要为容器准备必要的运行环境,如容器镜像下载和解压并转化为容器所需的文件系统、创建容器的网络等,然后调用低层运行时启动容器。主要的容器运行时的关系如下图所示。config.json:这是必需的配置文件,存放于文件系统包的根目录下。OCI运行时规范对Linux、Windows、Solaris和虚拟机4种平台的运行时做了相应的配置规范。
容器的根文件系统:容器启动后进程所使用的根文件系统,由 config.json 中的root.path属性确定该文件系统的路径,通常是“rootfs/”。
然后,在定义文件系统包的基础上,OCI运行时规范制定了运行时和生命周期管理规范。生命周期定义了容器从创建到删除的全过程。runCrunC 是 OCI 运行时规范的参考实现,也是最常用的容器运行时,被其他多个项目使用,如 containerd 和CRI-O等。runC也是低层容器运行时,开发人员可通过runC实现容器的生命周期管理,避免烦琐的操作系统调用。根据OCI运行时规范,runC不包括容器镜像的管理功能,它假定容器的文件包已经从镜像里解压出来并存放于文件系统中。runC创建的容器需要手动配置网络才能与其他容器或者网络节点连通,为此可在容器启动之前通过OCI定义的事件钩子来设置网络。由于runC提供的功能比较单一,复杂的环境需要更高层的容器运行时来生成,所以runC常常成为其他高层容器运行时的底层实现基础。containerd在OCI成立时,Docker公司把其Docker项目拆分为runC的低层运行时及高层运行时功能。2017年,Docker公司把这部分高层容器运行时的功能集中到containerd项目里,捐赠给云原生计算基金会。containerd 已经成为多个项目共同使用的高层容器运行时,提供了容器镜像的下载和解压等镜像管理功能,在运行容器时,containerd先把镜像解压成OCI的文件系统包,然后调用runC运行容器。containerd提供了API,其他应用程序可以通过API与containerd交互。“ctr”是containerd的命令行工具,和“docker”命令很相像。但作为容器运行时,containerd只注重在容器运行等方面,因而不包含开发者使用的镜像构建和镜像上传镜像仓库等功能。DockerDocker引擎是最早流行也是最广泛使用的容器运行时之一,是一个容器管理工具,架构如下图所示。Docker的客户端(命令行CLI工具)通过API调用容器引擎Docker Daemon(dockerd)的功能,完成各种容器管理任务。FROM ubuntu:18.04
EXPOSE 8080
CMD ["nginx", "-g", "daemon off;"]容器的镜像在构建之后被存放在本地镜像库里,当需要与其他节点共享镜像时,可上传镜像到镜像仓库(Registry)以供其他节点下载。Docker还提供了容器存储和网络映射到宿主机的功能,大部分由containerd实现。应用的数据可以被保存在容器的私有文件系统里面,这部分数据会随着容器一起被删除。对需要数据持久化的有状态应用来说,可用数据卷Volume的方式导入宿主机上的文件目录到容器中,对该目录的所有写操作都将被保存到宿主机的文件系统中。Docker可以把容器内的网络映射到宿主机的网络上,并且可以连接外部网络。CRI和CRI-OKubernetes是当今主流的容器编排平台,为了适应不同场景的需求,Kubernetes需要有使用不同容器运行时的能力。为此,Kubernetes从1.5版本开始,在kubelet中增加了一个容器运行时接口CRI(Container Runtime Interface),需要接入Kubernetes的容器运行时必须实现CRI接口。由于kubelet的任务是管理本节点的工作负载,需要有镜像管理和运行容器的能力,因此只有高层容器运行时才适合接入CRI。CRI和容器运行时的关系如下图所示。扫一扫,关注我们