如何快速打通镜像发布流程?
近期 Q-eye 发布了一篇镜像瘦身的分享,引起大家的关注,很多人来咨询镜像瘦身的方案。这篇分享从实例出发,证明了镜像优化的重要性和有效性,起到了很好的抛砖引玉的作用,但是其内容较短,原理性说明较少,希望通过本文来对镜像瘦身过程进行梳理,以实战案例进一步阐明该方案。
容器化发布通过将应用以及应用所依赖的环境(比如 JRE、动态库,环境变量,系统目录等)一起打包为镜像,解决了发布的一致性问题,即能够 Build Once,Run Everywhere,但是这种方案也带来一个副作用,就是镜像太大,容器镜像提供了分层机制,每次只需要传输变更的层,一定程度上降低了传输量。
为了解释清楚容器镜像的压缩和传输方法,首先简单介绍一下容器分层:
容器分层文件系统
当我们在主机上创建一个新的容器时,会为这个容器单独创建一个文件系统,这个分层的文件系统是将镜像中的文件层作为只读层,并新建一个可读可写的文件层叠加到镜像的只读层上面,容器内的所有操作都发生在读写层。这样做可以实现:
镜像文件都是只读的,可以使用一个镜像创建 N 个容器,每个容器之间都相互隔离
多个容器共享一个只读层,能够利用文件系统的缓存机制,加速数据的读取
当我们制作一个新的镜像时,过程也是类似的,会在基础镜像文件层的基础上新叠加一个读写层,并在读写层上放入新内容,然后将新的读写层变成一个新的只读层,形成一个新的镜像。所以新手容易碰到的一个问题是:我在 Dockerfile 里面使用 rm 删除了基础镜像中的一些文件,但是为什么最终镜像没有减小?理解了这个读写层的机制就会知道,任何操作都发生在读写层,因此删除文件时并不能真的去删除以前的文件,只是在读写层上做一个特殊标记,让文件系统看不到这个文件而已,因此镜像不会缩小。
因此分层机制有如下的弊端:
每次做修改都会新建一个层,这样层级就非常多
一旦写入文件并打成了镜像,后续基于这个镜像制作的镜像,就无法删除这个文件所对应的空间
镜像分层传输机制
一个容器镜像是由描述文件和一系列数据文件组成,每个数据文件对应于一个文件层。当我们拉取或者推送镜像时,首先会获取描述文件,然后根据描述文件判断哪些层本地已经有了(或者远端已经有了),然后就只传输不存在的层即可,大幅减少传输量。但是,如果两个系统之间无法直连,就无法判断哪些镜像层对端已经有了,因此这个机制就会失效。
了解了这些原理后,就可以进一步讨论如何优化镜像的大小了。
Dockerfile 上的优化
为了降低容器镜像的大小,在编写 Dockerfile 时注意参考如下经验:
各团队尽量使用统一的基础镜像。建立并维护公司统一的基础镜像列表。
减少 Dockerfile 的行数,使用 &&连接多个命令,因为每一行命令都会生成一个层。
将增加文件和清理文件的动作放到一行里面,比如 yum install 和 yum clean all,如果分为两行,第二条清理动作就无法真正删除文件。
只复制需要的文件,如果整个目录复制,一定要仔细检查目录下是否有隐藏文件、临时文件等不需要的内容。
容器镜像自身有压缩机制,因此把文件压缩成压缩文件然后打入容器,容器启动时解压的方法并不会有什么效果。
避免向生产镜像打入一些不必要的工具,比如有的团队打入了 sshd,不应该使用这种方案,增加安全风险。
尽量精简安装的内容,比如只安装工具的运行时,无须带上帮助文档、源码、样例等等。
镜像分层上的优化
一个典型的 java 应用的镜像的大小是在 500M 左右,其中 300M 左右是基础镜像(包含 OS/JRE 等),还有 200M 是应用相关的文件。虽然看上去并不多,但是在微服务架构下,应用的数量比较多,假设我们每个版本发布 30 个镜像,则总传输量会有 300M + 200M* 30,大概要 6G 左右,还是会比较大。
可以通过将应用进一步分层来减少每次发布量,有两种划分分层的方法:
1、相似的几个产品共享一个基础镜像,将公共的包放到基础镜像层
假设 A/B/C 三个产品都使用了相同的技术架构,比如都使用了 20 个相同的 jar 包,这些 jar 包一共 100M。如果这三个产品创建一个共享的中间层基础镜像,然后基于这个基础镜像再打各自的镜像,则每次应用层的发布数据量会由原来的 200M * 3 变成 100M + 100M * 3,这样就可以减少发布量。
2、根据冷热进行分离,将不变的部分做到基础镜像
假设 A 应用一共 200M,但是三方 jar 包就有 180M,自己的应用只有 20M,则可以创建一个中间层基础镜像,这样每次发版本时,如果三方 jar 包没有变动,则中间层不需要重新传递,只传递 20M 的自有应用,也可以降低发布的量。
我通常把这种为了减少发布量的分层叫做 offload 层。效果非常直接,但是 offload 层也会带来很多管理问题:
对于第一类,多个产品共享一个 offload 层,需要这几个产品保持密切的沟通,假设需要更新 offload 层某个组件的版本,则几个产品需要同时协同一起变更和发布。如果存在不一致则可能会引入问题。
第二个也会存在 offload 层更新的问题。比如:目前 Java 应用流行使用 maven 来管理依赖,如何根据 maven 的输出及时更新 offload 层?如果更新不及时,也会造成不一致。
Offload 层的更新问题如果依赖管理流程或者人为检查是不可靠的,我们建议将 offload 层当成 cache 一样使用,实现方案如下:
按照业务特点抽取出公共文件和冷文件做成 offload 层。
在制作镜像时,利用 multi-stage builds 机制,先将最新的全量的内容复制到 Stage 中,然后在 Stage 进行一次比对,如果 offload 层是最新的就用,如果不是最新的,则使用最新的替代。
以 Java 为例,Dockerfile 写法如下:
这样即使出现依赖包发生了更新,而 offload 层未能及时更新的情况,只会造成镜像 offload 失败,镜像比较大一些而已,不会造成故障。
注意: 目前发现 tomcat 如果要支持软链接,需要打开 allowLinking 开关,否则会失败。
镜像传输的优化
前文提到过,镜像分层传输必须是源仓库和目标仓库的网络能够互通的情况,但是实际场景往往比较复杂,比如很多项目都有严格的网络管控,不允许服务器直接访问外网;很多国际项目到国内的网络连接比较差,速度慢并且经常丢包。所以我们需要分情况来讨论。
在线的压缩和传输方案
假设能够找到一台机器,这台机器既能够访问源仓库也能够访问目标仓库,可以在这台机器行安装一个 Docker,然后直接使用 docker pull/push 的方式,拉取和上传的过程都是增量传输;但是这种方式需要安装 Docker,并且会占用文件系统空间(镜像会暂存在本地)。推荐使用我们开源的 image-transmit (https://github.com/wct-devops/image-transmit)工具,这个工具一端连接源仓库,一端连接目标仓库,直接将增量数据层转发过去,中间数据不落盘,效率是最高的。同时这个工具是一个绿色版的界面化工具,使用简单(也支持命令行),压缩后只有几 M 大小,资源消耗很少,可以在一些安全跳板机上稳定运行。
离线的压缩和传输方案
很多场景下我们无法实现在线的传输,需要先将镜像保存成一个文件,然后利用各种手段发给现场,比如通过百度云盘中转、存到 U 盘然后快递过去等。在离线传输模式下重点是考虑如何能够把镜像包压缩到最小,以及压缩和解压缩的时间。下面介绍几种模式:
docker save|gzip 方式,这种是最基本的方法,找一台安装有 Docker 的机器,将镜像拉取到本地,然后使用 save 命令保存并压缩。这种方式非常耗时,同时压缩包也是最大的。
使用上文提到的 image-transmit 工具进行离线打包,默认使用 tar 算法。这个工具可以直接从仓库上下载镜像的数据文件,合并成压缩包,比上一种方式减少了保存到本地然后导出以及压缩和解压缩的过程,速度可以提升 20 倍,同时如果一次压缩多个镜像,相同的镜像层只会保留一份,这能够降低压缩包的大小,以我们自己的镜像版本为例,使用工具得到的压缩包是 docker save 方法的 1/3 到 1/5 大小。
同时 image-transmit 还提供了 squashfs 算法,这种压缩算法在上一种方式的基础上更进一步,会把每一个镜像层都解压,对每一个文件进行固实压缩,举个例子,A 产品和 B 产品都用到了 demo.jar,但是这个 jar 并不在基础镜像层,上一种方式是无法识别这种重复的,但是 squashfs 压缩方式可以识别并将其压缩为一份,这样能够进一步降低压缩包的大小,以我们自己的镜像包为例,这种压缩方式可以在上一种方式的基础上再降低 30%~50%。但是这种压缩算法由于需要将所有的镜像层都解压然后进行压缩,导致其压缩时间非常长,上一种方式 5 分钟可以压缩完的包,这种方式要一个小时左右。
压缩算法的选择可以根据实际情况来选择,甚至把两种方式都试一下,综合选择一个合适的。离线方式因为无法直连仓库,所以上文中的方法是把所有的镜像层都保存到离线包中了,那么如何在离线模式下也能实现增量方式发布呢?
image-transmit 实现了这样一种增量发布方法:在制作压缩包时,可以根据上次的压缩包的信息自动跳过已经发送过的镜像层,只发增量变更的部分,这样就可以进一步降低压缩包的大小。不过这种方式也有弊端,比如必须严格按照顺序下载版本,如果遗漏某个版本可能会造成现场仓库缺失一些数据层,造成失败。如果版本发布比较多,可以采用类似如下的方案来规避这种风险:
每个月发布一个全量的版本包
每日/周的版本基于月初的全量包来制作增量包
这种准实时的增量同步加上定期的全量同步,即可以降低同步量又可以避免缺失一些层造成传输失败。
总结和后续规划
镜像传输还有一些优化方向亟待研究,比如:
业界有很多镜像,其容器内只有应用的二进制程序,因此其镜像大小与传统应用发布没有什么区别。但是这种镜像在做问题分析时,需要依赖外部的工具,一定程序上提高了故障分析的门槛,可以通过 sidecar 方式来提供排障工具。
镜像过大不仅仅影响发布,在版本升级、容器切换等容器重新创建的场景下,消耗大量的网络带宽和磁盘 IO,镜像仓库容易成为瓶颈,需要考虑类似 Dragonfly 的 P2P 分发方案。
目前我们主要使用 Centos 作为基础镜像,我们正在考虑更换更为轻量的,专门为容器设计的基础镜像。
镜像瘦身其实是个系统性的工程,需要多个团队相互配合,从技术平台到业务应用到交付相互配合一起落地。
版权声明: 本文为 InfoQ 作者【鲸品堂】的原创文章。
原文链接:【http://xie.infoq.cn/article/f49d6540ac2335ff35ecc943d】。文章转载请联系作者。
评论