Wiosenna reklama natywna w Google Cloud

1. Omówienie

W tym ćwiczeniu dowiesz się więcej o projekcie Spring Native, zbudujesz aplikację, która go używa, i wdrożysz ją w Google Cloud.

Omówimy jego komponenty, historię projektu, kilka przypadków użycia i oczywiście czynności, które należy wykonać, aby używać go w swoich projektach.

Projekt Spring Native jest obecnie w fazie eksperymentalnej, więc aby rozpocząć pracę, będzie wymagać określonej konfiguracji. Jednak zgodnie z zapowiedzią na konferencji SpringOne 2021, Spring Native ma zostać zintegrowany z Spring Framework 6.0 i Spring Boot 3.0 z najlepszym wsparciem, więc jest to idealny moment, aby przyjrzeć się bliżej temu projektowi na kilka miesięcy przed jego wydaniem.

Kompilacja w czasie wykonywania została bardzo dobrze zoptymalizowana pod kątem takich procesów jak długotrwałe wykonywanie, ale w pewnych przypadkach aplikacje skompilowane z wyprzedzeniem działają jeszcze lepiej. Omówimy to w ramach warsztatu.

Dowiesz się, jak

  • Korzystanie z Cloud Shell
  • Włączanie interfejsu Cloud Run API
  • Tworzenie i wdrażanie aplikacji natywnej Spring
  • Wdrażanie takiej aplikacji w Cloud Run

Czego potrzebujesz

Ankieta

Jak będziesz korzystać z tego samouczka?

Tylko przeczytać Przeczytać i wykonać ćwiczenia

Jak oceniasz swoje wrażenia z korzystania z języka Java?

Początkujący Średnio zaawansowany Zaawansowany

Jak oceniasz korzystanie z usług Google Cloud?

Początkujący Średnio zaawansowany Zaawansowany

2. Tło

Projekt Spring Native korzysta z kilku technologii, aby zapewnić deweloperom wydajność natywnych aplikacji.

Aby w pełni zrozumieć Spring Native, warto poznać kilka technologii komponentów, ich możliwości i sposób współpracy.

Kompilacja AOT

Gdy deweloperzy uruchamiają javac normalnie w czasie kompilacji, nasz kod źródłowy w formacie .java jest kompilowany do plików .class, które są napisane w bajtowym kodzie. Ten kod bajtowy jest zrozumiały tylko dla maszyny wirtualnej Java, więc aby można było go uruchomić, JVM musi go interpretować na innych maszynach.

To właśnie zapewnia przenośność podpisu Java, która pozwala nam „pisać raz i uruchamiać wszędzie”. Jest to jednak kosztowne w porównaniu z uruchamianiem kodu natywnych.

Na szczęście większość implementacji JVM korzysta z kompilacji just-in-time, aby ograniczyć koszty interpretacji. Jest to możliwe dzięki zliczaniu wywołań funkcji. Jeśli jest ona wywoływana wystarczająco często, aby przekroczyć określony próg ( domyślnie 10 tys. razów), jest kompilowana na kod natywny w czasie wykonywania, aby zapobiec dalszej kosztownej interpretacji.

Kompilacja z wyprzedzeniem działa na odwrót: kompiluje cały dostępny kod do natywnego pliku wykonywalnego w momencie kompilacji. W zamian za przenośność uzyskuje się wydajność pamięci i inne korzyści w zakresie wydajności w czasie wykonywania.

5042e8e62a05a27.png

To oczywiście kompromis, który nie zawsze jest opłacalny. Kompilacja AOT może jednak być przydatna w pewnych przypadkach, np.:

  • aplikacje o krótkim czasie działania, w których czas uruchamiania jest ważny;
  • Środowiska o bardzo ograniczonej ilości pamięci, w których kompilacja Just-In-Time może być zbyt kosztowna

Na marginesie: kompilacja AOT została wprowadzona jako funkcja eksperymentalna w JDK 9, ale jej utrzymanie było kosztowne i nigdy nie zyskała popularności, więc została po cichu usunięta w Java 17 na rzecz GraalVM.

GraalVM

GraalVM to zoptymalizowana dystrybucja JDK na licencji open source, która charakteryzuje się bardzo szybkim czasem uruchamiania, kompilacją natywnych obrazów AOT i możliwościami wielojęzycznymi, które umożliwiają deweloperom łączenie wielu języków w jednej aplikacji.

