Alguns consells sobre com accelerar la creació d'imatges de Docker. Per exemple, fins a 30 segons

Abans que una funció entri en producció, en aquests dies d'orquestradors complexos i CI/CD, hi ha un llarg camí per recórrer des del compromís fins a les proves i el lliurament. Anteriorment, podríeu pujar fitxers nous mitjançant FTP (ja no ho fa ningú, oi?), i el procés de "desplegament" va trigar segons. Ara heu de crear una sol·licitud de combinació i esperar molt de temps perquè la funció arribi als usuaris.

Part d'aquest camí és crear una imatge de Docker. De vegades el muntatge dura minuts, de vegades desenes de minuts, cosa que difícilment es pot dir normal. En aquest article, agafarem una aplicació senzilla que empaquetarem en una imatge, aplicarem diversos mètodes per accelerar la compilació i veurem els matisos de com funcionen aquests mètodes.

Alguns consells sobre com accelerar la creació d'imatges de Docker. Per exemple, fins a 30 segons

Tenim una bona experiència en la creació i suport de llocs web de mitjans: TASS, La Campana, "Nou diari", República… No fa molt que vam ampliar la nostra cartera llançant un lloc web de productes Recordatori. I tot i que es van afegir ràpidament noves funcions i es van corregir errors antics, el desplegament lent es va convertir en un gran problema.

Despleguem a GitLab. Recopilem imatges, les enviem a GitLab Registry i les distribuïm a la producció. El més llarg d'aquesta llista és muntar imatges. Per exemple: sense optimització, cada construcció de backend va trigar 14 minuts.

Alguns consells sobre com accelerar la creació d'imatges de Docker. Per exemple, fins a 30 segons

Al final, va quedar clar que ja no podíem viure així, i ens vam asseure per esbrinar per què les imatges tardaven tant a recollir-se. Com a resultat, vam aconseguir reduir el temps de muntatge a 30 segons!

Alguns consells sobre com accelerar la creació d'imatges de Docker. Per exemple, fins a 30 segons

Per a aquest article, per no estar lligat a l'entorn de Reminder, mirem un exemple de muntatge d'una aplicació Angular buida. Per tant, creem la nostra aplicació:

ng n app

Afegiu-hi PWA (som progressistes):

ng add @angular/pwa --project app

Mentre s'estan descarregant un milió de paquets npm, anem a esbrinar com funciona la imatge de Docker. Docker ofereix la possibilitat d'empaquetar aplicacions i executar-les en un entorn aïllat anomenat contenidor. Gràcies a l'aïllament, podeu executar molts contenidors simultàniament en un servidor. Els contenidors són molt més lleugers que les màquines virtuals perquè s'executen directament al nucli del sistema. Per executar un contenidor amb la nostra aplicació, primer hem de crear una imatge en la qual empaquetarem tot el que sigui necessari perquè la nostra aplicació s'executi. Bàsicament, una imatge és una còpia del sistema de fitxers. Per exemple, preneu el Dockerfile:

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

Un Dockerfile és un conjunt d'instruccions; En fer cadascun d'ells, Docker desarà els canvis al sistema de fitxers i els superposarà als anteriors. Cada equip crea la seva pròpia capa. I la imatge acabada són capes combinades.

El que és important saber: cada capa de Docker es pot emmagatzemar a la memòria cau. Si no ha canviat res des de l'última compilació, en lloc d'executar l'ordre, l'acoblador agafarà una capa ja feta. Atès que l'augment principal de la velocitat de construcció es deu a l'ús de la memòria cau, quan mesurem la velocitat de construcció prestarem atenció específicament a la creació d'una imatge amb una memòria cau ja feta. Així doncs, pas a pas:

  1. Esborram les imatges localment perquè les execucions anteriors no afectin la prova.
    docker rmi $(docker images -q)
  2. Llencem la construcció per primera vegada.
    time docker build -t app .
  3. Canviem el fitxer src/index.html: imitem el treball d'un programador.
  4. Executem la construcció per segona vegada.
    time docker build -t app .

