有关如何加快构建 Docker 镜像的一些技巧。 例如,最长 30 秒

在功能投入生产之前,在复杂的协调器和 CI/CD 的时代,从提交到测试和交付还有很长的路要走。 以前,您可以通过 FTP 上传新文件(现在已经没有人这样做了,对吧?),“部署”过程只需几秒钟。 现在您需要创建合并请求并等待很长时间才能让该功能到达用户手中。

此路径的一部分是构建 Docker 映像。 有时集会持续几分钟,有时长达数十分钟,这很难说是正常的。 在本文中,我们将采用一个简单的应用程序,将其打包到图像中,应用多种方法来加速构建,并了解这些方法如何工作的细微差别。

有关如何加快构建 Docker 镜像的一些技巧。 例如,最长 30 秒

我们在创建和支持媒体网站方面拥有丰富的经验: 塔斯社, 贝尔, 《新报纸》, 共和国…不久前,我们发布了产品网站,扩大了我们的产品组合 提醒。 虽然新功能很快被添加,旧错误也被修复,但缓慢的部署成为了一个大问题。

我们部署到 GitLab。 我们收集图像,将它们推送到 GitLab 注册表并将其投入生产。 这个列表中最长的事情是组装图像。 例如:在没有优化的情况下,每个后端构建需要 14 分钟。

有关如何加快构建 Docker 镜像的一些技巧。 例如,最长 30 秒

最后,我们清楚地意识到我们不能再这样生活了,我们坐下来思考为什么要花这么长时间来收集图像。 结果,我们成功地将组装时间缩短至 30 秒!

有关如何加快构建 Docker 镜像的一些技巧。 例如,最长 30 秒

在本文中,为了不被 Reminder 的环境所束缚,我们来看一个组装空 Angular 应用程序的示例。 那么,让我们创建我们的应用程序:

ng n app

添加 PWA(我们是渐进式的):

ng add @angular/pwa --project app

当一百万个 npm 包被下载时,让我们弄清楚 docker 镜像是如何工作的。 Docker 提供了打包应用程序并在称为容器的隔离环境中运行它们的能力。 由于隔离,您可以在一台服务器上同时运行多个容器。 容器比虚拟机轻得多,因为它们直接运行在系统内核上。 要使用我们的应用程序运行容器,我们首先需要创建一个映像,在其中打包应用程序运行所需的所有内容。 本质上,映像是文件系统的副本。 以 Dockerfile 为例:

FROM node:12.16.2
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build --prod

Dockerfile 是一组指令; 通过执行每一个操作,Docker 都会将更改保存到文件系统中,并将它们覆盖在之前的更改上。 每个团队创建自己的层。 最终的图像是层层组合在一起的。

需要了解的重要一点是:每个 Docker 层都可以缓存。 如果自上次构建以来没有任何变化,那么 docker 将采用现成的层,而不是执行命令。 由于构建速度的主要增长将归因于缓存的使用,因此在测量构建速度时,我们将特别注意使用现成的缓存构建图像。 所以,一步一步:

  1. 我们在本地删除图像,以便以前的运行不会影响测试。
    docker rmi $(docker images -q)
  2. 我们第一次启动构建。
    time docker build -t app .
  3. 我们更改 src/index.html 文件 - 我们模仿程序员的工作。
  4. 我们第二次运行构建。
    time docker build -t app .

如果构建镜像的环境配置正确(更多内容见下文),那么当构建开始时,Docker 已经拥有了一堆缓存。 我们的任务是学习如何使用缓存,以便构建尽快进行。 由于我们假设在没有缓存的情况下运行构建仅发生一次(第一次),因此我们可以忽略第一次的速度有多慢。 在测试中,当缓存已经预热并且我们准备好烘烤蛋糕时,构建的第二次运行对我们来说很重要。 然而,一些技巧也会影响第一次构建。

让我们将上面描述的 Dockerfile 放入项目文件夹中并开始构建。 为了便于阅读,所有列表都经过压缩。

$ time docker build -t app .
Sending build context to Docker daemon 409MB
Step 1/5 : FROM node:12.16.2
Status: Downloaded newer image for node:12.16.2
Step 2/5 : WORKDIR /app
Step 3/5 : COPY . .
Step 4/5 : RUN npm ci
added 1357 packages in 22.47s
Step 5/5 : RUN npm run build --prod
Date: 2020-04-16T19:20:09.664Z - Hash: fffa0fddaa3425c55dd3 - Time: 37581ms
Successfully built c8c279335f46
Successfully tagged app:latest

