Câteva sfaturi despre cum să accelerați construirea imaginilor Docker. De exemplu, până la 30 de secunde

Înainte ca o caracteristică să intre în producție, în zilele noastre de orchestratori complexe și CI/CD, există un drum lung de parcurs de la commit la teste și livrare. Anterior, puteai încărca fișiere noi prin FTP (nimeni nu mai face asta, nu?), iar procesul de „implementare” dura câteva secunde. Acum trebuie să creați o solicitare de îmbinare și să așteptați mult timp pentru ca caracteristica să ajungă la utilizatori.

O parte a acestei căi este construirea unei imagini Docker. Câteodată ansamblul durează minute, alteori zeci de minute, ceea ce cu greu poate fi numit normal. În acest articol, vom lua o aplicație simplă pe care o vom împacheta într-o imagine, vom aplica mai multe metode pentru a accelera construirea și vom analiza nuanțele modului în care funcționează aceste metode.

Câteva sfaturi despre cum să accelerați construirea imaginilor Docker. De exemplu, până la 30 de secunde

Avem o bună experiență în crearea și sprijinirea site-urilor web media: TASS, Bell, „Ziar nou”, Republică… Nu cu mult timp în urmă ne-am extins portofoliul prin lansarea unui site web de produse Memento. Și în timp ce noi funcții au fost adăugate rapid și erori vechi au fost remediate, implementarea lentă a devenit o mare problemă.

Implementăm în GitLab. Colectăm imagini, le trimitem la GitLab Registry și le lansăm în producție. Cel mai lung lucru din această listă este asamblarea imaginilor. De exemplu: fără optimizare, fiecare construcție backend a durat 14 minute.

Câteva sfaturi despre cum să accelerați construirea imaginilor Docker. De exemplu, până la 30 de secunde

În cele din urmă, a devenit clar că nu mai putem trăi așa și ne-am așezat să ne dăm seama de ce strângeau atât de mult imaginile. Drept urmare, am reușit să reducem timpul de asamblare la 30 de secunde!

Câteva sfaturi despre cum să accelerați construirea imaginilor Docker. De exemplu, până la 30 de secunde

Pentru acest articol, pentru a nu fi legat de mediul Reminder, să ne uităm la un exemplu de asamblare a unei aplicații Angular goale. Deci, să creăm aplicația noastră:

ng n app

Adăugați PWA la el (suntem progresivi):

ng add @angular/pwa --project app

În timp ce un milion de pachete npm sunt descărcate, să ne dăm seama cum funcționează imaginea docker. Docker oferă capacitatea de a împacheta aplicații și de a le rula într-un mediu izolat numit container. Datorită izolării, puteți rula mai multe containere simultan pe un singur server. Containerele sunt mult mai ușoare decât mașinile virtuale, deoarece rulează direct pe nucleul sistemului. Pentru a rula un container cu aplicația noastră, trebuie mai întâi să creăm o imagine în care vom împacheta tot ceea ce este necesar pentru ca aplicația noastră să ruleze. În esență, o imagine este o copie a sistemului de fișiere. De exemplu, luați Dockerfile:

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

Un Dockerfile este un set de instrucțiuni; Făcând fiecare dintre ele, Docker va salva modificările în sistemul de fișiere și le va suprapune pe cele anterioare. Fiecare echipă își creează propriul strat. Și imaginea finită este straturi combinate împreună.

Ce este important de știut: fiecare strat Docker poate stoca în cache. Dacă nimic nu s-a schimbat de la ultima versiune, atunci, în loc să execute comanda, docker-ul va prelua un strat gata făcut. Deoarece creșterea principală a vitezei de construire se va datora utilizării cache-ului, atunci când măsurăm viteza de construire vom acorda atenție în special construirii unei imagini cu un cache gata făcut. Deci, pas cu pas:

  1. Ștergem imaginile local, astfel încât rulările anterioare să nu afecteze testul.
    docker rmi $(docker images -q)
  2. Lansăm construcția pentru prima dată.
    time docker build -t app .
  3. Schimbăm fișierul src/index.html - imităm munca unui programator.
  4. Executăm construcția a doua oară.
    time docker build -t app .

Dacă mediul pentru construirea imaginilor este configurat corect (mai multe despre asta mai jos), atunci când începe construcția, Docker va avea deja o grămadă de cache-uri la bord. Sarcina noastră este să învățăm cum să folosim memoria cache, astfel încât construcția să meargă cât mai repede posibil. Deoarece presupunem că rularea unei versiuni fără cache are loc o singură dată - prima dată - putem ignora, prin urmare, cât de lentă a fost prima dată. În teste, a doua rundă a construcției este importantă pentru noi, când cache-urile sunt deja încălzite și suntem gata să ne coacem tortul. Cu toate acestea, unele sfaturi vor afecta și prima construcție.

