Enkele tips voor het versnellen van het maken van Docker-images. Bijvoorbeeld maximaal 30 seconden

Voordat een functie in productie gaat, is er in deze tijd van complexe orkestrators en CI/CD een lange weg te gaan van commit naar testen en oplevering. Voorheen kon je nieuwe bestanden uploaden via FTP (dat doet niemand meer, toch?) en het “implementatie”-proces duurde enkele seconden. Nu moet u een samenvoegverzoek maken en lang wachten voordat de functie gebruikers bereikt.

Onderdeel van dit pad is het bouwen van een Docker-installatiekopie. Soms duurt de montage minuten, soms tientallen minuten, wat nauwelijks normaal te noemen is. In dit artikel nemen we een eenvoudige applicatie die we in een afbeelding verpakken, passen we verschillende methoden toe om het bouwen te versnellen en kijken we naar de nuances van hoe deze methoden werken.

Enkele tips voor het versnellen van het maken van Docker-images. Bijvoorbeeld maximaal 30 seconden

Wij hebben goede ervaring met het maken en ondersteunen van mediawebsites: TASS, The Bell, "Nieuwe krant", Republiek… Nog niet zo lang geleden hebben we ons portfolio uitgebreid door een productwebsite uit te brengen Herinnering. En hoewel er snel nieuwe functies werden toegevoegd en oude bugs werden opgelost, werd een trage implementatie een groot probleem.

Wij implementeren in GitLab. We verzamelen afbeeldingen, pushen ze naar GitLab Registry en rollen ze uit naar productie. Het langste werk op deze lijst is het samenstellen van afbeeldingen. Bijvoorbeeld: zonder optimalisatie duurde elke backend-build 14 minuten.

Enkele tips voor het versnellen van het maken van Docker-images. Bijvoorbeeld maximaal 30 seconden

Uiteindelijk werd het duidelijk dat we zo niet langer konden leven, en we gingen zitten om erachter te komen waarom het zo lang duurde om de beelden te verzamelen. Hierdoor zijn we erin geslaagd de montagetijd terug te brengen tot 30 seconden!

Enkele tips voor het versnellen van het maken van Docker-images. Bijvoorbeeld maximaal 30 seconden

Laten we voor dit artikel, om niet gebonden te zijn aan de omgeving van Reminder, eens kijken naar een voorbeeld van het samenstellen van een lege Angular-applicatie. Laten we dus onze applicatie maken:

ng n app

Voeg er PWA aan toe (wij zijn vooruitstrevend):

ng add @angular/pwa --project app

Terwijl er een miljoen NPM-pakketten worden gedownload, gaan we uitzoeken hoe de docker-image werkt. Docker biedt de mogelijkheid om applicaties te verpakken en uit te voeren in een geïsoleerde omgeving die een container wordt genoemd. Dankzij isolatie kun je meerdere containers tegelijkertijd op één server draaien. Containers zijn veel lichter dan virtuele machines omdat ze rechtstreeks op de systeemkernel draaien. Om een ​​container met onze applicatie te kunnen draaien, moeten we eerst een image maken waarin we alles verpakken wat nodig is om onze applicatie te laten draaien. In wezen is een afbeelding een kopie van het bestandssysteem. Neem bijvoorbeeld het Dockerbestand:

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

Een Dockerfile is een set instructies; Door ze allemaal uit te voeren, slaat Docker de wijzigingen in het bestandssysteem op en legt ze over de vorige heen. Elk team creëert zijn eigen laag. En het uiteindelijke beeld bestaat uit lagen die met elkaar zijn gecombineerd.

Wat belangrijk is om te weten: elke Docker-laag kan cachen. Als er niets is veranderd sinds de laatste build, zal de docker in plaats van de opdracht uit te voeren een kant-en-klare laag nemen. Omdat de belangrijkste toename in bouwsnelheid te danken is aan het gebruik van de cache, zullen we bij het meten van de bouwsnelheid specifiek aandacht besteden aan het bouwen van een afbeelding met een kant-en-klare cache. Dus stap voor stap:

  1. We verwijderen de afbeeldingen lokaal zodat eerdere runs geen invloed hebben op de test.
    docker rmi $(docker images -q)
  2. We lanceren de build voor de eerste keer.
    time docker build -t app .
  3. We veranderen het bestand src/index.html - we imiteren het werk van een programmeur.
  4. We voeren de build een tweede keer uit.
    time docker build -t app .

Als de omgeving voor het bouwen van images correct is geconfigureerd (meer daarover hieronder), zal Docker bij het starten van de build al een aantal caches aan boord hebben. Onze taak is om te leren hoe we de cache kunnen gebruiken, zodat de build zo snel mogelijk verloopt. Omdat we ervan uitgaan dat het uitvoeren van een build zonder cache maar één keer gebeurt (de allereerste keer), kunnen we negeren hoe langzaam die eerste keer was. Bij tests is de tweede run van de build belangrijk voor ons, wanneer de caches al zijn opgewarmd en we klaar zijn om onze taart te bakken. Sommige tips hebben echter ook invloed op de eerste build.

Laten we het hierboven beschreven Dockerbestand in de projectmap plaatsen en de build starten. Alle lijsten zijn gecomprimeerd voor leesgemak.

$ 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

We veranderen de inhoud van src/index.html en voeren het een tweede keer uit.

$ 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

Om te zien of we de afbeelding hebben, voert u de opdracht uit docker images:

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

Voordat het gaat bouwen, neemt docker alle bestanden in de huidige context en stuurt ze naar de daemon Sending build context to Docker daemon 409MB. De build-context wordt opgegeven als het laatste argument voor de build-opdracht. In ons geval is dit de huidige map - “.”, - en Docker sleept alles wat we in deze map hebben. 409 MB is veel: laten we nadenken over hoe we dit kunnen oplossen.

De context verkleinen

Om de context te verkleinen zijn er twee opties. Of plaats alle bestanden die nodig zijn voor de montage in een aparte map en wijs de docker-context naar deze map. Dit is misschien niet altijd handig, dus het is mogelijk om uitzonderingen te specificeren: wat mag niet in de context worden gesleept. Om dit te doen, plaatst u het .dockerignore-bestand in het project en geeft u aan wat niet nodig is voor de build:

.git
/node_modules

en voer de build opnieuw uit:

$ 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 is veel beter dan 409 MB. We hebben ook de afbeeldingsgrootte verkleind van 1.74 naar 1.38 GB:

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

Laten we proberen de grootte van de afbeelding verder te verkleinen.

Wij gebruiken Alpine

Een andere manier om te besparen op de afbeeldingsgrootte is door een kleine bovenliggende afbeelding te gebruiken. Het ouderbeeld is het beeld op basis waarvan ons beeld wordt voorbereid. De onderste laag wordt gespecificeerd door de opdracht FROM in Dockerfile. In ons geval gebruiken we een op Ubuntu gebaseerde afbeelding waarop al nodejs is geïnstalleerd. En hij weegt...

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

... bijna een gigabyte. U kunt het volume aanzienlijk verminderen door een image te gebruiken op basis van Alpine Linux. Alpine is een heel kleine Linux. De docker-image voor nodejs op basis van alpine weegt slechts 88.5 MB. Laten we dus ons levendige imago in de huizen vervangen:

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

We moesten een aantal dingen installeren die nodig zijn om de applicatie te bouwen. Ja, Angular bouwt niet zonder Python ¯(°_o)/¯

Maar de afbeeldingsgrootte daalde naar 150 MB:

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

Laten we nog verder gaan.

Meertrapsmontage

Niet alles wat op de afbeelding staat, is wat we nodig hebben in de productie.

$ 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

Met docker run app ls -lah we hebben een container gelanceerd op basis van ons imago app en voerde daarin de opdracht uit ls -lah, waarna de container zijn werk voltooide.

Bij de productie hebben we alleen een map nodig dist. In dit geval moeten de bestanden op de een of andere manier naar buiten worden gebracht. U kunt een HTTP-server op nodejs draaien. Maar we zullen het makkelijker maken. Raad een Russisch woord met vier letters “y”. Rechts! Ynzhynyksy. Laten we een afbeelding maken met nginx en er een map in plaatsen dist en een kleine configuratie:

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

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

Een meerfasige opbouw zal ons hierbij helpen. Laten we ons Dockerbestand wijzigen:

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 hebben we twee instructies FROM in de Dockerfile voert elk van hen een andere bouwstap uit. Wij hebben de eerste gebeld builder, maar vanaf de laatste FROM wordt ons uiteindelijke beeld voorbereid. De laatste stap is het kopiëren van het artefact van onze assemblage in de vorige stap naar de uiteindelijke afbeelding met nginx. De grootte van de afbeelding is aanzienlijk afgenomen:

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

Laten we de container met onze afbeelding uitvoeren en ervoor zorgen dat alles werkt:

docker run -p8080:80 app

Met behulp van de optie -p8080:80 hebben we poort 8080 op onze hostmachine doorgestuurd naar poort 80 in de container waar nginx draait. Open in de browser http://localhost:8080/ en we zien onze applicatie. Alles werkt!

Enkele tips voor het versnellen van het maken van Docker-images. Bijvoorbeeld maximaal 30 seconden

Door de afbeeldingsgrootte te verkleinen van 1.74 GB naar 36 MB wordt de tijd die nodig is om uw applicatie in productie te nemen aanzienlijk verkort. Maar laten we teruggaan naar de montagetijd.

$ 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

De volgorde van lagen wijzigen

