Wydajne tworzenie obrazów dockerowych w Springu
Jak działa budowanie obrazów dockerowych?
Budowanie obrazów dockerowych polega na tworzeniu niemodyfikowalnych “szablonów” aplikacji i zależności, które mogą być uruchamiane w izolowanych kontenerach. Proces ten jest zautomatyzowany za pomocą plików o nazwie Dockerfile.
Każdy plik Dockerfile zawiera listę instrukcji wykonywanych w podanej kolejności w momencie budowania obrazu. Docker konwertuje otrzymaną listę instrukcji na warstwy, które składają się na budowany obraz i mają określony rozmiar w przestrzeni dyskowej.
Podczas uruchomienia budowania builder podejmuję próbę ponownego wykorzystania warstw z poprzednich wersji. Jeśli warstwa obrazu jest niezmieniona, builder wyciąga ją z cache. Jeśli warstwa uległa zmianie, tworzy ją na nowo. Ponowne tworzenie warstwy wiążę się również z unieważnieniem cache dla wszystkich następnych warstw. Jeśli plik .jar ulegnie zmianie, to schemat warstw przedstawia się następująco:
Aby z cache była pobierana jak największa liczba warstw, bardzo ważna jest kolejność deklarowania instrukcji. Dla powyższego przykładu możemy wykonać optymalizację:
Dzięki temu, przy następnym budowaniu, builder ponownie wykorzysta trzy, a nie tylko dwie warstwy.
Klasyczne podejście do budowania
W poprzedniej sekcji pokazaliśmy standardowy plik Dockerfile do zbudowania aplikacji Spring Bootowej. Plik ten zawiera między innymi instrukcję przekopiowania pliku .jar do obrazu. Standardowo wykonuje się to za pomocą jednej instrukcji, co oznacza, że jakakolwiek zmiana w którymkolwiek pliku aplikacji powoduje konieczność utworzenia warstwy od nowa. Jest to bardzo niekorzystne, ponieważ wiąże się z wykorzystaniem nadmiernej przestrzeni dyskowej. Przyjmując, że plik .jar waży około 20 MB (większość z tego to zależności aplikacji), to 10 wersji aplikacji zajmie 200 MB, pomimo że zmiany, są niewielkie i dotyczą tylko kodów źródłowych, ważących przeciętnie kilkanaście KB.
Tutaj naprzeciw wyszli nam twórcy Spring Boota, dodając od wersji 2.3 możliwość budowania warstwowego pliku jar (eng. layered jars).
Jak działa Spring Boot layered jar?
Spring Boot layered jar zmienia sposób budowania pliku .jar, dzieląc jego części na konkretne warstwy. Wykorzystuje do tego plik layers.idx
, który zawiera listę warstw oraz części pliku .jar zawarte w każdej z nich. Warstwy w pliku zapisane są w kolejności, w jakiej powinny zostać dodane do obrazu dockerowego. Domyślnie plik składa się z poniższych warstw:
- dependencies - zawiera wszystkie zależności aplikacji, które nie są w wersji SNAPSHOT
- spring-boot-loader - zawiera klasy ładujące plik .jar (odpowiadające za uruchomienie aplikacji)
- snapshot-dependencies - zawiera zależności aplikacji w wersji SNAPSHOT
- application - zawiera kod źródłowy aplikacji
Dzięki takiemu rozwiązaniu jesteśmy w stanie podzielić instrukcję kopiowania pliku .jar na kilka mniejszych instrukcji i zapewnić ponowne wykorzystanie warstw, gdy zmienimy tylko kod źródłowy aplikacji.
Jak skonfigurować Spring Boot layered jar?
- Modyfikujemy sposób budowania aplikacji:
- Dla budowania Gradle w zadaniach budujących naszą aplikację dodajemy:
tasks { bootJar { layered } }
- Dla budowania Mavenem, w konfiguracji plugina spring-boot-maven-plugin, dodajemy:
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <layers> <enabled>true</enabled> <includeLayerTools>true</includeLayerTools> </layers> </configuration> </plugin> </plugins> </build>
- Dla budowania Gradle w zadaniach budujących naszą aplikację dodajemy:
- Modyfikujemy sposób budowania obrazu w pliku Dockerfile:
- Przykładowy plik wygląda następująco:
FROM eclipse-temurin:17.0.9_9-jre-alpine as builder WORKDIR /work COPY dockerfile-example-snapshot.jar application.jar RUN java -Djarmode=layertools -jar application.jar extract FROM eclipse-temurin:17.0.9_9-jre-alpine WORKDIR /app COPY --from=builder /work/dependencies/ ./ COPY --from=builder /work/spring-boot-loader/ ./ COPY --from=builder /work/snapshot-dependencies/ ./ COPY --from=builder /work/application/ ./ CMD ["java", "org.springframework.boot.loader.launch.JarLauncher"]
- Przykładowy plik wygląda następująco:
Jak widać wyżej, budowanie zostało podzielone na dwa etapy:
- Skopiowanie oraz wypakowanie pliku .jar
- Skopiowanie warstw pliku .jar oraz zadeklarowanie polecenia, które zostanie wykonane przy starcie kontenera
Zastosowanie w praktyce
Do zaprezentowania działania wykorzystajmy przykładowy plik Dockerfile wskazany powyżej.
- Budujemy obraz:
docker build . --tag service1
- Wykonujemy polecenie listujące warstwy:
docker history service1
- Rezultat polecenia:
IMAGE CREATED CREATED BY SIZE a59bcc935804 About a minute ago /bin/sh -c #(nop) CMD ["java" "org.s… 0B <missing> About a minute ago /bin/sh -c #(nop) COPY dir:bdb78666255cc63e7… 3.71kB <missing> About a minute ago /bin/sh -c #(nop) COPY dir:f782fe956cf5892f5… 0B <missing> About a minute ago /bin/sh -c #(nop) COPY dir:3d769b9b5528fa54f… 387kB <missing> About a minute ago /bin/sh -c #(nop) COPY dir:24195f786b612de17… 19.5MB <missing> About a minute ago /bin/sh -c #(nop) WORKDIR /app 0B <missing> 8 days ago ENTRYPOINT ["/__cacert_entrypoint.sh"] 0B <missing> 8 days ago COPY entrypoint.sh /__cacert_entrypoint.sh #… 1.17kB <missing> 8 days ago RUN /bin/sh -c set -eux; echo "Verifying… 0B <missing> 8 days ago RUN /bin/sh -c set -eux; ARCH="$(apk --p… 140MB <missing> 8 days ago ENV JAVA_VERSION=jdk-17.0.11+9 0B <missing> 8 days ago RUN /bin/sh -c set -eux; apk add --no-ca… 17.3MB <missing> 8 days ago ENV LANG=en_US.UTF-8 LANGUAGE=en_US:en LC_AL… 0B <missing> 8 days ago ENV PATH=/opt/java/openjdk/bin:/usr/local/sb… 0B <missing> 8 days ago ENV JAVA_HOME=/opt/java/openjdk 0B <missing> 3 months ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B <missing> 3 months ago /bin/sh -c #(nop) ADD file:37a76ec18f9887751… 7.37MB
- Wykonujemy zmianę kodu źródłowego aplikacji i ponownie budujemy obraz z nowym tagiem:
docker build . --tag service2
- Wykonujemy polecenie listujące warstwy:
docker history service2
- Rezultat polecenia:
IMAGE CREATED CREATED BY SIZE e027785c6f71 34 seconds ago /bin/sh -c #(nop) CMD ["java" "org.s… 0B <missing> 34 seconds ago /bin/sh -c #(nop) COPY dir:0c4cebea0bf1ba4e8… 3.7kB <missing> 2 minutes ago /bin/sh -c #(nop) COPY dir:f782fe956cf5892f5… 0B <missing> 2 minutes ago /bin/sh -c #(nop) COPY dir:3d769b9b5528fa54f… 387kB <missing> 2 minutes ago /bin/sh -c #(nop) COPY dir:24195f786b612de17… 19.5MB <missing> 2 minutes ago /bin/sh -c #(nop) WORKDIR /app 0B <missing> 8 days ago ENTRYPOINT ["/__cacert_entrypoint.sh"] 0B <missing> 8 days ago COPY entrypoint.sh /__cacert_entrypoint.sh #… 1.17kB <missing> 8 days ago RUN /bin/sh -c set -eux; echo "Verifying… 0B <missing> 8 days ago RUN /bin/sh -c set -eux; ARCH="$(apk --p… 140MB <missing> 8 days ago ENV JAVA_VERSION=jdk-17.0.11+9 0B <missing> 8 days ago RUN /bin/sh -c set -eux; apk add --no-ca… 17.3MB <missing> 8 days ago ENV LANG=en_US.UTF-8 LANGUAGE=en_US:en LC_AL… 0B <missing> 8 days ago ENV PATH=/opt/java/openjdk/bin:/usr/local/sb… 0B <missing> 8 days ago ENV JAVA_HOME=/opt/java/openjdk 0B <missing> 3 months ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B <missing> 3 months ago /bin/sh -c #(nop) ADD file:37a76ec18f9887751… 7.37MB
Na powyższym przykładzie widzimy, że zależności naszej aplikacji ważą około 20 MB. Przy wykonywaniu drugiego obrazu warstwa zależności została pobrana z cache, dzięki czemu wykorzystaliśmy 20 MB zamiast 40 MB.
Definiowanie własnych warstw zależności
Jeżeli domyślna konfiguracja warstw nie jest wystarczająca, to możemy określić własne warstwy, zawierające konkretne zależności. Pozwoli to wydzielić podstawowe zależności, które są na przykład wykorzystane w kilku projektach.
Konfiguracja
W powyższym przykładzie wydzielimy zależności pakietu javax.xml.bind do osobnej warstwy.
Maven
- W katalogu
/src/resources
tworzymy pliklayers.xml
określający strukturę warstw naszego pliku .jar:
<layers xmlns="http://www.springframework.org/schema/boot/layers"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/boot/layers
https://www.springframework.org/schema/boot/layers/layers-{spring-boot-xsd-version}.xsd">
<application>
<into layer="spring-boot-loader">
<include>org/springframework/boot/loader/**</include>
</into>
<into layer="application" />
</application>
<dependencies>
<into layer="jaxb-dependencies">
<include>javax.xml.bind:*:*</include>
</into>
<into layer="snapshot-dependencies">
<include>*:*:*SNAPSHOT</include>
</into>
<into layer="dependencies"/>
</dependencies>
<layerOrder>
<layer>dependencies</layer>
<layer>spring-boot-loader</layer>
<layer>jaxb-dependencies</layer>
<layer>snapshot-dependencies</layer>
<layer>application</layer>
</layerOrder>
</layers>
Należy pamiętać, aby przy konfiguracji pliku layers.xml
uzupełnić wersję zapisaną pod zmienną spring-boot-xsd-version
- W konfiguracji plugina tworzącego warstwowy .jar wskazujemy plik z konfiguracją warstw:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<layers>
<enabled>true</enabled>
<configuration>${project.basedir}/src/resources/layers.xml</configuration>
</layers>
</configuration>
</plugin>
Gradle
- Konfigurujemy plugin, określając nowe warstwy. W pliku
build.gradle.kts
umieszczamy:tasks { bootJar { layered { application { intoLayer("spring-boot-loader") { include("org/springframework/boot/loader/**") } intoLayer("application") } dependencies { intoLayer("jaxb-dependencies") { include( "javax.xml.bind:*:*") } intoLayer("snapshot-dependencies") { include("*:*:*SNAPSHOT") } intoLayer("dependencies") } layerOrder = listOf("dependencies", "spring-boot-loader", "jaxb-dependencies", "snapshot-dependencies", "application") } } }
W pliku
Dockerfile
dodajemy kopiowanie nowej warstwy:FROM eclipse-temurin:17.0.11_9-jre-alpine as builder WORKDIR /work COPY dockerfile-example-snapshot.jar application.jar RUN java -Djarmode=layertools -jar application.jar extract FROM eclipse-temurin:17.0.11_9-jre-alpine WORKDIR /app COPY --from=builder /work/dependencies/ ./ COPY --from=builder /work/spring-boot-loader/ ./ COPY --from=builder /work/jaxb-dependencies/ ./ COPY --from=builder /work/snapshot-dependencies/ ./ COPY --from=builder /work/application/ ./ CMD ["java", "org.springframework.boot.loader.launch.JarLauncher"]
Rezultat
- Budujemy obraz:
docker build . --tag service1
- Wykonujemy polecenie listujące warstwy:
docker history service1
- Rezultat polecenia:
IMAGE CREATED CREATED BY SIZE 53a56b52bb9d About a minute ago /bin/sh -c #(nop) CMD ["java" "org.s… 0B <missing> About a minute ago /bin/sh -c #(nop) COPY dir:dd5854b870089072a… 6.32kB <missing> About a minute ago /bin/sh -c #(nop) COPY dir:f782fe956cf5892f5… 0B <missing> About a minute ago /bin/sh -c #(nop) COPY dir:a0166562a093edeb6… 128kB <missing> About a minute ago /bin/sh -c #(nop) COPY dir:3d769b9b5528fa54f… 387kB <missing> About a minute ago /bin/sh -c #(nop) COPY dir:5e9e8ed2b7656fb9f… 19.6MB <missing> About an hour ago /bin/sh -c #(nop) WORKDIR /app 0B <missing> 8 days ago ENTRYPOINT ["/__cacert_entrypoint.sh"] 0B <missing> 8 days ago COPY entrypoint.sh /__cacert_entrypoint.sh #… 1.17kB <missing> 8 days ago RUN /bin/sh -c set -eux; echo "Verifying… 0B <missing> 8 days ago RUN /bin/sh -c set -eux; ARCH="$(apk --p… 140MB <missing> 8 days ago ENV JAVA_VERSION=jdk-17.0.11+9 0B <missing> 8 days ago RUN /bin/sh -c set -eux; apk add --no-ca… 17.3MB <missing> 8 days ago ENV LANG=en_US.UTF-8 LANGUAGE=en_US:en LC_AL… 0B <missing> 8 days ago ENV PATH=/opt/java/openjdk/bin:/usr/local/sb… 0B <missing> 8 days ago ENV JAVA_HOME=/opt/java/openjdk 0B <missing> 3 months ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B <missing> 3 months ago /bin/sh -c #(nop) ADD file:37a76ec18f9887751… 7.37MB
- Wykonujemy zmianę wersji zależności z pakietu jaxb.xml.bind i ponownie budujemy obraz z nowym tagiem:
docker build . --tag service2
- Wykonujemy polecenie listujące warstwy:
docker history service2
- Rezultat polecenia:
IMAGE CREATED CREATED BY SIZE 0a6326aaf0a6 27 seconds ago /bin/sh -c #(nop) CMD ["java" "org.s… 0B <missing> 27 seconds ago /bin/sh -c #(nop) COPY dir:67d2736e371ec6127… 6.22kB <missing> 27 seconds ago /bin/sh -c #(nop) COPY dir:f782fe956cf5892f5… 0B <missing> 27 seconds ago /bin/sh -c #(nop) COPY dir:d10e5e52a40c11567… 126kB <missing> About an hour ago /bin/sh -c #(nop) COPY dir:3d769b9b5528fa54f… 387kB <missing> About an hour ago /bin/sh -c #(nop) COPY dir:24195f786b612de17… 19.5MB <missing> About an hour ago /bin/sh -c #(nop) WORKDIR /app 0B <missing> 8 days ago ENTRYPOINT ["/__cacert_entrypoint.sh"] 0B <missing> 8 days ago COPY entrypoint.sh /__cacert_entrypoint.sh #… 1.17kB <missing> 8 days ago RUN /bin/sh -c set -eux; echo "Verifying… 0B <missing> 8 days ago RUN /bin/sh -c set -eux; ARCH="$(apk --p… 140MB <missing> 8 days ago ENV JAVA_VERSION=jdk-17.0.11+9 0B <missing> 8 days ago RUN /bin/sh -c set -eux; apk add --no-ca… 17.3MB <missing> 8 days ago ENV LANG=en_US.UTF-8 LANGUAGE=en_US:en LC_AL… 0B <missing> 8 days ago ENV PATH=/opt/java/openjdk/bin:/usr/local/sb… 0B <missing> 8 days ago ENV JAVA_HOME=/opt/java/openjdk 0B <missing> 3 months ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B <missing> 3 months ago /bin/sh -c #(nop) ADD file:37a76ec18f9887751… 7.37MB
Widzimy, że podczas tworzenia nowej wersji obrazu cztery ostatnie operacje zostały wykonane ponownie, a operacja kopiowania pozostałych zależności została wykorzystana z cache.
Podsumowanie
Warstwowe budowanie plików .jar może znacznie zmniejszyć wykorzystanie pamięci dyskowej, w której przechowujemy gotowe obrazy Docker. Warto pamiętać o prawidłowej kolejności operacji w pliku Dockerfile oraz o możliwości definiowania własnych warstw zależności.
-
SENIOR FULLSTACK DEVELOPER (JAVA + ANGULAR) Poznań (hybrydowo) lub zdalnie UoP 14 900 - 20 590 PLN brutto
B2B 19 680 - 27 220 PLN netto -
REGULAR FULLSTACK DEVELOPER (JAVA + ANGULAR) Poznań (hybrydowo) lub zdalnie UoP 11 300 - 15 900 PLN brutto
B2B 14 950 - 21 000 PLN netto -
ZOBACZ WSZYSTKIE OGŁOSZENIA
newsletter
techniczny
-
SENIOR FULLSTACK DEVELOPER (JAVA + ANGULAR) Poznań (hybrydowo) lub zdalnie UoP 14 900 - 20 590 PLN brutto
B2B 19 680 - 27 220 PLN netto -
REGULAR FULLSTACK DEVELOPER (JAVA + ANGULAR) Poznań (hybrydowo) lub zdalnie UoP 11 300 - 15 900 PLN brutto
B2B 14 950 - 21 000 PLN netto -
ZOBACZ WSZYSTKIE OGŁOSZENIA