效率 | 如何高效率地构建镜像?

1. 单一职责 & 保持容器最小化

由于容器的本质是进程,一个容器代表一个进程,因此不同功能的应用应该尽量拆分为不同的容器,每个容器只负责单一业务进程。

应该避免安装无用的软件包,比如在一个 nginx 镜像中,我并不需要安装 vim 、gcc 等开发编译工具。这样不仅可以加快容器构建速度,而且可以避免镜像体积过大。

避免将太多内容COPY进容器,这样会不利于代码安全且会增大不必要的体积,可以采取映射数据卷-v的方式关联到容器内。1

2. 使用 .dockerignore 文件

在使用git时,我们可以使用.gitignore文件忽略一些不需要做版本管理的文件。同理,使用.dockerignore文件允许我们在构建时,忽略一些不需要参与构建的文件,从而提升构建效率。.dockerignore的定义类似于.gitignore。

.dockerignore的本质是文本文件,Docker 构建时可以使用换行符来解析文件定义,每一行可以忽略一些文件或者文件夹。具体使用方式如下:

规则 含义
# # 开头的表示注释,# 后面所有内容将会被忽略
/tmp 匹配当前目录下任何以 tmp 开头的文件或者文件夹
*.md 匹配以 .md 为后缀的任意文件
tem? 匹配以 tem 开头并且以任意字符结尾的文件,?代表任意一个字符
!README.md ! 表示排除忽略。 例如 .dockerignore 定义如下:*.md !README.md 表示除了 README.md 文件外所有以 .md 结尾的文件。

3. 分层构建

在 Dockerfile 中,每一条指令都会创建一个新的层,并且会将其结果保存为一个新的镜像。每一层都是基于前一层构建的。因此,如果你在 Dockerfile 的后面改变了某些东西,所有后续的层都需要被重新构建。

因此,分层构建就是将 Dockerfile 编写得尽可能优化,以最大限度地利用 Docker 的层缓存机制。通常的做法是将那些不经常改动的指令(例如安装依赖)放在 Dockerfile 的前面,并将那些经常改动的指令(例如复制你的应用代码)放在后面。

下面是一个分层构建的示例:

FROM python:3.7

# Set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

# Set work directory
WORKDIR /code

# Install dependencies
COPY requirements.txt /code/
RUN pip install --upgrade pip
RUN pip install -r requirements.txt

# Copy project
COPY . /code/

在这个例子中,我们首先复制了 requirements.txt 文件并安装了依赖。只有在 requirements.txt 文件改变时,这一层才需要重新构建。然后我们复制了应用代码。即使应用代码经常改变,也只有最后一层需要重新构建,而所有先前的层(包括安装依赖的那一层)都可以使用缓存。

通过这种方式,我们可以最小化每次构建需要重新构建的层数,并大大加快构建过程。

4. 使用缓存

Docker 会自动缓存构建过程中的每一层。当你再次构建同一个 Dockerfile 时,如果某一层和上次构建时相比没有发生变化(即该层对应的 Dockerfile 指令和上次一样,且其之前的所有层也都没有变化),那么 Docker 会自动使用上次构建的结果,而不是重新执行该指令。这就是 Docker 的层缓存机制。

以下是一些优化 Docker 构建过程以更好地利用缓存的建议:

  1. 将改动频率低的指令放在 Dockerfile 的前面:比如安装操作系统包的指令(RUN apt-get update && apt-get install -y ...)和安装 Python 库的指令(RUN pip install -r requirements.txt)。这样,即使你频繁地修改和重新构建你的应用代码,这些安装指令也不会被频繁地执行。
  2. 将改动频率高的指令放在 Dockerfile 的后面:比如复制应用代码的指令(COPY . /app)。这样,即使你频繁地修改和重新构建你的应用代码,只有这一层和之后的层需要被重新构建,而前面的层都可以使用缓存。
  3. 避免不必要的文件变动:比如,如果你的 requirements.txt 文件和你的应用代码在同一个目录,并且你使用了 COPY . /app 指令,那么只要你的应用代码有任何改动,requirements.txt 文件就会被认为发生了变动,即使实际上没有。这会导致 RUN pip install -r requirements.txt 这一层及之后的所有层都需要被重新构建。为了避免这种情况,你可以先用 COPY requirements.txt /app 指令复制 requirements.txt 文件,并在此之后立即运行 RUN pip install -r requirements.txt 指令,然后再用 COPY . /app 指令复制你的应用代码。
  4. 为不同阶段使用多个 Dockerfile:如果你的构建过程可以分为多个独立的阶段(比如编译阶段和运行阶段),那么你可以考虑为每个阶段使用一个单独的 Dockerfile。这样,即使某个阶段的 Dockerfile 发生了变化,也不会影响其他阶段的缓存。这需要使用 Docker 的多阶段构建功能。

请注意,对于使用了 --no-cache 选项的 docker build 命令,Docker 会忽略所有缓存,并重新执行每一条指令。

分层构建 & 使用缓存

在 Docker 构建过程中,“分层构建"和"使用缓存"是两个密切相关的概念。

  1. 分层构建:Docker 的镜像是由多个层(layers)构建而成的,每一个 Dockerfile 中的命令都会创建一个新的层。这种分层的结构使得 Docker 镜像可以复用共享的层,从而减少磁盘空间的占用,提高镜像构建和分发的效率。
  2. 使用缓存:在构建 Docker 镜像时,Docker 会默认使用缓存机制。也就是说,如果 Dockerfile 中的某个命令及其前面所有的命令都没有变化,那么 Docker 在构建过程中就会复用之前构建的结果,而不是重新执行该命令。这种机制可以大大提高镜像的构建速度。

这两个概念的关联点在于,Docker 的缓存机制是通过复用镜像的层来实现的。如果 Dockerfile 的某个命令发生变化,那么该命令和它之后的所有命令在构建过程中都将无法使用缓存,需要重新执行。因此,一种常见的优化方法是将不经常变化的命令放在 Dockerfile 的前面,将经常变化的命令放在后面,这样可以最大化地利用缓存,提高构建速度。

注意,虽然使用缓存可以提高构建速度,但是也可能导致某些更新无法被正确地应用到镜像中。在需要确保镜像完全基于最新状态构建的情况下,可以使用 docker build --no-cache 命令来禁用缓存。

5. 使用国内的镜像源

使用国内的 PyPI 镜像源,比如阿里云、豆瓣等,可以极大地提高下载速度。在 Dockerfile 中,你可以在 pip install 命令后添加 -i 参数来指定 PyPI 镜像源。例如,使用阿里云的 PyPI 镜像:

RUN pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/

6. 优化 requirements.txt 文件

检查你的 requirements.txt 文件,去掉不必要的包,减少安装的数量。

尝试将 requirements.txt 分成多个文件,按照它们的改动频率或者它们的重要性进行分组。例如,你可以把那些基础且稳定的库(比如 numpy, pandas)放在一个文件(比如 base_requirements.txt)中,然后把那些经常需要更新的库放在另一个文件(比如 dynamic_requirements.txt)中。

避免使用pip freeze > requirements.txt去生成文件pip freeze 命令会列出所有的库包括那些由其他库作为依赖被安装的库。如果直接使用此命令,可能会生成一个非常长的列表,而且很多库可能并不是你的项目直接依赖的。

pipenvpoetry 都是 Python 的依赖管理工具,它们可以帮助你管理你的项目的依赖库和虚拟环境。

使用 pipreqs 工具pipreqs 是一个可以自动扫描你的项目,找出所有直接依赖的工具。你可以使用以下命令安装和使用它:

pip install pipreqs
pipreqs ./

pipreqs 会在项目目录中生成一个 requirements.txt 文件,这个文件只包含直接依赖。

这样可以确保 requirements.txt 文件只包含那些你的代码直接使用到的库,而不会包含那些间接依赖的库

如果安装这个库没有添加至环境变量,可以这样解决:

  1. 在 Unix/Linux/macOS 中:

    你可以在你的 shell 配置文件(如 ~/.bashrc~/.bash_profile~/.zshrc/etc/profile)中添加以下行:

    export PATH=$(python -m site --user-base)/bin:$PATH
    

    然后,重新加载你的 shell 配置文件:

    source ~/.bashrc  # 或 ~/.bash_profile 或 ~/.zshrc
    
  2. 在 Windows 中:

    首先,你需要找出 Python 的 Scripts 目录的路径。你可以在 Python 解释器中运行以下代码来找到这个路径:

    import site
    print(site.USER_BASE, 'Scripts')
    

    然后,你需要将这个路径添加到系统的 PATH 环境变量中。这个步骤取决于你的 Windows 版本和配置。一般来说,你可以在系统设置的 “环境变量” 部分完成这个步骤。你可能需要重启你的命令行工具或者电脑才能让改动生效。


2926 字