Nogle tips til, hvordan man kan accelerere opbygningen af Docker-billeder. For eksempel ned til 30 sekunder.

Før en feature går i produktion, skal den i dag gennem en lang vej fra commit til tests og deployment i en verden med komplekse orkestratorer og CI/CD. Tidligere kunne man blot uploade nye filer via FTP (det gør ingen længere, vel?), og deployeringsprocessen tog sekunder. Nu skal man oprette en merge request og vente en del tid, før funktionen når brugerne.

En del af denne rejse er opbygningen af Docker-billedet. Nogle gange tager opbygningen minutter, nogle gange - titusinder af minutter, hvilket er svært at kalde normalt. I denne artikel vil vi tage en simpel applikation, pakke den ind i et billede, anvende forskellige metoder til at fremskynde opbygningen og se på nuancerne i disse metoder.

Nogle tips til, hvordan man kan accelerere opbygningen af Docker-billeder. For eksempel ned til 30 sekunder.

Vi har en god erfaring med at oprette og supportere nyhedswebsites: TASS, The Bell, "Nyayaaret", Republic… For nylig udvidede vi vores portefølje ved at lancere et website i produktion. Reminder. Og mens vi hurtigt færdiggjorde nye features og rettede gamle fejl, blev den langsomme deployment et stort problem.

Vi foretager deployment på GitLab. Vi bygger billeder, pusher dem til GitLab Registry og udruller dem i produktion. Det, der tager længst tid i denne proces, er opbygningen af billederne. Som eksempel: uden optimering tog hver backend-opbygning 14 minutter.

Nogle tips til, hvordan man kan accelerere opbygningen af Docker-billeder. For eksempel ned til 30 sekunder.

Til sidst blev det klart, at vi ikke kunne fortsætte sådan, så vi satte os ned for at finde ud af, hvorfor billederne tog så lang tid at opbygge. I sidste ende lykkedes det os at reducere opbygningstiden til 30 sekunder!

Nogle tips til, hvordan man kan accelerere opbygningen af Docker-billeder. For eksempel ned til 30 sekunder.

Til denne artikel, for ikke at knytte os til Reminder-miljøet, ser vi på et eksempel på opbygning af en tom applikation på Angular. Så lad os oprette vores applikation:

ng n app

Vi tilføjer PWA (vi er jo progressive):

ng add @angular/pwa --project app

Mens millioner af npm-pakker downloades, lad os se på, hvordan docker-billedet er opbygget. Docker giver mulighed for at pakke applikationer og køre dem i et isoleret miljø kaldet en container. Takket være isolation kan man køre mange containere samtidigt på en server. Containere er betydeligt lettere end virtuelle maskiner, da de kører direkte på systemets kerne. For at køre en container med vores applikation skal vi først oprette et billede, hvor vi pakker alt, hvad der er nødvendigt for at køre vores applikation. I bund og grund er et billede et aftryk af filsystemet. For eksempel, lad os tage Dockerfile:

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

Dockerfile er et sæt af instruktioner; ved at udføre hver enkelt vil Docker gemme ændringer i filsystemet og anvende dem på de tidligere. Hver kommando skaber sit lag. Det færdige billede er en samling af lagene.

Det er vigtigt at vide: hvert lag kan Docker cache. Hvis intet har ændret sig siden den sidste bygning, vil Docker tage det allerede færdige lag i stedet for at udføre kommandoen. Da den vigtigste hastighedsforøgelse i bygningen vil være gennem brug af cache, vil vi i målinger af byggehastighed særligt fokusere på bygningen af billedet med den eksisterende cache. Så lad os gå i gang:

  1. Vi fjerner billeder lokalt, så tidligere kørsel ikke påvirker testen.
    docker rmi $(docker images -q)
  2. Vi kører bygningen første gang.
    time docker build -t app .
  3. Vi ændrer filen src/index.html — simulerer programmørens arbejde.
  4. Vi kører bygningen anden gang.
    time docker build -t app .

Hvis miljøet for at bygge billeder er korrekt konfigureret (som beskrevet lidt senere), vil Docker allerede have en masse caches tilgængelige, når bygningen startes. Vores opgave er at lære at bruge cachen, så bygningen forløber så hurtigt som muligt. Da vi antager, at bygningen uden cache kun sker én gang — den allerførste — kan vi ignorere, hvor langsom denne første gang var. I vores tests er den anden bygning vigtig, når cachen allerede er klar, og vi er klar til at bage vores kage. Ikke desto mindre vil nogle råd også påvirke den første bygning.

Lad os placere Dockerfile, som beskrevet ovenfor, i projektmappen og starte bygningen. Alle nævnte lister er forkortet for at lette læsningen.

$ 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

Vi ændrer indholdet i src/index.html og kører den anden gang.

$ 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

For at se, om vi har fået billedet, kører vi kommandoen docker images:

REPOSITORY   TAG      IMAGE ID       CREATED              SIZE
app          latest   79f335df92d3   For omkring et minut siden   1.74GB

