Ottimizzazione delle applicazioni Java

Questa guida descrive le ottimizzazioni per i servizi di pubblicazione Knative scritti nel linguaggio di programmazione Java, insieme a informazioni di base per aiutarti a comprendere i compromessi coinvolti in alcune delle ottimizzazioni. La le informazioni riportate in questa pagina integrano il suggerimenti generali per l'ottimizzazione, che si applicano anche a Java.

Le applicazioni web Java tradizionali sono progettate per gestire richieste con alta concorrenza e bassa latenza e tendono a essere applicazioni a lungo termine. JVM ottimizza anche il codice di esecuzione nel tempo con JIT, in modo che i percorsi sono ottimizzate e le applicazioni vengono eseguite in modo più efficiente nel tempo.

Molte delle best practice e delle ottimizzazioni di queste applicazioni web basate su Java tradizionali ruotano attorno a:

  • Gestione di richieste in parallelo (I/O sia basati su thread che non bloccanti)
  • Riduzione della latenza di risposta mediante il pooling delle connessioni e l'aggregazione delle funzioni non critiche, ad esempio l'invio di tracce e metriche alle attività in background.

Sebbene molte di queste ottimizzazioni tradizionali funzionino bene per le applicazioni a lungo termine, potrebbero non funzionare altrettanto bene in un servizio Knative serving, che viene eseguito solo quando vengono pubblicate attivamente le richieste. In questa pagina vengono illustrati alcuni compromessi e ottimizzazioni per Knative serving che puoi utilizzare per ridurre il tempo di avvio e l'utilizzo della memoria.

Ottimizzare l'immagine container

Ottimizzando l'immagine del contenitore, puoi ridurre i tempi di caricamento e di avvio. Puoi ottimizzare l'immagine:

  • Minimizzare l'immagine del contenitore
  • Evitare l'utilizzo di JAR di archivi di librerie nidificati
  • Uso di Jib

Ridurre al minimo l'immagine container

Per ulteriori informazioni su questo problema, consulta la pagina dei suggerimenti generali sulla minimizzazione del contenitore. Nella pagina dei suggerimenti generali viene consigliato di ridurre i contenuti delle immagini del contenitore solo a ciò che è necessario. Ad esempio, assicurati che la tua immagine container non contiene :

  • Codice sorgente
  • Artefatti build Maven
  • Strumenti di creazione
  • Directory Git
  • Programmi binari inutilizzati

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

Evita i JAR degli archivi di librerie nidificati

Alcuni framework popolari, come Spring Boot, creano un file di archivio dell'applicazione (JAR) contenente file JAR di librerie aggiuntive (JAR nidificati). Questi file richiedono non pacchettizzati/decompressi durante il tempo di avvio e possono aumentare la velocità di avvio in Knative serving. Se possibile, crea un JAR sottile con librerie: questo può essere automatizzato utilizzando Jib per containerizzare la tua applicazione

Uso di Jib

Utilizza il plug-in Jib per creare un il container minimo e si appiattisce automaticamente l'archivio delle applicazioni. Lavorazione fiocco con Maven e Gradle e funziona con le applicazioni Spring Boot . Alcuni framework di applicazioni potrebbero richiedere configurazioni Jib aggiuntive.

Ottimizzazioni JVM

L'ottimizzazione della JVM per un servizio Knative serving può migliorare le prestazioni e l'utilizzo della memoria.

Utilizzo di versioni JVM consapevoli del contenitore

Nelle VM e nelle macchine, per le allocazioni di CPU e memoria, la JVM comprende il comportamento della CPU e memoria che può utilizzare da località note, ad esempio in Linux, /proc/cpuinfo e /proc/meminfo. Tuttavia, quando l'esecuzione avviene in un container, I vincoli di CPU e memoria vengono archiviati in /proc/cgroups/.... Le versioni precedenti delJDK continuano a cercare in /proc anziché in /proc/cgroups, il che può comportare un utilizzo maggiore di CPU e memoria rispetto a quanto assegnato. Ciò 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. JVM utilizza in modo aggressivo la memoria prima che venga eliminata la garbage collection. Ciò può causare il container per superare il limite di memoria del container e ottenere OOMKilled.

