Několik tipů, jak urychlit vytváření obrázků Docker. Například až 30 sekund

Než se funkce dostane do produkce, v dnešní době složitých orchestrátorů a CI/CD, je dlouhá cesta od odevzdání k testům a dodání. Dříve jste mohli nahrávat nové soubory přes FTP (to už nikdo nedělá, že?) a proces „nasazení“ trval několik sekund. Nyní je potřeba vytvořit žádost o sloučení a dlouho čekat, než se funkce dostane k uživatelům.

Součástí této cesty je vytvoření image Dockeru. Někdy montáž trvá minuty, někdy desítky minut, což lze jen stěží označit za normální. V tomto článku si vezmeme jednoduchou aplikaci, kterou zabalíme do obrázku, použijeme několik metod pro urychlení sestavení a podíváme se na nuance, jak tyto metody fungují.

Několik tipů, jak urychlit vytváření obrázků Docker. Například až 30 sekund

Máme dobré zkušenosti s tvorbou a podporou mediálních webů: TASS, Bell, "Nové noviny", Republika… Před nedávnem jsme rozšířili naše portfolio vydáním produktového webu Připomínka. A i když byly rychle přidány nové funkce a opraveny staré chyby, velkým problémem se stalo pomalé nasazení.

Nasazujeme na GitLab. Shromažďujeme obrázky, posíláme je do registru GitLab a vydáváme je do produkce. Nejdelší věcí na tomto seznamu je skládání obrázků. Například: bez optimalizace trvalo každé sestavení backendu 14 minut.

Několik tipů, jak urychlit vytváření obrázků Docker. Například až 30 sekund

Nakonec bylo jasné, že takhle už dál žít nemůžeme a sedli jsme si, abychom zjistili, proč se snímky sbírají tak dlouho. Díky tomu se nám podařilo zkrátit dobu montáže na 30 sekund!

Několik tipů, jak urychlit vytváření obrázků Docker. Například až 30 sekund

Pro tento článek, abychom nebyli vázáni na prostředí Reminderu, se podívejme na příklad sestavení prázdné aplikace Angular. Pojďme tedy vytvořit naši aplikaci:

ng n app

Přidejte k tomu PWA (jsme progresivní):

ng add @angular/pwa --project app

Zatímco se stahuje milion balíčků npm, pojďme zjistit, jak funguje obraz dockeru. Docker poskytuje možnost sbalit aplikace a spustit je v izolovaném prostředí zvaném kontejner. Díky izolaci můžete na jednom serveru provozovat více kontejnerů současně. Kontejnery jsou mnohem lehčí než virtuální stroje, protože běží přímo na jádře systému. Pro spuštění kontejneru s naší aplikací si nejprve musíme vytvořit image, do kterého zabalíme vše, co je pro běh naší aplikace potřeba. Obraz je v podstatě kopií systému souborů. Například vezměte Dockerfile:

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

Dockerfile je sada instrukcí; Provedením každého z nich Docker uloží změny do systému souborů a překryje je na předchozí. Každý tým si vytváří svou vlastní vrstvu. A hotový obrázek jsou vrstvy spojené dohromady.

Co je důležité vědět: každá vrstva Dockeru může ukládat do mezipaměti. Pokud se od posledního sestavení nic nezměnilo, docker místo provedení příkazu vezme hotovou vrstvu. Vzhledem k tomu, že hlavní zvýšení rychlosti sestavení bude způsobeno použitím mezipaměti, při měření rychlosti sestavení budeme věnovat pozornost konkrétně vytváření obrazu s hotovou mezipamětí. Takže krok za krokem:

  1. Snímky lokálně mažeme, aby předchozí běhy neovlivnily test.
    docker rmi $(docker images -q)
  2. Spouštíme sestavení poprvé.
    time docker build -t app .
  3. Změníme soubor src/index.html - napodobíme práci programátora.
  4. Spouštíme stavbu podruhé.
    time docker build -t app .

Pokud je správně nakonfigurováno prostředí pro vytváření obrazů (více o tom níže), pak při spuštění sestavování bude mít Docker již na palubě spoustu mezipamětí. Naším úkolem je naučit se používat cache tak, aby stavba probíhala co nejrychleji. Vzhledem k tomu, že předpokládáme, že ke spuštění sestavení bez mezipaměti dojde pouze jednou – úplně poprvé – můžeme tedy ignorovat, jak pomalé to poprvé bylo. V testech je pro nás důležité druhé spuštění sestavení, kdy jsou kešky již rozehřáté a my jsme připraveni upéct náš dort. Některé tipy však ovlivní i první sestavení.

