云原生时代,2个方案轻松加速百万级镜像
随着集群规模的扩大,您是否曾经因镜像分发问题而困扰过?根据不同的场景,我们利用不同的镜像分发方法:
- 基于 P2P 的 CNCF/ Dragonfly (以下称为“蜻蜓”)分发是缓解镜像中心带宽和减少分发时间的最直接方式。
- CNCF/containerd 中的远程文件系统快照程序直接远程存储镜像,使容器引擎通过网络读取镜像内容,几乎不需要时间分发。
你会发现第二种方式依赖于网络稳定性。本文将总结如何根据镜像内容读请求动态加载从远程到本地存储的镜像作为权衡,以及如何选择适合镜像分布的方式。
背景介绍
在业务很小的时候,可以单台机器部署单个容器;但当业务量大了,并且分为多个应用时,就需要将多个应用部署在同一台机器上;此时多个应用的依赖是一个比较难解决的问题。且多个应用同时运行,相互之间也有所干扰。
一开始,可以使用虚拟机的方式来实现,这样每个应用一个虚拟机,基本不存在依赖冲突,但是虚拟机还是会有启动慢,空间占用大等一系列问题。
后面随着技术的发展,有了 lxc 技术,它能让应用跑在一个单独的命名空间上,实现了应用的运行态隔离。
这里可以看到,虚拟机解决了静态隔离的问题,lcx 解决了运行态隔离的问题,而 Docker 则同时解决了运行态和静态隔离两个问题,因此得到亲睐。
Docker 核心
Docker 的三大核心:镜像、容器、仓库。容器在 Docker 之前已经有了不同的实现,比如 lxc。镜像和仓库是 Docker 的很大创新。镜像是一个实体,仓库是它的集中存储中心,而容器则是镜像的运行时。
Docker 通过镜像,可以很神奇地实现比如:开发的同学在自己喜欢的平台上面做开发测试(例如 ubuntu),而真正部署的节点,可能是 redhat/centos 的服务器。
Docker 是以镜像为基础,所以容器运行时必须要有镜像,而容器又可以通过 docker commit 生成一个镜像。但是第一个镜像从哪里来呢?可以通过 docker import/load 的方式导入。并且可以通过 docker push/pull 的方向将镜像上传到镜像中心,或从镜像中心下载镜像。
Docker 提供了一套 docker build 机制,方便来制作镜像。想像一下,没有 docker build 机制,制作镜像的步骤如下:
- 从一个基础镜像启动一个容器;
- 在容器中执行对应的命令,比如安装一个软件包,添加一个文件等;
- 使用 docker commit 命令,把刚才的容器 commit 成一个镜像;
- 如果还有其它步骤要做,就要从新镜像中启动一个容器,然后再 commit,直到完成所有操作。
有了 docker build 机制,只需要一个 dockerfile,然后 Docker 就会把上述步骤自动化了,最后生成一个目标镜像。
详细看一下是如何使用镜像的,镜像是一层一层的,设计上,镜像所有层是只读的,linux 通过 aufs/overlayfs 的方式将一个镜像 mount 到主机上后,从上往下看,最上面层是容器的可读写层,容器运行时的所有写操作都写到这一层上面,而下面的层都是镜像的只读层。
大规模集群镜像下载慢
在 Docker 的 registry 或其它网站上看到,像 busybox, nginx, mysql 等镜像都不大,也就是几十 M 到几百 M 的样子。
但是阿里巴巴的镜像普遍有 3~4G,镜像这么大原因有许多,比如:
- 集团使用了富容器的模式,这是一种类似虚拟机的方式,对开发较为友好,开发可以在以前的经验上做开发,不需要太多的学习成本,这就导致集团的基础镜像有 2~3G;
- 集团的业务都采用分布式的方式开发,依赖许多中间件,而中间件因版本的原因,没有完全统一起来,每个中间件都有几百 M;再加上本身 java 业务包也不小,所以普遍 3~4G 是很正常的;
- 集团有上十万的物理机,上面运行着上百万的容器,每天有着上千次发布。可以想像一下,就单单把镜像从镜像中心下载到物理机上面,每天消耗的带宽是巨大的。
这就导致一个问题:镜像中心很有可能被打爆。
这种情况在集团出现多次,在打爆的情况下,当前的镜像拉取被阻塞,同时又有更多的镜像拉取请求上来,导致镜像中心严重超负荷运行,并且致使问题越来越严重,可能会导致整个系统奔溃。因此,必须解决镜像下载导致的问题。
蜻蜓架构
集团开发了蜻蜓(Dragonfly )这套 P2P 下载系统。上面是蜻蜓的架构图,蜻蜓可以分为三层,最上层是一个配置中心,中间是超级节点,下面就是运行容器的物理节点。
配置中心管理整个蜻蜓集群,当物理节点上有镜像要下载时,它通过 dfget 命令向超级节点发出请求,而向哪些超级节点发出请求是由配置中心配置的。
以下图为例:
当下载一个文件时,物理节点发送了一个 dfget 请求到超级节点,超级节点会检查自身是否有这部分数据,如果有,则直接把数据传送给物理节点;否则会向镜像中心发出下载请求,这个下载请求是分块的,这样,只有下载失败的块需要重新下载,而下载成功的块,则不需要。
更为重要的是,如果是两个或更多物理节点请求同一份数据,那么蜻蜓收到请求后,只会往镜像中心发一个请求,极大地降低了镜像中心的压力。
关于 P2P,也就是说如果一个节点有了另一个节点的数据,为了减少超级节点的压力,两个节点可以完成数据的互传。这样更高效地利用了网络带宽。
蜻蜓效果
从上面的蜻蜓效果图可以看到:
随着镜像拉取的并发量越来越大,传统方式所消耗的时间会越来越多,后面的线条越来越陡;但是蜻蜓模式下,虽然耗时有增加,但只是轻微增加。效果还是非常明显的。
蜻蜓里程碑
应该说,集团内是先有蜻蜓,后面才有 docker 的。原因是集团在全面 docker 之前,使用了 t4 等技术,不管哪种技术,都需要在物理机上面下载应用的包,然后把应用跑起来。
所以,从里程碑上看,2015 年 6 月就已经有了蜻蜓 p2p 了,随着集团全面 Docker 化,在 2015 年 9 月,蜻蜓提供了对 Docker 镜像的支持,后面的几个时间点上,可以看到蜻蜓经受了几次双 11 的考验,并且经过软件迭代,在 2017 年 11 月的时候实现了开源。
现在蜻蜓还在不断演化,比如说全部使用 golang 重写,解决 bug,提供更多能力集,相信蜻蜓的明天更美好。
蜻蜓也不能完美解决的问题——镜像下载
蜻蜓在集团取得了极好的效果,但是并不能解决所有问题,比如我们就遇到了以下比较突出的问题:
- 镜像下载导致业务抖动
在实践中,上线的业务运行经常有抖动的现象,分析其中原因,有部分业务抖动时,都在做镜像下载。
分析原因发现:镜像层采用的是 gzip 压缩算法。这一古老的算法,是单线程运行,并且独占一个 CPU,更要命的是解压速度也很慢,如果几个镜像层同时展开的话,就要占用几个 CPU,这抢占了业务的 CPU 资源,导致了业务的抖动。
- 应用扩容要更好的时效性
平时应用发布、升级时,只需要选择在业务低峰,并且通过一定的算法,比如只让 10% 左右的容器做发布、升级操作,其它 90% 的容器还给用户提供服务。这种不影响业务、不影响用户体验的操作,对于时效性要求不高,只要在业务高峰来临前完成操作即可,所以耗时一两个小时也无所谓。
假设遇到大促场景,原计划 10 个容器可以服务 100 万用户,但是,突然来了 300 万用户,这时所有用户的体验将会下降,这时就需要通过扩容手段来增加服务器数量,此时对容器扩出来有着极高的时效要求。
- 如何而对云环境
现在所有公司都在上云,集团也在上云阶段,但云上服务器的一些特性与物理机还是有些差别,对镜像来讲感受最深的就是磁盘 IO 了,原来物理机的 SSD 磁盘,IO 可以达到 1G 以上,而使用 ECS 后,标准速度是 140M,速度只有原来的十分之一。
对于 gzip 的问题,通过实测及数据分析,如上图所示:
1 与 2 下载的是同一个镜像层,只是 1 下载的是一个 gzip 格式的,而 2 下载的是一个没有压缩的镜像层。可以看到 2 因为下载的数据量要大很多,所以下载的时间要长许多,但是 1 中将 400M 的 gzip 还原成 1G 的原始数据却又消耗了太多时间,所以最终两者总体时效是差不多的。
并且由于去掉 gzip,镜像下载不会抢占业务的 CPU 资源。自从上了这一方案后,业务抖动的次数明显减少了许多。
在蜻蜓的章节,镜像下载是镜像层从超级节点到物理机,在这一过程中,蜻蜓有一个动态压缩能力,比如使用更好的 lz4 算法,即达到了数据压缩的效果,又不会造成业务抖动。这就是图 3,整体上这一点在集团的应用效果很不错。
远程盘架构
解决了镜像下载对业务的干扰后,扩容、云环境的问题还没有解决。这时远程盘就派上用场了。
从上面的架构图看,集团有一套镜像转换机制,将原始的镜像层放在远端服务器上,第一个层都有一个唯一的远程盘与之对应。然后镜像中保存的是这个远程盘的 id,这样做下来,远程盘的镜像可以做到 kB 级别。对于 kB 级别的镜像,下载耗时在 1~2 秒之间。
通过远程盘,解决了镜像下载的问题,同时由于远程盘放在物理机同一个机房,容器运行时读取镜像数据,相当于从远程盘上面读取数据,因为在同一个机房,当然不能跟本地盘比,但是效果可以与云环境的云盘性能相媲美。
远程盘的问题
远程盘虽然解决了镜像下载的问题,但是所有镜像的数据都是从远程盘上读取,消耗比较大的网络带宽。当物理机上环境比较复杂时,远程盘的数据又不能缓存在内存时,所有数据都要从远端读取,当规模上来后,就会给网络带来不小的压力。
另外一个问题是,如果远程盘出现问题,导致 IO hang,对于容器进程来讲,就是僵尸进程,而僵尸进程是无法杀死的。对于应用来讲,一个容器要么是死,要么是活,都好办,死的了容器上面的流量分给活着的容器即可,但对于一个僵尸容器,流量没法摘除,导致这部分业务就受损了。
最后,远程盘因为要服务多台物理机,必然要在磁盘 IO 上面有比较好的性能,这就导致了成本较高。
DADI
针对上述问题,集团采用的 DADI 这一项技术,都是对症下药:
- 使用 P2P 技术
远程盘是物理机所有数据都要从远程盘上读,这样会导致对远程盘机器的配置较高的要求,并且压力也很大。
而通过 P2P 手段,可以将一部分压力分担掉,当一台机器已经有另一台需要的数据时,它俩之间可以完成数据互传。
- 高效压缩算法
同样是解决网络带宽的问题,远程盘对应的数据都是没有压缩的,传输会占用比较多的带宽。
而 DADI 则在传输过程中,跟蜻蜓一样,对数据进行压缩,减少网络压力。
- 本机缓存
这个是核心买点了,当容器启动后,DADI 会把整个镜像数据拉取到本地,这样即使网络有问题,也不会导致容器进程僵尸,解决了业务受损的问题。