GraalVM jest w trakcie aktywnego rozwoju, stale zyskując nowe możliwości i ulepszając te istniejące, więc zachęcam deweloperów do śledzenia postępów.

Oto kilka ostatnich kamieni milowych:

  • Nowy, przyjazny dla użytkownika format wyjściowy natywnych obrazów ( 18.01.2021)
  • Obsługa Java 17 ( 18.01.2022)
  • Włączanie kompilacji wielopoziomowej domyślnie w celu skrócenia czasu kompilacji wielojęzycznej ( 2021-04-20)

Spring Native

Krótko mówiąc, Spring Native umożliwia korzystanie z kompilatora natywnych obrazów GraalVM do przekształcania aplikacji Spring w natywne pliki wykonywalne.

Polega on na przeprowadzeniu statycznej analizy aplikacji w czasie kompilacji w celu znalezienia wszystkich metod w aplikacji, które są dostępne z punktu wejścia.

W podstawie tworzy to koncepcję „zamkniętego świata” aplikacji, w której zakłada się, że cały kod jest znany w czasie kompilacji, a w czasie wykonywania nie można wczytywać nowego kodu.

Pamiętaj, że generowanie natywnych obrazów to proces wymagający dużej ilości pamięci, który trwa dłużej niż kompilacja zwykłej aplikacji. Wymusza on też ograniczenia w niektórych aspektach Javy.

W niektórych przypadkach aplikacja nie wymaga żadnych zmian kodu, aby działać z Spring Native. W niektórych sytuacjach prawidłowe działanie wymaga jednak odpowiedniej konfiguracji natywnej. W takich sytuacjach Spring Native często udostępnia wskazówki dotyczące aplikacji natywnych, aby uprościć ten proces.

3. Konfiguracja/wstępne przygotowanie

Zanim zaczniemy wdrażać Spring Native, musimy utworzyć i wdrażać aplikację, aby ustalić bazowy poziom wydajności, który później można porównać z wersją natywnych.

1. Tworzenie projektu

Na początek pobierz aplikację ze strony start.spring.io:

curl https://start.spring.io/starter.zip -d dependencies=web \
           -d javaVersion=11 \
           -d bootVersion=2.6.4 -o io-native-starter.zip

Ta aplikacja startowa korzysta z Spring Boot 2.6.4, czyli najnowszej wersji, którą obsługuje projekt spring-native w momencie pisania tego tekstu.

Pamiętaj, że od wersji GraalVM 21.0.3 w tym przykładzie możesz też używać wersji Java 17. W tym samouczku nadal użyjemy Java 11, aby zminimalizować wymaganą konfigurację.

Gdy plik ZIP znajdzie się w wierszu poleceń, możemy utworzyć podfolder dla projektu i rozpakować w nim folder:

mkdir spring-native

cd spring-native

unzip ../io-native-starter.zip

2. Zmiany kodu

Gdy otworzymy projekt, szybko dodamy do niego sygnał życia i po jego uruchomieniu pokażemy skuteczność kampanii Spring Native.

Zmień plik DemoApplication.java tak, aby odpowiadał temu:

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.Duration;
import java.time.Instant;

@RestController
@SpringBootApplication
public class DemoApplication {
    private static Instant startTime;
    private static Instant readyTime;

    public static void main(String[] args) {
        startTime = Instant.now();
                SpringApplication.run(DemoApplication.class, args);
    }

    @GetMapping("/")
    public String index() {
        return "Time between start and ApplicationReadyEvent: "
                + Duration.between(startTime, readyTime).toMillis()
                + "ms";
    }

    @EventListener(ApplicationReadyEvent.class)
    public void ready() {
                readyTime = Instant.now();
    }
}

Na tym etapie nasza podstawowa aplikacja jest gotowa do użycia, więc możesz utworzyć obraz i uruchomić go lokalnie, aby uzyskać ogólne pojęcie o czasie uruchamiania, zanim przekształcimy ją w natywną aplikację.

Aby utworzyć obraz:

mvn spring-boot:build-image

Aby dowiedzieć się, jaki powinien być rozmiar obrazu bazowego, możesz też użyć docker images demo: 6ecb403e9af1475e.png

