Kaip optimizuoti Docker performance

Kodėl Docker kartais lėtėja ir ką su tuo daryti

Kai pirmą kartą pradedi naudoti Docker, viskas atrodo paprasta ir greita. Bet po kurio laiko pastebėji, kad konteineriai paleidžiami lėčiau, build procesas užtrunka amžinybę, o programos veikia ne taip sklandžiai kaip tikėjaisi. Tai normalu – Docker, kaip ir bet kuri technologija, reikalauja tam tikro optimizavimo, kad dirbtų efektyviai.

Problema dažniausiai slypi ne pačiame Docker, o kaip mes jį naudojame. Didelis Dockerfile’as su daugybe sluoksnių, netinkamai sukonfigūruotas tinklas, per daug resursų vienam konteineriui – visa tai kaupiasi ir galiausiai jaučiame, kad sistema nebeveikia taip greitai kaip norėtume.

Šiame straipsnyje pasigilinsime į konkrečius būdus, kaip priversti Docker veikti greičiau ir efektyviau. Kalbėsime apie realias problemas ir jų sprendimus, kuriuos galite pritaikyti jau šiandien.

Image’ų optimizavimas – mažiau reiškia daugiau

Viena didžiausių klaidų, kurią daro pradedantieji (ir ne tik), yra per didelių Docker image’ų kūrimas. Matau projektus, kur vienas image’as sveria 2-3 GB, nors galėtų būti 200 MB ar net mažesnis. Kuo didesnis image’as, tuo ilgiau jis atsisiunčiamas, tuo daugiau vietos užima diske, tuo lėčiau paleidžiamas konteineris.

Pirmiausia, naudokite Alpine Linux arba kitus minimalius base image’us. Vietoj `node:latest` (apie 900 MB) galite naudoti `node:alpine` (apie 170 MB). Skirtumas milžiniškas. Taip, kartais Alpine kelia problemų dėl musl libc vietoj glibc, bet dažniausiai tai išsprendžiama per kelias minutes.

Antra svarbi detalė – multi-stage builds. Tai viena galingiausių Docker funkcijų, kurią per mažai kas naudoja. Idėja paprasta: viename stage’e kompiliuojate kodą su visais reikalingais įrankiais, o kitame – tiesiog nukopijuojate gatavą rezultatą į minimalų image’ą.

Pavyzdžiui, Go programai:

„`dockerfile
# Build stage
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp

# Final stage
FROM alpine:latest
COPY –from=builder /app/myapp /myapp
CMD [„/myapp”]
„`

Taip galutinis image’as neturi nei Go kompiliatoriaus, nei source kodo – tik sukompiliuotą programą. Rezultatas: vietoj 800 MB gauname 15 MB image’ą.

Layer’ių tvarka ir cache’avimas

Docker naudoja sluoksniuotą sistemą – kiekviena Dockerfile instrukcija sukuria naują sluoksnį. Kai kuriate image’ą, Docker cache’ina kiekvieną sluoksnį. Jei sluoksnis nepasikeitė, jis nenaudojamas iš cache’o, o ne kuriamas iš naujo. Čia ir slypi optimizavimo galimybė.

Problema ta, kad kai tik vienas sluoksnis pasikeičia, visi po jo esantys sluoksniai persikuria iš naujo. Todėl instrukcijų tvarka Dockerfile’e yra kritiškai svarbi.

Blogas pavyzdys:

„`dockerfile
FROM node:alpine
WORKDIR /app
COPY . .
RUN npm install
CMD [„npm”, „start”]
„`

Čia kiekvieną kartą, kai pakeičiate bet kurį failą projekte, `COPY . .` nukopijuoja viską iš naujo, ir `npm install` vykdomas iš naujo, net jei package.json nepasikeitė.

Geras pavyzdys:

„`dockerfile
FROM node:alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD [„npm”, „start”]
„`

Dabar npm priklausomybės įdiegiamos tik tada, kai pasikeičia package.json. Visi kiti kodo pakeitimai neįtakoja šio sluoksnio. Build laikas gali sutrumpėti nuo 5 minučių iki 10 sekundžių.

Dar vienas patarimas: .dockerignore failas yra jūsų draugas. Jame nurodykite viską, ko nereikia kopijuoti į image’ą – node_modules, .git, test failus, dokumentaciją. Mažiau failų = greitesnis build procesas.

Resursų valdymas ir limitai

Pagal nutylėjimą Docker konteineris gali naudoti visus host sistemos resursus. Skamba gerai, bet praktikoje tai gali sukelti problemų. Vienas „išprotėjęs” konteineris gali paralyžiuoti visą sistemą.

Nustatykite CPU ir RAM limitus kiekvienam konteineriui. Docker Compose faile tai atrodo taip:

