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

供稿 | 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的搭配。那迁移前后的性能是否得到提升呢?我们在同一个节点上同时起 10 30 5080 的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安装包的,会报以下错误:

因为yu m在安装包的过程中,会先以 只读模 ,然后再以 写模式 去打开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大量优质职位,等的就是你!