Ottimizzazione delle applicazioni Java

Questa guida descrive le ottimizzazioni per i servizi Cloud Run for Anthos scritte nel linguaggio di programmazione Java, insieme a informazioni di base per aiutarti a comprendere i compromessi coinvolti in alcune ottimizzazioni. Le informazioni in questa pagina integrano i suggerimenti di ottimizzazione generali, che si applicano anche a Java.

Le tradizionali applicazioni basate sul Web Java sono progettate per soddisfare richieste ad alta contemporaneità e a bassa latenza, tendenzialmente di applicazioni a lunga esecuzione. La stessa JVM ottimizza inoltre il codice di esecuzione nel tempo con JIT, in modo che i percorsi di accesso attivi siano ottimizzati e le applicazioni vengano eseguite in modo più efficiente nel tempo.

Molte delle best practice e delle ottimizzazioni in queste tradizionali applicazioni basate su Web Java ruotano su:

  • Gestione di richieste in parallelo (I/O basati su thread e non di blocco)
  • Riduzione della latenza di risposta utilizzando il pool di connessioni e il batching di funzioni non critiche, ad esempio l'invio di tracce e metriche ad attività in background.

Sebbene molte di queste ottimizzazioni tradizionali funzionino bene per le applicazioni a lunga esecuzione, potrebbero non funzionare altrettanto bene in un servizio Cloud Run for Anthos, che viene eseguito solo quando vengono gestite attivamente le richieste. In questa pagina vengono illustrate alcune ottimizzazioni e compromessi relativi a Cloud Run for Anthos che puoi utilizzare per ridurre i tempi di avvio e l'utilizzo della memoria.

Ottimizzazione dell'immagine del container

Ottimizzando l'immagine del container, puoi ridurre i tempi di caricamento e di avvio. Puoi ottimizzare l'immagine in base a:

  • Ridurre al minimo l'immagine del container
  • Come evitare l'utilizzo di jar in archivio nidificati
  • Utilizzo di Jib

Riduzione dell'immagine del container in corso

Per ulteriori informazioni su questo problema, consulta la pagina dei suggerimenti generali per ridurre al minimo il container. La pagina dei suggerimenti generali consiglia di ridurre i contenuti delle immagini container solo ai contenuti necessari. Ad esempio, verifica che l'immagine del container non contenga:

  • Codice sorgente
  • Elementi build di Maven
  • Strumenti di creazione
  • Directory Git
  • utilità/programma binari non utilizzati

Se stai creando il codice all'interno di un Dockerfile, utilizza la build a più fasi di Docker in modo che l'immagine container finale abbia solo il JRE e il file JAR dell'applicazione.

Evitare i file jar archiviati nella libreria

Alcuni framework molto utilizzati, come Spring Boot, creano un file di archiviazione dell'applicazione (JAR) che contiene ulteriori file JAR di libreria (jar nidificati). Questi file devono essere decompressi/decompressi durante il tempo di avvio e possono aumentare la velocità di avvio in Cloud Run for Anthos. Quando possibile, crea un sottile jar con librerie esterne: può essere automatizzato utilizzando Jib per containerizzare la tua applicazione.

Utilizzo di Jib

Utilizza il plug-in Jib per creare un container minimo e suddividere automaticamente l'archivio delle applicazioni. Jib funziona sia con Maven che con Gradle e funziona immediatamente con le applicazioni Spring Boot. Alcuni framework applicativi potrebbero richiedere configurazioni Jib aggiuntive.

Ottimizzazioni della JVM

L'ottimizzazione della JVM per un servizio Cloud Run for Anthos può migliorare le prestazioni e l'utilizzo della memoria.

Utilizzo delle versioni JVM container-aware

Nelle VM e nelle macchine, per le allocazioni di CPU e memoria, JVM riconosce la CPU e la memoria che può utilizzare da località note, ad esempio Linux, /proc/cpuinfo e /proc/meminfo. Tuttavia, in esecuzione in un container, i vincoli di CPU e memoria sono archiviati in /proc/cgroups/.... La versione precedente di JDK continua a cercare in /proc anziché in /proc/cgroups, il che può comportare un utilizzo di CPU e memoria maggiore di quello assegnato. Questo può causare:

  • Un numero eccessivo di thread perché le dimensioni del pool di thread sono configurate da Runtime.availableProcessors()
  • Un heap massimo predefinito che supera il limite di memoria del container. La JVM utilizza in modo aggressivo la memoria prima di garbage collection. Ciò può causare facilmente il superamento del limite di memoria del container e comportare OOMKilled.

Quindi, utilizza una versione JVM sensibile al container. Per impostazione predefinita, le versioni OpenJDK maggiori o uguali alla versione di 8u192 sono basate su container.

Informazioni sull'utilizzo della memoria JVM

L'utilizzo della memoria JVM è composto da utilizzo della memoria nativa e heap. La memoria di lavoro dell'applicazione si trova generalmente nell'heap. La dimensione dell'heap è vincolata dalla configurazione massima dell'heap. Con un'istanza di Cloud Run for Anthos da 256 MB di RAM, non puoi assegnare tutti i 256 MB all'heap massimo, perché la JVM e il sistema operativo richiedono anche memoria nativa, ad esempio stack di thread, cache di codice, buffer di file, buffer e così via. Se l'applicazione sta ricevendo OOMKilled e devi conoscere l'utilizzo della memoria JVM (rilevamento di memoria nativa + heap). Se la tua applicazione riceve OOMKilled, non potrà stampare le informazioni. In questo caso, esegui prima l'applicazione con più memoria in modo che possa generare l'output in modo corretto.

Il monitoraggio della memoria nativa non può essere attivato tramite la variabile di ambiente JAVA_TOOL_OPTIONS. Devi aggiungere l'argomento di avvio della riga di comando di Java al punto di ingresso dell'immagine del container, in modo che l'applicazione venga avviata con i seguenti argomenti:

java -XX:NativeMemoryTracking=summary \
  -XX:+UnlockDiagnosticVMOptions \
  -XX:+PrintNMTStatistics \
  ...

L'utilizzo della memoria nativa può essere stimato in base al numero di classi da caricare. Valuta l'utilizzo di un Calcolatore per le memoria Java open source per stimare le esigenze di memoria.

Disattivazione del compilatore dell'ottimizzazione

Per impostazione predefinita, JVM prevede diverse fasi di compilazione JIT. Anche se queste fasi migliorano l'efficienza della tua applicazione nel tempo, possono anche aumentare l'overhead in termini di utilizzo della memoria e aumentare il tempo di avvio.

Per le applicazioni serverless a breve esecuzione (ad esempio, le funzioni), considera la possibilità di disattivare le fasi di ottimizzazione per gestire l'efficienza a lungo termine e ridurre i tempi di avvio.

Per un servizio Cloud Run for Anthos, configura la variabile ambientale:

JAVA_TOOL_OPTIONS="-XX:+TieredCompilation -XX:TieredStopAtLevel=1"

Utilizzo della condivisione dei dati della classe dell'applicazione

Per ridurre ulteriormente il tempo e l'utilizzo della memoria JIT, considera la possibilità di utilizzare la condivisione dei dati delle classi di applicazioni (AppCDS) per condividere le classi Java compilate in anticipo come archivio. L'archivio AppCDS può essere riutilizzato quando si avvia un'altra istanza della stessa applicazione Java. La JVM può riutilizzare i dati precalcolati dall'archivio, riducendo il tempo di avvio.

Per l'utilizzo di AppCDS si applicano le seguenti considerazioni:

  • L'archivio AppCDS da riutilizzare deve essere riprodotto esattamente dalla stessa distribuzione, versione e architettura di OpenJDK utilizzata in origine per la produzione.
  • Devi eseguire l'applicazione almeno una volta per generare l'elenco di classi da condividere, quindi utilizzare tale elenco per generare l'archivio AppCDS.
  • La copertura delle classi dipende dal percorso del codice eseguito durante l'esecuzione dell'applicazione. Per aumentare la copertura, attiva in modo programmatico più percorsi di codice.
  • Per generare questo elenco di corsi, l'applicazione deve uscire correttamente. Valuta l'implementazione di un flag dell'applicazione che sia utilizzato per indicare la generazione dell'archivio AppCDS e che possa uscire immediatamente.
  • L'archivio AppCDS può essere riutilizzato solo se avvii nuove istanze esattamente nello stesso modo in cui sono state generate.
  • L'archivio AppCDS funziona solo con un normale pacchetto di file JAR; non puoi utilizzare i JAR nidificati.

