Wiosenna reklama natywna w Google Cloud

1. Przegląd

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

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

Projekt Spring Native jest obecnie w fazie eksperymentu, więc aby zacząć z niego korzystać, musisz go odpowiednio skonfigurować. Jednak zgodnie z zapowiedzią z konferencji SpringOne 2021 Spring Native zostanie zintegrowany z platformą Spring Framework 6.0 i Spring Boot 3.0 z pełną obsługą, więc to idealny moment, aby przyjrzeć się bliżej temu projektowi na kilka miesięcy przed jego wydaniem.

Kompilacja JIT została bardzo dobrze zoptymalizowana pod kątem długotrwałych procesów, ale w niektórych przypadkach aplikacje skompilowane z wyprzedzeniem działają jeszcze lepiej. Omówimy to podczas warsztatów.

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 zamierzasz korzystać z tego samouczka?

Tylko przeczytaj Przeczytaj i wykonaj ćwiczenia

Jak oceniasz swoje doświadczenie z językiem 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 wykorzystuje kilka technologii, aby zapewnić deweloperom wydajność aplikacji natywnych.

Aby w pełni zrozumieć Spring Native, warto poznać kilka technologii składowych, ich możliwości i wzajemne zależności.

Kompilacja AOT

Gdy programiści uruchamiają javac w normalny sposób w czasie kompilacji, nasz kod źródłowy .java jest kompilowany do plików .class zapisanych w kodzie bajtowym. Ten kod bajtowy jest przeznaczony tylko dla maszyny wirtualnej Java, więc JVM będzie musiała zinterpretować ten kod na innych maszynach, abyśmy mogli uruchomić nasz kod.

Ten proces zapewnia Javie charakterystyczną przenośność, która pozwala „napisać kod raz i uruchomić go wszędzie”, ale jest kosztowny w porównaniu z uruchamianiem kodu natywnego.

Na szczęście większość implementacji JVM korzysta z kompilacji w odpowiednim czasie, aby zmniejszyć koszt interpretacji. Jest to możliwe dzięki zliczaniu wywołań funkcji. Jeśli funkcja jest wywoływana wystarczająco często, aby przekroczyć próg ( domyślnie 10 tysięcy), jest kompilowana do kodu natywnego w czasie działania, aby zapobiec dalszej kosztownej interpretacji.

Kompilacja z wyprzedzeniem działa w odwrotny sposób – kompiluje cały osiągalny kod do natywnego pliku wykonywalnego w czasie kompilowania. W ten sposób przenośność zostaje zastąpiona wydajnością pamięci i innymi korzyściami w zakresie wydajności w czasie działania.

5042e8e62a05a27.png

Oczywiście jest to kompromis, który nie zawsze jest opłacalny. Kompilacja AOT może jednak być przydatna w niektórych przypadkach, takich jak:

  • 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 JIT może być zbyt kosztowna;

Ciekawostka: kompilacja AOT została wprowadzona jako funkcja eksperymentalna w JDK 9, ale ta implementacja była kosztowna w utrzymaniu i nigdy nie zyskała popularności, więc w Javie 17 została cicho usunięta na rzecz GraalVM, z którego korzystają deweloperzy.

GraalVM

GraalVM to wysoce zoptymalizowana dystrybucja JDK o otwartym kodzie źródłowym, która charakteryzuje się bardzo szybkim czasem uruchamiania, kompilacją obrazów natywnych AOT i możliwościami wielojęzykowymi, które pozwalają programistom łączyć wiele języków w jednej aplikacji.

GraalVM jest aktywnie rozwijany, a jego możliwości są stale rozszerzane i ulepszane, dlatego zachęcam deweloperów do śledzenia nowości.

Oto kilka ostatnich osiągnięć:

  • Nowe, przyjazne dla użytkownika dane wyjściowe dotyczące tworzenia obrazów natywnych ( 18.01.2021)
  • Obsługa Javy 17 ( 18 stycznia 2022 r.)
  • Domyślne włączenie kompilacji wielopoziomowej w celu skrócenia czasu kompilacji w przypadku wielu języków ( 2021-04-20)