Quindi, utilizza una versione JVM sensibile al container. Le versioni di OpenJDK superiori o uguali alla versione 8u192 sono compatibili con i contenitori per impostazione predefinita.

Informazioni sull'utilizzo della memoria JVM

L'utilizzo della memoria JVM è composto dall'utilizzo della memoria nativa e dall'utilizzo dell'heap. La memoria di lavoro dell'applicazione si trova in genere nell'heap. Le dimensioni dell'heap sono vincolato dalla configurazione dell'heap massimo. Con un'istanza Knative che serve 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 dei thread, cache di codice, handle file, buffer e così via. Se la tua applicazione viene uccisa per OOM e devi conoscere l'utilizzo della memoria JVM (memoria nativa + heap), attiva il monitoraggio della memoria nativa per visualizzare gli utilizzi al termine dell'applicazione. Se l'applicazione viene uccisa per OOM, non potrà stampare le informazioni. In questo caso, eseguire prima l'applicazione con più memoria, in modo che possa generare l'output.

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 Java all'entrypoint dell'immagine del container in modo che l'applicazione venga avviata con questi 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 la possibilità di utilizzare un calcolatore della memoria Java open source per stimare le esigenze di memoria.

Disattivazione del compilatore di ottimizzazione

Per impostazione predefinita, la JVM ha diverse fasi di compilazione JIT. Sebbene queste fasi migliorino l'efficienza dell'applicazione nel tempo, possono anche aggiungere un overhead all'utilizzo della memoria e aumentare il tempo di avvio.

Per le applicazioni serverless a breve esecuzione (ad esempio, le funzioni), considera disattivare le fasi di ottimizzazione per scambiare l'efficienza a lungo termine con una riduzione all'avvio.

Per un servizio Knative serving, configura la variabile di ambiente:

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

Utilizzo della condivisione dei dati delle classi di applicazioni

Per ridurre ulteriormente il tempo e l'utilizzo della memoria del JIT, ti consigliamo di utilizzare la condivisione dei dati delle classi dell'applicazione (AppCDS) per condividere le classi Java compilate in anticipo come archivio. L'archivio AppCDS può essere riutilizzato quando si avvia un'altra istanza dello stesso file Java un'applicazione. La JVM può riutilizzare i dati precomputati dall'archivio, il che consente di ridurre i tempi di avvio.

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

  • L'archivio AppCDS da riutilizzare deve essere riprodotto esattamente con la stessa distribuzione, versione e architettura OpenJDK utilizzata inizialmente per produrlo.
  • Devi eseguire l'applicazione almeno una volta per generare l'elenco delle classi da condividere e poi utilizzare questo elenco per generare l'archivio AppCDS.
  • La copertura delle classi dipende dal codepath eseguito durante per l'esecuzione dell'applicazione. Per aumentare la copertura, attiva in modo programmatico un numero maggiore codepath.
  • L'applicazione deve uscire correttamente per generare questo elenco di corsi. Valuta la possibilità di implementare un flag di applicazione utilizzato per indicare la generazione dell'archivio AppCDS, in modo che possa uscire immediatamente.
  • L'archivio AppCDS può essere riutilizzato solo se avvii nuove istanze nello stesso modo in cui è stato generato l'archivio.
  • L'archivio AppCDS funziona solo con un normale pacchetto di file JAR; non puoi utilizzare JAR nidificati.

Esempio di Spring Boot che utilizza un file JAR ombreggiato

Per impostazione predefinita, le applicazioni Spring Boot utilizzano un JAR uber nidificato, che non funziona per AppCDS. Pertanto, se utilizzi AppCDS, devi creare un file JAR ombreggiato. Ad esempio, utilizzando 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 JAR ombreggiato contiene tutte le dipendenze, puoi generare un un semplice archivio durante la creazione del container utilizzando 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 della verifica del corso

