Wiosenna reklama natywna w Google Cloud

1. Omówienie

W ramach tych ćwiczeń w programie zapoznamy się z projektem Spring Native, tworzeniem aplikacji, która z niej korzysta, i wdrażaniem jej w Google Cloud.

Omówimy jego składniki, najnowszą historię projektu, niektóre przypadki użycia oraz oczywiście czynności, które trzeba wykonać, aby wykorzystać go w swoich projektach.

Projekt Spring Native jest obecnie w fazie eksperymentalnej, więc będzie wymagać określonej konfiguracji. Jak jednak ogłoszono na konferencji SpringOne 2021, komponent Spring Native zostanie zintegrowany z platformami Spring Framework 6.0 i Spring Boot 3.0 z obsługą pierwszej klasy, więc jest to doskonały moment, by przyjrzeć się temu projektowi kilka miesięcy przed jego premierą.

Kompilacja „just-in-time” jest bardzo dobrze zoptymalizowana pod kątem takich aspektów jak długotrwałe procesy, ale w pewnych przypadkach użycia skompilowanych z wyprzedzeniem aplikacji działa jeszcze lepiej. Omówimy to podczas ćwiczeń z programowania.

Dowiesz się, jak:

  • Użyj Cloud Shell
  • Włączanie Cloud Run API
  • Utwórz i wdróż aplikację natywną Spring
  • Wdrażanie takiej aplikacji w Cloud Run

Czego potrzebujesz

Ankieta

Jak wykorzystasz ten samouczek?

Tylko do przeczytania Przeczytaj go i wykonaj ćwiczenia

Jak oceniasz swoje doświadczenia z Javą?

Początkujący Poziom średnio zaawansowany Biegły

Jak oceniasz korzystanie z usług Google Cloud?

Początkujący Poziom średnio zaawansowany Biegły
.

2. Tło

Projekt Spring Native wykorzystuje kilka technologii, aby zapewnić deweloperom wydajność natywnych aplikacji.

Aby w pełni zrozumieć usługę Spring Native, warto poznać kilka z tych technologii oraz to, co nam w nich dają i jak współdziałają ze sobą.

Kompilacja AOT

Jeśli podczas kompilowania program uruchamia standard javac, kod źródłowy .java jest skompilowany do postaci plików .class zapisanych w kodzie bajtowym. Kod bajtowy jest zrozumiały tylko dla maszyny wirtualnej Java, więc JVM musi go zinterpretować na innych komputerach, abyśmy mogli uruchomić nasz kod.

Ten proces daje nam charakterystyczną przenośność Javy. Dzięki temu możemy „zapisać jednorazowo i uruchomić ją wszędzie”, ale jest to kosztowne w porównaniu z uruchamianiem kodu natywnego.

Na szczęście większość implementacji JVM korzysta z kompilacji „just-in-time”, aby zminimalizować koszt interpretacji. Jest to możliwe dzięki liczeniu wywołań funkcji i jeśli jest ona wywoływana wystarczająco często, by przekroczyć próg ( domyślnie 10 000), jest skompilowana do kodu natywnego w czasie działania, co uniemożliwia dalszą interpretację.

Kompilacja z wyprzedzeniem stosuje odwrotną metodę: w czasie kompilowania cały dostępny kod jest kompilowany do natywnego pliku wykonywalnego. Dzięki temu przenośność zapewnia wydajność pamięci i zwiększa wydajność w czasie działania.

5042e8e62a05a27.png

Jest to oczywiście kompromis i nie zawsze warto z tego korzystać. Kompilacja AOT może się jednak przydać w niektórych przypadkach, takich jak:

  • Aplikacje o krótkim czasie działania, w których czas uruchamiania ma duże znaczenie
  • Środowiska z dużym ograniczeniem pamięci, gdzie wdrożenie JIT może być zbyt kosztowne

Co ciekawe, kompilacja AOT została wprowadzona w JDK 9 jako funkcja eksperymentalna. Jednak utrzymanie tej implementacji było kosztowne i nigdy nie do końca ją opanowało. Z tego powodu w Jawie 17 została ona płynnie usunięta na rzecz deweloperów korzystających tylko z platformy GraalVM.

GraalVM

GraalVM to wysoce zoptymalizowana dystrybucja JDK typu open source, która charakteryzuje się ekstremalnie krótkim czasem uruchamiania, kompilacją obrazów natywnych AOT i funkcjami w języku poliglota, które pozwalają programistom łączyć wiele języków w jedną aplikację.

GraalVM jest w fazie rozwoju, na bieżąco zyskuje nowe możliwości i ulepsza dotychczasowe, więc zachęcam deweloperów do śledzenia aktualności.