Spring Native

Mówiąc wprost, Spring Native umożliwia użycie kompilatora obrazów natywnych GraalVM do przekształcania aplikacji Spring w natywne pliki wykonywalne.

Proces ten obejmuje przeprowadzenie analizy statycznej aplikacji w czasie kompilacji w celu znalezienia wszystkich metod w aplikacji, które są dostępne z punktu wejścia.

W ten sposób tworzysz koncepcję „zamkniętego świata” aplikacji, w której zakłada się, że cały kod jest znany w czasie kompilowania i nie można wczytywać nowego kodu w czasie działania programu.

Warto pamiętać, że generowanie obrazu natywnego to proces wymagający dużej ilości pamięci i trwający dłużej niż kompilowanie zwykłej aplikacji. Nakłada on ograniczenia na niektóre aspekty Javy.

W niektórych przypadkach aplikacja nie wymaga żadnych zmian w kodzie, aby współpracować z Spring Native. W niektórych sytuacjach jednak do prawidłowego działania wymagana jest określona konfiguracja natywna. W takich sytuacjach Spring Native często udostępnia wskazówki dotyczące kompilacji natywnej, aby uprościć ten proces.

3. Konfiguracja/przygotowanie

Zanim zaczniemy wdrażać Spring Native, musimy utworzyć i wdrożyć aplikację, aby ustalić podstawowe dane o wydajności, które później będziemy mogli porównać z wersją natywną.

1. Tworzenie projektu

Zaczniemy od pobrania aplikacji 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 w wersji 2.6.4, która w momencie pisania tego artykułu jest najnowszą wersją obsługiwaną przez projekt spring-native.

Pamiętaj, że od czasu wydania GraalVM 21.0.3 możesz też używać w tym przykładzie Javy 17. W tym samouczku nadal będziemy używać Javy 11, aby zminimalizować liczbę konfiguracji.

Gdy mamy już plik ZIP w wierszu poleceń, możemy utworzyć podkatalog dla naszego projektu i rozpakować w nim folder:

mkdir spring-native

cd spring-native

unzip ../io-native-starter.zip

2. Zmiany w kodzie

Po otwarciu projektu szybko dodamy sygnał życia i zaprezentujemy wydajność Spring Native po jego uruchomieniu.

Zmodyfikuj plik DemoApplication.java, aby wyglądał tak:

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 działania, więc możesz skompilować obraz i uruchomić go lokalnie, aby sprawdzić czas uruchamiania, zanim przekształcimy go w aplikację natywną.

Aby budować nasz wizerunek:

mvn spring-boot:build-image

Możesz też użyć docker images demo, aby sprawdzić rozmiar obrazu podstawowego: 6ecb403e9af1475e.png

Aby uruchomić aplikację:

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

3. Wdrażanie aplikacji podstawowej

Teraz wdrożymy aplikację i zanotujemy czasy, które później porównamy z czasami uruchamiania naszej aplikacji natywnej.

W zależności od typu tworzonej aplikacji możesz hostować swoje treści na kilka różnych sposobów.

Ponieważ jednak nasz przykład to bardzo prosta aplikacja internetowa, możemy uprościć sprawę i skorzystać z Cloud Run.

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

Jeśli korzystasz z Cloud Shell, wszystko zostanie załatwione i możesz po prostu uruchomić to polecenie w katalogu źródłowym:

gcloud run deploy

4. Konfiguracja aplikacji

1. Konfigurowanie repozytoriów Maven

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

W tym celu dodaj do pliku pom.xml te elementy. Możesz to zrobić 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 uruchomienia aplikacji Spring jako obrazu natywnego. Uwaga: ten krok nie jest konieczny, jeśli używasz Gradle.

