Docker 之十八镜像瘦身

缓存

Docker 的优点之一是提供缓存,帮助你更快地迭代镜像构建。在构建映像时,Docker 按步骤遍历 Dockerfile 中的指令,按顺序执行每个指令。在检查每个指令时,Docker 会在其缓存中寻找一个可以重用的现有中间镜像,而不是创建一个新的 (重复的) 中间镜像。如果缓存无效,让无效的指令和所有后续 Dockerfile 指令生成新的中间镜像。因此,从 Dockerfile 的顶部开始,如果基础镜像已经在缓存中,那么它将被重用。然后将下一条指令与从该基础镜像派生的缓存中的所有子镜像进行比较。比较每个缓存的中间镜像,看指令是否在缓存命中。如果缓存失败,则缓存无效。重复相同的过程,直到到达 Dockerfile 的末尾。

缓存陷阱

大多数新指令只是简单地与中间镜像中的指令进行比较。如果匹配,则使用缓存的副本。例如,当在 Dockerfile 中找到 RUN pip install -r requiremtes .txt 指令时,Docker 会在本地缓存的中间镜像中搜索相同的指令。不比较新旧 requirements.txt 文件的内容。如果使用新包来更新 requirements.txt 文件,并使用 RUN pip install 并希望使用新包名称重新运行包安装,则此行为可能会出现问题。后续会展示一些解决方案。与其他 Docker 指令不同,ADD 和 COPY 指令确实需要 Docker 查看文件的内容,以确定是否存在缓存命中。将引用文件的校验和与现有中间镜像中的校验和进行比较。如果文件内容或元数据发生了更改,则缓存无效。

有效使用缓存的技巧

  • 可以通过传递 –no-cache=True 给 docker build 关闭缓存。
  • 如果你要对指令进行更改,那么接下来的每一层都将频繁地重新构建。要利用缓存,请在 Dockerfile 中尽可能靠后放置可能更改的指令。
  • 合并 RUN apt-get update 和 apt-get install 命令,以避免缓存丢失问题。
  • 如果你正在使用带有 requirements.txt 文件的包安装程序 (如 pip),那么请遵循如下模型,以确保你不会收到带有 requirements.txt 中列出的旧包的陈旧的中间镜像。
    1
    2
    3
    COPY requirements.txt /tmp/
    RUN pip install -r /tmp/requirements.txt
    COPY . /tmp/

减少镜像体积

Docker 镜像可能会变得很大,让它们的体积变小,这样它们就可以快速拉起,使用很少的资源。Alpine Base 镜像是一个完整的 Linux 发行版,没有太多其他东西。下载通常小于 5mb,但是它需要更多的时间为构建应用程序所需的依赖项编写代码。如果你的容器中需要 Python, Python Alpine 构建是一个不错的折衷方案。它包含 Linux 和 Python,你可以提供大多数其他东西。用最新的 Python Alpine 构建的带有打印脚本 (“hello world”) 的镜像大小为 78.5 MB。Dockerfile 如下:

1
2
3
FROM python:3.7.2-alpine3.8
COPY . /app
ENTRYPOINT [“python”, “./app/my_script.py”, “my_var”]

多级构建镜像

在 Docker Hub 上,基础镜像的大小为 29MB,当构建子镜像时,需要下载并安装 Python,此时体积就变得很大。除了使用 Alpine 基础镜像,另一种减小镜像大小的方法是使用多级构建。但这种技术还会增加 Dockerfile 的复杂性。多级构建一般使用多个 FROM 指令,可以有选择地将文件 (称为构建工件) 从一个阶段复制到另一个阶段。你可以在最终的镜像中扔掉任何你不想要的东西。这种方法可以减少镜像整体的体积。具体构建流程如下:

  • 每个 FROM 指令
    • 开始构建的新阶段
    • 去掉在之前阶段留下的任何状态
    • 可以用不同的基础镜像
1
2
3
4
5
6
7
8
9
10
FROM golang:1.7.3 AS build
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=build /go/src/github.com/alexellis/href-counter/app .
CMD ["./app"]

上面是 DockerFile 中经过修改的多级构建示例,通过给 FROM 指令增加一个名字来命名第一阶段。然后被命名的阶段会通过 COPY –from= 指令引用到 Dockerfile 中。在制造大量容器的情况下,多级构建是有意义的。多级构建可以帮助你从镜像大小中挤出每一寸空间。然而有时多阶段构建会增加复杂性,使镜像更难维护,因此你可能不会在大多数构建中使用它们。

使用 .dockerignore 文件

建议使用 .dockerignore 文件来帮助保持 Docker 镜像的简洁。.dockerignore 类似于.gitignore,它是一个包含 Docker 模式列表的文件,Docker 在生成镜像时需要匹配文件名并排除这些模式。将.dockerignore 文件与 Dockerfile 和构建上下文的其余部分放在同一个文件夹中。运行 docker build 创建镜像时,Docker 会检查 .dockerignore 文件。如果找到则逐行检查文件并使用 Go 的 filepath 匹配规则和 Docker 自己的一些规则来匹配文件名以进行排除。想想 unix 风格的 glob 模式,而不是正则表达式。因此 *.jpg 将排除扩展名为 .jpg 的文件。可以使用以 # 开头的注释来解释在 .dockerignore 中所做的事情。

  • 使用 .dockerignore 文件的好处
    • 帮助你保守秘密。没有人想在镜像中使用密码。
    • 减少镜像大小。更少的文件意味着更小、更快的镜像。
    • 减少构建缓存失效。如果日志或其他文件正在发生变化,而你的镜像缓存因此而失效,则会减慢构建周期。

镜像体积检查

可以使用下面的命令行找到 Docker 镜像和容器的大小

  • 要查看正在运行的容器的大致大小,可以使用 docker container ls -s 命令。
  • 运行 docker image ls 显示镜像的大小。
  • 要查看组成镜像的中间镜像大小,请使用 docker image history my_image:my_tag。
  • 运行 Docker image inspect my_image:tag,该标签将显示跟镜像有关的信息,包括每个层的大小。
  • 安装和使用 dive 工具可以很容易地看到镜像的层内容。

Docker 镜像瘦身总结

  1. 尽可能使用正式的基础镜像。官方镜像定期更新,比非官方镜像更安全。
  2. 在可能的情况下使用不同的 Alpine 镜像,以保持你的镜像轻量级。
  3. 聪明地使用缓存,将可能发生更改的指令放在 Dockerfile 的末尾位置。
  4. 使用 .dockerignore 文件将不需要的和不必要的文件从镜像中排除。
  5. 使用 dive 工具,可以检查你的 Docker 镜像层,并帮助你削减多余的部分。
  6. 尽量不要安装你不需要的软件包,虽然这点很难做到。
  7. 在运行指令的末尾包含 && rm -rf /var/lib/apt/lists/*,以清理 apt 缓存,使其不存储在层中。
  8. 如果使用 apt,请在同一指令中将 RUN apt-get update 与 apt-get install 结合使用。然后在该指令中链接多个包。用 \ 字符在多行上按字母顺序列出包。这种方法减少了要构建的层的数量,并保持简洁,例如:
1
2
3
4
RUN apt-get update && apt-get install -y \
package-one \
package-two \
&& rm -rf /var/lib/apt/lists/*

本文引用

英文原文链接