Să punem Dockerfile descris mai sus în folderul proiectului și să începem construirea. Toate listele au fost condensate pentru ușurință de citit.

$ 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

Schimbăm conținutul src/index.html și îl rulăm a doua oară.

$ 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

Pentru a vedea dacă avem imaginea, rulați comanda docker images:

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

Înainte de a construi, docker preia toate fișierele în contextul curent și le trimite către demonul său Sending build context to Docker daemon 409MB. Contextul de construire este specificat ca ultimul argument al comenzii de construire. În cazul nostru, acesta este directorul curent - „.”, - și Docker trage tot ce avem în acest folder. 409 MB este mult: să ne gândim cum să-l reparăm.

Reducerea contextului

Pentru a reduce contextul, există două opțiuni. Sau puneți toate fișierele necesare pentru asamblare într-un folder separat și direcționați contextul docker către acest folder. Acest lucru poate să nu fie întotdeauna convenabil, așa că este posibil să specificați excepții: ceea ce nu ar trebui să fie tras în context. Pentru a face acest lucru, puneți fișierul .dockerignore în proiect și indicați ce nu este necesar pentru construcție:

.git
/node_modules

și rulați din nou build:

$ 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 este mult mai bun decât 409 MB. De asemenea, am redus dimensiunea imaginii de la 1.74 la 1.38 GB:

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

Să încercăm să reducem și mai mult dimensiunea imaginii.

Folosim Alpine

O altă modalitate de a economisi dimensiunea imaginii este să utilizați o imagine părinte mică. Imaginea parentală este imaginea pe baza căreia ne este pregătită imaginea. Stratul de jos este specificat de comandă FROM în Dockerfile. În cazul nostru, folosim o imagine bazată pe Ubuntu care are deja instalat nodejs. Și cântărește...

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

... aproape un gigaoctet. Puteți reduce semnificativ volumul utilizând o imagine bazată pe Alpine Linux. Alpine este un Linux foarte mic. Imaginea docker pentru nodejs bazată pe alpine cântărește doar 88.5 MB. Deci, să înlocuim imaginea noastră plină de viață în case:

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

A trebuit să instalăm câteva lucruri care sunt necesare pentru a construi aplicația. Da, Angular nu se construiește fără Python ¯(°_o)/¯

Dar dimensiunea imaginii a scăzut la 150 MB:

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

Să mergem și mai departe.

Asamblare în mai multe etape

Nu tot ceea ce este în imagine este ceea ce avem nevoie în producție.

$ 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

Cu docker run app ls -lah am lansat un container pe baza imaginii noastre app și a executat comanda în ea ls -lah, după care containerul și-a încheiat activitatea.

În producție avem nevoie doar de un folder dist. În acest caz, fișierele trebuie cumva să fie date afară. Puteți rula un server HTTP pe nodejs. Dar o vom face mai ușor. Ghiciți un cuvânt rusesc care are patru litere „y”. Dreapta! Ynzhynyksy. Să luăm o imagine cu nginx, să punem un folder în ea dist și o mică configurație:

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

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

Construirea în mai multe etape ne va ajuta să facem toate acestea. Să ne schimbăm 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 .

Acum avem două instrucțiuni FROM în Dockerfile, fiecare dintre ele rulează un pas de construcție diferit. L-am sunat pe primul builder, dar începând de la ultimul FROM se va pregăti imaginea noastră finală. Ultimul pas este să copiați artefactul ansamblului nostru din pasul anterior în imaginea finală cu nginx. Dimensiunea imaginii a scăzut semnificativ:

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

Să rulăm containerul cu imaginea noastră și să ne asigurăm că totul funcționează:

docker run -p8080:80 app

Folosind opțiunea -p8080:80, am redirecționat portul 8080 de pe mașina noastră gazdă la portul 80 din interiorul containerului în care rulează nginx. Deschis în browser http://localhost:8080/ și vedem aplicația noastră. Totul merge!

Câteva sfaturi despre cum să accelerați construirea imaginilor Docker. De exemplu, până la 30 de secunde

Reducerea dimensiunii imaginii de la 1.74 GB la 36 MB reduce semnificativ timpul necesar pentru livrarea aplicației dvs. în producție. Dar să ne întoarcem la timpul de asamblare.

$ 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

Schimbarea ordinii straturilor

