In diesem Leitfaden werden Optimierungen für Cloud Run-Dienste beschrieben, die in der Programmiersprache Java geschrieben sind. Außerdem finden Sie hier Hintergrundinformationen, damit Sie die Vor- und Nachteile einiger Optimierungen nachvollziehen können. Die Informationen auf dieser Seite ergänzen die allgemeinen Optimierungstipps, die auch für Java gelten.
Konventionelle Java-basierte Webanwendungen sind darauf ausgelegt, Anfragen mit hoher Parallelität und geringer Latenz zu bearbeiten und sind in der Regel langlaufende Anwendungen. Die JVM selbst optimiert mit JIT auch den Ausführungscode im Laufe der Zeit, sodass Hot Paths optimiert werden und Anwendungen im Laufe der Zeit effizienter ausgeführt werden.
Viele der Best Practices und Optimierungen in diesen herkömmlichen Java-basierten Webanwendungen drehen sich um Folgendes:
- Verarbeiten gleichzeitiger Anfragen (sowohl thread-basierter als auch nicht blockierender E/A-Vorgänge)
- Reduzieren der Antwortlatenz durch Verbindungspooling und Stapelverarbeitung nicht kritischer Funktionen, wie z. B. das Senden von Traces und Messwerten an Hintergrundaufgaben.
Während viele dieser herkömmlichen Optimierungen bei langlaufenden Anwendungen gut funktionieren, sind sie bei einem Cloud Run-Dienst, der nur bei der aktiven Bearbeitung von Anfragen ausgeführt wird, möglicherweise nicht so gut geeignet. Auf dieser Seite werden verschiedene Optimierungen und Kompromisse für Cloud Run erläutert, mit denen Sie die Startzeit und die Speichernutzung reduzieren können.
CPU-Boost beim Starten zum Reduzieren der Startlatenz verwenden
Sie können den CPU-Boost beim Start aktivieren, um die CPU-Zuweisung während des Starts einer Instanz vorübergehend zu erhöhen, um die Startlatenz zu reduzieren.
Die Messwerte von Google haben gezeigt, dass Java-Anwendungen profitieren, wenn sie die CPU-Start-Boost-Funktion verwenden. Dies kann die Startzeiten um bis zu 50 % reduzieren.
Optimieren Sie das Containerimage Ihrer Java-Anwendung.
Durch die Optimierung des Container-Images können Sie die Lade- und Startzeiten reduzieren. Sie können das Image so optimieren:
- Container-Image minimieren
- Vermeidung der Verwendung verschachtelter Bibliotheksarchiv-JARs
- Jib verwenden
Container-Image minimieren
Weitere Informationen zu diesem Thema finden Sie auf der Seite mit allgemeinen Tipps zum Minimieren von Containern. Auf der Seite mit den allgemeinen Tipps wird empfohlen, den Inhalt von Container-Images nur auf die benötigten Elemente zu beschränken. In Ihrem Container-Image sollte beispielsweise nicht enthalten sein:
- Quellcode
- Maven-Build-Artefakte
- Build-Tools
- Git-Verzeichnisse
- Nicht verwendete Binärdateien oder Hilfsprogramme
Wenn Sie den Code in einem Dockerfile erstellen, sollten Sie einen mehrstufigen Build-Prozess in Docker verwenden, damit das endgültige Container-Image nur die JRE und die JAR-Datei der Anwendung selbst hat.
JARs für verschachtelte Bibliothekenarchive vermeiden
Einige beliebte Frameworks wie Spring Boot erstellen eine Anwendungsarchiv-Datei (JAR), die zusätzliche Bibliotheks-JAR-Dateien (verschachtelte JARs) enthält. Diese Dateien müssen beim Startvorgang entpackt (dekomprimiert) werden, was die Startgeschwindigkeit von Cloud Run negativ beeinflussen kann. Erstellen Sie daher nach Möglichkeit ein schlankes JAR mit externen Bibliotheken: Dies kann durch die Verwendung von Jib zur Containerisierung Ihrer Anwendung automatisiert werden.
Jib verwenden
Verwenden Sie das Jib-Plug-in, um einen minimalen Container zu erstellen und das Anwendungsarchiv automatisch zu vereinfachen. Jib funktioniert sowohl mit Maven als auch mit Gradle und ist mit standardmäßigen Spring Boot-Anwendungen kompatibel. Für einige Anwendungs-Frameworks sind möglicherweise weitere Jib-Konfigurationen erforderlich.
JVM-Optimierungen für Cloud Run-Java-Anwendungen
Die Optimierung der JVM für einen Cloud Run-Dienst kann zu einer besseren Leistung und Speichernutzung führen.
Containersensitive JVM-Versionen verwenden
In VMs und Maschinen erkennt die JVM für die CPU- und Arbeitsspeicherzuweisungen die CPU und den Arbeitsspeicher, die sie von bekannten Speicherorten aus verwenden kann, z. B. unter Linux, /proc/cpuinfo und /proc/meminfo. Beim Ausführen in einem Container werden die CPU- und Speicherbeschränkungen jedoch in /proc/cgroups/... gespeichert. Ältere Versionen des JDK suchen weiterhin in /proc anstatt in /proc/cgroups, was zu einer höheren CPU- und Arbeitsspeichernutzung führen kann als zugewiesen. Das kann folgende Gründe haben:
- Eine übermäßige Anzahl von Threads, da die Größe des Threadpools von
Runtime.availableProcessors()konfiguriert wird. - Ein standardmäßiger maximaler Heap, der das Container-Speicherlimit überschreitet. Die JVM verwendet den Speicher aggressiv, bevor der Speicher automatisch bereinigt wird. Dies kann leicht dazu führen, dass der Container das Speicherlimit überschreitet und OOMKilling ausgelöst wird.
Verwenden Sie daher eine containersensitive JVM-Version. OpenJDK-Versionen größer oder gleich Version 8u192 sind standardmäßig containersensitiv.
Grundlegendes zur JVM-Arbeitsspeichernutzung
Die JVM-Speichernutzung setzt sich aus der nativen Speichernutzung und der Heap-Nutzung zusammen. Der Arbeitsspeicher der Anwendung befindet sich normalerweise im Heap. Die Größe des Heap wird durch die Max-Heap-Konfiguration begrenzt. Bei einer Cloud Run-Instanz mit 256 MB RAM können Sie nicht die gesamten 256 MB dem Max Heap zuweisen, da die JVM und das Betriebssystem auch nativen Speicher benötigen, beispielsweise für Thread-Stack, Code-Caches, Dateihandles, Puffer usw. Falls Ihre Anwendung aufgrund eines Speichermangels (OOMKilling) beendet wird und Sie die JVM-Speichernutzung (nativer Speicher + Heap) ermitteln müssen, aktivieren Sie die native Speicherüberwachung, um die Nutzung nach einem erfolgreichen Beenden der Anwendung anzuzeigen. Wenn Ihre Anwendung den Status „OOMKilled“ erhält, kann die Information nicht ausgegeben werden. Führen Sie in diesem Fall zuerst die Anwendung mit mehr Arbeitsspeicher aus, damit die Ausgabe erfolgreich generiert werden kann.
Natives Memory Tracking kann über die Umgebungsvariable JAVA_TOOL_OPTIONS nicht aktiviert werden. Sie müssen das Startargument für die Java-Befehlszeile zu Ihrem Container-Image-Einstiegspunkt hinzufügen, damit Ihre Anwendung mit den folgenden Argumenten gestartet wird:
java -XX:NativeMemoryTracking=summary \ -XX:+UnlockDiagnosticVMOptions \ -XX:+PrintNMTStatistics \ ...
Mit einem Open-Source-Java Memory Calculator können Sie die Speicheranforderungen schätzen.
Optimierungs-Compiler deaktivieren
Standardmäßig umfasst JVM mehrere Phasen der JIT-Kompilierung. Obwohl diese Phasen die Effizienz Ihrer Anwendung mit der Zeit verbessern, erhöhen sie auch die Arbeitsspeichernutzung und die Startzeit.
Bei kurzlaufenden, serverlosen Anwendungen (z. B. Funktionen) sollten Sie erwägen, die Optimierungsphasen abzuschalten, um die langfristige Effizienz gegen eine verkürzte Startzeit einzutauschen.
Konfigurieren Sie für einen Cloud Run-Dienst die Umgebungsvariable:
JAVA_TOOL_OPTIONS="-XX:+TieredCompilation -XX:TieredStopAtLevel=1"
Application Class Data Sharing verwenden
Um die JIT-Zeit und den Speicherverbrauch weiter zu reduzieren, sollten Sie application class data sharing (AppCDS) verwenden, um die im Voraus kompilierten Java-Klassen als Archiv zu teilen. Das AppCDS-Archiv kann beim Starten einer anderen Instanz derselben Java-Anwendung wiederverwendet werden. Die JVM kann die vorausberechneten Daten aus dem Archiv wiederverwenden, wodurch die Startzeit verkürzt wird.
Die folgenden Überlegungen gelten für die Verwendung von AppCDS:
- Das zu verwendende AppCDS-Archiv muss mit derselben OpenJDK-Distribution, -Version und -Architektur reproduziert werden, mit der es ursprünglich erzeugt wurde.
- Sie müssen Ihre Anwendung mindestens einmal ausführen, um die Liste der Klassen zu generieren, die freigegeben werden sollen. Anschließend können Sie diese Liste verwenden, um das AppCDS-Archiv zu generieren.
- Die Abdeckung der Klassen hängt vom Codepfad ab, der während der Ausführung der Anwendung ausgeführt wird. Zur Erhöhung der Abdeckung lösen Sie programmatisch weitere Codepfade aus.
- Die Anwendung muss beendet werden, um diese Klassenliste zu erstellen. Erwägen Sie die Implementierung eines Anwendungs-Flags, das die Generierung eines AppCDS-Archivs angibt, damit es sofort beendet werden kann.
- Das AppCDS-Archiv kann nur dann wiederverwendet werden, wenn Sie neue Instanzen auf die gleiche Weise starten wie das Archiv generiert wurde.
- Das AppCDS-Archiv funktioniert nur mit einem normalen JAR-Dateipaket. Sie können keine verschachtelten JARs verwenden.
Spring Boot-Beispiel mit einer Shading-JAR-Datei
Spring Boot-Anwendungen verwenden standardmäßig eine verschachtelte Uber-JAR-Datei, die für AppCDS nicht funktioniert. Wenn Sie also AppCDS verwenden, müssen Sie eine Shading-JAR-Datei erstellen. Hier ein Beispiel, wenn Sie Maven und das Maven-Shade-Plug-in verwenden:
<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>
Wenn Ihre Shading-JAR-Datei alle Abhängigkeiten enthält, können Sie während des Container-Builds mit einem Dockerfile ein einfaches Archiv erstellen:
# 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
Größe des Thread-Stacks reduzieren
Die meisten Java-Webanwendungen basieren auf Threads pro Verbindung. Jeder Java-Thread verbraucht nativen Arbeitsspeicher (nicht in Heap). Dies wird als Thread-Stack bezeichnet und ist standardmäßig auf 1 MB pro Thread eingestellt. Wenn Ihre Anwendung 80 gleichzeitige Anfragen verarbeitet, verfügt sie möglicherweise über mindestens 80 Threads, was 80 MB verwendetem Thread-Stack-Speicher entspricht. Der Arbeitsspeicher wird zusätzlich zur Heap-Größe angelegt. Die Standardeinstellung kann größer als erforderlich sein. Sie können die Größe des Thread-Stacks reduzieren.
Wenn Sie zu viel reduzieren, wird java.lang.StackOverflowError angezeigt. Sie können ein Profil für Ihre Anwendung erstellen und die optimale Thread-Stack-Größe ermitteln, die konfiguriert werden soll.
Konfigurieren Sie für einen Cloud Run-Dienst die Umgebungsvariable:
JAVA_TOOL_OPTIONS="-Xss256k"
Thread-Reduzierung für die Java-Anwendungsleistung
Sie können den Arbeitsspeicher optimieren, indem Sie die Anzahl der Threads reduzieren, indem Sie nicht blockierende reaktive Strategien verwenden und Hintergrundaktivitäten vermeiden.
Reduzieren Sie die Anzahl der Threads.
Jeder Java-Thread kann aufgrund des Thread-Stacks die Speichernutzung erhöhen.
Cloud Run erlaubt maximal 1.000 gleichzeitige Anfragen. Beim Thread-pro-Verbindung-Modell benötigen Sie maximal 1.000 Threads, um alle gleichzeitigen Anfragen zu bearbeiten.
Bei den meisten Webservern und Frameworks können Sie die maximale Anzahl von Threads und Verbindungen konfigurieren. In Spring Boot können Sie zum Beispiel die maximalen Verbindungen in der Datei applications.properties begrenzen:
server.tomcat.max-threads=80
Nicht blockierenden reaktiven Code zur Optimierung des Speichers und des Start-ups schreiben
Um die Anzahl der Threads wirklich zu reduzieren, sollten Sie ein nicht blockierendes reaktives Programmiermodell verwenden, damit die Anzahl der Threads erheblich reduziert werden kann, während mehr gleichzeitige Anfragen verarbeitet werden. Anwendungs-Frameworks wie Spring Boot mit Webflux, Micronaut und Quarkus unterstützen reaktive Webanwendungen.
Reaktive Frameworks wie Spring Boot mit Webflux, Micronaut, Quarkus haben in der Regel eine schnellere Startzeit.
Wenn Sie weiterhin blockierenden Code in einem nicht blockierenden Framework schreiben, sind die Durchsatz- und Fehlerraten in einem Cloud Run-Dienst erheblich schlechter. Dies liegt daran, dass nicht blockierende Frameworks nur wenige Threads enthalten, zum Beispiel 2 oder 4. Wenn Ihr Code blockiert, kann er nur sehr wenige gleichzeitige Anfragen verarbeiten.
Diese nicht blockierenden Frameworks können blockierenden Code auch in einen unbegrenzten Thread-Pool verlagern. Dies bedeutet, dass der blockierende Code zwar viele gleichzeitige Anfragen akzeptieren kann, jedoch in neuen Threads ausgeführt wird. Wenn sich Threads unbegrenzt ansammeln, erschöpfen Sie die CPU-Ressource und reagieren langsam nicht mehr. Die Latenz wird erheblich beeinträchtigt. Wenn Sie ein nicht blockierendes Framework verwenden, müssen Sie die Thread-Pool-Modelle verstehen und die Pools entsprechend binden.
Konfigurieren Sie die instanzbasierte Abrechnung, wenn Sie Hintergrundaktivitäten verwenden.
Als Hintergrundaktivität werden alle Aktivitäten bezeichnet, die nach Eingang der HTTP-Antwort erfolgen. Herkömmliche Arbeitslasten mit Hintergrundaufgaben müssen bei der Ausführung in Cloud Run besonders berücksichtigt werden.
Instanzbasierte Abrechnung konfigurieren
Wenn Sie Hintergrundaktivitäten in Ihrem Cloud Run-Dienst unterstützen möchten, stellen Sie Ihren Cloud Run-Dienst auf instanzbasierte Abrechnung ein, damit Sie Hintergrundaktivitäten außerhalb von Anfragen ausführen und trotzdem CPU-Zugriff haben können.
Vermeiden Sie Hintergrundaktivitäten bei Verwendung der anfragebasierten Abrechnung.
Wenn Sie Ihren Dienst auf anfragebasierte Abrechnung einstellen müssen, sollten Sie sich der möglichen Probleme mit Hintergrundaktivitäten bewusst sein. Wenn Sie beispielsweise Anwendungsmesswerte erfassen und die Messwerte im Hintergrund in Batches aufteilen, um sie regelmäßig zu senden, werden diese Messwerte nicht gesendet, wenn die anfragebasierte Abrechnung konfiguriert ist. Wenn Ihre Anwendung ständig Anfragen erhält, treten möglicherweise weniger Probleme auf. Bei einer niedrigen QPS Ihrer Anwendung kann es vorkommen, dass die im Hintergrund ausgeführte Aufgabe nie ausgeführt wird.
Einige bekannte, im Hintergrund stehende Muster, auf die Sie achten sollten, wenn Sie sich für anfragebasierte Abrechnung entscheiden:
- JDBC-Verbindungspools: Bereinigungen und Verbindungsprüfungen werden normalerweise im Hintergrund ausgeführt.
- Sender für verteilte Traces: Verteilte Traces werden in der Regel zusammengefasst und in regelmäßigen Abständen oder wenn der Puffer voll ist im Hintergrund gesendet.
- Messwertsender – Messwerte werden in der Regel zusammengefasst und in regelmäßigen Abständen im Hintergrund gesendet.
- In Spring Boot alle mit
@Asyncannotierten Methoden. - Timer: Alle Timer-basierten Trigger (z.B. ScheduledThreadPoolExecutor, Quartz oder
@Scheduled-Spring-Annotation) werden möglicherweise nicht ausgeführt, wenn die anfragebasierte Abrechnung konfiguriert ist. - Nachrichtenempfänger: Beispielsweise Pub/Sub-Streaming-Pull-Clients, JMS-Clients oder Kafka-Clients, Hintergrundthreads, die normalerweise ausgeführt werden, ohne Anfragen zu erfordern. Diese funktionieren nicht, wenn Ihre Anwendung keine Anfragen enthält. Das Empfangen von Nachrichten auf diese Weise wird in Cloud Run nicht empfohlen.
Anwendungsoptimierungen
In Ihrem Cloud Run-Dienstcode können Sie die Optimierung auch für schnellere Startzeiten und Speichernutzung optimieren.
Startaufgaben reduzieren
Beim Starten müssen Java-Webanwendungen oft mehrere Aufgaben ausführen, z. B. Daten vorab laden, Caches vorbereiten und Verbindungspools einrichten. Wenn Sie diese Aufgaben sequenziell ausführen, kann Ihre Anwendung langsam werden. Um diese Aufgaben parallel auszuführen, erhöhen Sie die Anzahl der CPU-Kerne.
Cloud Run sendet eine echte Benutzeranfrage, um eine Kaltstartinstanz auszulösen. Bei Benutzern, deren Anfrage einer neu gestarteten Instanz zugewiesen wurde, kann es zu langen Verzögerungen kommen.
Bei Anwendungen mit langen Startzeiten empfiehlt sich der Einsatz einer Startsonde. Ein Startcheck stellt sicher, dass Cloud Run Benutzeranfragen erst dann an eine Instanz sendet, wenn diese vollständig initialisiert ist und den Startzustandscheck bestanden hat. Weitere Informationen finden Sie unter Configure container health checks for services.
Verbindungs-Pooling verwenden
Wenn Sie Verbindungspools verwenden, beachten Sie, dass Verbindungspools nicht benötigte Verbindungen in den Hintergrund verweisen können (siehe Hintergrundaufgaben vermeiden). Wenn Ihre Anwendung eine niedrige Abfragen pro Sekunde-Rate hat und eine hohe Latenz akzeptiert, sollten Sie Verbindungen pro Anfrage öffnen und schließen. Wenn Ihre Anwendung eine hohe Abfragen pro Sekunde-Rate hat, werden Hintergrundverweisungen möglicherweise weiterhin ausgeführt, solange es aktive Anfragen gibt.
In beiden Fällen wird der Datenbankzugriff der Anwendung durch die maximal Anzahl zulässiger Verbindungen beeinträchtigt. Berechnen Sie die maximale Anzahl von Verbindungen, die Sie pro Cloud Run-Instanz herstellen können, und konfigurieren Sie die maximale Anzahl von Cloud Run-Instanzen so, dass die maximale Anzahl der Instanzen mal der Verbindungen pro Instanz kleiner als die maximale Anzahl zulässiger Verbindungen ist.
Wenn Sie Spring Boot verwenden
Wenn Sie Spring Boot verwenden, müssen Sie die folgenden Optimierungen berücksichtigen:
Spring Boot Version 2.2 oder höher verwenden
Seit Version 2.2 wurde Spring Boot besonders für die Startgeschwindigkeit optimiert. Wenn Sie eine Spring Boot-Version vor 2.2 verwenden, erwägen Sie ein Upgrade oder wenden Sie einzelne Optimierungen manuell an.
Verzögerte Initialisierung verwenden
Es gibt ein globales Flag für verzögerte Initialisierung, das in Spring Boot 2.2 und höher aktiviert werden kann. Hierdurch wird die Startgeschwindigkeit verbessert, doch mit dem Kompromiss, dass die erste Anfrage mehr Latenz haben kann, weil die erste Initialisierung von Komponenten abgewartet werden muss.
Sie können die verzögerte Initialisierung in application.properties aktivieren:
spring.main.lazy-initialization=true
Oder mithilfe einer Umweltvariablen:
SPRING_MAIN_LAZY_INITIALIZATIION=true
Wenn Sie jedoch Min-Instanzen verwenden, hilft eine verzögerte Initialisierung nicht, da die Initialisierung beim Start der Min-Instanz hätte erfolgen müssen.
Scannen von Klassen vermeiden
Durch das Scannen von Klassen werden in Cloud Run zusätzliche Lesevorgänge auf dem Laufwerk ausgelöst, da in Cloud Run der Laufwerkzugriff im Allgemeinen langsamer ist als eine reguläre Maschine. Der Komponentenscan sollte eingeschränkt oder vollständig vermieden werden.
Spring Boot-Entwicklertools verwenden, die nicht in der Produktion sind
Wenn Sie das Spring Boot-Entwicklertool während der Entwicklung verwenden, achten Sie darauf, dass es nicht im Produktions-Container-Image verpackt ist. Dies kann auftreten, wenn Sie die Spring Boot-Anwendung ohne die Spring Boot-Build-Plug-ins erstellt haben (z. B. mit dem Shade-Plug-in oder mit Jib für die Containerisierung).
Achten Sie in diesen Fällen darauf, dass das Build-Tool das Spring Boot-Entwicklertool explizit ausschließt. Alternativ können Sie das Spring Boot-Entwicklertool explizit deaktivieren.
Weitere Informationen
Weitere Tipps finden Sie unter