Nota:

Immagine nativa GraalVM, primavera e containerizzazione

Introduzione

Questo laboratorio è dedicato agli sviluppatori che desiderano ulteriori informazioni su come mettere in container le applicazioni GraalVM Native Image.

La tecnologia GraalVM Native Image compila il codice Java in anticipo in un file eseguibile nativo. Nel file eseguibile viene incluso solo il codice richiesto in fase di esecuzione dall'applicazione.

Un file eseguibile prodotto da Native Image ha diversi vantaggi importanti, in quanto:

Molti dei principali framework di microservizi supportano la compilazione in anticipo con l'immagine nativa GraalVM, tra cui Micronaut, Spring, Helidon e Quarkus.

Inoltre, ci sono plugin Maven e Gradle per l'immagine nativa in modo da poter creare, testare ed eseguire facilmente applicazioni Java come file eseguibili.

Nota: Oracle Cloud Infrastructure (OCI) offre GraalVM Enterprise senza costi aggiuntivi.

Tempo di laboratorio stimato: 90 minuti

Obiettivi laboratorio

In questo laboratorio:

NOTA: se nell'esercitazione viene visualizzata l'icona del laptop, ciò significa che è necessario eseguire operazioni quali l'immissione di un comando. Tieni d'occhio.

# This is where you will need to do something

STEP 1: connessione a un host virtuale e controllo dell'ambiente di sviluppo

L'ambiente di sviluppo viene fornito da un host remoto: un'istanza di computazione OCI con Oracle Linux 8, 4 core e 32 GB di memoria. L'ambiente desktop Luna Labs verrà visualizzato prima che l'host remoto sia pronto e può richiedere fino a due minuti.

Per connettersi all'host remoto, eseguire uno script di installazione nell'ambiente desktop Luna. Questo script è disponibile mediante la scheda Risorse.

  1. Fare doppio clic sull'icona di Luna Lab sul desktop per aprire il browser.

    Icona desktop Luna

  2. Verrà visualizzata la scheda Risorse. Si noti che la sintesi mostrata accanto al titolo Risorse verrà attivata mentre viene eseguito il provisioning dell'istanza di computazione nel cloud.

    Scheda Risorsa Luna

  3. Quando viene eseguito il provisioning dell'istanza (l'operazione potrebbe richiedere fino a 2 minuti), nella scheda Risorse verrà visualizzato quanto segue.

    Luna: scheda Risorse

  4. Copiare lo script di configurazione che imposta l'ambiente Codice VS dalla scheda Risorse. Fare clic sul collegamento Visualizza dettagli per visualizzare la configurazione. Copia questo come mostrato nello screenshot seguente:

    Copia script di configurazione

  5. Aprire un terminale, come mostrato nello screenshot seguente:

    Apri terminale

  6. Incollare il codice di configurazione nel terminale, che aprirà il codice VS.

    Incolla terminale 1

    Incolla terminale 2

  7. Una finestra Codice VS aprirà e si connetterà automaticamente all'istanza VM di cui è stato eseguito il provisioning. Fare clic su Continua per accettare l'impronta digitale del computer.

    Accettazione codice VS

Operazione completata. Congratulazioni. La connessione a un host remoto in Oracle Cloud è stata completata.

Lo script precedente aprirà il codice VS, connesso all'istanza di computazione remota con il codice sorgente per il laboratorio aperto.

In seguito sarà necessario aprire un terminale all'interno del codice VS. Questo terminale consente di interagire con l'host remoto. È possibile aprire un terminale in Codice VS tramite il menu: Terminale > Nuovo terminale.

Terminale codice VS

Utilizzeremo questo terminale nel resto del laboratorio.

Nota sull'ambiente di sviluppo

In questo laboratorio utilizzeremo GraalVM Enterprise 22 come strumento Java. GraalVM è una distribuzione JDK ad alte prestazioni offerta da Oracle costruita su Oracle Java SE, affidabile e sicuro.

L'ambiente di sviluppo è preconfigurato con GraalVM e gli strumenti nativi necessari per questo laboratorio.

È possibile controllare con facilità che eseguendo questi comandi nel terminale:

java -version

native-image --version

STEP 2: Presentazione dell'applicazione Java di esempio

In questo laboratorio potrai creare una semplice applicazione con un'API basata su REST estremamente ridotta. Questa applicazione verrà poi gestita in container mediante Docker. In primo luogo, esamina rapidamente la tua applicazione semplice.

Il codice sorgente e gli script build per questa applicazione e la cartella contenente il codice sorgente saranno aperti nel codice VS.

L'applicazione si basa sulla struttura Spring Boot e utilizza il progetto nativo Primavera (incubatore Spring per generare eseguibili nativi utilizzando l'immagine nativa GraalVM).

L'applicazione dispone di due classi, disponibili in src/main/java:

Quindi, che cosa fa l'applicazione? Se si chiama l'endpoint REST /jibber, definito all'interno dell'applicazione, verranno restituiti alcuni versi assenti generati nello stile del pagem Jabberwocky di Lewis Carroll. Il programma ottiene questo risultato utilizzando una catena di marcov per modellare il poem originale (sostanzialmente un modello statistico). Questo modello genera un nuovo testo.

Nell'applicazione di esempio forniamo all'applicazione il testo del poem, quindi generiamo un modello del testo che l'applicazione utilizza per generare un nuovo testo simile al testo originale. Stiamo utilizzando la libreria RiTa per compiere le gravose operazioni di sollevamento: questa libreria supporta la costruzione e l'uso di catene di Markov.

Di seguito sono riportati due snippet della classe utility com.example.demo.Jabberwocky che genera il modello. La variabile text contiene il testo del poem originale. Questo snippet mostra come creare il modello e quindi popolarlo con text. Viene richiamata dal costruttore della classe e la classe viene definita come Singolo (in modo che venga creata una sola istanza della classe).

this.r = new RiMarkov(3);
this.r.addText(text);

Qui è possibile vedere il metodo per generare nuove linee di versetto dal modello, in base al testo originale.

public String generate() {
    String[] lines = this.r.generate(10);
    StringBuffer b = new StringBuffer();
    for (int i=0; i< lines.length; i++) {
        b.append(lines[i]);
        b.append("<br/>\n");
    }
    return b.toString();
}

Prendi un po' per visualizzare il codice e per conoscerlo.

Per creare l'applicazione, verrà utilizzato Maven. Il file pom.xml è stato generato utilizzando Spring Initializr e contiene il supporto per l'uso degli strumenti Spring Native. Si tratta di una dipendenza aggiunta ai progetti di Spring Boot se si intende indirizzare un'immagine nativa GraalVM. Se si utilizza Maven, l'aggiunta del supporto per Spring Native inserirà il plugin seguente nella configurazione di build predefinita.

<plugin>
    <groupId>org.springframework.experimental</groupId>
    <artifactId>spring-aot-maven-plugin</artifactId>
    <version>${spring-native.version}</version>
    <executions>
        <execution>
            <id>test-generate</id>
            <goals>
                <goal>test-generate</goal>
            </goals>
        </execution>
        <execution>
            <id>generate</id>
            <goals>
                <goal>generate</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Ora crea l'applicazione. Dalla directory root del repository, eseguire i comandi seguenti nella shell:

mvn clean package

In questo modo verrà generato un file JAR "eseguibile", che contiene tutte le dipendenze dell'applicazione e anche un file MANIFEST configurato correttamente. È possibile eseguire questo file JAR e quindi "ping" l'endpoint dell'applicazione per visualizzare ciò che viene restituito: inserire il comando in background utilizzando & in modo da ottenere il prompt di nuovo.

java -jar ./target/jibber-0.0.1-SNAPSHOT-exec.jar &

Chiamare l'endpoint utilizzando il comando curl dalla riga di comando.

Quando si pubblica il comando nel terminale, VS Code potrebbe richiedere di aprire l'URL in un browser, chiudere la finestra di dialogo, come mostrato di seguito.

Codice VS

Eseguire le operazioni riportate di seguito per eseguire il test dell'endpoint HTTP.

curl http://localhost:8080/jibber

Hai ricevuto alcuni versi di assurdità? Ora che hai creato un'applicazione di lavoro, terminarla e continuare a gestirla in container. Porta l'applicazione in primo piano in modo da poterla terminare.

fg

Immettere <ctrl-c> per terminare l'applicazione.

<ctrl-c>

STEP 3: Container la tua applicazione Java con Docker

Per fortuna, la gestione in container dell'applicazione Java sotto forma di container Docker è relativamente semplice. Puoi creare una nuova immagine Docker basata su una contenente una distribuzione JDK. Pertanto, per questo laboratorio utilizzerai un container che contiene già un JDK container-registry.oracle.com/java/openjdk:17-oraclelinux8: questa è un'immagine Oracle Linux 8 con OpenJDK.

Di seguito è riportata una scomposizione del Dockerfile, che descrive come creare l'immagine Docker. Vedere i commenti per spiegare il contenuto.

FROM container-registry.oracle.com/java/openjdk:17-oraclelinux8 # Base Image

ARG JAR_FILE                   # Pass in the JAR file as an argument to the image build

EXPOSE 8080                    # This image will need to expose TCP port 8080, as this is the port on which your app will listen

COPY ${JAR_FILE} app.jar       # Copy the JAR file from the `target` directory into the root of the image 
ENTRYPOINT ["java"]            # Run Java when starting the container
CMD ["-jar","app.jar"]         # Pass in the parameters to the Java command that make it load and run your executable JAR file

Il Dockerfile per containerizzare la tua applicazione Java è disponibile nella directory 00-containerise.

Per creare un'immagine Docker contenente l'applicazione, eseguire i comandi riportati di seguito dal terminale:

docker build -f ./00-containerise/Dockerfile \
             --build-arg JAR_FILE=./target/jibber-0.0.1-SNAPSHOT-exec.jar \
             -t localhost/jibber:java.01 .

Esegui query sul Docker per esaminare l'immagine appena creata:

docker images | head -n2

Dovrebbe essere visualizzata una nuova immagine nell'elenco. Eseguire questa immagine come indicato di seguito.

docker run --rm -d --name "jibber-java" -p 8080:8080 localhost/jibber:java.01

Quindi chiamare l'endpoint come prima di utilizzare il comando curl:

curl http://localhost:8080/jibber

Avete visto il verso assente? Verificare per quanto tempo ha impiegato l'avvio dell'applicazione. È possibile estrarre l'operazione dai log, poiché le applicazioni di boot di primavera scrivono l'ora di avvio nei log:

docker logs jibber-java

Ad esempio, l'applicazione è stata avviata in 3.896s. Di seguito è riportata l'estrazione dai log.

2022-03-09 19:48:09.511  INFO 1 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 3.896 seconds (JVM running for 4.583)

OK, terminare il container e passare a:

docker kill jibber-java

Puoi anche eseguire una query sul Docker per ottenere le dimensioni dell'immagine. Abbiamo fornito uno script che fa questo per voi. Eseguire le operazioni seguenti nel terminale:

echo $((`docker inspect -f "" localhost/jibber:java.01`/1024/1024))

Viene stampata la dimensione dell'immagine espressa in MB, ovvero 606 MB.

STEP 4: creazione di un eseguibile nativo

Riepilogo ciò che hai finora:

  1. È stata creata un'applicazione Spring Boot con un endpoint HTTP, /jibber
  2. Containerizzazione completata

Ora scoprirai come creare un eseguibile nativo dall'applicazione utilizzando l'immagine nativa di GraalVM. Questo eseguibile nativo avrà una serie di caratteristiche interessanti, vale a dire:

  1. Inizierà molto rapidamente
  2. Utilizza meno risorse rispetto all'applicazione Java corrispondente

È possibile utilizzare gli strumenti Immagine nativa installati con GraalVM per creare un eseguibile nativo di un'applicazione dalla riga di comando. Tuttavia, mentre stai già utilizzando Maven, sarai in grado di applicare gli strumenti di creazione nativi GraalVM per Maven, che ti consentiranno di continuare a usare Maven per la creazione.

Un modo per aggiungere il supporto per la creazione di un eseguibile nativo è utilizzare un profilo Maven, che consente di decidere se creare solo il file JAR o un eseguibile nativo.

Nel file pom.xml Maven fornito, è stato aggiunto un profilo che crea un eseguibile nativo. Osservare meglio:

In primo luogo, è necessario dichiarare il profilo e assegnare un nome.

<profiles>
    <profile>
        <id>native</id>
        <!-- Rest of profile hidden, to highlight relevant parts -->
    </profile>
</profiles>

Successivamente, all'interno del profilo, includiamo il plugin strumenti di creazione dell'immagine nativa GraalVM e lo collegheremo alla fase package in Maven. Ciò significa che verrà eseguito come parte della fase package. Si noti che è possibile passare gli argomenti di configurazione allo strumento di creazione dell'immagine nativa di base utilizzando la sezione <buildArgs>. Nei singoli tag buildArg è possibile passare i parametri esattamente allo strumento native-image. È pertanto possibile utilizzare tutti i parametri che utilizzano lo strumento native-image:

<build>
    <plugins>
        <plugin>
            <groupId>org.graalvm.buildtools</groupId>
            <artifactId>native-maven-plugin</artifactId>
            <version>${native-buildtools.version}</version>
            <extensions>true</extensions>
            <executions>
                <execution>
                    <id>build-native</id>
                    <phase>package</phase>
                    <goals>
                        <goal>build</goal>
                    </goals>
                </execution>
            </executions>
            <configuration>
                <imageName>jibber</imageName>
                <buildArgs>
                    <buildArg>-H:+ReportExceptionStackTraces</buildArg>
                </buildArgs>
            </configuration>
        </plugin>
        <!-- Rest of profile hidden, to high-light relevant parts -->
    </plugins>
</build>

Eseguire ora la build Maven utilizzando il profilo, come indicato di seguito (il nome del profilo viene specificato con il flag -P):

mvn package -Pnative

Questo genererà un eseguibile nativo per la piattaforma nella directory target, denominata jibber. Esaminare le dimensioni del file:

ls -lh target/jibber

Eseguire questo eseguibile nativo ed eseguirne il test. Eseguire il comando riportato di seguito nel terminale per eseguire l'eseguibile nativo e inserirlo in background, utilizzando &:

./target/jibber &

Chiamare l'endpoint utilizzando il comando curl:

curl http://localhost:8080/jibber

Ora hai un eseguibile nativo dell'applicazione che inizia molto velocemente!

Terminare l'applicazione prima di continuare. Porta l'applicazione in primo piano:

fg

Terminarlo con <ctrl-c>:

<ctrl-c>

STEP 5: Containerizzare l'eseguibile nativo

Ora, poiché si dispone di una versione eseguibile nativa dell'applicazione e l'utente lo ha visto funzionare, conservarla in container.

Abbiamo fornito un Dockerfile semplice per il packaging di questo eseguibile nativo: si trova nella directory native-image/containerisation/lab/01-native-image/Dockerfile. I contenuti sono visualizzati di seguito, insieme ai commenti per spiegare ogni riga.

FROM container-registry.oracle.com/os/oraclelinux:8-slim

ARG APP_FILE                 # Pass in the native executable
EXPOSE 8080                  # This image will need to expose TCP port 8080, as this is port your app will listen on

COPY ${APP_FILE} app  # Copy the native executable into the root directory and call it "app"
ENTRYPOINT ["/app"]          # Just run the native executable :)

Per creare, eseguire le seguenti operazioni dal terminale:

docker build -f ./01-native-image/Dockerfile \
             --build-arg APP_FILE=./target/jibber \
             -t localhost/jibber:native.01 .

Esaminare l'immagine appena creata:

docker images | head -n2

Ora è possibile eseguire questo e testarlo come segue dal terminale:

docker run --rm -d --name "jibber-native" -p 8080:8080 localhost/jibber:native.01

Chiamare l'endpoint dal terminale utilizzando curl:

curl http://localhost:8080/jibber

Ancora una volta, dovresti aver visto più assurdità verso lo stile del poema Jabberwocky. È possibile osservare la durata dell'avvio dell'applicazione esaminando i log prodotti dall'applicazione in precedenza. Dal terminale, eseguire le operazioni riportate di seguito e cercare l'ora di avvio:

docker logs jibber-native

Abbiamo visto quanto segue che mostra che l'applicazione è stata avviata in 0.074s. Questo è un grande miglioramento rispetto all'originale di 3.896s!

2022-03-09 19:44:12.642  INFO 1 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 0.074 seconds (JVM running for 0.081)

Terminare il container e andare al passaggio successivo:

docker kill jibber-native

Ma prima di andare al passo successivo, dare un'occhiata alla dimensione del contenitore prodotto:

echo $((`docker inspect -f "" localhost/jibber:native.01`/1024/1024))

Le dimensioni dell'immagine contenitore che abbiamo visto sono 199 MB. Molto più piccolo del nostro contenitore Java originale.

STEP 6: creazione di un eseguibile più statico e creazione di un packaging in un'immagine semplice

Riassumendo, ciò che è stato fatto finora:

  1. È stata creata un'applicazione Spring Boot con un endpoint HTTP, /jibber
  2. Containerizzazione completata
  3. È stato creato un eseguibile nativo dell'applicazione mediante gli strumenti di creazione di immagini native per Maven
  4. È stato inserito in container l'eseguibile nativo

Sarebbe fantastico se potessimo ridurre ulteriormente le dimensioni dei container, perché i contenitori più piccoli sono più veloci da scaricare e iniziare. Con l'immagine nativa GraalVM puoi collegare staticamente le librerie di sistema all'eseguibile nativo generato. Se si crea un eseguibile nativo collegato in modo statico, è possibile creare il package dell'eseguibile nativo direttamente in un'immagine Docker vuota, anche nota come contenitore scratch.

Un'altra opzione è quella di produrre ciò che è noto come eseguibile nativo con collegamento prevalentemente statico. Questa operazione consente di eseguire il collegamento statico in tutte le librerie di sistema, ad eccezione della libreria C standard, glibc. Con un eseguibile nativo simile è possibile utilizzare un contenitore di piccole dimensioni, ad esempio Google's Distroless che contiene la libreria glibc, alcuni file standard e certificati di sicurezza SSL. Il contenitore Distroless standard ha una dimensione di circa 20 MB.

Si creerà un eseguibile con collegamento prevalentemente statico e quindi lo inserirà in un contenitore Distroless.

Abbiamo aggiunto un altro profilo Maven per creare questo eseguibile nativo con collegamenti per lo più statici. Questo profilo è denominato distroless. L'unica differenza tra questo profilo e quello utilizzato in precedenza, native, consiste nel passare un parametro, -H:+StaticExecutableWithDynamicLibC. Come si può supporre, ciò indica a native-image di creare un eseguibile nativo con collegamenti per lo più statici.

È possibile creare l'eseguibile nativo perlopiù collegato staticamente come riportato di seguito.

mvn package -Pdistroless

È sufficiente. L'eseguibile nativo generato si trova nella directory di destinazione jibber-distroless.

Ora lo inserisci in un contenitore Distroless. Il Dockerfile per eseguire questa operazione è disponibile nella directory native-image/containerisation/lab/02-smaller-containers/Dockerfile. Dai un'occhiata al contenuto del Dockerfile, che contiene commenti per spiegare ogni riga:

FROM gcr.io/distroless/base # Our base image, which is Distroless

ARG APP_FILE                # Everything else is the same :)
EXPOSE 8080

COPY ${APP_FILE} app
ENTRYPOINT ["/app"]

Per creare, eseguire le seguenti operazioni dal terminale:

docker build -f ./02-smaller-containers/Dockerfile \
             --build-arg APP_FILE=./target/jibber-distroless \
             -t localhost/jibber:distroless.01 .

Dai un'occhiata all'immagine Distroless appena costruita:

docker images | head -n2

Ora è possibile eseguire e testare come segue:

docker run --rm -d --name "jibber-distroless" -p 8080:8080 localhost/jibber:distroless.01

curl http://localhost:8080/jibber

Ottimo! Ha funzionato. Ma quanto è piccolo, o grande, il tuo contenitore? Utilizzare lo script per controllare le dimensioni dell'immagine:

echo $((`docker inspect -f "" localhost/jibber:distroless.01`/1024/1024))

La dimensione è di circa 107 MB! Quindi abbiamo rotto il contenitore di 92 MB. Un lungo cammino dalla nostra dimensione iniziale, per il contenitore Java, di circa 600 MB.

Conclusione

Ci auguriamo che tu abbia apprezzato questo laboratorio e apprendi qualche cosa lungo la strada. Abbiamo esaminato il modo in cui puoi containerizzare un'applicazione Java. Quindi abbiamo visto come convertire tale applicazione Java in un eseguibile nativo, che inizia notevolmente più velocemente dell'applicazione Java. Abbiamo quindi containerizzato l'eseguibile nativo e abbiamo visto che la dimensione dell'immagine Docker, con l'eseguibile nativo in esso contenuto, è molto più piccola dell'immagine Java Docker.

Infine, abbiamo esaminato il modo in cui possiamo costruire eseguibili nativi prevalentemente collegati staticamente con l'immagine nativa. Questi possono essere imballati in contenitori più piccoli, come Distroless, e ciò ci consentirà di ridurre ulteriormente le dimensioni dell'immagine Docker.

Per saperne di più

Altre risorse di apprendimento

Esplora altri laboratori su docs.oracle.com/learn o accedi a più contenuti di apprendimento gratuito sul canale Oracle Learning YouTube. Inoltre, visitare education.oracle.com/learning-explorer per diventare Oracle Learning Explorer.

Per la documentazione del prodotto, visitare il sito Oracle Help Center.