日前专为开发者提供技术分享的又拍云 OpenTalk 公开课邀请了网易有道资深运维开发工程师张晋涛,直播分享《Containerd 上手实践 》,详细介绍 Containerd 的发展历程、主要特性,以及如何将其作为 Kubernetes runtime 的上手实践。以下是直播内容整理

关于作者:张晋涛,现就职于网易有道, 对 Docker、Kubernetes 及相关生态有大量实践及深入源码的研究,《Docker 核心知识必知必会》专栏作者。PS 讲师长期坚持更新 K8S 生态周报,如有兴趣可订阅其公众号【MoeLove】。

大家好,今天分享的内容将会从 Kubernetes 宣布弃用 dockershim 说起,介绍 Containerd 相关特性,分享如何将 Containerd 用作 Kubernetes 的 runtime。


Kubernetes 宣布弃用 dockershim

很多媒体将该事件宣称为 Kubernetes 宣布弃用 Docker,其实这是一种误导。那么应该如何正确的去看待呢?首先是了解整个事情的前因后果,得需要知道 dockershim 是什么。

dockershim

dockershim 是 Kubernetes 的一个组件,主要目的是为了通过 CRI 操作 Docker。Docker在 2013 年就出现了,2014 年 Kubernetes 发布并默认使用 Docker 作为容器运行时,而 dockershim首次正式出现是在 2016 年。Docker 在创建之初并没有考虑到容器编排或者是考虑 Kubernetes,但 Kubernetes 在创建之初便采用Docker 作为它的默认容器进行时,后续代码当中包含了很多对 Docker 相关的操作逻辑。后期 Kubernetes 为了能够做解耦,兼容更多的容器进行时,将操作 Docker 相关逻辑整体独立起来组成了 dockershim。

Container Runtime Interface

再说 CRI(Container Runtime Interface)即容器运行时接口,该概念是由Kubernetes 提出并在 2016 年底开始应用,其主要目标是增强 Kubernetes 的可扩展性,可以不固定、不捆绑某一个容器运行时,实现可插拔式的容器进行时。比如可以使用 Docker 为容器运行时也可以使用其他的例如 rkt,并且希望通过开放 CRI 这个统一的接口来提高代码的可维护性,而不是需要支持 Docker 时就对 Docker 进行适配,需要支持另一个运行时就得对其做相关的适配。它希望是任何一个成为 Kubernetes 的容器运行时都遵守 CRI 统一的接口与规范,实现了 CRI 就可以作为 Kubernetes 的运行时,并不需要关注具体是什么。

为什么要弃用 dockershim

dockershim 的目的是为了 Kubernetes 通过 CRI 操作 Docker,所以Kubernetes 任何的功能变动或 Docker 有任何的功能特性变更,dockershim 代码必须加以改动保证能够支持变更。另外一个原因是随着容器技术的推进,容器运行时已经多种多样了,比如本次分享的主角 Containerd,还有 cri-o 以及 rkt 的容器运行时,不过这个 rkt 容器运行时的项目已经不维护了。

此外,原先 Kubernetes 需要去调用 dockershim 跟 Docker 做沟通,Docker 的底层运行时是 containerd,可以发现最终都是要调用 containerd,并且 containerd 自身也是可以支持 CRI 的。那为什么要先绕过一层 Docker 呢?是不是可以直接通过 CRI 跟 Containerd 进行交互呢?这也就造成了现在 Kubernetes 社区希望弃用 dockershim。

弃用 dockershim 的影响

终端⽤⼾⽆任何影响。这里指的是使用来自云厂商/使用别人提供的Kubernetes 集群的终端用户,他们不需要关注集群本身的容器运行时到底是什么,因为和你交互的都是 Kubernetes 自身的 CRI,而任何一个可以作为 Kubernetes 底层容器运行时的东西都必须是兼容 CRI 接口的,上层就已经屏蔽掉这个细节了。

对负责维护 Kubernetes 集群的工作人员有一定影响。当升级 Kubernetes 集群版本时,需要考虑是否切换容器进行时。如果目前在用最新版本 Kubernetes V1.20,并且使用 Docker 作为容器运行时,要考虑它能否正常工作。其实是可以正常工作的,只是在启动 Kubernetes 的时候会发现一条日志,提醒你当前使用的容器运行时 Docker 已经不再被 Kubernetes 支持,因为已经准备弃用dockershim,因此会有这个提醒。