„`yaml
services:
myapp:
image: myapp:latest
deploy:
resources:
limits:
cpus: ‘0.5’
memory: 512M
reservations:
cpus: ‘0.25’
memory: 256M
„`

Čia sakome, kad konteineris gali naudoti maksimaliai pusę CPU branduolio ir 512 MB RAM, bet garantuojame jam bent ketvirtadalį branduolio ir 256 MB.

Taip pat svarbu suprasti, kaip veikia storage driver’iai. Overlay2 yra greičiausias ir rekomenduojamas daugumai sistemų. Patikrinti, kurį naudojate, galite komanda:

„`bash
docker info | grep „Storage Driver”
„`

Jei matote ką nors kita nei overlay2 (pvz., devicemapper), verta pakeisti konfigūraciją. Overlay2 yra ženkliai greitesnis skaitant ir rašant failus.

Tinklo optimizavimas ir konteinerių komunikacija

Docker tinklas – tai sritis, kur dažnai prarandama daug performance. Pagal nutylėjimą Docker naudoja bridge tinklą, kuris veikia per NAT. Tai prideda papildomo overhead, ypač kai konteineriai intensyviai komunikuoja tarpusavyje.

Jei turite kelis konteinerius, kurie daug bendrauja, naudokite user-defined bridge network arba dar geriau – host network mode (jei saugumas leidžia). Host režimu konteineris naudoja host sistemos tinklą tiesiogiai, be jokio NAT ar bridge overhead.

„`bash
docker run –network host myapp
„`

Tačiau atminkite: host režimu prarandate tinklo izoliaciją ir port mapping galimybes. Tai tinka ne visais atvejais.

Kitas svarbus dalykas – DNS resolution. Docker turi integruotą DNS serverį, bet jei turite daug konteinerių, DNS užklausos gali tapti bottleneck. Galite optimizuoti tai naudodami:

„`yaml
services:
myapp:
dns:
– 8.8.8.8
– 8.8.4.4
dns_opt:
– ndots:1
„`

Parametras `ndots:1` sumažina DNS užklausų skaičių, kai ieškoma host vardų.

Volume’ų tipai ir jų įtaka greičiui

Failų sistemos operacijos Docker konteineriuose gali būti lėtos, ypač macOS ir Windows sistemose. Tai susiję su tuo, kaip Docker Desktop veikia šiose platformose – jis faktiškai sukuria virtualią mašiną su Linux.

Yra trys pagrindiniai volume tipai: bind mounts, named volumes ir tmpfs. Kiekvienas turi savo privalumų ir trūkumų.

Bind mounts – tai kai tiesiogiai prijungiate host sistemos direktoriją prie konteinerio. Patogu development metu, bet macOS ir Windows sistemose gali būti labai lėta. Jei turite projektą su tūkstančiais mažų failų (pvz., node_modules), operacijos su jais gali užtrukti.

Named volumes – tai Docker valdomos saugyklos vietos. Jos yra greitesnės nei bind mounts ne-Linux sistemose, nes duomenys saugomi VM viduje, ne sinchronizuojami su host sistema.

Tmpfs mounts – tai RAM’e esanti failų sistema. Supergreita, bet duomenys prarandami išjungus konteinerį. Puikiai tinka laikiniems failams, cache’ui, session duomenims.

Praktinis patarimas development aplinkoje su Node.js:

„`yaml
services:
app:
volumes:
– .:/app
– /app/node_modules # Išimtis – nenaudoti bind mount
„`

Taip source kodas sinchronizuojamas, bet node_modules lieka konteinerio viduje, kas gerokai pagreitina darbą.

Logging ir monitoring – kas vyksta po gaubtu

Logging gali būti netikėtas performance killer. Pagal nutylėjimą Docker naudoja json-file logging driver’į, kuris saugo visus logus diske. Jei programa generuoja daug logų, failai greitai išauga iki gigabaitų ir sulėtina sistemą.

Pirmas žingsnis – nustatykite log rotation:

„`yaml
services:
myapp:
logging:
driver: „json-file”
options:
max-size: „10m”
max-file: „3”
„`

Taip kiekvienas log failas neviršys 10 MB, ir bus saugomi tik 3 naujausi failai.

Dar geriau – naudokite efektyvesnį logging driver’į, pvz., `local`, kuris optimizuotas greitaveikai:

„`yaml
logging:
driver: „local”
options:
max-size: „10m”
„`

Jei naudojate centralizuotą logging sistemą (ELK, Splunk, Loki), galite siųsti logus tiesiogiai ten ir išvis nenaudoti Docker logging:

„`yaml
logging:
driver: „syslog”
options:
syslog-address: „tcp://192.168.0.42:514”
„`