real 5m4.541s
user 0m0.000s
sys 0m0.000s

我们更改 src/index.html 的内容并再次运行它。

$ time docker build -t app .
Sending build context to Docker daemon 409MB
Step 1/5 : FROM node:12.16.2
Step 2/5 : WORKDIR /app
 ---> Using cache
Step 3/5 : COPY . .
Step 4/5 : RUN npm ci
added 1357 packages in 22.47s
Step 5/5 : RUN npm run build --prod
Date: 2020-04-16T19:26:26.587Z - Hash: fffa0fddaa3425c55dd3 - Time: 37902ms
Successfully built 79f335df92d3
Successfully tagged app:latest

real 3m33.262s
user 0m0.000s
sys 0m0.000s

要查看我们是否有图像,请运行命令 docker images:

REPOSITORY   TAG      IMAGE ID       CREATED              SIZE
app          latest   79f335df92d3   About a minute ago   1.74GB

在构建之前,docker 获取当前上下文中的所有文件并将它们发送到其守护进程 Sending build context to Docker daemon 409MB。 构建上下文被指定为构建命令的最后一个参数。 在我们的例子中,这是当前目录 - “.”, - Docker 会将我们拥有的所有内容拖到此文件夹中。 409 MB 太大了:让我们考虑一下如何解决它。

减少上下文

为了减少上下文,有两种选择。 或者将组装所需的所有文件放在一个单独的文件夹中,并将 docker 上下文指向该文件夹。 这可能并不总是很方便,因此可以指定例外:不应将哪些内容拖到上下文中。 为此,请将 .dockerignore 文件放入项目中并指出构建不需要的内容:

.git
/node_modules

并再次运行构建:

$ time docker build -t app .
Sending build context to Docker daemon 607.2kB
Step 1/5 : FROM node:12.16.2
Step 2/5 : WORKDIR /app
 ---> Using cache
Step 3/5 : COPY . .
Step 4/5 : RUN npm ci
added 1357 packages in 22.47s
Step 5/5 : RUN npm run build --prod
Date: 2020-04-16T19:33:54.338Z - Hash: fffa0fddaa3425c55dd3 - Time: 37313ms
Successfully built 4942f010792a
Successfully tagged app:latest

real 1m47.763s
user 0m0.000s
sys 0m0.000s

607.2 KB 比 409 MB 好得多。 我们还将图像大小从 1.74 GB 减少到 1.38 GB:

REPOSITORY   TAG      IMAGE ID       CREATED         SIZE
app          latest   4942f010792a   3 minutes ago   1.38GB

让我们尝试进一步减小图像的大小。

我们用阿尔派

节省图像大小的另一种方法是使用较小的父图像。 父母的形象是我们的形象准备的基础上的形象。 底层由命令指定 FROM 在 Dockerfile 中。 在我们的例子中,我们使用的是已经安装了nodejs的基于Ubuntu的映像。 而且它的重量...

$ docker images -a | grep node
node 12.16.2 406aa3abbc6c 17 minutes ago 916MB

...几乎是千兆字节。 使用基于Alpine Linux的镜像可以显着减小体积。 Alpine 是一个非常小的Linux。 基于 alpine 的 Nodejs 的 docker 镜像仅重 88.5 MB。 因此,让我们更换房屋中的活泼形象:

FROM node:12.16.2-alpine3.11
RUN apk --no-cache --update --virtual build-dependencies add 
    python 
    make 
    g++
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build --prod

我们必须安装一些构建应用程序所需的东西。 是的,没有 Python 就无法构建 Angular ¯(°_o)/¯

但图像大小下降到 150 MB:

REPOSITORY   TAG      IMAGE ID       CREATED          SIZE
app          latest   aa031edc315a   22 minutes ago   761MB

让我们更进一步。

多级组装

并非图像中的所有内容都是我们在生产中需要的。