Aby uruchomić aplikację:

docker run --rm -p 8080:8080 demo:0.0.1-SNAPSHOT

3. Wdrażanie aplikacji podstawowej

Teraz, gdy mamy już aplikację, wdrożymy ją i zanotujemy czasy, które później porównamy z czasem uruchamiania natywnej aplikacji.

W zależności od typu tworzonej aplikacji możesz skorzystać z różnych usług hostowania.

Ponieważ jednak nasz przykład to bardzo prosta aplikacja internetowa, możemy zachować prostotę i polecić Cloud Run.

Jeśli wykonujesz te czynności na swoim komputerze, upewnij się, że masz zainstalowane i zaktualizowane narzędzie gcloud CLI.

Jeśli korzystasz z Cloud Shell, nie musisz się o to martwić. Wystarczy, że w katalogu źródłowym uruchomisz to polecenie:

gcloud run deploy

4. Konfiguracja aplikacji

1. Konfigurowanie repozytoriów Maven

Ten projekt jest jeszcze w fazie eksperymentalnej, więc musimy skonfigurować naszą aplikację, aby mogła znajdować eksperymentalne artefakty, które nie są dostępne w centralnym repozytorium Maven.

W tym celu dodaj te elementy do pliku pom.xml w dowolnym edytorze.

Dodaj do pliku pom te sekcje repozytoriów i pluginRepositories:

<repositories>
    <repository>
        <id>spring-release</id>
        <name>Spring release</name>
        <url>https://repo.spring.io/release</url>
    </repository>
</repositories>

<pluginRepositories>
    <pluginRepository>
        <id>spring-release</id>
        <name>Spring release</name>
        <url>https://repo.spring.io/release</url>
    </pluginRepository>
</pluginRepositories>

2. Dodawanie zależności

Następnie dodaj zależność spring-native, która jest wymagana do uruchamiania aplikacji Spring jako obrazu natywnego. Uwaga: jeśli używasz Gradle, ten krok nie jest konieczny

<dependencies>
    <!-- ... -->
    <dependency>
        <groupId>org.springframework.experimental</groupId>
        <artifactId>spring-native</artifactId>
        <version>0.11.2</version>
    </dependency>
</dependencies>

3. Dodawanie i włączanie naszych wtyczek

Teraz dodaj wtyczkę AOT, aby poprawić zgodność i ślad natywnych obrazów ( więcej informacji):

<plugins>
    <!-- ... -->
    <plugin>
        <groupId>org.springframework.experimental</groupId>
        <artifactId>spring-aot-maven-plugin</artifactId>
        <version>0.11.2</version>
        <executions>
            <execution>
                <id>generate</id>
                <goals>
                    <goal>generate</goal>
                </goals>
            </execution>
        </executions>
    </plugin>
</plugins>

Teraz zaktualizujemy wtyczkę spring-boot-maven, aby umożliwić obsługę obrazu natywnego, i użyjemy pakietu kompilacyjnego paketo do utworzenia obrazu natywnego:

<plugins>
    <!-- ... -->
    <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
            <image>
                <builder>paketobuildpacks/builder:tiny</builder>
                <env>
                    <BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE>
                </env>
            </image>
        </configuration>
    </plugin>    
</plugins>

Pamiętaj, że obraz tiny builder to tylko jedna z wielu opcji. Jest to dobry wybór w naszym przypadku, ponieważ zawiera bardzo niewiele dodatkowych bibliotek i narzędzi, co pomaga zminimalizować powierzchnię ataku.

Jeśli na przykład tworzysz aplikację, która potrzebuje dostępu do niektórych popularnych bibliotek C, lub nie masz jeszcze pewności co do wymagań aplikacji, full-builder może być lepszym rozwiązaniem.

5. Kompilowanie i uruchamianie aplikacji natywnej

Gdy wszystko będzie gotowe, powinniśmy móc skompilować obraz i uruchomić natywny, skompilowany program.

Zanim uruchomisz wersję, pamiętaj o tych kwestiach:

  • Może to zająć więcej czasu niż zwykła kompilacja (kilka minut) d420322893640701.png
  • Proces kompilacji może zużyć dużo pamięci (kilka gigabajtów) cda24e1eb11fdbea.png
  • Ten proces kompilacji wymaga dostępu do demona Dockera.
  • W tym przykładzie proces jest przeprowadzany ręcznie, ale możesz też skonfigurować fazy kompilacji, aby profil kompilacji natywnej był uruchamiany automatycznie.

