容器运行时从Docker到containerd的迁移


【编者的话】目前,Docker是Kubernetes默认的容器运行时(Container Runtime)。由于Docker过于复杂,操作不便,eBay将容器运行时从Docker迁移到containerd,并将存储驱动程序Device Mapper换成Overlayfs。尽管在迁移过程中,我们遇到了不少挑战,但都一一克服并最终完成了此次迁移。

容器运行时(Container Runtime),运行于Kubernetes集群的每个节点中,负责容器的整个生命周期。其中Docker是目前应用最广的。随着容器云的发展,越来越多的容器运行时涌现。为了解决这些容器运行时和Kubernetes的集成问题,在Kubernetes 1.5版本中,社区推出了CRI(Container Runtime Interface,容器运行时接口)(如图1所示),以支持更多的容器运行时。如果你想和更多Kubernetes技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态

Kubelet通过CRI和容器运行时进行通信,使得容器运行时能够像插件一样单独运行。可以说每个容器运行时都有自己的优势,这就允许用户更容易选择和替换自己的容器运行时。
1.png

图1 CRI在Kubernetes中的位置

CRI & OCI

CRI是Kubernetes定义的一组gRPC服务。Kubelet作为客户端,基于gRPC框架,通过Socket和容器运行时通信。它包括两类服务:镜像服务(Image Service)和运行时服务(Runtime Service)。镜像服务提供下载、检查和删除镜像的远程程序调用。运行时服务包含用于管理容器生命周期,以及与容器交互的调用(exec / attach / port-forward)的远程程序调用。

如图2所示,dockershim,containerd和cri-o都是遵循CRI的容器运行时,我们称他们为高层级运行时(High-level Runtime)。
2.png

图2 常用的运行时举例

OCI(Open Container Initiative,开放容器计划)定义了创建容器的格式和运行时的开源行业标准,包括镜像规范(Image Specification)和运行时规范(Runtime Specification)。

镜像规范定义了OCI镜像的标准。如图2所示,高层级运行时将会下载一个OCI 镜像,并把它解压成OCI 运行时文件系统包(filesystem bundle)。

运行时规范则描述了如何从OCI 运行时文件系统包运行容器程序,并且定义它的配置、运行环境和生命周期。如何为新容器设置命名空间(namepsaces)和控制组(cgroups),以及挂载根文件系统等等操作,都是在这里定义的。它的一个参考实现是runC。我们称其为低层级运行时(Low-level Runtime)。除runC以外,也有很多其他的运行时遵循OCI标准,例如kata-runtime。

Containerd vs Cri-o

目前Docker仍是Kubernetes默认的容器运行时。那为什么会选择换掉Docker呢?主要的原因是它的复杂性。

如图3所示,我们总结了Docker,containerd以及cri-o的详细调用层级。Docker的多层封装和调用,导致其在可维护性上略逊一筹,增加了线上问题的定位难度(貌似除了重启Docker,我们就毫无他法了)。Containerd和cri-o的方案比起Docker简洁很多。因此我们更偏向于选用更加简单和纯粹的containerd和cri-o作为我们的容器运行时。
3.png

图3 容器运行时调用层级

我们对containerd和cri-o进行了一组性能测试,包括创建、启动、停止和删除容器,以比较它们所耗的时间。如图4所示,containerd在各个方面都表现良好,除了启动容器这项。从总用时来看,containerd的用时还是要比cri-o要短的。
4.jpg

图4 containerd和crio的性能比较

如图5所示,从功能性来讲,containerd和cri-o都符合CRI和OCI的标准。从稳定性来说,单独使用containerd和cri-o都没有足够的生产环境经验。但庆幸的是,containerd一直在Docker里使用,而Docker的生产环境经验可以说比较充足。可见在稳定性上containerd略胜一筹。所以我们最终选用了containerd。
5.png

图5 containerd和cri-o的综合比较

Device Mapper vs. Overlayfs

容器运行时使用存储驱动程序(storage driver)来管理镜像和容器的数据。目前我们生产环境选用的是Device Mapper。然而目前Device Mapper在新版本的Docker中已经被弃用,containerd也放弃对Device Mapper的支持。

当初选用Device Mapper,也是有历史原因的。我们大概是在2014年开始Kubernetes这个项目的。那时候Overlayfs都还没合进kernel。当时我们评估了Docker支持的存储驱动程序,发现Device Mapper是最稳定的。所以我们选用了Device Mapper。但是实际使用下来,由Device Mapper引起的Docker问题也不少。所以我们也借这个契机把Device Mapper给换掉,换成现在containerd和Docker都默认的Overlayfs。

从图6的测试结果来看,Overlayfs的IO性能比Device Mapper好很多。Overlayfs的IOPS大体上能比Device Mapper高20%,和直接操作主机路径差不多。
6.jpg

