docker镜像的优化
一、时间优化
时间优化,就是优化 Docker 镜像构建速度,是优化构建的时间。从实际情况而言,镜像时间的优化可能更重要些,因为Devops
流水线中最多的问题也是镜像构建的时间,如果构建时间长,那就会影响功能的部署,从而影响交付的时间。
1、网络优化
优化网络主要是为了让镜像下载或者依赖的安装、代码下载在网络通道上更加通畅。一般就是调整构建机器网络质量、配置就近的加速仓库地址等等。
2、构建缓存
-
本地缓存
Docker 构建时会下载基础镜像文件,并进行缓存,所以构建缓存优化时,为了减少镜像的传输下载时间,建议使用固定的机器来专门进行镜像的构建。
-
镜像分层缓存
Docker 的一大特色就是镜像的存储分层,在
dockerfile
中的每一个指令会对应到镜像的每一层,并且默认启用缓存,所以构建时每一层是否会缓存取决于三个关键因素。- 镜像父层没有发生变化
- 构建指令不变
- 添加文件校验和一致
构建指令满足这三个条件,这一层镜像构建就不会再执行,而是直接利用之前构建的结果,也就是会利用缓存。
LABEL maintainer
COPY app/ /app
EXPOSE 8080
CMD ["/usr/bin/java", "-cp", "/app", "org.springframework.boot.loader.JarLauncher"]
针对这个特性,可以优化上例中的 dockerfile
,将 COPY app/ /app
指令做一个拆解,将不变化文件(如固定不变的依赖库、固定不变的静态文件或下载文件等)单独摘出来作为一个独立指令,拆解为如下:
WORKDIR /app
COPY target/BOOT-INF/lib ./lib
COPY target/org/ ./org
COPY target/META-INF/ ./META-INF
COPY target/BOOT-INF/classes ./classes
3、编写.dockerignore
编写.dockerignore
文件用于过滤构建过程中不必要的文件,或者创建单独的目录(如 .git
文件夹,markdown 文档等),从而减小目标镜像大小,加快构造速度。
二、空间优化
空间优化,也就是优化 Docker 镜像体积(也称呼为“镜像瘦身”)。
1、镜像的体积优化
为了优化 Docker 体积,通常建议使用 Alpine 类型的版本,因为 Alpine 镜像和类似的其他镜像都经过了优化,其中仅包含最少、必须的软件包,所以它能够节省很多体积。
但是在使用Alpine 镜像时,最好评估好 Alpine 的弊端和风险。如果一定需要使用 Alpine 镜像,最好的方式是先使用 Alpine 镜像做基础镜像,在项目直接使用 Alpine 镜像之前进行一次初始化,然后再用初始化后的版本作为通用的基础镜像。
因为Alpine镜像体积小,其安装的依赖也少,所以会存在各种问题。如下:
-
使用 Alpine 镜像程序容易报错
Alpine 为了追求精简,很多依赖库都没有,需要一些依赖动态链接库的程序运行时就容易报错,比如 Go 的 cgo 调用。
-
域名解析行为跟 glibc 有差异
Alpine 镜像的底层库是 musl libc,域名解析行为跟标准 glibc 有差异,需要特殊作一些修复配置,并且有部分选项在 resolv.conf 中配置不支持。
-
运行 bash 脚本不兼容
因为没有内置 bash,所以运行 bash 的 shell 脚本会不兼容。
-
使用 Alpine 镜像还会导致时区不一致、无法通过 lxcfs 提升容器资源可见性等问题。
2、规划镜像的层数
在编写 dockerfile 时,根据实际情况去合并一些指令,尽量减少镜像层级,以此来优化体积。
在 dockerfile 中每执行一条指令,就会提交一次修改,这次修改会保存成一个只读层挂载到联合文件系统,上面层的文件如果和下面层有冲突或不同,会覆盖隐藏底层的文件,所以每增加一层,镜像大小就会增加,但是在 Docker1.10 后有所改变,只有 RUN、COPY、ADD 指令会创建层,其他指令会创建临时的中间镜像,不会直接增加构建的镜像大小 。
3、多阶段构建镜像
对于镜像优化可以换一个角度去思考,将 dockerfile 的构建配置环境分为软件的编译环境和运行环境,这种方式就是多阶段构建思路。
在 dockerfile 中使用多个 FROM 语句,每个 FROM 指令都可以使用不同的基础镜像,并且配置出独立的子构建阶段,达到构建安全、构建速度快并且镜像文件体积小的目的。
比如:构建一个 Java 程序的 Docker 镜像所配置的 dockerfile
\# 第一阶段 编译基础环境
FROM maven:3.6.3-openjdk-8 as builder
\# 打包pom.xml、src源代码
WORKDIR /usr/src/test
COPY ./ /usr/src/test
RUN [ "mvn" , "clean" , "install" ]
\# 编译
RUN mvn clean package
\# 第二阶段构建,运行程序
FROM openjdk:8
COPY --from=builder /usr/src/test/target /usr/src/test
WORKDIR /usr/src/test
CMD ["java", "-jar" , "app-v1.jar"]
可以看到如上这个 dockerfile 中使用了二阶段构建。
- 第一阶段:选择 Maven 基础镜像
- 第二阶段:拷贝第一阶段生成的 Jar 包到 OpenJDK 镜像中,运行服务
最终构建的镜像将不包含任何源代码信息,这样保障了数据安全性,因为构建的镜像仅包含基础镜像和编译制品 app-v1.jar,所以体积也会减少。假如项目还有制作多系统架构的 Docker 镜像等需求,就可以结合启用 BuildKit 等方式做到多阶段镜像并发执行多个构建流程,从而缩短构建消耗时间。