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. Le informazioni riportate in questa pagina integrano i 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.
Utilizza il boosting della 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 traggono vantaggio dall'utilizzo dell'aumento della CPU all'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. Puoi ottimizzare l'immagine:
- Ridurre a icona l'immagine container
- Evitare l'utilizzo di file JAR di archivi di librerie nidificati
- 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. Nella pagina dei suggerimenti generali viene consigliato di ridurre i contenuti delle immagini del contenitore solo a ciò che è necessario. Ad esempio, assicurati che l'immagine del container non contenga:
- Codice sorgente
- Artefatti della build Maven
- Strumenti di creazione
- Directory Git
- Binari/utility 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 devono essere scompattati/decompressi durante l'avvio, il che può influire negativamente sulla velocità di avvio in Cloud Run. Pertanto, se possibile, crea un JAR snello con librerie estruse: questa operazione può essere automatizzata utilizzando Jib per eseguire il containerizzazione della tua applicazione.
Usa jib
Utilizza il plug-in Jib per creare un contenuto minimo e appiattire automaticamente l'archivio dell'applicazione. Jib è compatibile sia con Maven che con Gradle e funziona immediatamente 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 viene eseguito in un contenitore, i vincoli di CPU e memoria vengono memorizzati 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 eliminata la garbage collection. Ciò può facilmente causare il superamento del limite di memoria del contenitore e l'interruzione OOM.
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. 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 Cloud Run 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 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.
Disattivare il compilatore di ottimizzazione
Per impostazione predefinita, la JVM ha diverse fasi di compilazione JIT. Anche se queste fasi migliorare l'efficienza dell'applicazione nel tempo, possono anche aumentare l'overhead e aumenta il tempo di avvio.
Per le applicazioni serverless con breve tempo di esecuzione (ad esempio le funzioni), valuta la possibilità di disattivare le fasi di ottimizzazione per scambiare l'efficienza a lungo termine con un tempo di avvio ridotto.
Per un servizio Cloud Run, configura la variabile di ambiente:
JAVA_TOOL_OPTIONS="-XX:+TieredCompilation -XX:TieredStopAtLevel=1"
Utilizzare la condivisione dei dati a livello di classe dell'applicazione
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 viene avviata un'altra istanza della stessa applicazione Java. La JVM può riutilizzare i dati precomputati dall'archivio, il che consente di ridurre i tempi di avvio.
Le seguenti considerazioni si applicano all'utilizzo di AppCDS:
- 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 di classi da condividere e usare quell'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 tramite programmazione più percorsi di codice.
- Per generare questo elenco di classi, l'applicazione deve essere chiusa. 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 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 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 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 è basata su thread per connessione. Ogni thread Java consuma memoria nativa (non nell'heap). È nota come stack di 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 alla dimensione dello heap. Il valore predefinito potrebbe essere più grande del necessario. Puoi ridurre le dimensioni dello stack del 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 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 richieste contemporaneamente. 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 numero massimo di connessioni nel file applications.properties
:
server.tomcat.max-threads=80
Scrivi 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 applicativi come Spring Boot con Webflux, Micronaut e Quarkus supportano 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 Cloud Run. 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 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.
Configura la CPU in modo che sia sempre allocata se utilizzi attività in background
Per attività in background si intende tutto ciò che accade dopo che la risposta HTTP è stata disponibili. I carichi di lavoro tradizionali con attività in background richiedono un'attenzione particolare quando vengono eseguiti 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 le attività in background se la CPU viene allocata solo durante l'elaborazione delle richieste
Se devi impostare il servizio in modo da allocare la CPU solo durante l'elaborazione delle richieste, devi essere consapevole di potenziali problemi con le attività in background. Ad esempio, se raccogli le metriche dell'applicazione e le raggruppi in background per inviarle periodicamente, queste non verranno inviate quando la CPU non è allocata. Se la tua applicazione riceve costantemente richieste, potresti riscontrare 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 attivatore basato su timer (ad es. ScheduledThreadPoolExecutor, Quartz o
@Scheduled
annotazione Spring) potrebbe non essere eseguita quando le CPU non sono allocate. - 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 che hanno una richiesta assegnata a un'istanza appena avviata potrebbero subire lunghi ritardi. Cloud Run attualmente non ha un livello di "idoneità" seleziona questa opzione per evitare l'invio di richieste ad applicazioni non pronte.
Utilizzare il pool di connessioni
Se utilizzi pool di connessione, tieni presente che questi potrebbero eliminare le connessioni non necessarie in background (vedi Evitare le attività in background). Se la tua applicazione ha un QPS basso e può tollerare una latenza elevata, valuta la possibilità di aprire e chiudere le connessioni per 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à limitato dalle connessioni massime 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
Utilizza Spring Boot versione 2.2 o successive
A partire dalla versione 2.2, Spring Boot è stato ottimizzato notevolmente per la velocità di avvio. Se utilizzi versioni precedenti alla 2.2 di Spring Boot, valuta la possibilità di eseguire l'upgrade, oppure applica manualmente le singole ottimizzazioni.
Utilizza l'inizializzazione lazy
È presente un flag di inizializzazione lazy globale che può essere attivato in Spring Boot 2.2 e successive. In questo modo, la velocità di avvio migliorerà, ma la prima richiesta potrebbe avere una latenza più lunga perché dovrà attendere l'inizializzazione dei componenti 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 istanze minime, l'inizializzazione lazy non sarà di aiuto, poiché l'inizializzazione dovrebbe essere avvenuta 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 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, in pom.xml
Maven aggiungi la dipendenza dall'indice (in realtà è un elaboratore di annotazioni):
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-indexer</artifactId>
<optional>true</optional>
</dependency>
Utilizzare gli strumenti per sviluppatori Spring Boot non in produzione
Se utilizzi lo strumento per sviluppatori Spring Boot durante lo sviluppo, assicurati che non sia pacchettizzato nell'immagine del contenitore di produzione. 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 compilazione escluda esplicitamente lo strumento di sviluppo Spring Boot. In alternativa, disattiva esplicitamente lo strumento per sviluppatori Spring Boot.
Passaggi successivi
Per altri suggerimenti, vedi