Vložme Dockerfile popsaný výše do složky projektu a spusťte sestavování. Všechny výpisy byly zhuštěné pro snadnější čtení.

$ 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

Změníme obsah souboru src/index.html a spustíme jej podruhé.

$ 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

Chcete-li zjistit, zda máme obrázek, spusťte příkaz docker images:

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

Před sestavením docker vezme všechny soubory v aktuálním kontextu a odešle je svému démonovi Sending build context to Docker daemon 409MB. Kontext sestavení je zadán jako poslední argument příkazu sestavení. V našem případě je to aktuální adresář - ".", - a Docker přetáhne vše, co v této složce máme. 409 MB je hodně: zamysleme se nad tím, jak to opravit.

Omezení kontextu

Chcete-li omezit kontext, existují dvě možnosti. Nebo umístěte všechny soubory potřebné pro sestavení do samostatné složky a nasměrujte kontext dockeru na tuto složku. To nemusí být vždy vhodné, takže je možné specifikovat výjimky: co by se nemělo zatahovat do kontextu. Chcete-li to provést, vložte soubor .dockerignore do projektu a uveďte, co není pro sestavení potřeba:

.git
/node_modules

a znovu spusťte sestavení:

$ 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 je mnohem lepší než 409 MB. Také jsme snížili velikost obrázku z 1.74 na 1.38 GB:

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

Zkusme ještě zmenšit velikost obrázku.

Používáme Alpine

Dalším způsobem, jak ušetřit na velikosti obrázku, je použít malý rodičovský obrázek. Rodičovský obrázek je obrázek, na jehož základě je připraven náš obrázek. Spodní vrstva je určena příkazem FROM v Dockerfile. V našem případě používáme obraz založený na Ubuntu, který již má nainstalovaný nodejs. A váží...

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

... téměř gigabajt. Hlasitost můžete výrazně snížit použitím obrazu založeného na Alpine Linuxu. Alpine je velmi malý Linux. Docker image pro nodejs založený na alpine váží pouze 88.5 MB. Pojďme tedy nahradit náš živý obraz v domech:

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

Museli jsme nainstalovat některé věci, které jsou nutné k sestavení aplikace. Ano, Angular se nesestaví bez Pythonu ¯(°_o)/¯

Ale velikost obrázku klesla na 150 MB:

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

Pojďme ještě dále.

Vícestupňová montáž

Ne vše, co je na obrázku, je to, co potřebujeme ve výrobě.

$ 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

S docker run app ls -lah spustili jsme kontejner založený na našem obrázku app a provedl v něm příkaz ls -lah, načež kontejner dokončil svou práci.

Při výrobě potřebujeme pouze složku dist. V tomto případě je třeba soubory nějak dát ven. Na nodejs můžete spustit nějaký HTTP server. Ale my si to usnadníme. Hádejte ruské slovo, které má čtyři písmena „y“. Že jo! Ynzhynyksy. Udělejme obrázek s nginx, dáme do něj složku dist a malá konfigurace:

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

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

K tomu všemu nám pomůže vícestupňové sestavení. Pojďme změnit náš 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 .

Nyní máme dva pokyny FROM v Dockerfile každý z nich spustí jiný krok sestavení. Zavolali jsme prvnímu builder, ale počínaje posledním FROM bude připraven náš konečný obrázek. Posledním krokem je zkopírování artefaktu naší sestavy v předchozím kroku do výsledného obrázku pomocí nginx. Velikost obrázku se výrazně zmenšila:

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

Spusťte kontejner s naším obrázkem a ujistěte se, že vše funguje:

docker run -p8080:80 app

Pomocí volby -p8080:80 jsme předali port 8080 na našem hostitelském počítači na port 80 uvnitř kontejneru, kde běží nginx. Otevřít v prohlížeči http://localhost:8080/ a vidíme naši aplikaci. Všechno funguje!

Několik tipů, jak urychlit vytváření obrázků Docker. Například až 30 sekund

Zmenšením velikosti obrazu z 1.74 GB na 36 MB se výrazně zkrátí čas potřebný k dodání vaší aplikace do produkce. Ale vraťme se k době montáže.

$ 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

Změna pořadí vrstev