$ docker run app ls -lah
total 576K
drwxr-xr-x 1 root root 4.0K Apr 16 19:54 .
drwxr-xr-x 1 root root 4.0K Apr 16 20:00 ..
-rwxr-xr-x 1 root root 19 Apr 17 2020 .dockerignore
-rwxr-xr-x 1 root root 246 Apr 17 2020 .editorconfig
-rwxr-xr-x 1 root root 631 Apr 17 2020 .gitignore
-rwxr-xr-x 1 root root 181 Apr 17 2020 Dockerfile
-rwxr-xr-x 1 root root 1020 Apr 17 2020 README.md
-rwxr-xr-x 1 root root 3.6K Apr 17 2020 angular.json
-rwxr-xr-x 1 root root 429 Apr 17 2020 browserslist
drwxr-xr-x 3 root root 4.0K Apr 16 19:54 dist
drwxr-xr-x 3 root root 4.0K Apr 17 2020 e2e
-rwxr-xr-x 1 root root 1015 Apr 17 2020 karma.conf.js
-rwxr-xr-x 1 root root 620 Apr 17 2020 ngsw-config.json
drwxr-xr-x 1 root root 4.0K Apr 16 19:54 node_modules
-rwxr-xr-x 1 root root 494.9K Apr 17 2020 package-lock.json
-rwxr-xr-x 1 root root 1.3K Apr 17 2020 package.json
drwxr-xr-x 5 root root 4.0K Apr 17 2020 src
-rwxr-xr-x 1 root root 210 Apr 17 2020 tsconfig.app.json
-rwxr-xr-x 1 root root 489 Apr 17 2020 tsconfig.json
-rwxr-xr-x 1 root root 270 Apr 17 2020 tsconfig.spec.json
-rwxr-xr-x 1 root root 1.9K Apr 17 2020 tslint.json

docker run app ls -lah 我们根据我们的镜像启动了一个容器 app 并执行其中的命令 ls -lah,之后容器完成其工作。

在生产中我们只需要一个文件夹 dist。 在这种情况下,文件需要以某种方式提供给外部。 你可以在nodejs上运行一些HTTP服务器。 但我们会让事情变得更容易。 猜猜一个有四个字母“y”的俄语单词。 正确的! Ynzhynyksy。 让我们用 nginx 拍摄一个图像,在其中放入一个文件夹 dist 和一个小配置:

server {
    listen 80 default_server;
    server_name localhost;
    charset utf-8;
    root /app/dist;

    location / {
        try_files $uri $uri/ /index.html;
    }
}

多阶段构建将帮助我们完成这一切。 让我们更改我们的 Dockerfile:

FROM node:12.16.2-alpine3.11 as builder
RUN apk --no-cache --update --virtual build-dependencies add 
    python 
    make 
    g++
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build --prod

FROM nginx:1.17.10-alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx/static.conf /etc/nginx/conf.d
COPY --from=builder /app/dist/app .

现在我们有两条指令 FROM 在 Dockerfile 中,它们每个都运行不同的构建步骤。 我们叫第一个 builder,但是从最后一个 FROM 开始,我们的最终图像将会准备好。 最后一步是使用 nginx 将上一步中的程序集复制到最终映像中。 图像的大小显着减小:

REPOSITORY   TAG      IMAGE ID       CREATED          SIZE
app          latest   2c6c5da07802   29 minutes ago   36MB

让我们用我们的镜像运行容器并确保一切正常:

docker run -p8080:80 app

使用 -p8080:80 选项,我们将主机上的端口 8080 转发到运行 nginx 的容器内的端口 80。 在浏览器中打开 http://localhost:8080/ 我们看到了我们的应用程序。 一切正常!

有关如何加快构建 Docker 镜像的一些技巧。 例如,最长 30 秒

将映像大小从 1.74 GB 减小到 36 MB 可以显着缩短将应用程序交付到生产环境所需的时间。 但让我们回到集合时间。

$ time docker build -t app .
Sending build context to Docker daemon 608.8kB
Step 1/11 : FROM node:12.16.2-alpine3.11 as builder
Step 2/11 : RUN apk --no-cache --update --virtual build-dependencies add python make g++
 ---> Using cache
Step 3/11 : WORKDIR /app
 ---> Using cache
Step 4/11 : COPY . .
Step 5/11 : RUN npm ci
added 1357 packages in 47.338s
Step 6/11 : RUN npm run build --prod
Date: 2020-04-16T21:16:03.899Z - Hash: fffa0fddaa3425c55dd3 - Time: 39948ms
 ---> 27f1479221e4
Step 7/11 : FROM nginx:stable-alpine
Step 8/11 : WORKDIR /app
 ---> Using cache
Step 9/11 : RUN rm /etc/nginx/conf.d/default.conf
 ---> Using cache
Step 10/11 : COPY nginx/static.conf /etc/nginx/conf.d
 ---> Using cache
Step 11/11 : COPY --from=builder /app/dist/app .
Successfully built d201471c91ad
Successfully tagged app:latest

real 2m17.700s
user 0m0.000s
sys 0m0.000s

更改图层顺序

我们的前三个步骤已被缓存(提示 Using cache)。 第四步,复制所有项目文件,第五步安装依赖项 RUN npm ci - 长达 47.338 秒。 如果依赖项很少发生变化,为什么每次都要重新安装依赖项呢? 让我们找出为什么它们没有被缓存。 重点是,Docker 会逐层检查命令以及与其关联的文件是否发生了变化。 第四步,我们复制了我们项目的所有文件,其中当然也有变化,所以Docker不仅不从缓存中取出这一层,还包括所有后续的! 让我们对 Dockerfile 进行一些小更改。

FROM node:12.16.2-alpine3.11 as builder
RUN apk --no-cache --update --virtual build-dependencies add 
    python 
    make 
    g++
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build --prod

FROM nginx:1.17.10-alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx/static.conf /etc/nginx/conf.d
COPY --from=builder /app/dist/app .

首先,复制package.json和package-lock.json,然后安装依赖项,然后复制整个项目。 因此:

$ time docker build -t app .
Sending build context to Docker daemon 608.8kB
Step 1/12 : FROM node:12.16.2-alpine3.11 as builder
Step 2/12 : RUN apk --no-cache --update --virtual build-dependencies add python make g++
 ---> Using cache
Step 3/12 : WORKDIR /app
 ---> Using cache
Step 4/12 : COPY package*.json ./
 ---> Using cache
Step 5/12 : RUN npm ci
 ---> Using cache
Step 6/12 : COPY . .
Step 7/12 : RUN npm run build --prod
Date: 2020-04-16T21:29:44.770Z - Hash: fffa0fddaa3425c55dd3 - Time: 38287ms
 ---> 1b9448c73558
Step 8/12 : FROM nginx:stable-alpine
Step 9/12 : WORKDIR /app
 ---> Using cache
Step 10/12 : RUN rm /etc/nginx/conf.d/default.conf
 ---> Using cache
Step 11/12 : COPY nginx/static.conf /etc/nginx/conf.d
 ---> Using cache
Step 12/12 : COPY --from=builder /app/dist/app .
Successfully built a44dd7c217c3
Successfully tagged app:latest

real 0m46.497s
user 0m0.000s
sys 0m0.000s

46 秒而不是 3 分钟 - 好多了! 正确的层顺序很重要:首先复制不变的内容,然后复制很少变化的内容,最后复制经常变化的内容。

接下来,简单介绍一下在 CI/CD 系统中组装镜像的情况。

使用以前的图像进行缓存

如果我们使用某种 SaaS 解决方案进行构建,那么本地 Docker 缓存可能会是干净且新鲜的。 为了给docker一个地方来获取烘焙层,给他之前构建的镜像。

让我们以在 GitHub Actions 中构建应用程序为例。 我们使用这个配置

on:
  push:
    branches:
      - master

name: Test docker build

jobs:
  deploy:
    name: Build
    runs-on: ubuntu-latest
    env:
      IMAGE_NAME: docker.pkg.github.com/${{ github.repository }}/app
      IMAGE_TAG: ${{ github.sha }}

    steps:
    - name: Checkout
      uses: actions/checkout@v2

    - name: Login to GitHub Packages
      env:
        TOKEN: ${{ secrets.GITHUB_TOKEN }}
      run: |
        docker login docker.pkg.github.com -u $GITHUB_ACTOR -p $TOKEN

    - name: Build
      run: |
        docker build 
          -t $IMAGE_NAME:$IMAGE_TAG 
          -t $IMAGE_NAME:latest 
          .

    - name: Push image to GitHub Packages
      run: |
        docker push $IMAGE_NAME:latest
        docker push $IMAGE_NAME:$IMAGE_TAG

    - name: Logout
      run: |
        docker logout docker.pkg.github.com