图6 后端存储文件系统性能比较

迁移方案

最终,我们选用了containerd,并以Overlayfs作为存储后端的文件系统,替换了原有的Docker加Device Mapper的搭配。那迁移前后的性能是否得到提升呢?我们在同一个节点上同时起10,30,50和80的Pod,然后再同时删除,去比较迁移前后创建和删除的用时。从图7和图8可知,containerd用时明显优于Docker。
7.jpg

图7 创建Pod的用时比较

8.png

图8 删除Pod的用时比较

迁移挑战

从Docker+Device Mapper到containerd+ Overlayfs,容器运行时的迁移并非易事。这个过程中需要删除Device Mapper的thin_pool,全部重新下载用户的容器镜像,全新重建用户的容器。

如图9所示,迁移过程看似简单,但是这对于已运行了5年且拥有100K+光怪陆离的应用程序的集群而言,如何将用户的影响降到最低才是最难的。Containerd在我们生产环境中是否会出现“重大”问题也未可知。
9.png

图9 具体的迁移步骤

针对这些挑战,我们也从下面几个方面做出了优化,来保证我们迁移过程的顺利进行。

多样的迁移策略

最基本的是以容错域(Fault Domain,fd)为单元迁移。针对我们集群,是以rack(机架)为单元(rack by rack)迁移。针对云原生(cloud-native)且跨容错域部署的应用程序,此升级策略最为安全有效。针对非云原生的应用程序,我们根据其特性和部署拓扑,定制了专属他们的升级策略,例如针对Cassini的集群,我们采用了jenga(层层叠)的升级策略,保证应用程序0宕机。

自动化的迁移过程

以rack by rack的策略为例,需要等到一个rack迁移完成以后且客户应用程序恢复到迁移前的状态,才能进行下一个rack的迁移。因此我们对迁移控制器(Controller)进行了加强,利用控制平面(Control Plane)的监控指标(Metrics)和数据平面(Data Plane, 即应用程序)的告警(Alerts),实现典型问题的自动干预和修复功能,详见图10。如果问题不能被修复,错误率达到阈值,迁移才会被暂停。对于大集群,实现了人为的0干预。
10.png

图10 自动化迁移流程

高可用的镜像仓库

一个rack共有76台机器。假设每个机器上只有50个Pod,就可能最多有3800个镜像需要下载。这对镜像仓库的压力是非常大的。除了使用本地仓库,这次迁移过程中还使用了基于gossip协议的镜像本地缓存的功能,来减少远端服务端的压力,具体参见图11。
11.jpg

图11 镜像仓库架构

可逆的迁移过程

虽然我们对containerd的问题修复是有信心的,但是毕竟缺少生产环境经验,得做好随时回退的准备。一旦发现迁移后,存在极大程度影响集群的可靠性和可用性的问题,我们就要换回Docker。虽然迁移后,在线上的确发现了镜像不能成功下载,容器不能启动和删除等问题,但是我们都找到了根本原因,并修复。所以令人庆幸的是,这个回退方法并未发挥其作用。

用户体验

容器运行时是Kubernetes的后端服务。容器运行时的迁移不会改变任何的用户体验。但是有一个Overlayfs的问题需要特别说明一下。如果容器的基础镜像(Base Image)是CentOS 6,利用Dockerfile去创建镜像时,如果用yum去安装包,或者在运行的CentOS 6容器中用yum安装包的,会报以下错误:
12.png

因为yum在安装包的过程中,会先以只读模式,然后再以写模式去打开rmpdb文件。
13.png

如图12所示,对于Overlayfs来说,以只读模式打开一个文件的话,文件直接在下层(lower layer)被打开,我们得到一个fd1。当我们再以写模式打开,就会触发一个copy_up。rmpdb就会拷贝到上层(upper layer)。文件就会在上层打开得到fd2。这两个fd本来是想打开同一个文件,事实却并非如此。
14.png

图12

15.png

图13

解决方案就是在执行yum命令之前先装一个yum-plugin-ovl插件。这个插件就是去做一个初始的copy_up,如图13所示。将rpmdb先拷贝到上层,参考Dockerfile如下:
16.png

如果基础镜像是CentOS 7,则没有这个问题,因为CentOS 7的基础镜像已经有这个规避方法了。

总结

目前我们50个集群,20K+的节点已经全部迁到containerd,历时2个月(非执行时间)。从目前情况来看,还比较稳定。虽然迁移过程中也出了不少问题,但经过各个小组的不懈努力,此次迁移终于顺利完成了。

原文链接:https://mp.weixin.qq.com/s/9dhmQeCPuA_TysMwhzKqGA

0 个评论

要回复文章请先登录注册