Ottimizza le applicazioni Java per Cloud Run

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

Le applicazioni tradizionali Java basate sul web sono progettate per gestire richieste con con contemporaneità e bassa latenza e tendono ad essere applicazioni a lunga esecuzione. 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 in queste best practice Java un'applicazione basata sul web ruota intorno a:

  • Gestione di richieste in parallelo (I/O sia basati su thread che non bloccanti)
  • Riduzione della latenza di risposta utilizzando il pool di connessioni e il raggruppamento in batch non critici come l'invio di tracce e metriche alle attività in background.

Sebbene molte di queste ottimizzazioni tradizionali funzionino per applicazioni a lunga esecuzione, potrebbero non funzionare altrettanto bene in Cloud Run che viene eseguito solo se le richieste vengono pubblicate attivamente. Questa pagina ti porta alcune ottimizzazioni e vari compromessi per Cloud Run. che puoi utilizzare per ridurre i tempi di avvio e l'utilizzo della memoria.

Usa il boost CPU all'avvio per ridurre la latenza di avvio

Puoi abilitare il booster della CPU all'avvio per aumentare temporaneamente l'allocazione della CPU durante l'avvio dell'istanza in modo da per ridurre la latenza di avvio.

Le metriche di Google hanno dimostrato che le app Java sono utili se utilizzano CPU di avvio che può ridurre i tempi di avvio fino al 50%.

Ottimizza l'immagine container

Ottimizzando l'immagine container, puoi ridurre i tempi di caricamento e avvio. Tu puoi ottimizzare l'immagine:

  • Ridurre a icona l'immagine container
  • Evitare l'uso di JAR di archivi di librerie nidificate
  • Uso di Jib

Riduci a icona l'immagine container

Consulta la pagina dei suggerimenti generali su riducendo al minimo il container contesto in merito a questo problema. La pagina dei suggerimenti generali consiglia di ridurre i contenuti delle immagini solo a ciò che serve. 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 da un Dockerfile, usa Docker a più fasi in modo che l'immagine container finale contenga solo il JRE e il JAR dell'applicazione .

Evita i JAR negli archivi di librerie nidificati

Alcuni framework popolari, come Spring Boot, creano un archivio di applicazioni (JAR) contenente file JAR della libreria aggiuntivi (JAR nidificati). Questi file richiedono non pacchettizzati/decompressi durante l'avvio, il che può avere un impatto negativo la velocità di avvio in Cloud Run. Quindi, se possibile, crea un thin JAR con librerie esterne: questa operazione può essere automatizzata utilizzando Jib per containerizzare la tua applicazione

Usa 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 dell'applicazione potrebbero richiedere configurazioni Jib aggiuntive.

Ottimizzazioni JVM

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

Utilizza versioni JVM sensibili ai container

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/.... Versione precedente di il JDK continua a cercare in /proc anziché in /proc/cgroups, il che può comporta un utilizzo di CPU e memoria maggiore di quello assegnato. Ciò può causare:

  • Un numero eccessivo di thread perché la dimensione del pool di thread è configurata 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 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. Versioni OpenJDK maggiori o uguali a La versione 8u192 è sensibile al container per impostazione predefinita.

Comprendere l'utilizzo della memoria JVM

L'utilizzo della memoria JVM è costituito dall'utilizzo della memoria nativa e dall'utilizzo di heap. Il tuo la memoria di lavoro dell'applicazione si trova solitamente nell'heap. Le dimensioni dell'heap sono vincolato dalla configurazione dell'heap massimo. Con 256 MB di RAM di Cloud Run non è possibile assegnare tutti i 256 MB all'heap massimo, perché la JVM e la Il sistema operativo richiede anche memoria nativa, ad esempio stack di thread, cache di codice handle di file, buffer, ecc. Se la tua applicazione riceve OOMKilled e devi conoscere l'utilizzo della memoria JVM (memoria nativa + heap), attiva la memoria nativa Monitoraggio per vedere gli utilizzi dopo l'uscita dall'applicazione. Se la tua applicazione o che viene arrestata OOM, non sarà più in grado di stampare le informazioni. In questo caso, eseguire prima l'applicazione con più memoria, in modo che possa generare l'output.

Il monitoraggio della memoria nativo non può essere attivato tramite JAVA_TOOL_OPTIONS variabile di ambiente. Devi aggiungere il comando Java al punto di ingresso dell'immagine container, in modo che viene 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 modello open source Calcolatrice di memoria Java per stimare le esigenze di memoria.

Disattiva il compilatore dell'ottimizzazione

Per impostazione predefinita, la JVM prevede diverse fasi della compilazione JIT. Sebbene queste fasi migliorare l'efficienza dell'applicazione nel tempo, possono anche aumentare l'overhead e aumenta 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 Cloud Run, configura la variabile di ambiente:

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

Utilizza la condivisione dei dati delle classi dell'applicazione

Per ridurre ulteriormente il tempo di JIT e l'utilizzo della memoria, valuta la possibilità di usare condivisione dei dati delle classi di applicazioni (AppCDS) per condividono 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 precalcolati dell'archivio, riduce i tempi di avvio.

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

  • L'archivio AppCDS da riutilizzare deve essere riprodotto esattamente dallo stesso OpenJDK distribuzione, versione e architettura utilizzate in origine per produrlo.
  • Devi eseguire l'applicazione almeno una volta per generare l'elenco di classi da condividere e usare quell'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.
  • Per generare questo elenco di classi, l'applicazione deve essere chiusa. Prendi in considerazione implementare un flag dell'applicazione usato per indicare la generazione di AppCDS Archive, in modo da poter uscire immediatamente.
  • L'archivio AppCDS può essere riutilizzato solo se si avviano nuove istanze esattamente 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 Avvio a molla con un file JAR ombreggiato