该镜像在 20 分 XNUMX 秒内完成组装并推送到 GitHub Packages:

有关如何加快构建 Docker 镜像的一些技巧。 例如,最长 30 秒

现在让我们更改构建,以便基于以前构建的图像使用缓存:

on:
  push:
    branches:
      - master

name: Test docker build

jobs:
  deploy:
    name: Build
    runs-on: ubuntu-latest
    env:
      IMAGE_NAME: docker.pkg.github.com/${{ github.repository }}/app
      IMAGE_TAG: ${{ github.sha }}

    steps:
    - name: Checkout
      uses: actions/checkout@v2

    - name: Login to GitHub Packages
      env:
        TOKEN: ${{ secrets.GITHUB_TOKEN }}
      run: |
        docker login docker.pkg.github.com -u $GITHUB_ACTOR -p $TOKEN

    - name: Pull latest images
      run: |
        docker pull $IMAGE_NAME:latest || true
        docker pull $IMAGE_NAME-builder-stage:latest || true

    - name: Images list
      run: |
        docker images

    - name: Build
      run: |
        docker build 
          --target builder 
          --cache-from $IMAGE_NAME-builder-stage:latest 
          -t $IMAGE_NAME-builder-stage 
          .
        docker build 
          --cache-from $IMAGE_NAME-builder-stage:latest 
          --cache-from $IMAGE_NAME:latest 
          -t $IMAGE_NAME:$IMAGE_TAG 
          -t $IMAGE_NAME:latest 
          .

    - name: Push image to GitHub Packages
      run: |
        docker push $IMAGE_NAME-builder-stage:latest
        docker push $IMAGE_NAME:latest
        docker push $IMAGE_NAME:$IMAGE_TAG

    - name: Logout
      run: |
        docker logout docker.pkg.github.com

首先我们需要告诉你为什么启动两个命令 build。 事实上,在多阶段装配中,生成的图像将是来自最后阶段的一组层。 在这种情况下,先前图层中的图层将不会包含在图像中。 因此,当使用先前构建的最终镜像时,Docker 将无法找到准备好的层来使用 nodejs(构建器阶段)构建镜像。 为了解决这个问题,创建了中间图像 $IMAGE_NAME-builder-stage 并被推送到 GitHub Packages,以便可以在后续构建中用作缓存源。

有关如何加快构建 Docker 镜像的一些技巧。 例如,最长 30 秒

总组装时间减少至一分半钟。 花了半分钟来调出以前的图像。

预成像

解决干净 Docker 缓存问题的另一种方法是将某些层移动到另一个 Dockerfile 中,单独构建它,将其推送到容器注册表中并将其用作父级。

我们创建自己的 Nodejs 映像来构建 Angular 应用程序。 在项目中创建Dockerfile.node

FROM node:12.16.2-alpine3.11
RUN apk --no-cache --update --virtual build-dependencies add 
    python 
    make 
    g++

我们在 Docker Hub 中收集并推送公共镜像:

docker build -t exsmund/node-for-angular -f Dockerfile.node .
docker push exsmund/node-for-angular:latest

现在在我们的主 Dockerfile 中我们使用完成的镜像:

FROM exsmund/node-for-angular:latest as builder
...

在我们的示例中,构建时间并没有减少,但如果您有许多项目并且必须在每个项目中安装相同的依赖项,则预构建的映像可能会很有用。

有关如何加快构建 Docker 镜像的一些技巧。 例如,最长 30 秒

我们研究了几种加速 docker 镜像构建的方法。 如果您希望部署快速进行,请尝试在您的项目中使用它:

  • 减少背景;
  • 使用小的父图像;
  • 多级组装;
  • 更改 Dockerfile 中的指令顺序以有效利用缓存;
  • 在 CI/CD 系统中设置缓存;
  • 图像的初步创建。

我希望这个示例能够让您更清楚地了解 Docker 的工作原理,并且您将能够以最佳方式配置您的部署。 为了使用本文中的示例,创建了一个存储库 https://github.com/devopsprodigy/test-docker-build.

来源: habr.com

添加评论