Før Docker bygger, tager den alle filer i den nuværende kontekst og sender dem til sin demon Sender byggekontext til Docker-demon 409MB. Bygge konteksten angives som det sidste argument i build-kommandoen. I vores tilfælde er det den nuværende mappe — «.», — og Docker trækker alt, hvad vi har i denne mappe. 409 MB er meget: lad os overveje, hvordan vi kan løse dette.

Reducerer konteksten

For at reducere konteksten er der to muligheder. Enten læg alle filer, der er nødvendige for bygningen, i en separat mappe og angiv konteksten til Docker specifikt til den mappe. Dette kan ikke altid være bekvemt, så der er mulighed for at angive undtagelser: hvad der ikke skal medtages i konteksten. For at gøre dette lægger vi en .dockerignore-fil i projektet og angiver, hvad der ikke er nødvendigt til bygningen:

.git
/node_modules

og vi kører bygningen igen:

$ time docker build -t app .
Sender byggekontext til Docker-demon 607.2kB
Trin 1/5: FROM node:12.16.2
Trin 2/5: WORKDIR /app
 ---> Bruger cache
Trin 3/5: COPY . .
Trin 4/5: RUN npm ci
tilføjet 1357 pakker på 22.47s
Trin 5/5: RUN npm run build --prod
Dato: 2020-04-16T19:33:54.338Z - Hash: fffa0fddaa3425c55dd3 - Tid: 37313ms
Succesfuldt bygget 4942f010792a
Succesfuldt mærket app:latest

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

607.2 Kbyte — meget bedre end 409 MB. Og vi har også reduceret størrelsen af billedet fra 1.74 til 1.38GB:

REPOSITORY   TAG      IMAGE ID       CREATED         SIZE
app          latest   4942f010792a   3 minutter siden   1.38GB

Lad os prøve at reducere billedstørrelsen yderligere.

Bruger Alpine

En anden måde at spare på billedstørrelsen — er at bruge et lille forældrebillede. Forældrebilledet er det billede, vores billede er baseret på. Den nederste lag angives med kommandoen FROM i Dockerfile. I vores tilfælde er vi ved at bruge et billede baseret på Ubuntu, som allerede har nodejs installeret. Og det vejer …

$ docker images -a | grep node
node 12.16.2 406aa3abbc6c 17 minutter siden 916MB

… næsten en gigabyte. Man kan betydeligt reducere størrelsen ved at bruge et billede baseret på Alpine Linux. Alpine er en meget lille Linux. Docker-billedet for nodejs baseret på Alpine vejer kun 88.5 MB. Så lad os erstatte vores fede forældrebillede med:

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

Vi måtte installere nogle ting, der er nødvendige for at bygge applikationen. Ja, Angular bygger ikke uden Python ¯(°_o)¯

Men billedstørrelsen faldt med 150 MB:

REPOSITORY   TAG      IMAGE ID       CREATED          SIZE
app          latest   aa031edc315a   22 minutter siden   761MB

Vi går videre.

Multi-stage bygning

Ikke alt, hvad der er i billedet, er nødvendigt for os i produktionen.

$ 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

Ved hjælp af docker run app ls -lah Vi har startet en container baseret på vores billede app og udført kommandoen i den ls -lah, hvorefter containeren blev stoppet.

I produktionen har vi kun brug for mappen dist. Files skal deles på en eller anden måde. Vi kan starte en HTTP-server med nodejs. Men vi gør det lettere. Gæt det russiske ord, der har fire bogstaver 'ы'. Rigtigt! Ынжыныксы. Vi tager et billede med nginx, lægger mappen dist og en lille konfiguration:

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

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

Multi-stage build vil hjælpe os med dette. Lad os ændre vores 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 .

Nu har vi to instruktioner FROM i Dockerfile, hver af dem starter sin egen byggefase. Den første kaldte vi builder, men fra den sidste FROM begynder vi at forberede vores endelige billede. Som det sidste skridt kopierer vi artefakten fra vores tidligere byggefase til det endelige billede med nginx. Størrelsen på billedet er væsentligt reduceret:

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

Lad os starte containeren med vores billede og sikre os, at alt fungerer:

docker run -p8080:80 app

Med -p8080:80 har vi forwarded port 8080 på vores værtsmaskine til port 80 inde i containeren, hvor nginx kører. Åbn i browseren http://localhost:8080/ og se vores application. Alt fungerer!

Nogle tips til, hvordan man kan accelerere opbygningen af Docker-billeder. For eksempel ned til 30 sekunder.

Reduktionen af billedstørrelsen fra 1,74 GB til 36 MB reducerer betydeligt tiden til at levere din applikation til produktion. Men lad os vende tilbage til byggetiden.

$ 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

Lad os ændre rækkefølgen af lagene