Aby utworzyć obraz:

mvn spring-boot:build-image

Gdy to zrobisz, możesz zobaczyć naszą natywną aplikację w działaniu.

Aby uruchomić aplikację:

docker run --rm -p 8080:8080 demo:0.0.1-SNAPSHOT

W tej chwili mamy doskonały wgląd w obie strony równania dotyczącego natywnych aplikacji.

Zrezygnowaliśmy z trochę czasu i dodatkowego wykorzystania pamięci podczas kompilacji, ale w zamian otrzymujemy aplikację, która uruchamia się znacznie szybciej i korzysta z znacznie mniejszej ilości pamięci (w zależności od obciążenia pracą).

Jeśli uruchomimy docker images demo, aby porównać rozmiar natywnego obrazu z oryginałem, zauważymy znaczne zmniejszenie:

e667f65a011c1328.png

Należy też pamiętać, że w bardziej złożonych przypadkach użycia konieczne są dodatkowe modyfikacje, aby poinformować kompilator AOT o tym, co aplikacja będzie robić w czasie wykonywania. Z tego powodu niektóre przewidywalne obciążenia (np. zadania zbiorcze) mogą być bardzo odpowiednie do tego, podczas gdy inne mogą wymagać większego wysiłku.

6. Wdrażanie aplikacji natywnej

Aby wdrożyć aplikację w Cloud Run, musimy przesłać nasz natywny obraz do menedżera pakietów, takiego jak Artifact Registry.

1. Przygotowywanie repozytorium Dockera

Proces ten rozpoczynamy od utworzenia repozytorium:

gcloud artifacts repositories create native-image-docker-repo --repository-format=docker \
--location=us-central1 --description="Repository for our native images"

Następnie musimy się upewnić, że mamy uprawnienia do przesyłania do naszego nowego rejestru.

Narzędzie gcloud CLI może nieco uprościć ten proces:

gcloud auth configure-docker us-central1-docker.pkg.dev

2. Przesyłanie obrazu do Artifact Registry

Teraz oznaczymy obraz tagiem:

export PROJECT_ID=$(gcloud config list --format 'value(core.project)')


docker tag  demo:0.0.1-SNAPSHOT \
us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2

Następnie możemy użyć docker push, aby wysłać go do Artifact Registry:

docker push us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2

3. Wdrażanie w Cloud Run

Możemy teraz wdrożyć obraz przechowywany w Artifact Registry w Cloud Run:

gcloud run deploy --image us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2

Ponieważ nasza aplikacja została skompilowana i wdrożona jako obraz natywny, możemy mieć pewność, że podczas działania korzysta z naszych kosztów infrastruktury w najlepszy sposób.

Porównaj czas uruchamiania naszej podstawowej aplikacji z tą nową natywnych.

6dde63d35959b1bb.png

7. Podsumowanie/czyszczenie

Gratulujemy utworzenia i wdrożenia aplikacji Spring Native w Google Cloud.

Mamy nadzieję, że ten samouczek zachęci Cię do zapoznania się z projektem Spring Native i pamiętania o nim, jeśli w przyszłości będzie on spełniać Twoje potrzeby.

Opcjonalnie: czyszczenie lub wyłączenie usługi

Niezależnie od tego, czy utworzysz projekt Google Cloud na potrzeby tego samouczka, czy użyjesz istniejącego, pamiętaj, aby uniknąć zbędnych opłat za zasoby, których użyliśmy.

Możesz usunąć lub wyłączyć utworzone przez nas usługi Cloud Run, usunąć obraz, który hostujemy, lub wyłączyć cały projekt.

8. Dodatkowe materiały

Chociaż projekt Spring Native jest obecnie nowy i eksperymentalny, istnieje już wiele przydatnych materiałów, które pomogą użytkownikom wczesnej wersji rozwiązywać problemy i uczestniczyć w programie:

Dodatkowe materiały

Poniżej znajdziesz zasoby online, które mogą być przydatne w tym samouczku:

Licencja

To zadanie jest licencjonowane na podstawie ogólnej licencji Creative Commons Attribution 2.0.