Si l'entorn per a la creació d'imatges està configurat correctament (més informació a continuació), quan s'iniciï la compilació, Docker ja tindrà un munt de memòria cau a bord. La nostra tasca és aprendre a utilitzar la memòria cau perquè la compilació vagi el més ràpid possible. Com que suposem que l'execució d'una compilació sense memòria cau només es fa una vegada, la primera vegada, podem ignorar la lentitud que va ser aquesta primera vegada. A les proves, la segona execució de la construcció és important per a nosaltres, quan els cachés ja estan escalfats i estem preparats per coure el nostre pastís. Tanmateix, alguns consells també afectaran la primera construcció.

Posem el Dockerfile descrit anteriorment a la carpeta del projecte i comencem la compilació. Tots els llistats s'han condensat per facilitar la lectura.

$ 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

Canviem el contingut de src/index.html i l'executem una segona vegada.

$ 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

Per veure si tenim la imatge, executeu l'ordre docker images:

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

Abans de construir, Docker agafa tots els fitxers en el context actual i els envia al seu dimoni Sending build context to Docker daemon 409MB. El context de compilació s'especifica com l'últim argument de l'ordre de compilació. En el nostre cas, aquest és el directori actual - ".", - i Docker arrossega tot el que tenim en aquesta carpeta. 409 MB són molts: pensem com solucionar-ho.

Reduint el context

Per reduir el context, hi ha dues opcions. O col·loqueu tots els fitxers necessaris per al muntatge en una carpeta separada i assenyaleu el context del docker a aquesta carpeta. Això pot no ser sempre convenient, de manera que és possible especificar excepcions: què no s'ha d'arrossegar al context. Per fer-ho, poseu el fitxer .dockerignore al projecte i indiqueu què no és necessari per a la compilació:

.git
/node_modules

i torna a executar la construcció:

$ 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 és molt millor que 409 MB. També hem reduït la mida de la imatge d'1.74 a 1.38 GB:

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

Intentem reduir encara més la mida de la imatge.

Utilitzem Alpine

Una altra manera d'estalviar la mida de la imatge és utilitzar una imatge principal petita. La imatge parental és la imatge a partir de la qual es prepara la nostra imatge. La capa inferior s'especifica mitjançant l'ordre FROM a Dockerfile. En el nostre cas, estem utilitzant una imatge basada en Ubuntu que ja té instal·lat nodejs. I pesa...

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

... gairebé un gigabyte. Podeu reduir significativament el volum utilitzant una imatge basada en Alpine Linux. Alpine és un Linux molt petit. La imatge docker per a nodejs basada en alpine només pesa 88.5 MB. Així que substituïm la nostra imatge animada a les cases:

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

Hem hagut d'instal·lar algunes coses que són necessàries per construir l'aplicació. Sí, Angular no es construeix sense Python ¯(°_o)/¯

Però la mida de la imatge va baixar a 150 MB:

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

Anem encara més enllà.

Muntatge multietapa

No tot el que hi ha a la imatge és el que necessitem a la producció.

$ 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

Amb docker run app ls -lah vam llançar un contenidor basat en la nostra imatge app i hi va executar l'ordre ls -lah, després del qual el contenidor va completar la seva feina.

En producció només necessitem una carpeta dist. En aquest cas, els fitxers s'han de donar d'alguna manera fora. Podeu executar algun servidor HTTP a nodejs. Però ho posarem més fàcil. Endevina una paraula russa que tingui quatre lletres "y". Dret! Ynzhynyksy. Prenguem una imatge amb nginx, posem-hi una carpeta dist i una petita configuració:

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

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

La construcció en diverses etapes ens ajudarà a fer tot això. Canviem el nostre 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 .

Ara tenim dues instruccions FROM al Dockerfile, cadascun d'ells executa un pas de construcció diferent. Vam trucar al primer builder, però a partir de l'últim FROM, es prepararà la nostra imatge final. L'últim pas és copiar l'artefacte del nostre conjunt del pas anterior a la imatge final amb nginx. La mida de la imatge ha disminuït significativament:

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

Executem el contenidor amb la nostra imatge i assegurem-nos que tot funciona:

docker run -p8080:80 app

Utilitzant l'opció -p8080:80, vam reenviar el port 8080 de la nostra màquina amfitrió al port 80 dins del contenidor on s'executa nginx. Oberta al navegador http://localhost:8080/ i veiem la nostra aplicació. Tot funciona!

Alguns consells sobre com accelerar la creació d'imatges de Docker. Per exemple, fins a 30 segons

La reducció de la mida de la imatge d'1.74 GB a 36 MB redueix significativament el temps que triga a lliurar la vostra aplicació a producció. Però tornem al temps del muntatge.