Naše první tři kroky byly uloženy do mezipaměti (nápověda Using cache). Ve čtvrtém kroku se zkopírují všechny soubory projektu a v pátém kroku se nainstalují závislosti RUN npm ci - až 47.338 s. Proč pokaždé znovu instalovat závislosti, když se mění velmi zřídka? Pojďme zjistit, proč nebyly uloženy do mezipaměti. Jde o to, že Docker bude vrstvu po vrstvě kontrolovat, zda se příkaz a soubory s ním spojené změnily. Ve čtvrtém kroku zkopírujeme všechny soubory našeho projektu a mezi nimi jsou samozřejmě změny, takže Docker nejen že nebere tuto vrstvu z mezipaměti, ale ani všechny následující! Udělejme nějaké malé změny v 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 .

Nejprve se zkopírují package.json a package-lock.json, poté se nainstalují závislosti a teprve poté se zkopíruje celý projekt. Jako výsledek:

$ 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 sekund místo 3 minut – mnohem lepší! Důležité je správné pořadí vrstev: nejprve zkopírujeme to, co se nemění, pak to, co se mění zřídka, a nakonec to, co se často mění.

Dále pár slov o sestavování obrazů v systémech CI/CD.

Použití předchozích obrázků pro vyrovnávací paměť

Pokud pro sestavení použijeme nějaký druh řešení SaaS, pak může být místní mezipaměť Dockeru čistá a čerstvá. Chcete-li dockerovi poskytnout místo, kde může získat pečené vrstvy, dejte mu předchozí vytvořený obrázek.

Vezměme si příklad vytváření naší aplikace v GitHub Actions. Používáme tuto konfiguraci

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

Obrázek je sestaven a odeslán do balíčků GitHub za dvě minuty a 20 sekund:

Několik tipů, jak urychlit vytváření obrázků Docker. Například až 30 sekund

Nyní změňme sestavení tak, aby se mezipaměť používala na základě předchozích vytvořených obrázků:

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

Nejprve vám musíme říci, proč jsou spuštěny dva příkazy build. Faktem je, že ve vícestupňové sestavě bude výsledným obrazem sada vrstev z poslední fáze. V tomto případě nebudou do obrázku zahrnuty vrstvy z předchozích vrstev. Proto při použití konečného obrazu z předchozího sestavení Docker nebude schopen najít připravené vrstvy pro sestavení obrazu pomocí nodejs (fáze tvůrce). Za účelem vyřešení tohoto problému je vytvořen meziobraz $IMAGE_NAME-builder-stage a je odeslán do balíčků GitHub, aby mohl být použit v následném sestavení jako zdroj mezipaměti.

Několik tipů, jak urychlit vytváření obrázků Docker. Například až 30 sekund

Celková doba montáže se zkrátila na jednu a půl minuty. Půl minuty strávíte stahováním předchozích obrázků.

Preimaging

Dalším způsobem, jak vyřešit problém čisté mezipaměti Dockeru, je přesunout některé vrstvy do jiného souboru Dockerfile, sestavit jej samostatně, vložit jej do registru kontejnerů a použít jej jako nadřazený soubor.

Vytváříme vlastní image nodejs pro vytvoření aplikace Angular. V projektu vytvořte Dockerfile.node

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

Shromažďujeme a posíláme veřejný obrázek v Docker Hub:

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

Nyní v našem hlavním Dockerfile používáme hotový obrázek:

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

V našem příkladu se doba sestavení nezkrátila, ale předpřipravené obrazy mohou být užitečné, pokud máte mnoho projektů a musíte do každého z nich nainstalovat stejné závislosti.

Několik tipů, jak urychlit vytváření obrázků Docker. Například až 30 sekund

Podívali jsme se na několik metod, jak urychlit vytváření obrázků dockeru. Pokud chcete, aby nasazení proběhlo rychle, zkuste ve svém projektu použít toto:

  • redukce kontextu;
  • použití malých rodičovských obrázků;
  • vícestupňová montáž;
  • změna pořadí instrukcí v Dockerfile za účelem efektivního využití mezipaměti;
  • nastavení mezipaměti v systémech CI/CD;
  • předběžná tvorba obrázků.

Doufám, že příklad objasní, jak Docker funguje, a budete moci optimálně nakonfigurovat své nasazení. Abychom si mohli pohrát s příklady z článku, bylo vytvořeno úložiště https://github.com/devopsprodigy/test-docker-build.

Zdroj: www.habr.com

Přidat komentář