De første tre trin har været cached (hint Using cache). I det fjerde trin kopieres alle projekfiler og i det femte trin installeres afhængighederne RUN npm ci — hele 47,338 sek. Hvorfor skal man hver gang installere afhængighederne igen, når de sjældent ændrer sig? Lad os finde ud af, hvorfor de ikke er blevet cachet. Sagen er, at Docker tjekker lag for lag, om kommandoen og de relaterede filer er ændret. På det fjerde trin kopierer vi alle filerne fra vores projekt, og blandt dem er der selvfølgelig ændringer, hvilket betyder, at Docker ikke kun undlader at tage dette lag fra cachen, men også alle efterfølgende! Lad os foretage en lille ændring i 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 .

Først kopieres package.json og package-lock.json, derefter installeres afhængighederne, og først derefter kopieres hele projektet. Resultatet:

$ time docker build -t app .
Sender byggekontext til Docker dæmon 608.8kB
Trin 1/12 : FROM node:12.16.2-alpine3.11 as builder
Trin 2/12 : RUN apk --no-cache --update --virtual build-dependencies add python make g++
 ---> Bruger cache
Trin 3/12 : WORKDIR /app
 ---> Bruger cache
Trin 4/12 : COPY package*.json ./
 ---> Bruger cache
Trin 5/12 : RUN npm ci
 ---> Bruger cache
Trin 6/12 : COPY . .
Trin 7/12 : RUN npm run build --prod
Dato: 2020-04-16T21:29:44.770Z - Hash: fffa0fddaa3425c55dd3 - Tid: 38287ms
 ---> 1b9448c73558
Trin 8/12 : FROM nginx:stable-alpine
Trin 9/12 : WORKDIR /app
 ---> Bruger cache
Trin 10/12 : RUN rm /etc/nginx/conf.d/default.conf
 ---> Bruger cache
Trin 11/12 : COPY nginx/static.conf /etc/nginx/conf.d
 ---> Bruger cache
Trin 12/12 : COPY --from=builder /app/dist/app .
Bygget succesfuldt a44dd7c217c3
Succesen tagget app:latest

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

46 sekunder i stedet for 3 minutter — betydeligt bedre! Den rette rækkefølge af lag er vigtig: Først kopierer vi det, der ikke ændrer sig, derefter det, der sjældent ændrer sig, og til sidst det, der ofte ændrer sig.

Dernæst nogle ord om opbygning af billeder i CI/CD-systemer.

Brug af tidligere billeder til caching

Hvis vi bruger en SaaS-løsning til opbygningen, kan den lokale Docker-cache vise sig at være ren og frisk. For at Docker har noget at hente de bagte lag fra, skal du give det det tidligere opbyggede billede.

Lad os tage et eksempel på, hvordan vores applikation bygges i GitHub Actions. Vi bruger denne konfiguration

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

Billed image is built and pushed to GitHub Packages in two minutes and 20 seconds:

Nogle tips til, hvordan man kan accelerere opbygningen af Docker-billeder. For eksempel ned til 30 sekunder.

Now let's modify the build to use cache based on previously built images:

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

First, we need to explain why two commands are executed build. The matter is that in a multi-stage build, the resulting image will consist of layers from the last stage. The layers from the previous stages will not be included in the image. So when using the final image from the previous build, Docker won't be able to find the ready layers to build the image with nodejs (builder stage). To solve this problem, an intermediate image is created $IMAGE_NAME-builder-stage and sent to GitHub Packages, so it can be used in the next build as a cache source.

Nogle tips til, hvordan man kan accelerere opbygningen af Docker-billeder. For eksempel ned til 30 sekunder.

The total build time has been reduced to one and a half minutes. Half a minute is spent pulling previous images.

Pre-building images

En anden måde at løse problemet med ren Docker-cache på er at flytte nogle lag til en anden Dockerfile, bygge den separat, skubbe den til Container Registry og bruge den som forælder.

Vi opretter vores eget Node.js-billede til at bygge et Angular-program. Opret Dockerfile.node i projektet.

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

Vi bygger og skubber et offentligt billede til Docker Hub:

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

Nu bruger vi det færdige billede i vores hoved-Dockerfile:

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

I vores eksempel er byggetiden ikke reduceret, men forudbyggede billeder kan være nyttige, hvis du har mange projekter, og hver af dem kræver de samme afhængigheder.

Nogle tips til, hvordan man kan accelerere opbygningen af Docker-billeder. For eksempel ned til 30 sekunder.

Vi har gennemgået flere metoder til at fremskynde bygningen af Docker-billeder. Hvis du ønsker, at deployment skal forløbe hurtigt, så prøv at anvende følgende i dit projekt:

  • reduktion af konteksten;
  • brug af små forældre-billeder;
  • multi-stage bygning;
  • ændring af rækkefølgen af instruktioner i Dockerfile for effektivt at bruge cachen;
  • konfiguration af cachen i CI/CD-systemer;
  • forudgående oprettelse af billeder.

Jeg håber, eksemplet vil gøre det lettere at forstå, hvordan Docker fungerer, og at du kan optimalt indstille din deployment. For at lege med eksemplerne fra artiklen er der oprettet et repository. https://github.com/devopsprodigy/test-docker-build.

Kilde: habr.com

Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere 🔥 Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere | ProHoster