ProHoster > Blog > administracja > Tworzenie zoptymalizowanych obrazów platformy Docker dla aplikacji Spring Boot
Tworzenie zoptymalizowanych obrazów platformy Docker dla aplikacji Spring Boot
Kontenery stały się preferowanym sposobem pakowania aplikacji ze wszystkimi zależnościami oprogramowania i systemu operacyjnego, a następnie dostarczania ich do różnych środowisk.
W tym artykule opisano różne sposoby konteneryzacji aplikacji Spring Boot:
utworzenie obrazu Dockera przy użyciu pliku Docker,
tworzenie obrazu OCI ze źródła przy użyciu Cloud-Native Buildpack,
oraz optymalizację obrazu w czasie wykonywania poprzez rozdzielenie części pliku JAR na różne warstwy przy użyciu narzędzi wielowarstwowych.
Przykład kodu
Do artykułu dołączony jest przykład działającego kodu na GitHub .
Terminologia kontenerowa
Zaczniemy od terminologii kontenerowej użytej w artykule:
Obraz kontenera: plik o określonym formacie. Przekonwertujemy naszą aplikację na obraz kontenera, uruchamiając narzędzie do kompilacji.
pojemnik: Wykonywalna instancja obrazu kontenera.
Silnik kontenerowy: Proces demona odpowiedzialny za uruchomienie kontenera.
Host kontenerowy: Komputer hosta, na którym działa silnik kontenera.
Rejestr kontenerów: Ogólna lokalizacja używana do publikowania i rozpowszechniania obrazu kontenera.
Norma OCI: Inicjatywa Open Container (OCI) to lekka, otwarta struktura zarządzania utworzona w ramach Linux Foundation. Specyfikacja obrazu OCI definiuje standardy branżowe dla formatów obrazów kontenerów i środowiska wykonawczego, aby zapewnić, że wszystkie silniki kontenerów będą mogły uruchamiać obrazy kontenerów utworzone za pomocą dowolnego narzędzia do kompilacji.
Aby konteneryzować aplikację, otaczamy ją obrazem kontenera i publikujemy ten obraz we wspólnym rejestrze. Środowisko wykonawcze kontenera pobiera ten obraz z rejestru, rozpakowuje go i uruchamia w nim aplikację.
Wersja 2.3 Spring Boot zawiera wtyczki do tworzenia obrazów OCI.
Doker to najczęściej używana implementacja kontenera, a w naszych przykładach używamy Dockera, więc wszystkie kolejne odniesienia do kontenerów w tym artykule będą odnosić się do Dockera.
Budowanie obrazu kontenera w tradycyjny sposób
Tworzenie obrazów Dockera dla aplikacji Spring Boot jest bardzo proste poprzez dodanie kilku instrukcji do pliku Docker.
Najpierw tworzymy wykonywalny plik JAR i w ramach instrukcji pliku Docker kopiujemy wykonywalny plik JAR na podstawowy obraz JRE po zastosowaniu niezbędnych ustawień.
Stwórzmy naszą aplikację Spring na Wiosna Initializr z zależnościami web, lombokи actuator. Dodajemy także kontroler odpoczynku, za pomocą którego można udostępniać interfejs API GETmetoda.
Tworzenie pliku Dockerfile
Następnie konteneryzujemy tę aplikację, dodając Dockerfile:
Nasz plik Docker zawiera obraz podstawowy z adoptopenjdk, na który kopiujemy nasz plik JAR i następnie otwieramy port, 8080który będzie nasłuchiwał próśb.
Budowanie aplikacji
Najpierw musisz stworzyć aplikację za pomocą Mavena lub Gradle. Tutaj używamy Mavena:
mvn clean package
Spowoduje to utworzenie wykonywalnego pliku JAR dla aplikacji. Musimy przekonwertować ten wykonywalny plik JAR na obraz Dockera, aby działał w silniku Dockera.
Tworzenie obrazu kontenera
Następnie umieszczamy ten wykonywalny plik JAR w obrazie Dockera, uruchamiając polecenie docker buildz katalogu głównego projektu zawierającego utworzony wcześniej plik Dockerfile:
docker build -t usersignup:v1 .
Nasz obraz możemy zobaczyć na liście za pomocą polecenia:
docker images
Dane wyjściowe powyższego polecenia zawierają nasz obraz usersignupwraz z obrazem bazowym, adoptopenjdkokreślone w naszym pliku Docker.
REPOSITORY TAG SIZE
usersignup v1 249MB
adoptopenjdk 11-jre-hotspot 229MB
Wyświetl warstwy wewnątrz obrazu kontenera
Przyjrzyjmy się stosowi warstw wewnątrz obrazu. Użyjemy инструмент nurkować aby wyświetlić te warstwy:
dive usersignup:v1
Oto część wyników polecenia Dive:
Jak widać, warstwa aplikacji stanowi znaczną część rozmiaru obrazu. W ramach naszej optymalizacji chcemy zmniejszyć rozmiar tej warstwy w kolejnych sekcjach.
Tworzenie obrazu kontenera za pomocą Buildpack
Pakiety montażowe (Pakiety konstrukcyjne) to ogólny termin używany w różnych ofertach Platform as a Service (PAAS) do tworzenia obrazu kontenera z kodu źródłowego. Został uruchomiony przez Heroku w 2011 roku i od tego czasu został przyjęty przez Cloud Foundry, Google App Engine, Gitlab, Knative i kilka innych.
Zaleta pakietów do budowania w chmurze
Jedną z głównych zalet używania Buildpack do tworzenia obrazów jest to Zmianami konfiguracji obrazu można zarządzać centralnie (konstruktor) i propagować je do wszystkich aplikacji za pomocą kreatora.
Pakiety kompilacji były ściśle powiązane z platformą. Pakiety kompilacji natywne dla chmury zapewniają standaryzację na różnych platformach, obsługując format obrazu OCI, co gwarantuje, że obraz może być uruchamiany przez silnik Docker.
Korzystanie z wtyczki Spring Boot
Wtyczka Spring Boot buduje obrazy OCI ze źródła za pomocą Buildpack. Obrazy tworzone są za pomocą bootBuildImagezadania (Gradle) lub spring-boot:build-imagetargets (Maven) i lokalna instalacja Dockera.
Możemy dostosować nazwę obrazu potrzebnego do przekazania do rejestru Dockera, podając nazwę w image tag:
Z danych wyjściowych to widzimy paketo Cloud-Native buildpackużywany do tworzenia działającego obrazu OCI. Tak jak poprzednio, możemy zobaczyć obraz wymieniony jako obraz Dockera, uruchamiając polecenie:
Następnie uruchamiamy wtyczkę Jib za pomocą polecenia Maven w celu zbudowania aplikacji i utworzenia obrazu kontenera. Tak jak poprzednio, nie używamy tutaj żadnych plików Dockera:
Po wykonaniu powyższego polecenia Maven otrzymujemy następujące dane wyjściowe:
[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
Dane wyjściowe pokazują, że obraz kontenera został utworzony i umieszczony w rejestrze.
Motywacje i techniki tworzenia zoptymalizowanych obrazów
Mamy dwa główne powody optymalizacji:
produktywność: W systemie orkiestracji kontenerów obraz kontenera jest pobierany z rejestru obrazów do hosta, na którym działa silnik kontenera. Proces ten nazywa się planowaniem. Wyciąganie dużych obrazów z rejestru skutkuje długimi czasami planowania w systemach orkiestracji kontenerów i długimi czasami kompilacji w potokach CI.
bezpieczeństwo: Większe obrazy mają również większy obszar dla luk w zabezpieczeniach.
Obraz Dockera składa się ze stosu warstw, z których każda reprezentuje instrukcję w naszym pliku Dockerfile. Każda warstwa reprezentuje deltę zmian w warstwie bazowej. Kiedy pobieramy obraz Dockera z rejestru, jest on pobierany warstwami i buforowany na hoście.
Używa Spring Boot „gruby słoik” w jako domyślny format opakowania. Kiedy patrzymy na gruby plik JAR, widzimy, że aplikacja stanowi bardzo małą część całego pliku JAR. To część, która zmienia się najczęściej. Pozostała część składa się z zależności Spring Framework.
Formuła optymalizacyjna koncentruje się na izolowaniu aplikacji na innym poziomie od zależności Spring Framework.
Warstwa zależności, która stanowi większość grubego pliku JAR, jest pobierana tylko raz i buforowana w systemie hosta.
Podczas aktualizacji aplikacji i planowania kontenerów pobierana jest tylko cienka warstwa aplikacji. jak pokazano na tym schemacie:
W poniższych sekcjach przyjrzymy się, jak utworzyć zoptymalizowane obrazy dla aplikacji Spring Boot.
Tworzenie zoptymalizowanego obrazu kontenera dla aplikacji Spring Boot przy użyciu pakietu Buildpack
Spring Boot 2.3 obsługuje nakładanie warstw, wyodrębniając części grubego pliku JAR na osobne warstwy. Funkcja warstw jest domyślnie wyłączona i należy ją jawnie włączyć za pomocą wtyczki Spring Boot Maven:
W poniższych sekcjach użyjemy tej konfiguracji do zbudowania obrazu kontenera najpierw za pomocą pakietu Buildpack, a następnie platformy Docker.
Uruchommy build-imageCel Mavena do tworzenia obrazu kontenera:
mvn spring-boot:build-image
Jeśli uruchomimy Dive, aby zobaczyć warstwy na powstałym obrazie, zobaczymy, że warstwa aplikacji (zaznaczona na czerwono) jest znacznie mniejsza w zakresie kilobajtów w porównaniu do tego, co otrzymaliśmy przy użyciu grubego formatu JAR:
Tworzenie zoptymalizowanego obrazu kontenera dla aplikacji Spring Boot przy użyciu platformy Docker
Zamiast używać wtyczki Maven lub Gradle, możemy również utworzyć warstwowy obraz JAR Dockera z plikiem Docker.
Kiedy używamy Dockera, musimy wykonać dwa dodatkowe kroki, aby wyodrębnić warstwy i skopiować je do końcowego obrazu.
Zawartość wynikowego pliku JAR po zbudowaniu przy użyciu Mavena z włączonym warstwowaniem będzie wyglądać następująco:
Dane wyjściowe pokazują dodatkowy plik JAR o nazwie spring-boot-jarmode-layertoolsи layersfle.idxplik. Ten dodatkowy plik JAR zapewnia możliwości przetwarzania warstwowego, jak opisano w następnej sekcji.
Wyodrębnianie zależności na poszczególnych warstwach
Aby wyświetlić i wyodrębnić warstwy z naszego warstwowego pliku JAR, używamy właściwości systemowej -Djarmode=layertoolsNa początek spring-boot-jarmode-layertoolsJAR zamiast aplikacji:
Uruchomienie tego polecenia generuje dane wyjściowe zawierające dostępne opcje polecenia:
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
Dane wyjściowe pokazują polecenia list, extractи helpс helpbyć domyślnym. Uruchommy polecenie za pomocą listopcja:
java -Djarmode=layertools -jar target/usersignup-0.0.1-SNAPSHOT.jar list
Widzimy listę zależności, które można dodać jako warstwy.
Domyślne warstwy:
Nazwa warstwy
Zawartość
dependencies
dowolna zależność, której wersja nie zawiera SNAPSHOT
spring-boot-loader
Klasy modułu ładującego JAR
snapshot-dependencies
dowolna zależność, której wersja zawiera SNAPSHOT
application
klasy aplikacji i zasoby
Warstwy są zdefiniowane w layers.idxw kolejności, w jakiej powinny być dodawane do obrazu Dockera. Warstwy te są buforowane na hoście po pierwszym pobraniu, ponieważ się nie zmieniają. Na host pobierana jest tylko zaktualizowana warstwa aplikacji, która jest szybsza ze względu na zmniejszony rozmiar .
Budowanie obrazu z zależnościami wyodrębnionymi w osobnych warstwach
Ostateczny obraz zbudujemy w dwóch etapach, stosując metodę tzw montaż wieloetapowy . W pierwszym kroku wyodrębnimy zależności, a w drugim skopiujemy wyodrębnione zależności do końcowego obrazu.
Zmodyfikujmy nasz plik Dockerfile, aby uzyskać wieloetapową kompilację:
# 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"]
Zapisujemy tę konfigurację w osobnym pliku - Dockerfile2.
Budujemy obraz Dockera za pomocą polecenia:
docker build -f Dockerfile2 -t usersignup:v1 .
Po uruchomieniu tego polecenia otrzymamy następujące dane wyjściowe:
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
Widzimy, że obraz Dockera jest tworzony z identyfikatorem obrazu, a następnie tagowany.
Na koniec, jak poprzednio, uruchamiamy polecenie Dive, aby sprawdzić warstwy wewnątrz wygenerowanego obrazu Dockera. Możemy podać identyfikator obrazu lub znacznik jako dane wejściowe polecenia Dive:
dive userssignup:v1
Jak widać na wynikach, warstwa zawierająca aplikację ma teraz tylko 11 KB, a zależności są buforowane w oddzielnych warstwach.
Wyodrębnianie zależności wewnętrznych na poszczególnych warstwach
Możemy jeszcze bardziej zmniejszyć rozmiar warstwy aplikacji, wyodrębniając dowolne z naszych niestandardowych zależności do osobnej warstwy, zamiast pakować je w aplikację, deklarując je w ymlpodobny plik o nazwie layers.idx:
W tym pliku layers.idxdodaliśmy niestandardową zależność o nazwie, io.myorgzawierające zależności organizacyjne pobrane ze wspólnego repozytorium.
Wniosek
W tym artykule przyjrzeliśmy się używaniu pakietów kompilacji natywnych dla chmury do tworzenia obrazu kontenera bezpośrednio z kodu źródłowego. Jest to alternatywa dla używania Dockera do tworzenia obrazu kontenera w zwykły sposób: najpierw utwórz gruby wykonywalny plik JAR, a następnie spakuj go do obrazu kontenera, określając instrukcje w pliku Dockera.
Przyjrzeliśmy się także optymalizacji naszego kontenera, włączając funkcję warstw, która ściąga zależności do oddzielnych warstw buforowanych na hoście, a cienka warstwa aplikacji jest ładowana w czasie planowania w silnikach wykonawczych kontenera.
Cały kod źródłowy użyty w artykule można znaleźć pod adresem Github .
Odniesienie do poleceń
Oto krótki przegląd poleceń, których użyliśmy w tym artykule.
Czyszczenie kontekstu:
docker system prune -a
Tworzenie obrazu kontenera przy użyciu pliku Docker:
docker build -f <Docker file name> -t <tag> .
Budujemy obraz kontenera z kodu źródłowego (bez Dockerfile):
mvn spring-boot:build-image
Wyświetl warstwy zależności. Przed zbudowaniem pliku JAR aplikacji upewnij się, że funkcja warstw jest włączona we wtyczce Spring-boot-maven:
java -Djarmode=layertools -jar application.jar list
Wyodrębnianie warstw zależności. Przed zbudowaniem pliku JAR aplikacji upewnij się, że funkcja warstw jest włączona we wtyczce Spring-boot-maven: