优化docker镜像的几种方法
Devops和k8s的火热,越来越多的企业将docker运用到自动化运维中,不管是为了保证开发、测试、生产环境的环境一致性,还是和CI/CD工具的集成度,比如jenkins对docker或k8s的自动构建部署等,亦或利用docker进行自动化测试等
那么,在现在这种随随便便一天动辄几十次的快速构建迭代中,镜像作为一个贯穿整个自动化过程中的一个关键,怎么保证自动化构建部署的效率?就是镜像尽可能的小
要保证镜像尽可能小,可以从五个方面
基础镜像小
层级尽量少
去除不必要
复用镜像层
分阶段构建
基础镜像小
基础镜像小,主要是保证镜像层底层或者说From的镜像本身小
每个企业或个人使用容器,都是应对不同的业务场景,没有完全一致的业务场景,所以你最好不要直接用别人的第三方镜像,除非你了解该镜像的所有层级内容,而且从安全角度考虑,也尽量使用官方镜像,它没有太多第三方的,你不需要的东西,你可以在此基础上增加你的业务部分内容
选择Alpine镜像代替Ubuntu、CentOS、Debian等镜像
虽然Alpine没有其他系统完备的库、依赖,但是基本的应用它都是支持的,都可以通过apk去安装(apk包管理) ,而且官方现在也推荐用Alpine,很多开源项目都有基于Alpine的官方镜像
但如果你的项目涉及到编译,比如python等涉及编译的项目,要注意,Alpine用的是musl,因为它原本是用作嵌入式系统的,所以并没有glibc那么完整的C标准库
另外如果你要在Alpine中跑一些脚本的话,那你要注意一些shell和linux下的还是有所区别的,Alpine是基于busybox的,同样也是设计于嵌入式的,所以很多shell命令做了裁剪,并不具备Ubuntu、CentOS、Debian等系统中那么完整的功能
除了使用小镜像之外,可以使用空镜像scratch,自己手动添加rootfs来构建镜像,这个不建议新手,因为你可能不知道你需要些啥,折腾半天反而比官方镜像还要大
层级尽量少
前面文章有介绍docker的联合文件系统"Docker挂了,数据如何找回",Dockerfile构建镜像流程大致如下:
docker从基础镜像运行一个容器
执行一条指令对容器进行修改
执行类似docker commit 的操作提交这次修改
docker再基于刚提交的镜像运行一个新容器
执行Dockerfile中的下一条指令,依次循环,直到命令执行完成
所以每执行一条Dockerfile中的指令,就会提交一次修改,这次修改会保存成一个只读层挂载到联合文件系统,看过上面的文章应该知道,上面层的文件,如果和下面层有冲突或不同,会覆盖隐藏底层的文件,所以每增加一层,镜像大小就会增加,虽然在docker1.10后只有RUN、COPY、ADD指令会创建层,其他指令会创建临时的中间镜像,不会直接增加构建的镜像大小
所以在编写Dockerfile时,我们可以根据实际情况去合并一些指令,比如我们在编译安装nginx时,解压、编译、安装以及删除源文件的指令可以放在一起,以减少最终的镜像层
去除不必要
前面提到的用空镜像,或者裁剪过的小镜像来做基础镜像,其实就是一种去除不必要的依赖、库的一种形式
除了以上的这种形式,还有必要去除的,就是Dockerfile构建过程中或手动构建后commit的过程中所产生的临时文件
比如源码包、编译过程中产生的日志文件、添加的包管理仓库、包管理缓存,以及构建过程中安装的一些当时又用过后没用的软件或工具
如果可以,甚至建议不在容器中进行编译,如果二进制binary文件可以执行的话,在本地编译后,将binary文件copy到容器内
除了上面的,还有一些不常更新的文件,比如web静态资源文件css、js以及图片、视频等资源,建议存储OSS或共享存储系统nfs、mfs等,这些文件不应该打包到镜像里面,而应该通过OSS调用或通过共享存储挂载
对于不需要build进镜像的资源,可以使用.dockerignore文件进行指定要忽略的文件或目录
当然,如果你想基于别人的镜像来做优化的话,可以通过docker history命令来查看镜像的层级关系,做相应的优化,更好的工具推荐dive
当然也可以用自动化的镜像瘦身工具docker-slim,它支持静态分析和动态分析,静态分析主要是通过分析镜像历史信息,获取生成镜像的dockerfile文件及相关的配置信息,而动态分析主要是通过ptrace、pevent、fanotify解析出镜像中必要的文件和文件依赖,将对应文件组织成新镜像来减小镜像体积
另外还可以通过docker-squash来压缩镜像层级,但是要考虑实际情况,并不是压缩一定是好的
复用镜像层
接上面为什么压缩不一定是好,压缩的原理是将镜像导出,然后删除所有中间层,将镜像的当前状态保存为单一层,达到压缩层级的效果
当你使用单一镜像或者少量镜像的时候可能没有太大问题,但是这样完全破坏了镜像的层级缓存功能
还是之前的文章中提的关于docker的存储的,docker镜像的每个层级会存一个hash计算后的目录,那么Dockerfile构建过程中怎么利用缓存?
在镜像的构建过程中,Docker根据Dockerfile指定的顺序执行每个指令。在执行每条指令之前,Docker都会在缓存中查找是否已经存在可重用的镜像,如果有就使用现存的镜像,不再重复创建
而如果压缩为单一的层之后,缓存就失效了,不会命中缓存的层级,所以每次构建或者pull的时候,都是整个镜像构建或pull
缓存命中除了和分层有关系,还和指令执行编排顺序有关系,首先看下缓存匹配遵循的基本规则:
从一个基础镜像开始(FROM指令指定),下一条指令将和该基础镜像的所有子镜像进行匹配,检查这些子镜像被创建时使用的指令是否和被检查的指令完全一样。如果不是,则缓存失效
在大多数情况下,只需要简单地对比Dockerfile中的指令和子镜像。然而,有些指令需要更多的检查和解释
对于ADD和COPY指令,镜像中对应文件的内容也会被检查,每个文件都会计算出一个校验值。这些文件的修改时间和最后访问时间不会被纳入校验的范围。在缓存的查找过程中,会将这些校验和和已存在镜像中的文件校验值进行对比。如果文件有任何改变,比如内容和元数据,则缓存失效
除了ADD和COPY指令,缓存匹配过程不会查看临时容器中的文件来决定缓存是否匹配。例如,当执行完RUN apt-get -y update指令后,容器中一些文件被更新,但Docker不会检查这些文件。这种情况下,只有指令字符串本身被用来匹配缓存
一旦缓存失效,所有后续的Dockerfile指令都将产生新的镜像,缓存不会被使用
所以为什么和指令执行顺序和编排有关系,或者说我们在合并命令减少层级的时候不能一味的追求合并,而是需要合理的合并一些指令
举个例子,比如我们用同一个基础镜像,分别编译nginx和php,那么nginx也需要pcre库依赖,php也需要,那我们是不是可以提取共同的依赖用一条RUN指令去执行,而不是每次构建都执行
再或者最简单的,添加镜像仓库,安装基本的编译工具,比如gcc、autoconf、make、zlib等这些不常改动,但是常用的指令放在前面去执行,这样后面构建用到的所有镜像都不会再重新安装
这样合理的利用层级缓存,不管是在jenkins中自动构建镜像,还是push到远程仓库、亦或是在部署pull的时候,都能够利用缓存,从而节省传输带宽和时间
分阶段构建
最后一个更重要的是分阶段构建,或者多阶段构建,其实它也是一种减少分层或者去除不必要的一种方式,单独列出来,是觉得这个方式应该是推荐的一种方式,在docker17.05中开始支持
具体的多阶段构建,就是通过将构建过程分析,分成多个阶段来执行,后面的或者最终的构建可以使用前面构建的结果,而不需要所有的构建都包含到最终的镜像中
拿nginx构建来举个例子
上图是一个nginx的Dockerfile,构建之后查看大小
然后通过多阶段构建改造Dockerfile
再构建后查看镜像大小
只比基础镜像多了1MB,之前的所有构建阶段as build在打包镜像的时候全部被抛弃,只最后FROM生成最终镜像
基于多阶段构建,google推出了distroless,更加轻量级,它只包含应用程序机器运行时依赖项,不包含程序管理器、shell以及标准liunx发行版中可以找到的任何其他程序
这应该就是正是我们所需要的,而我们又不会剪裁,distroless帮我们做了这个工作
还是基于刚才的nginx,我们不以rhel7为基本镜像了,我们以distroless为基本镜像构建
构建后查看大小
通过以上几种方式,可以有效精简docker镜像,从而提高自动化运维过程中的CI/CD效率,缩短交付时间
Docker挂了,数据如何找回为什么你的docker容器刚启动就停了Gitlab+docker玩CI/CD
更多精彩内容请扫描下方二维码关注公众号