查看原文
其他

干货 | 容器运行时从docker到containerd的迁移

苏菲 eBay技术荟 2019-08-06

供稿 | eBay Infrastructure Engineering 苏菲

翻译&编辑 | 顾欣怡

本文2634字,预计阅读时间8分钟

更多干货请关注“eBay技术荟”公众号



导读


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



容器运行时(Container Runtime),运行于kubernetes(k8s)集群的每个节点中,负责容器的整个生命周期。其中docker是目前应用最广的。随着容器云的发展,越来越多的容器运行时涌现。为了解决这些容器运行时和k8s的集成问题,在k8s 1.5版本中,社区推出了CRI(Container Runtime Interface, 容器运行时接口)(如图1所示),以支持更多的容器运行时。

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

图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 常用的运行时举例

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 容器运行时调用层级

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

图4 containerd和crio的性能比较

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

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


三、Device Mapper vs. Overlayfs

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

当初选用Device Mapper,也是有历史原因的。我们大概是在2014年开始k8s这个项目的。那时候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 后端存储文件系统性能比较


四、迁移方案

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

图7 创建pod的用时比较

图8 删除pod的用时比较


五、迁移挑战

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

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

图9 具体的迁移步骤

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


01

多样的迁移策略

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


02

自动化的迁移过程

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

图10 自动化迁移流程


03

高可用的镜像仓库

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

图11 镜像仓库架构


04


可逆的迁移过程

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


六、用户体验

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

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

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

图12

图13

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

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


七、总结

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


↓点击阅读原文,一键投递

 eBay大量优质职位,等的就是你!


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存