Opbygning af optimerede Docker-billeder til en Spring Boot-applikation
Containere er blevet det foretrukne middel til at pakke en applikation med alle dens software- og operativsystemafhængigheder og derefter levere dem til forskellige miljøer.
Denne artikel dækker forskellige måder at beholde en Spring Boot-applikation på:
opbygning af et docker-billede ved hjælp af en dockerfil,
opbygning af et OCI-billede fra kilde ved hjælp af Cloud-Native Buildpack,
og billedoptimering under kørsel ved at adskille JAR-dele i forskellige niveauer ved hjælp af lagdelte værktøjer.
Kode eksempel
Denne artikel er ledsaget af et eksempel på en arbejdskode på GitHub .
Container terminologi
Vi starter med containerterminologien, der bruges gennem artiklen:
Containerbillede: en fil i et bestemt format. Vi konverterer vores applikation til et containerbillede ved at køre byggeværktøjet.
container: En eksekverbar forekomst af containerbilledet.
Container motor: Dæmonprocessen, der er ansvarlig for at køre containeren.
Container vært: Værtsmaskinen, som containermotoren kører på.
Containerregistrering: Den generelle placering, der bruges til at publicere og distribuere containerbilledet.
OCI standard: Open Container Initiative (OCI) er en let, open source-administrationsramme dannet af Linux Foundation. OCI-billedspecifikationen definerer industristandarder for containerbilledformater og kørselstiden for at sikre, at alle containermotorer kan køre containerbilleder, der er oprettet af ethvert byggeværktøj.
For at containerisere en applikation pakker vi vores applikation ind i et containerbillede og offentliggør dette billede til det offentlige register. Containerruntiden henter dette billede fra registreringsdatabasen, pakker det ud og kører programmet inde i det.
Version 2.3 af Spring Boot giver plugins til opbygning af OCI-billeder.
Docker er den mest brugte containerimplementering, og vi bruger Docker i vores eksempler, så alle efterfølgende containerreferencer i denne artikel vil referere til Docker.
Opbygning af et containerbillede på traditionel vis
Det er meget nemt at bygge Docker-billeder til Spring Boot-applikationer ved at tilføje nogle få instruktioner til din Dockerfile.
Vi opretter først en eksekverbar JAR, og som en del af Dockerfile-instruktionerne kopierer vi den eksekverbare JAR oven på basis-JRE-billedet efter at have anvendt de nødvendige tilpasninger.
Lad os oprette vores forårsapplikation på Spring Initializr med afhængigheder web, lombokи actuator. Vi tilføjer også en hvile-controller til at give en API med GETmetode.
Oprettelse af en Dockerfile
Vi placerer derefter denne applikation i en beholder ved at tilføje Dockerfile:
Vores Dockerfile indeholder et basisbillede, fra adoptopenjdk, hvorpå vi kopierer vores JAR-fil og åbner derefter porten, 8080som vil lytte efter anmodninger.
Applikationsmontage
Først skal du oprette en applikation ved hjælp af Maven eller Gradle. Her bruger vi Maven:
mvn clean package
Dette opretter en eksekverbar JAR-fil til applikationen. Vi skal konvertere denne eksekverbare JAR til et Docker-billede for at køre på Docker-motoren.
Opret et containerbillede
Vi sætter derefter denne JAR-eksekverbare i Docker-billedet ved at køre kommandoen docker buildfra rodmappen på projektet, der indeholder den tidligere oprettede Dockerfil:
docker build -t usersignup:v1 .
Vi kan se vores billede på listen med kommandoen:
docker images
Outputtet af ovenstående kommando inkluderer vores billede usersignupsammen med basisbilledet, adoptopenjdkspecificeret i vores Dockerfile.
REPOSITORY TAG SIZE
usersignup v1 249MB
adoptopenjdk 11-jre-hotspot 229MB
Se lag inde i et containerbillede
Lad os se på stakken af lag inde i billedet. Vi vil bruge инструмент dykke, for at se disse lag:
dive usersignup:v1
Her er en del af outputtet fra Dive-kommandoen:
Som vi kan se, udgør applikationslaget en betydelig del af billedstørrelsen. Vi ønsker at reducere størrelsen af dette lag i de følgende afsnit som en del af vores optimering.
Opbygning af et containerbillede med Buildpack
Samle pakker (Buildpacks) er et generisk udtryk, der bruges af forskellige Platform as a Service-tilbud (PAAS) til at skabe et containerbillede fra kildekoden. Det blev lanceret af Heroku i 2011 og er siden blevet adopteret af Cloud Foundry, Google App Engine, Gitlab, Knative og et par andre.
Fordel ved Cloud Build-pakker
En af de vigtigste fordele ved at bruge Buildpack til at bygge billeder er, at billedkonfigurationsændringer kan administreres centralt (builder) og udbredes til alle applikationer ved hjælp af builder.
Byggepakkerne var tæt knyttet til platformen. Cloud-Native Buildpacks giver standardisering på tværs af platforme ved at understøtte OCI-billedformatet, som sikrer, at billedet kan køres af Docker-motoren.
Brug af Spring Boot Plugin
Spring Boot-plugin'et bygger OCI-billeder fra kilden ved hjælp af Buildpack. Billeder er lavet vha bootBuildImageopgaver (Gradle) el spring-boot:build-imagetarget (Maven) og lokal Docker-installation.
Vi kan tilpasse navnet på det billede, vi skal skubbe til Docker-registret, ved at angive navnet i image tag:
Lad os bruge Maven til at udføre build-imagemål for oprettelse af en applikation og oprettelse af et containerbillede. Vi bruger i øjeblikket ingen Dockerfiler.
Ud fra outputtet ser vi det paketo Cloud-Native buildpackbruges til at skabe et fungerende OCI-billede. Som før kan vi se billedet opført som et Docker-billede ved at køre kommandoen:
Dernæst kører vi Jib-plugin'et ved hjælp af Maven-kommandoen til at bygge applikationen og oprette containerbilledet. Som før bruger vi ingen Dockerfiler her:
Efter at have udført ovenstående Maven-kommando får vi følgende output:
[INFO] Containerizing application to pratikdas/usersignup:v1...
.
.
[INFO] Container entrypoint set to [java, -cp, /app/resources:/app/classes:/app/libs/*, io.pratik.users.UsersignupApplication]
[INFO]
[INFO] Built and pushed image as pratikdas/usersignup:v1
[INFO] Executing tasks:
[INFO] [==============================] 100.0% complete
Outputtet viser, at containerbilledet er blevet oprettet og placeret i registreringsdatabasen.
Motivationer og metoder til at skabe optimerede billeder
Vi har to hovedårsager til at optimere:
Ydelse: I et containerorkestreringssystem trækkes containerbilledet fra billedregistret til værten, der kører containermotoren. Denne proces kaldes planlægning. At trække store billeder fra registreringsdatabasen resulterer i lange planlægningstider i containerorkestreringssystemer og lange byggetider i CI-pipelines.
Безопасность: store billeder har også et stort område for sårbarheder.
Et Docker-billede består af en stak af lag, der hver repræsenterer et udsagn i vores Dockerfile. Hvert lag repræsenterer deltaet af ændringer i det underliggende lag. Når vi trækker et Docker-billede fra registreringsdatabasen, trækkes det i lag og cachelagres på værten.
Spring Boot bruger "fat JAR" i som standard emballageformat. Når vi ser på en fed JAR, ser vi, at applikationen er en meget lille del af hele JAR. Det er den del, der ændrer sig mest. Resten består af Spring Framework-afhængigheder.
Optimeringsformlen er centreret omkring at isolere applikationen på et separat niveau fra Spring Framework-afhængigheder.
Afhængighedslaget, der udgør hovedparten af den tykke JAR-fil, downloades kun én gang og cachelagres på værtssystemet.
Kun et tyndt lag af appen trækkes under appopdateringer og containerplanlægning, som vist i dette diagram:
I de følgende afsnit vil vi se på, hvordan du opretter disse optimerede billeder til en Spring Boot-applikation.
Opbygning af et optimeret containerbillede til en Spring Boot-applikation med Buildpack
Spring Boot 2.3 understøtter lagdeling ved at udtrække dele af en tyk JAR-fil i separate lag. Lagdelingsfunktionen er deaktiveret som standard og skal udtrykkeligt aktiveres ved hjælp af Spring Boot Maven-plugin:
Vi vil bruge denne konfiguration til at bygge vores containerbillede først med Buildpack og derefter med Docker i de følgende afsnit.
Lad os løbe build-imageMaven mål for at oprette et containerbillede:
mvn spring-boot:build-image
Hvis vi kører Dive for at se lagene i det resulterende billede, kan vi se, at applikationslaget (cirklet med rødt) er meget mindre i kilobyteområdet sammenlignet med det, vi fik ved at bruge det tykke JAR-format:
Opbygning af et optimeret containerbillede til en Spring Boot-applikation med Docker
I stedet for at bruge et Maven- eller Gradle-plugin, kan vi også oprette et lagdelt Docker JAR-billede med en Docker-fil.
Når vi bruger Docker, skal vi tage to ekstra trin for at udtrække lagene og kopiere dem til det endelige billede.
Indholdet af den resulterende JAR efter bygning med Maven med lagdeling aktiveret vil se sådan ud:
Udgangen viser en ekstra JAR navngivet spring-boot-jarmode-layertoolsи layersfle.idxfil. Denne ekstra JAR-fil giver mulighed for lagdeling, som beskrevet i næste afsnit.
Uddrag afhængigheder på separate lag
For at se og udtrække lag fra vores lagdelte JAR bruger vi systemegenskaben -Djarmode=layertoolstil start spring-boot-jarmode-layertoolsJAR i stedet for applikation:
Kørsel af denne kommando producerer et output, der indeholder de tilgængelige kommandomuligheder:
Usage:
java -Djarmode=layertools -jar usersignup-0.0.1-SNAPSHOT.jar
Available commands:
list List layers from the jar that can be extracted
extract Extracts layers from the jar for image creation
help Help about any command
Udgangen viser kommandoerne list, extractи helpс helpvære standard. Lad os køre kommandoen med listmulighed:
java -Djarmode=layertools -jar target/usersignup-0.0.1-SNAPSHOT.jar list
Vi ser en liste over afhængigheder, der kan tilføjes som lag.
Lag som standard:
Lagnavn
Indhold
dependencies
enhver afhængighed, hvis version ikke indeholder SNAPSHOT
spring-boot-loader
JAR-læsserklasser
snapshot-dependencies
enhver afhængighed, hvis version indeholder SNAPSHOT
application
applikationsklasser og ressourcer
Lag er defineret i layers.idxfilen i den rækkefølge, de skal tilføjes til Docker-billedet. Disse lag cachelagres på værten efter den første hentning, fordi de ikke ændres. Kun det opdaterede applikationslag downloades til værten, hvilket er hurtigere på grund af den reducerede størrelse .
Opbygning af et billede med afhængigheder udtrukket i separate lag
Vi vil bygge det endelige billede i to trin ved hjælp af en metode kaldet montering i flere trin . I det første trin vil vi udtrække afhængighederne, og i det andet trin vil vi kopiere de udpakkede afhængigheder til det endelige.
Lad os ændre vores Dockerfile til en multi-stage build:
# the first stage of our build will extract the layers
FROM adoptopenjdk:14-jre-hotspot as builder
WORKDIR application
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract
# the second stage of our build will copy the extracted layers
FROM adoptopenjdk:14-jre-hotspot
WORKDIR application
COPY --from=builder application/dependencies/ ./
COPY --from=builder application/spring-boot-loader/ ./
COPY --from=builder application/snapshot-dependencies/ ./
COPY --from=builder application/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]
Vi gemmer denne konfiguration i en separat fil - Dockerfile2.
Vi bygger Docker-billedet ved hjælp af kommandoen:
docker build -f Dockerfile2 -t usersignup:v1 .
Efter at have udført denne kommando, får vi følgende output:
Sending build context to Docker daemon 20.41MB
Step 1/12 : FROM adoptopenjdk:14-jre-hotspot as builder
14-jre-hotspot: Pulling from library/adoptopenjdk
.
.
Successfully built a9ebf6970841
Successfully tagged userssignup:v1
Vi kan se, at Docker-billedet er oprettet med et billed-id og derefter tagget.
Til sidst kører vi Dive-kommandoen som før for at tjekke lagene inde i det genererede Docker-billede. Vi kan give et billed-id eller tag som input til Dive-kommandoen:
dive userssignup:v1
Som du kan se fra outputtet, er laget, der indeholder applikationen, nu kun 11 KB, og afhængighederne er cachelagret i separate lag.
Uddrag interne afhængigheder på separate lag
Vi kan yderligere reducere størrelsen af applikationslaget ved at udtrække enhver af vores tilpassede afhængigheder i et separat lag i stedet for at pakke dem sammen med applikationen ved at erklære dem i ymllignende fil navngivet layers.idx:
I denne fil layers.idxvi har tilføjet en tilpasset afhængighed ved navn, io.myorgindeholdende organisationsafhængigheder hentet fra det delte lager.
Output
I denne artikel så vi på at bruge Cloud-Native Buildpacks til at bygge et containerbillede direkte fra kilden. Dette er et alternativ til at bruge Docker til at skabe et containerbillede på den sædvanlige måde: først oprettes en tyk eksekverbar JAR-fil og pakkes derefter ind i et containerbillede ved at specificere instruktionerne i Dockerfilen.
Vi så også på at optimere vores container ved at inkludere en lagdelingsfunktion, der udtrækker afhængigheder i separate lag, der er cachelagret på værten, og et tyndt applikationslag indlæses på tidspunktet for planlægning i containerens udførelsesmotorer.
Du kan finde al kildekoden brugt i artiklen på Github .
Kommandoreference
Her er en oversigt over de kommandoer, vi brugte i denne artikel for en hurtig reference.
Kontekstrydning:
docker system prune -a
Opbygning af et containerbillede med en Dockerfile:
docker build -f <Docker file name> -t <tag> .
Byg containerbillede fra kilden (uden Dockerfile):
mvn spring-boot:build-image
Se afhængighedslag. Før du bygger applikationsjar-filen, skal du sørge for, at lagdelingsfunktionen er aktiveret i spring-boot-maven-plugin:
java -Djarmode=layertools -jar application.jar list
Udtræk afhængighedslag. Før du bygger applikationsjar-filen, skal du sørge for, at lagdelingsfunktionen er aktiveret i spring-boot-maven-plugin: