1. Panoramica
In questo codelab, apprenderemo il progetto Spring Native, creeremo un'app che lo utilizza e ne eseguiremo il deployment su Google Cloud.
Esamineremo i suoi componenti, la storia recente del progetto, alcuni casi d'uso e, naturalmente, i passaggi necessari per utilizzarlo nei tuoi progetti.
Il progetto Spring Native è attualmente in fase sperimentale, quindi per iniziare è necessaria una configurazione specifica. Tuttavia, come annunciato a SpringOne 2021, Spring Native dovrebbe essere integrato in Spring Framework 6.0 e Spring Boot 3.0 con supporto di prima classe, quindi è il momento perfetto per dare un'occhiata più da vicino al progetto qualche mese prima del rilascio.
Sebbene la compilazione just-in-time sia stata ottimizzata molto bene per elementi come i processi a lungo termine, esistono determinati casi d'uso in cui le applicazioni compilate in anticipo hanno un rendimento ancora migliore, di cui parleremo durante il codelab.
Imparerai a utilizzare
- Utilizzo di Cloud Shell
- Abilita l'API Cloud Run
- Creare ed eseguire il deployment di un'app Spring Native
- Esegui il deployment di un'app di questo tipo in Cloud Run
Che cosa ti serve
- Un progetto Google Cloud con un account di fatturazione Google Cloud attivo
- gcloud CLI installato o accesso a Cloud Shell
- Competenze di base di Java e XML
- Conoscenza pratica dei comandi Linux comuni
Sondaggio
Come utilizzerai questo tutorial?
Come valuteresti la tua esperienza con Java?
Come valuteresti la tua esperienza con l'utilizzo dei servizi Google Cloud?
2. Sfondo
Il progetto Spring Native utilizza diverse tecnologie per offrire agli sviluppatori il rendimento delle applicazioni native.
Per comprendere appieno Spring Native, è utile conoscere alcune di queste tecnologie dei componenti, cosa ci consentono e come interagiscono tra loro.
Compilazione AOT
Quando gli sviluppatori eseguono normalmente javac in fase di compilazione, il codice sorgente .java viene compilato in file .class scritti in bytecode. Questo bytecode è pensato per essere compreso solo dalla Java Virtual Machine, quindi la JVM dovrà interpretare questo codice su altre macchine per consentirci di eseguirlo.
Questo processo ci offre la portabilità delle firme di Java, che ci consente di "scrivere una volta ed eseguire ovunque ", ma è costoso rispetto all'esecuzione del codice nativo.
Fortunatamente, la maggior parte delle implementazioni della JVM utilizza la compilazione just-in-time per ridurre questo costo di interpretazione. Questo viene ottenuto conteggiando le invocazioni di una funzione e, se viene invocata abbastanza spesso da superare una soglia ( 10.000 per impostazione predefinita), viene compilata in codice nativo in fase di esecuzione per evitare un'ulteriore interpretazione dispendiosa.
La compilazione anticipata adotta l'approccio opposto, compilando tutto il codice raggiungibile in un file eseguibile nativo in fase di compilazione. In questo modo si sacrifica la portabilità in favore dell'efficienza della memoria e di altri miglioramenti delle prestazioni in fase di esecuzione.
Naturalmente, si tratta di un compromesso e non sempre vale la pena accettarlo. Tuttavia, la compilazione AOT può dare il meglio di sé in alcuni casi d'uso, ad esempio:
- Applicazioni di breve durata in cui il tempo di avvio è importante
- Ambienti con limitazioni di memoria elevate in cui il compilatore Just-in-Time potrebbe essere troppo costoso
Come curiosità, la compilazione AOT è stata introdotta come funzionalità sperimentale in JDK 9, anche se questa implementazione era costosa da mantenere e non ha mai preso piede, quindi è stata rimessa silenziosamente in Java 17 a favore degli sviluppatori che utilizzano solo GraalVM.
GraalVM
GraalVM è una distribuzione JDK open source altamente ottimizzata che vanta tempi di avvio estremamente rapidi, compilazione di immagini native AOT e funzionalità poliglotte che consentono agli sviluppatori di combinare più linguaggi in un'unica applicazione.
GraalVM è in fase di sviluppo attivo, acquisisce continuamente nuove funzionalità e migliora quelle esistenti, quindi incoraggio gli sviluppatori a rimanere sintonizzati.
Ecco alcuni traguardi recenti:
- Un nuovo output di compilazione di immagini native facile da usare ( 18/01/2021)
- Supporto di Java 17 ( 18/01/2022)
- Attivazione della compilazione a più livelli per impostazione predefinita per migliorare i tempi di compilazione di Polyglot ( 20/04/2021)
Spring Native
In parole povere, Spring Native consente di utilizzare il compilatore di immagini native di GraalVM per trasformare le applicazioni Spring in eseguibili nativi.
Questa procedura prevede l'esecuzione di un'analisi statica dell'applicazione in fase di compilazione per trovare tutti i metodi dell'applicazione raggiungibili dal punto di contatto.
In sostanza, viene creata una concezione "a mondo chiuso" dell'applicazione, in cui si presume che tutto il codice sia noto in fase di compilazione e non sia consentito caricare nuovo codice in fase di esecuzione.
È importante notare che la generazione di immagini native è un processo che richiede molta memoria e più tempo della compilazione di un'applicazione normale e impone limitazioni su alcuni aspetti di Java.
In alcuni casi, non sono necessarie modifiche al codice per il funzionamento di un'applicazione con Spring Native. Tuttavia, in alcuni casi è necessaria una configurazione nativa specifica per il corretto funzionamento. In questi casi, Spring Native fornisce spesso suggerimenti nativi per semplificare questa procedura.
3. Configurazione/preparazione
Prima di iniziare a implementare Spring Native, dobbiamo creare e implementare la nostra app per stabilire una linea di base delle prestazioni che potremo confrontare con la versione nativa in un secondo momento.
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 di avvio utilizza Spring Boot 2.6.4, ovvero la versione più recente supportata dal progetto spring-native al momento della stesura di questo articolo.
Tieni presente che, dalla release di GraalVM 21.0.3, puoi utilizzare Java 17 anche per questo esempio. Per questo tutorial utilizzeremo ancora Java 11 per ridurre al minimo la configurazione richiesta.
Una volta che abbiamo il file ZIP sulla 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 indicatore di attività e mostreremo le prestazioni di Spring Native una volta eseguito.
Modifica DemoApplication.java in modo che corrisponda a questo:
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 riferimento è pronta per l'uso, quindi non esitare a creare un'immagine ed eseguirla localmente per farti 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 farti 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 l'app, la implementeremo e prenderemo nota dei tempi, che in seguito verranno confrontati con i tempi di avvio dell'app nativa.
A seconda del tipo di applicazione che stai creando, esistono diversi modi per ospitare i tuoi contenuti.
Tuttavia, poiché il nostro esempio è un'applicazione web molto semplice e lineare, possiamo mantenere la semplicità e fare affidamento su Cloud Run.
Se segui la procedura sulla tua macchina, assicurati di avere installato e aggiornato lo strumento gcloud CLI.
Se utilizzi Cloud Shell, tutto verrà gestito 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 in modo da poter trovare gli elementi sperimentali, che non sono disponibili nel repository centrale di Maven.
Dovrai aggiungere i seguenti elementi al nostro pom.xml, che puoi fare nell'editor che preferisci.
Aggiungi le seguenti sezioni repositories e pluginRepositories 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. Aggiunta delle nostre dipendenze
Aggiungi la dipendenza spring-native, necessaria per eseguire un'applicazione Spring come immagine nativa. Nota: questo passaggio non è necessario se utilizzi 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 per abilitare il supporto delle immagini native e utilizzeremo 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 tiny builder è solo una delle diverse opzioni disponibili. È una buona scelta per il nostro caso d'uso perché ha pochissime librerie e utilità aggiuntive, il che contribuisce a ridurre al minimo la nostra superficie di attacco.
Ad esempio, se stai creando un'app che ha bisogno di accedere ad alcune librerie C comuni o se non hai ancora le idee chiare sui requisiti della tua app, il full-builder potrebbe essere una soluzione migliore.
5. Crea ed esegui un'app nativa
Una volta completata la configurazione, dovremmo essere in grado di creare l'immagine ed eseguire l'app nativa compilata.
Prima di eseguire la compilazione, tieni presente quanto segue:
- L'operazione richiederà più tempo di una normale compilazione (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 la procedura viene eseguita manualmente, puoi anche configurare le fasi di compilazione in modo da attivare automaticamente un profilo di compilazione nativo.
Per creare la nostra immagine:
mvn spring-boot:build-image
Una volta completata la compilazione, possiamo vedere l'app 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 ottimale per vedere entrambi i lati dell'equazione delle applicazioni native.
Abbiamo rinunciato a un po' di tempo e a un utilizzo extra della memoria in fase di compilazione, ma in cambio abbiamo ottenuto un'applicazione che può avviarsi molto più rapidamente e consumare molta meno memoria (a seconda del carico di lavoro).
Se eseguiamo docker images demo
per confrontare le dimensioni dell'immagine nativa con quelle dell'originale, possiamo notare una notevole riduzione:
Inoltre, tieni presente che in casi d'uso più complessi sono necessarie ulteriori modifiche per comunicare al compilatore AOT cosa farà la tua app in fase di esecuzione. Per questo motivo, alcuni carichi di lavoro prevedibili (come i job batch) potrebbero essere molto adatti a questo scopo, mentre altri potrebbero avere un impatto maggiore.
6. Eseguire il deployment della nostra app nativa
Per eseguire il deployment della nostra app in Cloud Run, dobbiamo inserire la nostra immagine nativa in un gestore pacchetti come Artifact Registry.
1. Preparazione del repository Docker
Possiamo iniziare questa 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, dobbiamo assicurarci di essere autenticati per il push nel nostro nuovo registry.
La CLI gcloud può semplificare notevolmente questa procedura:
gcloud auth configure-docker us-central1-docker.pkg.dev
2. Esegui il push dell'immagine in Artifact Registry
Ora taggheremo l'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
e poi 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 è tutto pronto per eseguire il deployment dell'immagine archiviata in Artifact Registry in Cloud Run:
gcloud run deploy --image us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2
Poiché abbiamo creato e implementato la nostra app come immagine nativa, possiamo stare certi che la nostra applicazione utilizzi in modo eccellente i costi dell'infrastruttura durante l'esecuzione.
Non esitare a confrontare i tempi di avvio della nostra app di riferimento con quelli di questa nuova app nativa.
7. Riepilogo/pulizia
Congratulazioni per aver creato ed eseguito il deployment di un'applicazione Spring Native su Google Cloud.
Ci auguriamo che questo tutorial ti incoraggi a familiarizzare di più con il progetto Spring Native e a tenerlo presente se dovesse soddisfare le tue esigenze in futuro.
(Facoltativo) Ripulisci e/o disattiva il servizio
Indipendentemente dal fatto che tu abbia creato un progetto Google Cloud per questo codelab o ne stia riutilizzando uno esistente, fai attenzione ad evitare addebiti non necessari per le risorse che abbiamo utilizzato.
Puoi eliminare o disattivare 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 progetto nuovo ed sperimentale, esistono già molte risorse utili per aiutare gli early adopter a risolvere i problemi e a partecipare:
Risorse aggiuntive
Di seguito sono riportate alcune risorse online che potrebbero essere pertinenti per questo tutorial:
- Scopri di più sugli indicatori 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.