Esempio di Spring Boot con un file JAR ombreggiato

Le applicazioni Spring Boot utilizzano per impostazione predefinita un jar uber nidificato, che non funziona per AppCDS. Quindi, se utilizzi AppCDS, devi creare un JAR ombreggiato. Ad esempio, se utilizzi Maven e il plug-in Maven Shade:

<build>
  <finalName>helloworld</finalName>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-shade-plugin</artifactId>
      <configuration>
        <keepDependenciesWithProvidedScope>true</keepDependenciesWithProvidedScope>
        <createDependencyReducedPom>true</createDependencyReducedPom>
        <filters>
          <filter>
            <artifact>*:*</artifact>
            <excludes>
              <exclude>META-INF/*.SF</exclude>
              <exclude>META-INF/*.DSA</exclude>
              <exclude>META-INF/*.RSA</exclude>
            </excludes>
          </filter>
        </filters>
      </configuration>
      <executions>
        <execution>
          <phase>package</phase>
          <goals><goal>shade</goal></goals>
          <configuration>
            <transformers>
              <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                <resource>META-INF/spring.handlers</resource>
              </transformer>
              <transformer implementation="org.springframework.boot.maven.PropertiesMergingResourceTransformer">
                <resource>META-INF/spring.factories</resource>
              </transformer>
              <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                <resource>META-INF/spring.schemas</resource>
              </transformer>
              <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
              <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                <mainClass>${mainClass}</mainClass>
              </transformer>
            </transformers>
          </configuration>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

Se il tuo JAR ombreggiato contiene tutte le dipendenze, puoi produrre un archivio semplice durante la creazione dei container utilizzando un Dockerfile:

# Use Docker's multi-stage build
FROM adoptopenjdk:11-jre-hotspot as APPCDS

COPY target/helloworld.jar /helloworld.jar

# Run the application, but with a custom trigger that exits immediately.
# In this particular example, the application looks for the '--appcds' flag.
# You can implement a similar flag in your own application.
RUN java -XX:DumpLoadedClassList=classes.lst -jar helloworld.jar --appcds=true

# From the captured list of classes (based on execution coverage),
# generate the AppCDS archive file.
RUN java -Xshare:dump -XX:SharedClassListFile=classes.lst -XX:SharedArchiveFile=appcds.jsa --class-path helloworld.jar

FROM adoptopenjdk:11-jre-hotspot

# Copy both the JAR file and the AppCDS archive file to the runtime container.
COPY --from=APPCDS /helloworld.jar /helloworld.jar
COPY --from=APPCDS /appcds.jsa /appcds.jsa

# Enable Application Class-Data sharing
ENTRYPOINT java -Xshare:on -XX:SharedArchiveFile=appcds.jsa -jar helloworld.jar

Disattivazione verifica del corso

Quando la JVM carica le classi in memoria per l'esecuzione, verifica che la classe non sia manomessa e non abbia modifiche dannose o danneggiamenti. Se la tua pipeline di distribuzione del software è considerata attendibile (ad esempio, puoi verificare e convalidare ogni output), se puoi fidarti completamente del bytecode nell'immagine del container e se l'applicazione non carica le classi da origini remote arbitrarie, potresti prendere in considerazione la disattivazione della verifica. La disattivazione della verifica può migliorare la velocità di avvio se al momento dell'avvio viene caricato un numero elevato di classi.

Per un servizio Cloud Run for Anthos, configura la variabile ambientale:

JAVA_TOOL_OPTIONS="-noverify"

Riduzione della dimensione dello stack di thread

La maggior parte delle applicazioni web Java è basata su thread per connessione. Ogni thread Java utilizza memoria nativa (non nell'heap). Tale file è noto come stack di thread e ha un valore predefinito di 1 MB per thread. Se la tua applicazione gestisce 80 richieste in parallelo, potrebbe avere almeno 80 thread, che si traduce in 80 MB di spazio di stack di thread utilizzati. La memoria si aggiunge alle dimensioni dello heap. Il valore predefinito può essere maggiore del necessario. Puoi ridurre le dimensioni dello stack di thread.

Se lo riduci troppo, vedrai java.lang.StackOverflowError. Puoi creare un profilo per la tua applicazione e trovare le dimensioni ottimali dello stack di thread da configurare.

Per un servizio Cloud Run for Anthos, configura la variabile ambientale:

JAVA_TOOL_OPTIONS="-Xss256k"

Riduzione dei thread

Puoi ottimizzare la memoria riducendo il numero di thread, utilizzando strategie reattive non di blocco ed evitando attività in background.

Riduzione del numero di thread

Ogni thread Java potrebbe aumentare l'utilizzo della memoria a causa dello stack di thread. Cloud Run for Anthos consente un massimo di 80 richieste in parallelo. Con il modello di thread per connessione, sono necessari al massimo 80 thread per gestire tutte le richieste in parallelo. La maggior parte dei server web e dei framework consente di configurare il numero massimo di thread e connessioni. Ad esempio, in Spring Boot puoi limitare il numero massimo di connessioni nel file applications.properties:

server.tomcat.max-threads=80

Scrittura di codice reattivo non bloccante per ottimizzare memoria e avvio

Per ridurre realmente il numero di thread, valuta la possibilità di adottare un modello di programmazione reattiva che non blocchi, in modo da ridurre significativamente il numero di thread mentre gestisci più richieste simultanee. I framework di applicazioni come Spring Boot con Webflux, Micronaut e Quarkus supportano le applicazioni web reattive.

I framework reattivi come Spring Boot con Webflux, Micronaut, Quarkus in genere hanno tempi di avvio più rapidi.

Se continui a scrivere il codice di blocco in un framework non di blocco, le percentuali di velocità effettiva e di errore saranno notevolmente peggiori in un servizio Cloud Run for Anthos. Questo perché i framework non di blocco avranno solo pochi thread, ad esempio 2 o 4. Se il codice si blocca, è in grado di gestire pochissime richieste in parallelo.

Questi framework non di blocco possono anche eseguire l'offload del codice di blocco in un pool di thread illimitato. Ciò significa che, sebbene possa accettare molte richieste in parallelo, il codice di blocco verrà eseguito in nuovi thread. Se i thread si accumulano in modo illimitato, esaurirai la risorsa della CPU e inizierai a thrash. La latenza ne risentirà. Se utilizzi un framework non di blocco, assicurati di comprendere i modelli di pool di thread e di associare i pool di conseguenza.

Evitare attività in background

Cloud Run for Anthos limita una CPU dell'istanza quando questa istanza non riceve più richieste. I carichi di lavoro tradizionali con attività in background richiedono particolare attenzione durante l'esecuzione in Cloud Run for Anthos.

Ad esempio, se raccogli metriche relative alle applicazioni e raggruppi le metriche in background per inviarle periodicamente, queste non verranno inviate quando la CPU viene limitata. Se la tua applicazione riceve costantemente richieste, potresti notare meno problemi. Se la tua applicazione ha un QPS basso, l'attività in background potrebbe non essere mai eseguita.

Alcuni pattern noti che sono indicati in background a cui devi prestare attenzione:

  • Pool di connessione JDBC: le operazioni di pulizia e i controlli della connessione di solito vengono eseguiti in background
  • Mittenti con tracce distribuite: in genere le tracce distribuite vengono raggruppate e inviate periodicamente o quando il buffer è esaurito in background.
  • Mittenti per le metriche: in genere le metriche vengono raggruppate e inviate periodicamente in background.
  • Per Spring Boot, qualsiasi metodo annotato con l'annotazione @Async
  • Timer: qualsiasi attivatore basato su timer (ad es. L'annotazione Spring-PoolExecutor, Quartz o @Scheduled Spring) potrebbe non essere eseguita quando le CPU vengono limitate.
  • Ricevitori di messaggi. Ad esempio, i client pull di flussi Pub/Sub, i client JMS o i client Kafka, di solito vengono eseguiti in thread in background senza bisogno di richieste. Questi elementi non funzioneranno quando la tua applicazione non ha richieste. La ricezione di messaggi di questo tipo non è consigliata in Cloud Run for Anthos.

Ottimizzazioni delle applicazioni

Nel codice del servizio Cloud Run for Anthos, puoi anche ottimizzare per tempi di avvio e utilizzo della memoria più rapidi.

Riduzione delle attività di avvio

Le tradizionali applicazioni basate su Web Java possono completare molte attività durante l'avvio, ad esempio il precaricamento dei dati, il riscaldamento della cache, la definizione di pool di connessioni e così via. Queste attività, se eseguite in sequenza, possono essere lente. Tuttavia, se vuoi che vengano eseguiti in parallelo, devi aumentare il numero di core della CPU.

Al momento Cloud Run for Anthos invia una richiesta utente reale per attivare un'istanza di avvio a freddo. Gli utenti che hanno una richiesta assegnata a un'istanza appena avviata potrebbero riscontrare lunghi ritardi. Attualmente Cloud Run for Anthos non dispone di un controllo di idoneità per evitare l'invio di richieste ad applicazioni non pronte.

Utilizzo del pool di connessioni

Se utilizzi pool di connessioni, tieni presente che questi pool possono eliminare le connessioni non necessarie in background (vedi Evitare le attività in background). Se la tua applicazione ha un QPS basso e può tollerare un'elevata latenza, valuta la possibilità di aprire e chiudere le connessioni per richiesta. Se la tua applicazione presenta un numero elevato di QPS, gli sfratti in background possono continuare a essere eseguiti finché ci sono richieste attive.

In entrambi i casi, l'accesso al database dell'applicazione sarà limitato dalle connessioni massime consentite dal database. Calcola il numero massimo di connessioni che puoi stabilire per ogni istanza di Cloud Run for Anthos e configura le istanze massime di Cloud Run for Anthos in modo che il numero massimo di istanze per cui le connessioni per istanza sia inferiore al numero massimo di connessioni consentite.

Utilizzo di Spring Boot

Se utilizzi Spring Boot, devi prendere in considerazione le seguenti ottimizzazioni

Utilizzo di Spring Boot versione 2.2 o successiva

A partire dalla versione 2.2, Spring Boot è stato notevolmente ottimizzato per la velocità di avvio. Se utilizzi versioni di Spring Boot inferiori alla 2.2, valuta l'upgrade o applica le ottimizzazioni individuali manualmente.

Utilizzo dell'inizializzazione lento

Esiste un flag di inizializzazione Lazy globale che può essere attivato in Spring Boot 2.2 e versioni successive. In questo modo miglioreremo la velocità di avvio, ma con il compromesso che la prima richiesta potrebbe avere una latenza maggiore, perché dovrà attendere l'inizializzazione dei componenti per la prima volta.

Puoi attivare l'inizializzazione lento in application.properties:

spring.main.lazy-initialization=true

Altrimenti, utilizzando una variabile d'ambiente:

SPRING_MAIN_LAZY_INITIALIZATIION=true

Tuttavia, se utilizzi istanze minime, l'inizializzazione non sarà più utile, perché l'inizializzazione avrebbe dovuto avvenire all'avvio dell'istanza minima.

Evitare la scansione dei corsi

L'analisi delle classi causerà letture di dischi aggiuntive in Cloud Run for Anthos, perché in Cloud Run for Anthos, in genere l'accesso al disco è più lento di una normale macchina. Assicurati che la scansione dei componenti sia limitata o completamente evitata. Valuta la possibilità di utilizzare l'indicizzatore del contesto di Spring per pregenerare un indice. Il miglioramento della velocità di avvio varierà a seconda dell'applicazione.

Ad esempio, nel tuo Maven pom.xml aggiungi la dipendenza dell'indicista (si tratta di un elaboratore di annotazioni):

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context-indexer</artifactId>
  <optional>true</optional>
</dependency>

Utilizzo degli strumenti per sviluppatori Spring Boot non in produzione

Se utilizzi lo strumento Spring Boot Developer durante lo sviluppo, assicurati che non sia incluso nell'immagine del container di produzione. Ciò può verificarsi se hai creato l'applicazione Spring Boot senza i plug-in della build Spring Boot (ad esempio, utilizzando il plug-in Shade o Jib per containerizzare).

In questi casi, assicurati che lo strumento di compilazione escluda esplicitamente lo strumento Spring Boot Dev. In alternativa, disattiva lo strumento Spring Boot Developer esplicitamente.

Passaggi successivi

Per ulteriori suggerimenti, consulta