Quando la JVM carica le classi in memoria per l'esecuzione, verifica che la classe non è manomesso e non presenta modifiche o corruzioni dannose. Se il software la pipeline di distribuzione sia affidabile (ad esempio, puoi verificare e convalidare ogni output), se puoi considerare attendibile il bytecode dell'immagine container e dell'applicazione non carica classi da origini remote arbitrarie, allora puoi considerare la disattivazione della verifica. La disattivazione della verifica può migliorare la velocità di avvio se viene caricato un numero elevato di corsi al momento dell'avvio.

Per un servizio Knative serving, configura la variabile di ambiente:

JAVA_TOOL_OPTIONS="-noverify"

Riduzione delle dimensioni dello stack di thread

La maggior parte delle applicazioni web Java è basata su thread per connessione. Ogni thread Java consuma memoria nativa (non nell'heap). Questo è noto come stack del thread e il valore predefinito è 1 MB per thread. Se la tua applicazione gestisce 80 richieste contemporaneamente, potrebbe avere almeno 80 thread, il che si traduce in 80 MB di spazio dello stack del thread utilizzato. La memoria si aggiunge alle dimensioni dell'heap. Il valore predefinito potrebbe essere più grande del necessario. Puoi ridurre le dimensioni della pila di thread.

Se riduci troppo, vedrai java.lang.StackOverflowError. Tu puoi profilare la tua applicazione e trovare la dimensione ottimale dello stack di thread da configurare.

Per un servizio Knative serving, configura la variabile di ambiente:

JAVA_TOOL_OPTIONS="-Xss256k"

Riduzione dei thread

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

Riduzione del numero di thread

Ogni thread Java può aumentare l'utilizzo della memoria a causa della pila thread. Knative serving consente un massimo di 80 elementi in parallelo richieste. Con il modello 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 la memoria e l'avvio

Per ridurre davvero il numero di thread, ti consigliamo di adottare un modello di programmazione reattiva non bloccante, in modo da ridurre notevolmente il numero di thread e gestire più richieste simultanee. 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 codice bloccante in un framework non bloccante, il throughput e i tassi di errore peggioreranno notevolmente in un servizio di pubblicazione Knative. Questo perché i framework non bloccanti avranno solo pochi thread, ad esempio 2 o 4. Se il codice è in blocco, può gestire pochissime richieste contemporaneamente.

Questi framework non bloccanti possono anche scaricare il codice di blocco in un pool di thread illimitato, il che significa che, sebbene possa accettare molte richieste contemporaneamente, 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 eseguire il thrash. La latenza ne sarà gravemente compromessa. Se utilizzi un framework non bloccante, assicurati di comprendere i modelli di pool di thread e di associarli di conseguenza.

Come evitare attività in background

Knative serving limita la CPU di un'istanza quando questa non riceve più richieste. Carichi di lavoro tradizionali con le attività in background richiedono un'attenzione particolare quando vengono eseguite in Knative serving.

Ad esempio, se stai raccogliendo metriche dell'applicazione e raggruppandole in batch in background per l'invio periodico, queste metriche non verranno inviate La CPU è limitata. Se la tua applicazione riceve costantemente richieste, puoi meno problemi. Se la tua applicazione ha un QPS basso, l'attività in background potrebbe non essere mai eseguita.

Alcuni pattern ben noti in background a cui devi prestare attenzione:

  • Pool di connessioni JDBC: le pulizie e i controlli della connessione di solito vengono eseguiti in background
  • Mittenti di tracce distribuite: in genere le tracce distribuite vengono raggruppate e inviate periodicamente o quando il buffer è pieno in background.
  • Mittenti delle metriche: in genere, le metriche vengono raggruppate e inviate periodicamente in background.
  • Per Spring Boot, tutti i metodi annotati con l'annotazione @Async
  • Timer: qualsiasi attivatore basato su timer (ad es. L'annotazione ScheduleThreadPoolExecutor, Quartz o @Scheduled Spring) non può essere eseguita quando le CPU sono limitate.
  • Destinatari dei messaggi: ad esempio, i client di pull del flusso Pub/Sub, i client JMS o i client Kafka, di solito vengono eseguiti nei thread in background senza bisogno di richieste. Questi non funzioneranno se la tua applicazione non ha richieste. La ricezione dei messaggi in questo modo non è consigliata in Knative serving.

Ottimizzazioni delle applicazioni

Nel codice del servizio Knative serving, puoi anche ottimizzare per velocizzare tempi di avvio e utilizzo della memoria.

Ridurre le attività di avvio

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

Al momento, Knative serving invia una richiesta di un utente reale per attivare un'istanza di avvio a freddo. Gli utenti con una richiesta assegnata a un'istanza appena avviata possono che subiscano lunghi ritardi. Al momento Knative serving non dispone di un controllo di "idoneità" per evitare di inviare richieste ad applicazioni non pronte.

Utilizzo del pool di connessioni

Se utilizzi pool di connessioni, tieni presente che i pool di connessioni potrebbero essere rimossi non necessari connessioni in background (vedi Evitare attività in background). Se la tua applicazione ha un valore QPS basso e può tollerare un'elevata latenza, prendi in considerazione l'apertura e la chiusura di connessioni su richiesta. Se la tua applicazione ha un QPS elevato, le espulsioni in background potrebbero continuare a essere eseguite finché ci sono richieste attivate.

In entrambi i casi, l'accesso al database dell'applicazione verrà sottoposto a colli di bottiglia il numero massimo di connessioni consentite dal database. Calcolare il numero massimo di connessioni che puoi definire per istanza di Knative serving configurare il numero massimo di istanze Knative serving in modo che il numero massimo di istanze per il numero di connessioni per istanza sia inferiore al numero massimo connessioni consentite.

Utilizzo di Spring Boot

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

Utilizzare Spring Boot versione 2.2 o successive

A partire dalla versione 2.2, Spring Boot è stato fortemente ottimizzato per le startup la velocità. Se utilizzi versioni precedenti alla 2.2 di Spring Boot, valuta la possibilità di eseguire l'upgrade, oppure applica manualmente le singole ottimizzazioni.

Utilizzo dell'inizializzazione lazy

È presente un flag di inizializzazione lazy globale che può essere attivato in Spring Boot 2.2 e successive. Questo migliorerà la velocità di avvio, ma con un compromesso che la prima richiesta potrebbe avere una latenza maggiore perché dovrà attendere da inizializzare per la prima volta.

Puoi attivare l'inizializzazione lazy in application.properties:

spring.main.lazy-initialization=true

In alternativa, utilizzando una variabile di ambiente:

SPRING_MAIN_LAZY_INITIALIZATIION=true

Tuttavia, se utilizzi il numero minimo di istanze, l'inizializzazione lazy non poiché l'inizializzazione dovrebbe essersi verificata all'avvio dell'istanza minima.

Evitare la scansione dei corsi

La scansione delle classi causerà ulteriori letture del disco in Knative serving in Knative serving, l'accesso al disco è generalmente più lento rispetto a una macchina normale. Assicurati che la ricerca componenti sia limitata o completamente evitata. Valuta l'uso di Indicizzatore di contesto Spring, per pregenerare un indice. L'eventuale miglioramento della velocità di avvio dipenderà dall'applicazione.

Ad esempio, nel pom.xml Maven, aggiungi la dipendenza dell'indicizzatore (in realtà è un processore di annotazioni):

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

Utilizzo di strumenti per sviluppatori Spring Boot non in produzione

Se utilizzi Strumento per sviluppatori Spring Boot durante lo sviluppo, assicurati che non sia pacchettizzato nel container di produzione dell'immagine. Questo può accadere se hai creato l'applicazione Spring Boot senza Plug-in di build Spring Boot (ad esempio, utilizzando il plug-in Shade o Jib per containerizzare).

In questi casi, assicurati che lo strumento di creazione escluda lo strumento di sviluppo Spring Boot in modo esplicito. Oppure disattivi esplicitamente lo strumento per sviluppatori Spring Boot.

Passaggi successivi

Per altri suggerimenti, vedi