Le applicazioni Spring Boot utilizzano per impostazione predefinita un file uber JAR nidificato, che non funziona per le app Quindi, se utilizzi AppCDS, devi creare un JAR ombreggiato. Per 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 eclipse-temurin:11-jre 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 eclipse-temurin:11-jre

# 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

Riduci le dimensioni dello stack di thread

La maggior parte delle applicazioni web Java si basa su thread per connessione. Ogni thread Java consuma memoria nativa (non nell'heap). È noto come stack di thread e il valore predefinito è 1 MB per thread. Se la tua applicazione gestisce 80 elementi richieste, allora potrebbe avere almeno 80 thread, che si traducono in 80 MB di dello stack di thread utilizzato. La memoria si aggiunge alla dimensione dello heap. La valore predefinito può essere più grande del necessario. Puoi ridurre le dimensioni della pila di thread.

Se la 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 Cloud Run, configura la variabile di ambiente:

JAVA_TOOL_OPTIONS="-Xss256k"

Riduzione dei thread

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

Riduci il numero di thread

Ogni thread Java può aumentare la memoria utilizzata a causa dello stack di thread. Cloud Run consente un massimo di 1000 in simultanea richieste. Con il modello thread per connessione, è necessaria al massimo 1000 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 limite massimo connessioni nel file applications.properties:

server.tomcat.max-threads=80

Scrivi codice reattivo non bloccante per ottimizzare memoria e avvio

Per ridurre veramente il numero di thread, valuta l'adozione di un modello reattivo non bloccante di programmazione di un modello di programmazione, in modo che il numero di thread possa essere ridotto in modo significativo e gestire al contempo più richieste in parallelo. Framework applicativi come Spring Boot con Webflux, Micronaut e Quarkus supportano applicazioni web reattive.

Framework reattivi come Spring Boot with Webflux, Micronaut, Quarkus di solito hanno tempi di avvio più rapidi.

Se continui a scrivere il codice di blocco in un framework non di blocco, la velocità effettiva e i tassi di errore saranno notevolmente peggiori in un ambiente Cloud Run completamente gestito di Google Cloud. Questo perché i framework non bloccanti avranno solo pochi thread, ad esempio 2 o 4. Se il codice è bloccato, può gestire pochi richieste in parallelo.

Questi framework non bloccanti possono anche trasferire il codice di blocco a un pool di thread illimitato, il che significa che, sebbene possa accettare molti richieste, 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 che non blocca, per comprendere i modelli di pool di thread e vincolare i pool di conseguenza.

Configura la CPU in modo che venga sempre allocata se utilizzi attività in background

Per attività in background si intende tutto ciò che accade dopo che la risposta HTTP è stata sono recapitate. I carichi di lavoro tradizionali con attività in background richiedono particolari da tenere in considerazione durante l'esecuzione in Cloud Run.

Configura la CPU da allocare sempre

Se vuoi supportare attività in background in Cloud Run di servizio, imposta la CPU del servizio Cloud Run in modo che sempre allocati, eseguire attività in background al di fuori delle richieste e continuare ad avere accesso alla CPU.

Evita attività in background se la CPU viene allocata solo durante l'elaborazione delle richieste

Se devi impostare il servizio su alloca la CPU solo durante l'elaborazione delle richieste, devi essere a conoscenza dei potenziali problemi delle attività in background. Ad esempio: se raccogli metriche dell'applicazione e raggruppa le metriche in batch in background per l'invio periodico, queste metriche non verranno inviate CPU non allocata. Se la tua applicazione riceve costantemente richieste, puoi meno problemi. Se la tua applicazione ha QPS basse, l'attività in background non può mai essere eseguito.

Alcuni pattern ben noti in background a cui devi prestare attenzione se scegli di allocare la CPU solo durante l'elaborazione delle richieste:

  • 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 trigger basato su timer (ad es. l'annotazione ScheduleThreadPoolExecutor, Quartz o @Scheduled Spring) non può essere eseguito quando non sono allocate CPU.
  • 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. Non funzioneranno se la tua applicazione non ha richieste. La ricezione di messaggi in questo modo non è consigliata in Cloud Run.

Ottimizzazioni delle applicazioni

Nel codice del servizio Cloud Run, puoi anche ottimizzare tempi di avvio e utilizzo della memoria.

Riduci 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.

Cloud Run attualmente invia una richiesta a un utente reale per attivare un avvio a freddo in esecuzione in un'istanza Compute Engine. Gli utenti con una richiesta assegnata a un'istanza appena avviata possono che subiscano lunghi ritardi. Cloud Run attualmente non ha un livello di "idoneità" seleziona questa opzione per evitare l'invio di richieste ad applicazioni non pronte.

Usa pooling 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 QPS elevato, gli eliminazioni dei precedenti potrebbero continuare a essere eseguiti finché sono presenti richieste.

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

Se utilizzi Spring Boot

Se utilizzi Spring Boot, devi considerare le seguenti ottimizzazioni

Usa Spring Boot 2.2 o versioni 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.

Usa l'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 il 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.

Evita la scansione dei corsi

La scansione delle classi causerà ulteriori letture del disco in Cloud Run perché in Cloud Run, l'accesso al disco è generalmente più lento rispetto a una macchina normale. Assicurati che la scansione dei componenti sia limitata o completamente evitata. Valuta l'uso Indicizzatore di contesto Spring, per pregenerare un indice. Se questo migliorerà il tuo la velocità di avvio varia in base all'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>

Utilizza gli strumenti per sviluppatori di Spring Boot non in produzione

Se utilizzi lo 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. In alternativa, disattiva esplicitamente lo strumento per sviluppatori di avvio a molla.

Passaggi successivi

Per altri suggerimenti, vedi