Monitoring yra ne mažiau svarbus. Naudokite `docker stats` komandą arba įrankius kaip cAdvisor, Prometheus su node_exporter. Tai padės identifikuoti, kurie konteineriai naudoja daugiausiai resursų ir kur yra bottleneck’ai.

Build proceso pagreitinimas su BuildKit

BuildKit – tai naujos kartos Docker build engine, kuris yra daug greitesnis ir funkcionalus už senąjį. Jis įjungtas pagal nutylėjimą Docker 23.0+ versijose, bet jei turite senesnę versiją, galite jį aktyvuoti:

„`bash
export DOCKER_BUILDKIT=1
docker build .
„`

BuildKit privalumai:
– Lygiagretusis build’ų vykdymas
– Geresnis cache’avimas
– Galimybė praleisti nenaudojamus stage’us
– SSH ir secret mounting build metu

Praktinis pavyzdys su cache mount, kuris labai pagreitina priklausomybių diegimą:

„`dockerfile
FROM node:alpine
WORKDIR /app
COPY package*.json ./
RUN –mount=type=cache,target=/root/.npm \
npm install
COPY . .
„`

Čia npm cache išsaugomas tarp build’ų, todėl pakartotinai diegiant priklausomybes, daugelis paketų tiesiog paimami iš cache’o.

Dar viena galinga BuildKit funkcija – build secrets. Vietoj to, kad įterptumėte slaptažodžius ar API raktus į image’ą, galite juos mount’inti tik build metu:

„`dockerfile
RUN –mount=type=secret,id=npmrc,target=/root/.npmrc \
npm install
„`

„`bash
docker build –secret id=npmrc,src=$HOME/.npmrc .
„`

Taip slaptažodžiai niekada nepateks į finalinį image’ą ar build cache’ą.

Kai viskas veikia, bet vis tiek per lėtai

Kartais padarote viską teisingai, bet Docker vis tiek veikia lėčiau nei norėtumėte. Čia keletas pažangesnių optimizavimo būdų.

Kernel parametrų tuning. Linux sistemose galite optimizuoti network stack’ą:

„`bash
# /etc/sysctl.conf
net.core.somaxconn = 1024
net.ipv4.tcp_max_syn_backlog = 2048
net.ipv4.ip_local_port_range = 1024 65535
„`

Storage optimizavimas. Jei naudojate SSD, įsitikinkite, kad įjungtas TRIM:

„`bash
sudo fstrim -v /var/lib/docker
„`

Arba nustatykite automatinį TRIM cron job’ą.

Swap išjungimas. Docker aplinkoje swap dažnai daro daugiau žalos nei naudos. Jei turite pakankamai RAM, geriau jį išjungti:

„`bash
sudo swapoff -a
„`

Konteinerių skaičiaus mažinimas. Kartais geriau turėti vieną konteinerį su keliais procesais nei dešimt atskirų konteinerių. Taip, tai prieštarauja „vienas procesas per konteinerį” principui, bet realybėje kartais tai yra efektyvesnis sprendimas.

Pavyzdžiui, vietoj atskirų konteinerių nginx, PHP-FPM ir cron, galite naudoti supervisord ir valdyti visus procesus viename konteineryje. Taip sumažinate network overhead ir paprastinate deployment’ą.

Kaip išspausti maksimumą iš Docker be galvos skausmo

Docker optimizavimas nėra vienkartinis veiksmas – tai nuolatinis procesas. Pradėkite nuo paprastų dalykų: mažinkite image’ų dydį, tvarkingai rašykite Dockerfile’us, nustatykite resursų limitus. Tai duos didžiausią efektą mažiausiomis pastangomis.

Vėliau, kai sistema auga, pradėkite gilintis į tinklo optimizavimą, storage driver’ius, logging konfigūraciją. Naudokite monitoring įrankius – negalite optimizuoti to, ko nematote. Reguliariai tikrinkite `docker system df`, valykite nenaudojamus image’us ir konteinerius su `docker system prune`.

Atminkite, kad optimizavimas turi prasmę tik tada, kai yra reali problema. Jei jūsų aplikacija veikia pakankamai greitai, nešvaistykit laiko mikrooptimizacijoms. Bet kai pajuntate, kad Docker lėtina darbą, turite arsenalą įrankių ir metodų, kaip tai išspręsti.

Svarbiausia – eksperimentuokite. Išbandykite skirtingus base image’us, storage driver’ius, network režimus. Matuokite rezultatus. Tai, kas veikia vienam projektui, nebūtinai tiks kitam. Docker lankstumas leidžia pritaikyti beveik viską, tereikia žinoti, ką ir kaip keisti.