Primii noștri trei pași au fost stocați în cache (hint Using cache). La pasul al patrulea, toate fișierele de proiect sunt copiate, iar la pasul al cincilea sunt instalate dependențe RUN npm ci - până la 47.338s. De ce să reinstalați dependențele de fiecare dată dacă acestea se schimbă foarte rar? Să ne dăm seama de ce nu au fost memorate în cache. Ideea este că Docker va verifica strat cu strat pentru a vedea dacă comanda și fișierele asociate acesteia s-au schimbat. La al patrulea pas, copiem toate fișierele proiectului nostru, iar printre ele, desigur, există modificări, așa că Docker nu numai că nu ia acest strat din cache, ci și pe toate ulterioare! Să facem câteva mici modificări în fișierul Docker.

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 .

Mai întâi, package.json și package-lock.json sunt copiate, apoi sunt instalate dependențe și numai după aceea este copiat întregul proiect. Ca urmare:

$ 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 de secunde în loc de 3 minute - mult mai bine! Ordinea corectă a straturilor este importantă: mai întâi copiem ceea ce nu se schimbă, apoi ceea ce se schimbă rar și, în final, ceea ce se schimbă des.

În continuare, câteva cuvinte despre asamblarea imaginilor în sistemele CI/CD.

Utilizarea imaginilor anterioare pentru cache

Dacă folosim un fel de soluție SaaS pentru construcție, atunci memoria cache locală Docker poate fi curată și proaspătă. Pentru a oferi dockerului un loc pentru a obține straturile coapte, dă-i imaginea construită anterioară.

Să luăm un exemplu de construire a aplicației noastre în GitHub Actions. Folosim această configurație

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

Imaginea este asamblată și trimisă la pachetele GitHub în două minute și 20 de secunde:

Câteva sfaturi despre cum să accelerați construirea imaginilor Docker. De exemplu, până la 30 de secunde

Acum haideți să modificăm construcția, astfel încât să fie folosită un cache pe baza imaginilor construite anterioare:

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

Mai întâi trebuie să vă spunem de ce sunt lansate două comenzi build. Faptul este că într-un ansamblu în mai multe etape imaginea rezultată va fi un set de straturi din ultima etapă. În acest caz, straturile din straturile anterioare nu vor fi incluse în imagine. Prin urmare, atunci când se utilizează imaginea finală dintr-o versiune anterioară, Docker nu va putea găsi straturi pregătite pentru a construi imaginea cu nodejs (etapa de constructor). Pentru a rezolva această problemă, se creează o imagine intermediară $IMAGE_NAME-builder-stage și este împins la pachetele GitHub, astfel încât să poată fi utilizat într-o construcție ulterioară ca sursă de cache.

Câteva sfaturi despre cum să accelerați construirea imaginilor Docker. De exemplu, până la 30 de secunde

Timpul total de asamblare a fost redus la un minut și jumătate. O jumătate de minut este petrecută pentru a ridica imaginile anterioare.

Preimaging

O altă modalitate de a rezolva problema unui cache Docker curat este să mutați unele dintre straturi într-un alt Dockerfile, să îl construiți separat, să îl introduceți în Container Registry și să îl utilizați ca părinte.

Creăm propria noastră imagine nodejs pentru a construi o aplicație Angular. Creați Dockerfile.node în proiect

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

Colectăm și împingem o imagine publică în Docker Hub:

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

Acum, în fișierul nostru principal Docker, folosim imaginea finală:

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

În exemplul nostru, timpul de construire nu a scăzut, dar imaginile prefabricate pot fi utile dacă aveți multe proiecte și trebuie să instalați aceleași dependențe în fiecare dintre ele.

Câteva sfaturi despre cum să accelerați construirea imaginilor Docker. De exemplu, până la 30 de secunde

Am analizat mai multe metode pentru a accelera construirea imaginilor docker. Dacă doriți ca implementarea să meargă rapid, încercați să utilizați acest lucru în proiectul dvs.:

  • reducerea contextului;
  • utilizarea imaginilor parentale mici;
  • montaj în mai multe etape;
  • modificarea ordinii instrucțiunilor din fișierul Docker pentru a utiliza eficient memoria cache;
  • configurarea unui cache în sistemele CI/CD;
  • crearea prealabilă a imaginilor.

Sper că exemplul va clarifica modul în care funcționează Docker și veți putea să vă configurați în mod optim implementarea. Pentru a te juca cu exemplele din articol, a fost creat un depozit https://github.com/devopsprodigy/test-docker-build.

Sursa: www.habr.com

Adauga un comentariu