编者按:正文开始前,请允许小编再次介绍下作者,这位充电宝都在公司充的北大学霸,不但会过日子,还讲究情怀,是个锤粉。因此他对精益求精都有一种偏执的追求,比如,不但要让 Docker 使开发,测试,生产环境的统一变得容易,还要达到如丝般润滑的持续集成。
作者简介:刘梦馨,灵雀云高级软件工程师,兼相声大师,目前在灵雀云从事 CaaS 平台的研发工作。从事过开发、测试、运维相关职位,专注于云计算和虚拟化技术。个人博客 http://oilbeater.com ,微博 @oilbeater 。学霸的其它文章:《 Docker 工程师必读论文: Google Borg 》,《使用云计算的正确姿势》。
下面进入正文:
Docker 的出现使开发,测试,生产环境的统一变得容易,但是在搭建好基于 Docker 的一整套流水线之后,却发现它运行得不像丝般润滑,甚至比在本地开发测试的效率还低。为什么呢?让我们来看一下这个过程中 Docker 的使用以及 Docker 自身存在着哪些问题,我们又该如何克服这些问题,从而达到如丝般润滑的持续集成。
我们首先来分解一下现在常见的一种利用 Docker 做持续部署的流程:
在这五步中, 1 和 5 的耗时都比较短,主要耗时集中在中间 3 步,也就是 docker build , push , pull 的时间消耗,我们就来分别看一下如何加速这三个步骤。
由于 dockerhub 的官方镜像再国外,而这些基础镜像的软件源都在国外,国内构建的时候网络会是很大的瓶颈,有能力在国外机器进行构建,并且可以通过专线和国内进行传输的话,还是优先将构建节点放在国外,会省很多无谓的在网络上的纠缠,并且很多软件源国外的也要更稳定写,更新也更及时。
如果只能在国内进行构建的话,建议使用国内的镜像,或者自己在私有仓库存一份官方镜像,并且对镜像进行改造,做一份软件源都在国内的基础镜像,把构建过程中的网络传输都控制在国内或者内网,这样就不用和网络进行纠缠了。
.dockerignore 可以减少构建时的文件传输,一般通过 git 进行持续构建的时候不做设置都会把 .git 文件夹进行传输造成很多无用的传输,一些与构建无关的代码也尽量写在.dockerigonre 文件中。
dockerfile 的优化也是一个比较直接的优化方式,优化的核心就是能充分利用 build cache ,把每次变化的部分放在最后,一般把加入代码放在最后一步,这样每次构建只有最后一层是新的,其他部分都是可以用 cache 的。对于 node 、 python 、 go 之类要在构建过程中安装依赖的服务,可以把安装依赖和加入代码分两步完成,这样在依赖不变的情况下这部分的缓存也是可以利用的。以 node 为例:
COPY package.json /usr/src/app/
RUN npm install
COPY . /usr/src/app
其他关于 dockerfile 优化的建议可以再单独开一篇了,基本上每个命令都需要特殊对待才能不掉坑里,可以参考一个在线 dockerfile 语法优化器( http://dockerfile-linter.com/),里面会提供一些相关的 dockerfile 优化建议和一些资源,作者一定是个大好人。
在单机模式下充分利用 build cache 是个不错的注意,但是在多个构建机器的情况下就会有问题了。出于磁盘空间考量不可能所有机器都存着所有的镜像,这样缓存优化的 dockerfile 就没有用武之地了。为了让 cache 重新发挥作用我们可以在构建开始时将旧的镜像 pull 下来,这样一来就可以再次利用 cache 了。但是:
pull 镜像也是需要很多时间的,并且 pull 下来的镜像并不会全部有用,会浪费一定的时间;
而来如果 dockerfile 变化比较大有可能没有一层能用 pull 下来反而会浪费更多的时间;
三来仓库内可能会有其他的镜像更适合做当前构建的缓存所以我们需要实现一个精准的镜像拉取,不能出错也不能浪费。
举个栗子,如下图所示:
想要构建 node:wheezy 的话,那么 node:0-wheezy 是一个比较合适的镜像来做 cache 而想要构建 node:5 的话那么 node:wheezy 和 node:0-wheezy 都不太合适,反而是 python:latest 会更合适。如果我们把仓库中所有的镜像都做成这样一个森林,利用 tire 树可以很精准的知道,哪个镜像的哪几层是 cache 的最好选择,这样精确制导不会有一点浪费。
docker registry 在升级到 v2 后加入了很多安全相关检查,使得原来很多在 v1 already exist 的层依然要 push 到 registry,并且由于 v2 中的存储格式变成了 gzip ,在镜像压缩过程中占用的时间很有可能比网络传输还要多。我们简单分解一下 docker push 的流程。
此外判断 already exist skip pushing 的条件变严格了,必须是本地计算过 digest 且 该 digest 对应的文件属在对应 repo 存在才可以。
换句话说就是如果这个镜像层是 pull 下来的,那么是没有 digest 的还是要把整个压缩包传输并计算 digest ,如果这个镜像你之前并没有比如 ubuntu 的 base image 你的 repo 第一次创建之前没传输过,那么第一次也要你传输一次确认你真的有 ubuntu 。
这里面的改进点就是在太多了,先列举 Docker 官方已经做得和正在做的。
但是这只解决了一小部分问题, push 依然会很慢, docker 和 registry 的设计更多的考虑了公有云的环境设置了过多的安全防范为了防止镜像的伪造和越权获取,但是在一个可信的环境内如果 build 和 push 过程都是自己掌控的,那么很多措施都是多余的,我们可以设计一个自己的 smart pusher 挖掘性能的最大潜力:
有了 smart pusher , push 时间的绝大多数都被隐藏到了 build 的时间中,我们把并发和流水线的技术都用上,充分发挥了多核的优势。
docker pull 镜像的速度对服务的启动速度至关重要,好在 registry v2 后可以并行 pull 了,速度有了很大的改善。但是依然有一些小的问题影响了启动的速度:
为此我们还需要一个独特设计的 smart puller 帮助我们解决最后的问题:
有了 smart puller 我们自然的将 docker pull 的工作和 docker daemon 解耦了,这样再不会发生 pull 导致的 docker hang,服务稳定性也得到了增强,解绑后其实 Docker 只是做一个 runtime 这一部分也可考虑改成 runc 去除掉 daemon 这个单点,不过这个工作量就比较大了。此外 smart puller 也可以帮助我们实现在 smart cache 中的精确 pull 以及 pull cache 的加速,可谓一举多得。
将 push 和 pull 的工作和 daemon 解绑,把 smart cache , smart puller 和 smart pusher 用上后,持续集成如丝般润滑。 smart 系列已在灵雀云小部分上线测试,还请大家持续关注。
至于我把全系列工具都以 smart 命名,主要是为了给 Smartisan T2 开卖造势 (逃)
欢迎大家在讨论区有血性的争论、动手、拍砖、捅刀子,亮出你的看法来!