<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 zwiększyć zgodność obrazu natywnego i zmniejszyć jego rozmiar ( 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-plugin, aby włączyć obsługę obrazów natywnych i użyć kreatora 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 małego narzędzia do tworzenia to tylko jedna z kilku opcji. Jest to dobra opcja w naszym przypadku, ponieważ zawiera bardzo mało dodatkowych bibliotek i narzędzi, co pomaga zminimalizować obszar narażony na atak.

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, lepszym rozwiązaniem może być pełny kompilator.

5. Tworzenie i uruchamianie aplikacji natywnej

Gdy wszystko będzie gotowe, powinniśmy móc skompilować obraz i uruchomić natywną, skompilowaną aplikację.

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

  • Zajmie to więcej czasu niż zwykłe kompilowanie (kilka minut). d420322893640701.png
  • Proces kompilacji może zużywać dużo pamięci (kilka gigabajtów).cda24e1eb11fdbea.png
  • Ten proces kompilacji wymaga, aby demon Dockera był dostępny.
  • W tym przykładzie proces przeprowadzamy ręcznie, ale możesz też skonfigurować fazy kompilacji tak, aby automatycznie wywoływały profil kompilacji natywnej.

Aby budować nasz wizerunek:

mvn spring-boot:build-image

Gdy to zrobisz, możesz zobaczyć, jak działa aplikacja natywna.

Aby uruchomić aplikację:

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

W tym momencie możemy już spojrzeć na obie strony równania aplikacji natywnej.

Poświęciliśmy trochę czasu i dodatkowego wykorzystania pamięci w czasie kompilowania, ale w zamian otrzymaliśmy aplikację, która może uruchamiać się znacznie szybciej i zużywać znacznie mniej pamięci (w zależności od obciążenia).

Jeśli wykonamy działanie docker images demo, aby porównać rozmiar obrazu natywnego z oryginalnym, zauważymy znaczną redukcję:

e667f65a011c1328.png

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 działania. Z tego powodu niektóre przewidywalne zadania (np. zadania wsadowe) mogą być do tego bardzo dobrze dostosowane, a inne mogą wymagać większego nakładu pracy.

6. Wdrażanie aplikacji natywnej

Aby wdrożyć aplikację w Cloud Run, musimy umieścić obraz natywny w menedżerze pakietów, takim jak Artifact Registry.

1. Przygotowywanie repozytorium Dockera

Możemy rozpocząć ten proces 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 uwierzytelnienie do przesyłania danych do nowego rejestru.

Interfejs gcloud CLI może znacznie uprościć ten proces:

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

2. Przesyłanie obrazu do Artifact Registry

Następnie dodamy tag do obrazu:

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ć w Cloud Run obraz, który został zapisany w Artifact Registry:

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

Ponieważ nasza aplikacja została utworzona i wdrożona jako obraz natywny, możemy mieć pewność, że podczas działania optymalnie wykorzystuje koszty infrastruktury.

Możesz samodzielnie porównać czas uruchamiania naszej podstawowej aplikacji z czasem uruchamiania nowej aplikacji natywnej.

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 bliższego zapoznania się z projektem Spring Native i do rozważenia jego wykorzystania w przyszłości, jeśli będzie to odpowiadać Twoim potrzebom.

Opcjonalnie: zwalnianie miejsca i/lub wyłączanie usługi

Niezależnie od tego, czy projekt Google Cloud został utworzony na potrzeby tego ćwiczenia w Codelabs, czy używasz istniejącego projektu, unikaj niepotrzebnych opłat za zasoby, z których korzystaliśmy.

Możesz usunąć lub wyłączyć utworzone przez nas usługi Cloud Run, usunąć hostowany przez nas obraz lub zamknąć cały projekt.

8. Dodatkowe materiały

Projekt Spring Native jest obecnie nowy i eksperymentalny, ale istnieje już wiele przydatnych materiałów, które pomogą użytkownikom wczesnej wersji rozwiązywać problemy i zaangażować się w rozwój projektu:

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.