Oto kilka z ostatnich kamieni milowych:

  • Nowy, łatwy w użyciu natywny obraz wyjściowy ( 18.01.2021)
  • Obsługa środowiska Java 17 ( 18.01.2022)
  • Domyślnie włącz kompilację wielopoziomową, aby poprawić czasy kompilacji poligllotów ( 20.04.2021)

Wiosenna reklama natywna

Mówiąc najprościej, Spring Native umożliwia wykorzystanie natywnego kompilatora obrazów GraalVM, aby zamienić aplikacje Spring w natywne pliki wykonywalne.

Ten proces obejmuje przeprowadzenie statycznej analizy aplikacji podczas kompilowania w celu znalezienia wszystkich metod w aplikacji, które są osiągalne w punkcie wejścia.

W ten sposób tworzymy „zamknięty świat” koncepcję aplikacji, w której cały kod przyjmuje się jako znany w czasie kompilacji i żadny nowy kod nie może być ładowany w czasie działania.

Warto zauważyć, że generowanie obrazów natywnych to proces wymagający dużo pamięci, który trwa dłużej niż kompilowanie zwykłej aplikacji i nakłada ograniczenia na niektóre aspekty języka Java.

W niektórych przypadkach do działania aplikacji Spring Native nie są wymagane żadne zmiany w kodzie. Jednak w niektórych sytuacjach do prawidłowego działania wymagana jest określona konfiguracja natywna. W takich sytuacjach firma Spring Native często udostępnia wskazówki natywne, by uprościć ten proces.

3. Konfiguracja/praca

Przed wdrożeniem wiosennej reklamy natywnej musimy utworzyć i wdrożyć aplikację, aby ustalić jej wydajność i ustalić poziom odniesienia, który później będziemy mogli porównać z wersją natywną.

1. Tworzę projekt

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 wykorzystuje Spring Boot 2.6.4, czyli najnowszą wersję obsługiwaną przez natywny projekt wiosenny w momencie pisania.

Od wdrożenia GraalVM w wersji 21.0.3 możesz używać środowiska Java 17 również w tym przykładzie. W tym samouczku nadal będziemy używać środowiska Java 11, aby zminimalizować potrzebną konfigurację.

Po utworzeniu pliku ZIP w wierszu poleceń możemy utworzyć podkatalog dla projektu i rozpakować znajdujący się w nim folder:

mkdir spring-native

cd spring-native

unzip ../io-native-starter.zip

2. Zmiany w kodzie

Po uruchomieniu projektu szybko dodamy oznakę życia i po jego uruchomieniu zaprezentujemy skuteczność reklamy wiosennej reklamy natywnej.

Edytuj plik DemoApplication.java tak, aby pasował do tego:

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 aplikacja bazowa jest gotowa, więc możesz utworzyć obraz i uruchomić go lokalnie, by poznać czas uruchamiania, zanim przekonwertujemy go na aplikację natywną.

Aby utworzyć nasz wizerunek:

mvn spring-boot:build-image

Możesz też użyć wartości docker images demo, by określić rozmiar obrazu podstawowego: 6ecb403e9af1475e.png

Aby uruchomić aplikację:

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

3. Wdróż aplikację podstawową

Po przygotowaniu aplikacji wdrożymy ją i zanotujemy czasy jej uruchamiania, które później porównamy z czasem uruchomienia aplikacji natywnej.

W zależności od typu aplikacji, którą tworzysz, istnieje kilka różnych opcji hostingu danych.

Ponieważ jednak nasz przykład jest bardzo prostą i funkcjonalną aplikacją internetową, możemy to uprościć i zdać się na Cloud Run.

Jeśli wykonasz te czynności we własnej maszynie, pamiętaj o zainstalowaniu i zaktualizowaniu narzędzia gcloud CLI.

Jeśli pracujesz w Cloud Shell, wykonaj te czynności, aby uruchomić w katalogu źródłowym to polecenie:

gcloud run deploy

4. Konfiguracja aplikacji

1. Konfigurowanie repozytoriów Maven

Ten projekt jest nadal w fazie eksperymentalnej, dlatego będziemy musieli skonfigurować naszą aplikację, aby mogła znajdować artefakty eksperymentalne, które nie są dostępne w centralnym repozytorium Maven.

Wiąże się to z dodaniem tych elementów do pliku pom.xml, które możesz wykonać w wybranym edytorze.

Dodaj do naszego systemu pom te sekcje repozytoriów i wtyczek:

<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ść wiosenną, która jest wymagana do uruchomienia aplikacji Spring jako obrazu natywnego. Uwaga: ten krok nie jest wymagany, 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 wtyczek

Teraz dodaj wtyczkę AOT, aby poprawić zgodność i zasięg obrazów natywnych ( 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 włączyć obsługę natywnych obrazów i użyliśmy 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>

Obraz małego konstruktora to tylko jedna z kilku dostępnych opcji. W naszym przypadku to dobry wybór, ponieważ ma bardzo mało dodatkowych bibliotek i narzędzi, co pomaga zminimalizować powierzchnię ataku.

Jeśli na przykład tworzysz aplikację, która wymaga dostępu do kilku popularnych bibliotek C, lub nie masz jeszcze pewności, jakie są jej wymagania, lepszym rozwiązaniem może być kompilator.

5. Tworzenie i uruchamianie aplikacji natywnej

Gdy wszystko będzie gotowe, będziemy mogli utworzyć obraz i uruchomić naszą natywną, skompilowaną aplikację.

Przed uruchomieniem kompilacji pamiętaj o kilku kwestiach:

  • Potrwa to dłużej niż w przypadku standardowej kompilacji (kilka minut) d420322893640701.png
  • Ten proces kompilacji może zająć dużo pamięci (kilka gigabajtów) cda24e1eb11fdbea.png
  • Ten proces kompilacji wymaga, aby demon Dockera był osiągalny
  • W tym przykładzie cały proces realizujemy ręcznie, ale możesz też skonfigurować etapy kompilacji tak, aby automatycznie aktywowały natywny profil kompilacji.

Aby utworzyć nasz wizerunek:

mvn spring-boot:build-image

Gdy jest to gotowe, możemy zobaczyć, jak działa aplikacja natywna.

Aby uruchomić aplikację:

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

Jesteśmy w doskonałej sytuacji, żeby zobaczyć obie strony równania aplikacji natywnej.

Podczas kompilacji poświęciliśmy nieco czasu i dodatkowo wykorzystaliśmy pamięć, ale w zamian otrzymujemy aplikację, która może się znacznie szybciej uruchamiać i zużywa znacznie mniej pamięci (w zależności od obciążenia).

Jeśli użyjesz funkcji docker images demo, aby porównać rozmiar obrazu natywnego z oryginałem, możemy zaobserwować znaczny spadek:

e667f65a011c1328.png

Warto też pamiętać, że w bardziej złożonych przypadkach użycia wymagane są dodatkowe zmiany, które będą informować kompilator AOT o tym, co aplikacja zrobi w czasie działania. Z tego powodu niektóre przewidywalne zbiory zadań (takie jak zadania wsadowe) mogą się do tego świetnie nadawać, podczas gdy inne mogą wymagać większego wzrostu.

6. Wdrażanie naszej aplikacji natywnej

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

1. Przygotowujemy 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 upewnij się, że jesteśmy uwierzytelnieni w przekazywaniu wiadomości do naszego nowego rejestru.

Interfejs wiersza poleceń gcloud może znacznie uprościć ten proces:

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

2. Przekazujemy obraz do Artifact Registry

Następnie otagujemy obraz:

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 za pomocą docker push możemy 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

Teraz możemy wdrożyć w Cloud Run obraz zapisany w Artifact Registry:

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

Ponieważ stworzyliśmy i wdrożyliśmy naszą aplikację jako obraz natywny, możemy mieć pewność, że w miarę jej działania zużywamy koszty naszej infrastruktury.

Możesz porównać czasy uruchamiania naszej bazowej aplikacji z czasem uruchomienia nowej aplikacji natywnej.

6dde63d35959b1bb.png

7. Podsumowanie/Czyszczenie

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

Mamy nadzieję, że ten samouczek pomoże Ci lepiej poznać projekt Spring Native i pamiętaj o nim, jeśli w przyszłości spełni Twoje oczekiwania.

Opcjonalnie: wyczyść lub wyłącz usługę

Niezależnie od tego, czy na potrzeby tego ćwiczenia w programie został przez Ciebie utworzony projekt Google Cloud, czy wykorzystujesz istniejący projekt, unikaj niepotrzebnych opłat za zasoby, które wykorzystaliśmy.

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

8. Dodatkowe materiały

Choć projekt Spring Native jest obecnie nowym i eksperymentalnym projektem, ma już wiele przydatnych zasobów, które pomogą użytkownikom wczesnej wersji rozwiązać problemy i zaangażować się w zaangażowanie:

Dodatkowe materiały

Poniżej znajdują się zasoby online, które mogą być pomocne w przypadku tego samouczka:

Licencja

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