Kubernetes 社区计划在 2021 年将dockershim 正式移除。换个角度考虑,既然社区不想在 Kubernetes 源代码当中维护 dockershim 了,那是不是可以把 dockershim 组件给单独的拿出来呢?答案是可以的,现在 Mirantis 和 Docker 已经决定之后共同合作维护 dockershim 组件。此外,还可以通过树外的 dockershim 独立组件,继续使用 Docker 作为容器运行时,并且使用这种方式只需要做一些简单的配置,把原先使用内置的Kubernetes 自身携带的 dockershim 组件,改成使用一个独立的 dockershim 组件,本身变动很小。

那么 Docker 到底还能否使用呢?在我看来,毋庸置疑,Docker 仍然是现阶段容器构建和运行的最佳选择。


快速了解 Containerd


Containerd 是中间层的容器运行时。它构建在平台之下,作为平台下层的一个容器运行时,但又比最底层的容器运行时像 runc、gVisor 要高一点,所以被称为中间层的容器运行时。除此之外也可称作为资源管理器,可以用来管理容器的进程、镜像以及管理文件系统的快照,还有元数据和依赖的管理。既然它可以作为一个资源管理器来使用,如果想要在此之上构建一个属于自己的容器平台就会很方便。

Containerd 是由 Docker 公司创建,并且在 2017年捐赠给了 CNCF,2019 年 Containerd 从 CNCF 正式毕业。Containerd 项目一开始的目标是用来管理容器的进程,之后逐步变更成为一个完整的容器运行时,是 Docker 的底层容器运行时。需要说明的是,containerd 是可以抛开 Docker 与 Kubernetes 自身独立工作的。

Containerd 与 CRI

1.jpg

Containerd 在之前的版本中考虑到了 CRI,但它是将CRI 作为独立的进程存在的。在上图中看到,CRI-Containerd 其实是一个独立组件,Kubernetes 通过 CRI 接口调用 CRI-Containerd,再由这个组件去调用 containerd。在 Containerd1.1 版本之后对该特性做了重新的设计,它将 CRI 的支持通过插件化的方式来实现,Kubernetes 通过 CRI 接口调用的其实是 Containerd 当中 CNI 的插件,以此来达到通信的目的,调用链更少更短了。

Containerd 的特性

  • 支持 OCI 镜像规范,即前文所提到的 runc

  • 支持 OCI 运行时规范。Docker 引导了 OCI 组织的成立,该组织主要有两个规范:镜像规范与运行时规范。这两个规范在 Docker 成立时把 Docker 镜像规范与底层容器运行时规范都给捐赠出来作为它的初始工作

  • 支持镜像的 push/pull 作用

  • 支持容器网络管理。因为可以启动和运行容器,容器启动后支持相互之间的访问,或彼此之间网络的隔离,所以需要支持容器网络的管理
  • 存储支持多租户。Containerd 的相关操作有通过 namespace 来做隔离的,可以指定不同的 namespace 来实现,它默认的 namespace 叫 default,在 default 的 namespace下面下载多个镜像。但是在其他的 namespace 下看不到这些镜像,也用不到,以此来达到多租户的隔离
  • 支持容器运行时和容器的生命周期管理

  • 支持管理网络名称空间容器以加入现有名称空间,可以让某一个容器加入到现有的 namespace 当中

Containerd 的整体架构

2.png

上图是 Containerd 整体的架构。由下往上,Containerd支持的操作系统和架构有 Linux、Windows 以及像 ARM 的一些平台。在这些底层的操作系统之上运行的就是底层容器运行时,其中有上文提到的runc、gVisor 等。在底层容器运行时之上的是Containerd 相关的组件,比如 Containerd 的 runtime、core、API、backend、store 还有metadata 等等。构筑在 Containerd 组件之上以及跟这些组件做交互的都是 Containerd 的 client,Kubernetes 跟 Containerd 通过 CRI 做交互时,本身也作为 Containerd 的一个 client。Containerd 本身有提供了一个 CRI,叫 ctr,不过这个命令行工具并不是很好用。

在这些组件之上就是真正的平台,Google Cloud、Docker、IBM、阿里云、微软云还有RANCHER等等都是,这些平台目前都已经支持 containerd, 并且有些已经作为自己的默认容器运行时了。

Containerd 主要功能与上手实践

镜像管理

3.png

首先是上文中频繁提到的镜像管理。具体操作是需要通过一个 client 去跟 Containerd 做交互。如图中所示,这里选择了ctr 的命令行工具。ctr指定一个address 参数跟 Containerd交互,它是在后台持续运行的一个服务,需要指定它的地址。图中是通过 pull 一个 redis alpine linux 的镜像,接下来通过 image ls就可以看到已经成功 pull 下来的镜像。

