1. Panoramica
In questo codelab scopriremo il progetto Spring Native, creeremo un'app che lo utilizza e ne eseguiremo il deployment su Google Cloud.
Vedremo i suoi componenti, la cronologia recente del progetto, alcuni casi d'uso e, ovviamente, i passaggi necessari per utilizzarlo nei tuoi progetti.
Il progetto Spring Native è attualmente in fase sperimentale, pertanto per iniziare sarà necessaria una configurazione specifica. Tuttavia, come annunciato a SpringOne 2021, Spring Native verrà integrato in Spring Framework 6.0 e Spring Boot 3.0 con supporto di primo livello, quindi questo è il momento ideale per dare un'occhiata più da vicino al progetto alcuni mesi prima del suo rilascio.
Mentre la compilazione just-in-time è stata molto ben ottimizzata per aspetti come i processi a lunga esecuzione, ci sono alcuni casi d'uso in cui le applicazioni compilate in anticipo offrono prestazioni ancora migliori, di cui parleremo durante il codelab.
Imparerai a utilizzare
- Utilizzo di Cloud Shell
- Abilita l'API Cloud Run
- Crea ed esegui il deployment di un'app nativa Spring
- Esegui il deployment di un'app di questo tipo in Cloud Run
Che cosa ti serve
- Un progetto piattaforma Google Cloud con un account di fatturazione Google Cloud attivo
- gcloud cli installato o l'accesso a Cloud Shell
- Competenze Java e XML di base
- Conoscenza pratica dei comandi Linux più comuni
Sondaggio
Come utilizzerai questo tutorial?
Come giudichi la tua esperienza con Java?
Come giudichi la tua esperienza di utilizzo dei servizi Google Cloud?
2. Sfondo
Il progetto Spring Native utilizza diverse tecnologie per fornire agli sviluppatori prestazioni delle applicazioni native.
Per comprendere appieno Spring Native, è utile comprendere alcune di queste tecnologie dei componenti, cosa consentono per noi e come interagiscono in questo ambito.
Compilation AOT
Quando gli sviluppatori eseguono javac normalmente al momento della compilazione, il nostro codice sorgente .java viene compilato in file .class scritti in bytecode. Questo bytecode deve essere compreso solo dalla Java Virtual Machine, quindi la JVM dovrà interpretare questo codice su altre macchine per consentirci di eseguire il nostro codice.
Questo processo è ciò che ci dà la portabilità della firma di Java, consentendoci di "scrivere una volta ed eseguire ovunque", ma è costoso rispetto all'esecuzione di codice nativo.
Fortunatamente, la maggior parte delle implementazioni JVM utilizza la compilazione just-in-time per mitigare questo costo di interpretazione. Ciò si ottiene contando le chiamate per una funzione e, se viene richiamata abbastanza spesso da superare una soglia ( 10.000 per impostazione predefinita), viene compilata in codice nativo in fase di esecuzione per evitare ulteriori interpretazioni costose.
La compilazione anticipata adotta l'approccio opposto, ovvero la compilazione di tutto il codice raggiungibile in un file eseguibile nativo al momento della compilazione. Questo scambia la portabilità con l'efficienza della memoria e altri miglioramenti delle prestazioni in fase di esecuzione.
Questo è ovviamente un compromesso e non sempre vale la pena di fare. Tuttavia, la compilazione AOT può eccellere in determinati casi d'uso, come:
- Applicazioni di breve durata in cui il tempo di avvio è importante
- Ambienti con memoria elevata in cui JIT potrebbe essere troppo costoso
Curiosità, la compilazione AOT è stata introdotta come funzionalità sperimentale in JDK 9, anche se questa implementazione è stata costosa da mantenere e non è mai stata messa in atto, quindi è stata decisamente rimossa in Java 17 a favore degli sviluppatori che utilizzavano semplicemente GraalVM.
GraalVM
GraalVM è una distribuzione JDK open source altamente ottimizzata che vanta tempi di avvio estremamente rapidi, compilazione di immagini native AOT e funzionalità poliglotta che consentono agli sviluppatori di combinare più lingue in un'unica applicazione.
GraalVM è in fase di sviluppo attivo, acquisendo nuove funzionalità e migliorando costantemente quelle esistenti, quindi incoraggio gli sviluppatori a restare sintonizzati.
Alcuni traguardi recenti sono:
- Un nuovo output di build di un'immagine nativa intuitiva ( 18/01/2021)
- Supporto di Java 17 ( 18/01/2022)
- Abilitazione della compilazione multi-livello per impostazione predefinita per migliorare i tempi di compilazione dei poliglotti ( 20/04/2021)
Nativo primaverile
In poche parole: Spring Native consente l'utilizzo del compilatore di immagini native di GraalVM per trasformare le applicazioni Spring in eseguibili nativi.
Questo processo prevede l'esecuzione di un'analisi statica della tua applicazione al momento della compilazione per trovare tutti i metodi dell'applicazione raggiungibili dal punto di ingresso.
Questo crea essenzialmente un "mondo chiuso" della tua applicazione, in cui tutto il codice è presumibile sia noto al momento della compilazione e non è consentito caricare nuovo codice in fase di runtime.
È importante notare che la generazione di immagini native è un processo ad alta intensità di memoria che richiede più tempo rispetto alla compilazione di un'applicazione normale e impone limitazioni su alcuni aspetti di Java.
In alcuni casi, affinché un'applicazione funzioni con Spring Native, non sono necessarie modifiche al codice. Tuttavia, alcune situazioni richiedono una configurazione nativa specifica per funzionare correttamente. In queste situazioni, Spring Native fornisce spesso suggerimenti nativi per semplificare il processo.
3. Configurazione/pre-lavoro
Prima di iniziare a implementare Spring Native, dovremo creare ed eseguire il deployment della nostra app per stabilire una base di riferimento per le prestazioni da confrontare in seguito con la versione nativa.
1. Creazione del progetto
Inizieremo recuperando la nostra app da 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
Questa app iniziale utilizza Spring Boot 2.6.4, che è l'ultima versione supportata dal progetto nativo della primavera al momento della scrittura.
Tieni presente che dopo il rilascio di GraalVM 21.0.3, potresti utilizzare anche Java 17 per questo esempio. Useremo comunque Java 11 per questo tutorial in modo da ridurre al minimo la configurazione necessaria.
Una volta inserito il file ZIP nella riga di comando, possiamo creare una sottodirectory per il progetto e decomprimere la cartella al suo interno:
mkdir spring-native cd spring-native unzip ../io-native-starter.zip
2. Modifiche al codice
Una volta aperto il progetto, aggiungeremo rapidamente un segno di vita e mostreremo il rendimento di Spring Native una volta eseguito.
Modifica il file DemoApplication.java in modo che corrisponda a quanto segue:
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();
}
}
A questo punto la nostra app di base è pronta per l'uso, quindi puoi creare un'immagine ed eseguirla localmente per avere un'idea del tempo di avvio prima di convertirla in un'applicazione nativa.
Per creare la nostra immagine:
mvn spring-boot:build-image
Puoi anche utilizzare docker images demo
per avere un'idea delle dimensioni dell'immagine di riferimento:
Per eseguire la nostra app:
docker run --rm -p 8080:8080 demo:0.0.1-SNAPSHOT
3. Esegui il deployment dell'app di riferimento
Ora che abbiamo la nostra app, ne eseguiremo il deployment e prenderemo nota degli orari, che confronteremo con quelli della nostra app nativa in seguito.
A seconda del tipo di applicazione che stai creando, esistono diversi tipi di hosting per i tuoi contenuti.
Tuttavia, poiché il nostro esempio è un'applicazione web molto semplice e diretta, possiamo semplificare le cose e affidarci a Cloud Run.
Se procedi sulla tua macchina, assicurati che lo strumento gcloud CLI sia installato e aggiornato.
Se stai utilizzando Cloud Shell, sarà tutto risolto e potrai semplicemente eseguire quanto segue nella directory di origine:
gcloud run deploy
4. Configurazione applicazione
1. Configurazione dei nostri repository Maven
Poiché questo progetto è ancora in fase sperimentale, dovremo configurare la nostra app per poter trovare artefatti sperimentali che non sono disponibili nel repository centrale di Maven.
Sarà necessario aggiungere i seguenti elementi al file pom.xml, operazione che puoi eseguire nell'editor di tua scelta.
Aggiungi le seguenti sezioni di repository e plug-in Repositories al nostro pom:
<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. Aggiungi le nostre dipendenze
Poi aggiungi la dipendenza nativa della primavera, necessaria per eseguire un'applicazione Spring come immagine nativa. Nota: questo passaggio non è necessario se usi Gradle
<dependencies>
<!-- ... -->
<dependency>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-native</artifactId>
<version>0.11.2</version>
</dependency>
</dependencies>
3. Aggiunta/attivazione dei nostri plug-in
Ora aggiungi il plug-in AOT per migliorare la compatibilità e l'impronta delle immagini native ( Scopri di più):
<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>
Ora aggiorneremo il plug-in spring-boot-maven-plugin per abilitare il supporto delle immagini native e useremo il builder paketo per creare la nostra immagine nativa:
<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>
Tieni presente che l'immagine del generatore di piccole dimensioni è solo una delle tante opzioni disponibili. È una buona scelta per il nostro caso d'uso perché ha pochissime librerie e utilità aggiuntive, il che aiuta a ridurre al minimo la superficie di attacco.
Se, ad esempio, stessi creando un'app che necessitava di accesso ad alcune librerie C comuni o non eri ancora sicuro dei requisiti della tua app, full-builder potrebbe essere la soluzione più adatta.
5. Crea ed esegui un'app nativa
Una volta fatto tutto, dovremmo essere in grado di creare l'immagine ed eseguire la nostra app nativa compilata.
Prima di eseguire la build, tieni presente quanto segue:
- Questa operazione richiederà più tempo rispetto a una build normale (alcuni minuti)
- Questo processo di compilazione può richiedere molta memoria (alcuni gigabyte)
- Questo processo di compilazione richiede che il daemon Docker sia raggiungibile
- Anche se in questo esempio eseguiamo la procedura manualmente, puoi anche configurare le fasi di build per attivare automaticamente un profilo di build nativo.
Per creare la nostra immagine:
mvn spring-boot:build-image
Una volta realizzata l'app, è tutto pronto per vedere l'applicazione nativa in azione.
Per eseguire la nostra app:
docker run --rm -p 8080:8080 demo:0.0.1-SNAPSHOT
A questo punto siamo in una posizione ottima per vedere entrambi i membri dell'equazione dell'applicazione nativa.
Abbiamo risparmiato un po' di tempo e l'utilizzo di memoria in più al momento della compilazione, ma in cambio abbiamo un'applicazione che può avviarsi molto più rapidamente e consumare molto meno memoria (a seconda del carico di lavoro).
Se eseguiamo docker images demo
per confrontare le dimensioni dell'immagine nativa con l'originale, possiamo notare una riduzione significativa:
Occorre inoltre notare che, nei casi d'uso più complessi, sono necessarie ulteriori modifiche per comunicare al compilatore AOT le operazioni eseguite dall'app in fase di runtime. Per questo motivo, alcuni carichi di lavoro prevedibili (come i job batch) potrebbero essere particolarmente adatti a questo scopo, mentre altri possono rappresentare un incremento maggiore.
6. Deployment della nostra app nativa
Per eseguire il deployment della nostra app in Cloud Run, dobbiamo inserire la nostra immagine nativa in un gestore di pacchetti come Artifact Registry.
1. preparazione del repository Docker
Possiamo iniziare la procedura creando un repository:
gcloud artifacts repositories create native-image-docker-repo --repository-format=docker \
--location=us-central1 --description="Repository for our native images"
Successivamente, ci assicuriamo di avere l'autenticazione per il push al nostro nuovo registry.
gcloud CLI può semplificare un po' questo processo:
gcloud auth configure-docker us-central1-docker.pkg.dev
2. esegui il push dell'immagine in Artifact Registry
Successivamente, taggheremo la nostra immagine:
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
Quindi possiamo utilizzare docker push
per inviarlo ad Artifact Registry:
docker push us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2
3. Deployment in Cloud Run
Ora siamo pronti per eseguire il deployment in Cloud Run dell'immagine che abbiamo archiviato in Artifact Registry:
gcloud run deploy --image us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2
Poiché abbiamo creato ed eseguito il deployment della nostra app come immagine nativa, possiamo avere la certezza che l'applicazione sta facendo un uso eccellente dei costi dell'infrastruttura durante l'esecuzione.
Confronta personalmente i tempi di avvio della nostra app di base con quelli della nuova app nativa.
7. Riepilogo/Pulizia
Complimenti per la creazione e il deployment di un'applicazione nativa Spring su Google Cloud.
Ci auguriamo che questo tutorial ti incoraggi ad acquisire familiarità con il progetto Spring Native e a tenerlo a mente nel caso in cui dovesse soddisfare le tue esigenze in futuro.
(Facoltativo) Esegui la pulizia e/o disattiva il servizio
Se hai creato un progetto Google Cloud per questo codelab o stai riutilizzando uno esistente, assicurati di evitare addebiti inutili da parte delle risorse che abbiamo utilizzato.
Puoi eliminare o disabilitare i servizi Cloud Run che abbiamo creato, eliminare l'immagine che abbiamo ospitato o chiudere l'intero progetto.
8. Risorse aggiuntive
Anche se il progetto Spring Native è attualmente un nuovo progetto sperimentale, esistono già molte risorse utili per aiutare i primi utenti a risolvere i problemi e partecipare:
Risorse aggiuntive
Di seguito sono riportate alcune risorse online che potrebbero essere pertinenti per questa esercitazione:
- Scopri di più sui suggerimenti nativi
- Scopri di più su GraalVM
- Come partecipare
- Errore di memoria insufficiente durante la creazione di immagini native
- Errore di avvio dell'applicazione
Licenza
Questo lavoro è concesso in licenza ai sensi di una licenza Creative Commons Attribution 2.0 Generic.