$ 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

Canvi de l'ordre de les capes

Els nostres primers tres passos es van guardar a la memòria cau (suggerència Using cache). Al quart pas, es copien tots els fitxers del projecte i al cinquè pas s'instal·len les dependències RUN npm ci - fins a 47.338 s. Per què tornar a instal·lar les dependències cada cop si canvien molt poques vegades? Anem a esbrinar per què no es van guardar a la memòria cau. La qüestió és que Docker comprovarà capa per capa per veure si l'ordre i els fitxers associats amb ella han canviat. En el quart pas, copiem tots els fitxers del nostre projecte, i entre ells, per descomptat, hi ha canvis, de manera que Docker no només no treu aquesta capa de la memòria cau, sinó també totes les posteriors! Anem a fer alguns petits canvis al 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 .

Primer, es copien package.json i package-lock.json, després s'instal·len les dependències i només després es copia tot el projecte. Com a resultat:

$ 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 segons en lloc de 3 minuts, molt millor! L'ordre correcte de les capes és important: primer copiem allò que no canvia, després allò que canvia poques vegades i, finalment, allò que canvia sovint.

A continuació, algunes paraules sobre el muntatge d'imatges en sistemes CI/CD.

Ús d'imatges anteriors per a la memòria cau

Si utilitzem algun tipus de solució SaaS per a la compilació, la memòria cau local de Docker pot estar neta i fresca. Per donar-li al docker un lloc per obtenir les capes al forn, doneu-li la imatge construïda anterior.

Prenguem un exemple de creació de la nostra aplicació a GitHub Actions. Utilitzem aquesta configuració

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

La imatge es munta i s'envia als paquets GitHub en dos minuts i 20 segons:

Alguns consells sobre com accelerar la creació d'imatges de Docker. Per exemple, fins a 30 segons

Ara canviem la compilació de manera que s'utilitzi una memòria cau basada en imatges construïdes anteriors:

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

Primer hem d'explicar-vos per què s'executen dues ordres build. El fet és que en un muntatge multietapa la imatge resultant serà un conjunt de capes de l'última etapa. En aquest cas, les capes de les capes anteriors no s'inclouran a la imatge. Per tant, quan utilitzeu la imatge final d'una compilació anterior, Docker no podrà trobar capes preparades per construir la imatge amb nodejs (etapa de constructor). Per solucionar aquest problema, es crea una imatge intermèdia $IMAGE_NAME-builder-stage i s'envia als paquets GitHub perquè es pugui utilitzar en una compilació posterior com a font de memòria cau.

Alguns consells sobre com accelerar la creació d'imatges de Docker. Per exemple, fins a 30 segons

El temps total de muntatge es va reduir a un minut i mig. Es dediquen mig minut a treure imatges anteriors.

Preimatge

Una altra manera de resoldre el problema d'una memòria cau de Docker neta és moure algunes de les capes a un altre Dockerfile, crear-lo per separat, introduir-lo al Registre de contenidors i utilitzar-lo com a pare.

Creem la nostra pròpia imatge de nodejs per construir una aplicació Angular. Creeu Dockerfile.node al projecte

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

Recopilem i emeten una imatge pública a Docker Hub:

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

Ara al nostre Dockerfile principal fem servir la imatge acabada:

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

En el nostre exemple, el temps de construcció no va disminuir, però les imatges preconstruïdes poden ser útils si teniu molts projectes i heu d'instal·lar les mateixes dependències en cadascun d'ells.

Alguns consells sobre com accelerar la creació d'imatges de Docker. Per exemple, fins a 30 segons

Hem analitzat diversos mètodes per accelerar la creació d'imatges docker. Si voleu que el desplegament vagi ràpidament, proveu d'utilitzar això al vostre projecte:

  • reducció del context;
  • ús de petites imatges dels pares;
  • muntatge en diverses etapes;
  • canviar l'ordre de les instruccions al Dockerfile per fer un ús eficient de la memòria cau;
  • configurar una memòria cau en sistemes CI/CD;
  • creació preliminar d'imatges.

Espero que l'exemple deixi més clar com funciona Docker i podreu configurar de manera òptima el vostre desplegament. Per jugar amb els exemples de l'article, s'ha creat un repositori https://github.com/devopsprodigy/test-docker-build.

Font: www.habr.com

Afegeix comentari