Onze eerste drie stappen zijn in de cache opgeslagen (hint Using cache). Bij de vierde stap worden alle projectbestanden gekopieerd en bij de vijfde stap worden afhankelijkheden geïnstalleerd RUN npm ci - maar liefst 47.338s. Waarom afhankelijkheden elke keer opnieuw installeren als ze zeer zelden veranderen? Laten we uitzoeken waarom ze niet in de cache zijn opgeslagen. Het punt is dat Docker laag voor laag controleert of de opdracht en de bijbehorende bestanden zijn gewijzigd. Bij de vierde stap kopiëren we alle bestanden van ons project, en daartussen zijn er natuurlijk wijzigingen, dus Docker haalt niet alleen deze laag niet uit de cache, maar ook alle daaropvolgende! Laten we enkele kleine wijzigingen aanbrengen in het Dockerbestand.

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 .

Eerst worden package.json en package-lock.json gekopieerd, vervolgens worden de afhankelijkheden geïnstalleerd en pas daarna wordt het hele project gekopieerd. Als gevolg:

$ 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 seconden in plaats van 3 minuten - veel beter! De juiste volgorde van de lagen is belangrijk: eerst kopiëren we wat niet verandert, vervolgens wat zelden verandert en ten slotte wat vaak verandert.

Vervolgens een paar woorden over het samenstellen van afbeeldingen in CI/CD-systemen.

Gebruik van eerdere afbeeldingen voor cache

Als we een soort SaaS-oplossing gebruiken voor de build, kan de lokale Docker-cache schoon en fris zijn. Om de havenarbeider een plaats te geven waar hij de gebakken lagen kan ophalen, geeft u hem de eerder gebouwde afbeelding.

Laten we een voorbeeld nemen van het bouwen van onze applicatie in GitHub Actions. Wij gebruiken deze configuratie

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

De afbeelding wordt in twee minuten en 20 seconden samengesteld en naar GitHub-pakketten gepusht:

Enkele tips voor het versnellen van het maken van Docker-images. Bijvoorbeeld maximaal 30 seconden

Laten we nu de build wijzigen zodat een cache wordt gebruikt op basis van eerder gebouwde afbeeldingen:

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

Eerst moeten we u vertellen waarom twee opdrachten worden gestart build. Feit is dat bij een meertrapsassemblage het resulterende beeld een reeks lagen uit de laatste fase zal zijn. In dit geval worden lagen uit voorgaande lagen niet in de afbeelding opgenomen. Wanneer u de definitieve afbeelding van een eerdere build gebruikt, kan Docker daarom geen kant-en-klare lagen vinden om de afbeelding met nodejs te bouwen (builder-fase). Om dit probleem op te lossen wordt een tussenbeeld gecreëerd $IMAGE_NAME-builder-stage en wordt naar GitHub-pakketten gepusht, zodat het in een volgende build als cachebron kan worden gebruikt.

Enkele tips voor het versnellen van het maken van Docker-images. Bijvoorbeeld maximaal 30 seconden

De totale montagetijd werd teruggebracht tot anderhalve minuut. Er wordt een halve minuut besteed aan het ophalen van eerdere afbeeldingen.

Voorafbeelding maken

Een andere manier om het probleem van een schone Docker-cache op te lossen, is door enkele lagen naar een ander Docker-bestand te verplaatsen, het afzonderlijk te bouwen, het in de Container Registry te plaatsen en het als ouder te gebruiken.

We creëren onze eigen nodejs-image om een ​​Angular-applicatie te bouwen. Maak Dockerfile.node in het project

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

We verzamelen en pushen een publieke afbeelding in Docker Hub:

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

Nu gebruiken we in ons hoofd Dockerbestand de voltooide afbeelding:

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

In ons voorbeeld is de bouwtijd niet afgenomen, maar vooraf gebouwde afbeeldingen kunnen handig zijn als u veel projecten heeft en in elk ervan dezelfde afhankelijkheden moet installeren.

Enkele tips voor het versnellen van het maken van Docker-images. Bijvoorbeeld maximaal 30 seconden

We hebben verschillende methoden bekeken om het bouwen van docker-images te versnellen. Als u wilt dat de implementatie snel verloopt, kunt u dit in uw project proberen:

  • context verkleinen;
  • gebruik van kleine ouderafbeeldingen;
  • meertrapsmontage;
  • het veranderen van de volgorde van instructies in het Dockerfile om efficiënt gebruik te maken van de cache;
  • het opzetten van een cache in CI/CD-systemen;
  • voorlopige creatie van afbeeldingen.

Ik hoop dat het voorbeeld duidelijker maakt hoe Docker werkt, en dat je je implementatie optimaal kunt configureren. Om met de voorbeelden uit het artikel te spelen, is er een repository gemaakt https://github.com/devopsprodigy/test-docker-build.

Bron: www.habr.com

Voeg een reactie