容器管理

4.png

作为一个容器运行时对容器进行管理是必不可少的功能。同样的通过 -a 参数来指定 address 与Containerd 进行通信,通过 container create+镜像名称+容器名称来创建一个容器,通过 container ls 可以看到刚才创建的容器。需要注意的是,最后一列叫做 runtime,它的 runtime 叫做 io.containerd.runc.v2,表明是 v2 版本的 Containerd API。

在 Containerd 中通过 container create 创建出来的容器其实并不管用,还需要让其运行起来。这时通常会把它当作一个 task,对它执行 task start,就可以把刚才创建的镜像跑起来了。通过 task ls 就可以看到名叫 redis 的 Containerd,其中有一个正在运行的进程,并且展现出了进程号。

命名空间

5.png

需要说明的是,可以通过 -n 来指定一个默认的叫做 default 的命名空间,而后通过 task ls 就看到刚才启动容器的进程,它其实是在运行中的。如果把 namespace 换一个,比如图中的 moby 就是 Docker 项目当前使用的 namespace 名称,Docker 在使用 Containerd 作为容器运行时的时候,会默认使用它。

继续往下看,通过 ctr -n 指定使用 moby 命名空间,-a 参数指定containerd 的地址,然后 task ls 来看看moby 项目当中到底运行着什么。可以看到有一条记录是正在运行当中的。这条记录如何和 Docker 当中的容器或任务做匹配比较呢?一个简单的办法就是通过 docker ps --no-trunc-- format 跟容器完整的 ID,然后 grep 就可以看到刚才通过 ctr 命令得到的ID 了。

需要注意的是,如果使用 Containerd 作为 Kubernetes 的容器运行时,那么它的 namespace 叫 k8s.io。到这里可能有些人已经发现,Containerd 作为 Docker 的运行时可以使用不同的命名空间,比如 moby。用作 Kubernetes 容器运行时也可以使用不同的命名空间,比如 k8s.io。那是否存在一种办法可以让平台当中既有 Kubernetes 又有 Docker,还有 Containerd 呢?答案是肯定的,直接将其全部装到一起,但不配置 Docker 作为容器运行时,先观察一段时间看看,这也是一种办法。


Containerd 用作 Kubernetes 的 runtime

6.png



上图是 Containerd 用作 Kubernetes 的 runtime 整个流程图。Kubernetes 通过 CRI 接口,调用到 CRI plugin,plugin 是Containerd 一个内置的插件,其中包含了最主要的两部分:一是 image service,包含了镜像服务相关的;二是 runtime service,即运行时的服务。如果在 containerd 当中部署了一个项目或服务,它首先会调度到某一台机器,这台机器上的 Kubernetes 就会开始工作,它会查询服务、需要哪些镜像,而后把相关的镜像让运行时给拉下来,再去启动对应的 pod 或者相关的容器。

其次它还会跟 CNI 做交互。CNI 即 Container Network Interface,是Kubernetes 提供的一个容器网络接口)。主要注意的是,交互过程中可能会提前创建出来 pause的 container,是一个占位的过程,这里先不对此做更深入的介绍。

当 Containerd 作为 Kubernetes 的容器运行时,配置相对很简单:通过 containerd config default命令可以直接查询到 Containerd 完整的默认配置,下图中可以看到主要是配置 CRI。所以在这里的配置文件当中,通过 plugins.io.containerd.grpc.vi.cri 对其进行配置,首先是 default-runtime,其次配置一个 runtime.runc。


7.png

这里简单介绍配置 runc 需要注意的参数。比如 io.containerd.runc.v2 需要配置runtime.type;涉及配置与 runc 相关的一些配置会包含一些 CNI 的配置、目录之类的,具体的配置上图中已经展示了。总而言之如果想要提起来一个服务、一个 pod 或是 container,要注意都是需要配置的。它都会有一个 pause的镜像,即 sandbox_image,可以从中指定一个默认的镜像。当然也可以通过此处换源,加快国内环境下的拉取速度。

最后还有些其他的资源,如本人长期参与的一个项目 KIND( Kubernetes in  docker)。这个项目相当于是使用docker 容器作为不同的 node,可以把这些 node 组成一个集群网络,搭建一套 Kubernetes。而这个集群使用的容器运行时就是 containerd,虽然一开始使用的是 Docker,但后期逐步都将其替换成了 containerd,类似的还有包括 K3C、K3S,效果都是差不多的。