Heim > Java > javaLernprogramm > Hauptteil

Modernisierung von Java-Monolithen für bessere Leistung mit asynchronen und nicht blockierenden Architekturen

Susan Sarandon
Freigeben: 2024-11-17 06:13:03
Original
474 Leute haben es durchsucht

Modernizing Java Monoliths for Better Performance with Async and Non-Blocking Architectures

In einem aktuellen Projekt habe ich einen in die Jahre gekommenen monolithischen Java-Webdienst modernisiert, der in Dropwizard geschrieben wurde. Dieser Dienst handhabte eine Reihe von Abhängigkeiten von Drittanbietern (3P) über AWS Lambda-Funktionen, die Leistung war jedoch aufgrund der synchronen, blockierenden Natur der Architektur zurückgeblieben. Das Setup hatte eine P99-Latenz von 20 Sekunden und blockierte Anforderungsthreads, während auf den Abschluss der serverlosen Funktionen gewartet wurde. Diese Blockierung führte zu einer Sättigung des Thread-Pools, was zu häufigen Anforderungsfehlern während des Spitzenverkehrs führte.

Identifizieren des Leistungsengpasses

Der Kern des Problems bestand darin, dass jede Anfrage an eine Lambda-Funktion einen Anfragethread im Java-Dienst belegte. Da die Ausführung dieser 3P-Funktionen oft viel Zeit in Anspruch nahm, blieben die sie verarbeitenden Threads blockiert, was Ressourcen verbrauchte und die Skalierbarkeit einschränkte. Hier ist ein Beispiel dafür, wie dieses Blockierungsverhalten im Code aussieht:

// Blocking code example
public String callLambdaService(String payload) {
    String response = externalLambdaService.invoke(payload);
    return response;
}
Nach dem Login kopieren
Nach dem Login kopieren

In diesem Beispiel wartet die callLambdaService-Methode, bis externalLambdaService.invoke() eine Antwort zurückgibt. In der Zwischenzeit können keine anderen Aufgaben den Thread nutzen.

Lösung: Migration zu asynchronen, nicht blockierenden Mustern

Um diese Engpässe zu beheben, habe ich den Dienst mithilfe asynchroner und nicht blockierender Methoden neu strukturiert. Diese Änderung umfasste die Verwendung eines HTTP-Clients, der die Lambda-Funktionen aufrief, um AsyncHttpClient aus der Bibliothek org.asynchttpclient zu verwenden, die intern eine EventLoopGroup verwendet, um Anforderungen asynchron zu verarbeiten.

Die Verwendung von AsyncHttpClient hat dazu beigetragen, Blockierungsvorgänge auszulagern, ohne Threads aus dem Pool zu verbrauchen. Hier ist ein Beispiel dafür, wie der aktualisierte nicht blockierende Anruf aussieht:

// Non-blocking code example
public CompletableFuture<String> callLambdaServiceAsync(String payload) {
    return CompletableFuture.supplyAsync(() -> {
        return asyncHttpClient.invoke(payload);
    });
}
Nach dem Login kopieren
Nach dem Login kopieren

Nutzung von Javas CompletableFuture zur Verkettung asynchroner Aufrufe

Zusätzlich dazu, dass einzelne Anrufe nicht blockiert werden, habe ich mithilfe von CompletableFuture mehrere Abhängigkeitsaufrufe verkettet. Mit Methoden wie thenCombine und thenApply konnte ich Daten aus mehreren Quellen asynchron abrufen und kombinieren und so den Durchsatz erheblich steigern.

CompletableFuture<String> future1 = callLambdaServiceAsync(payload1);
CompletableFuture<String> future2 = callLambdaServiceAsync(payload2);

CompletableFuture<String> combinedResult = future1.thenCombine(future2, (result1, result2) -> {
    return processResults(result1, result2);
});
Nach dem Login kopieren
Nach dem Login kopieren

Einführung in die Typensicherheit mit einer benutzerdefinierten SafeAsyncResponse-Klasse

Während der Implementierung habe ich festgestellt, dass es dem standardmäßigen AsyncResponse-Objekt von Java an Typsicherheit mangelte, sodass beliebige Java-Objekte weitergegeben werden konnten. Um dieses Problem zu beheben, habe ich eine SafeAsyncResponse-Klasse mit Generika erstellt, die sicherstellte, dass nur der angegebene Antworttyp zurückgegeben werden konnte, was die Wartbarkeit fördert und das Risiko von Laufzeitfehlern verringert. Diese Klasse protokolliert auch Fehler, wenn eine Antwort mehr als einmal geschrieben wird.

// Blocking code example
public String callLambdaService(String payload) {
    String response = externalLambdaService.invoke(payload);
    return response;
}
Nach dem Login kopieren
Nach dem Login kopieren

Beispielverwendung von SafeAsyncResponse

// Non-blocking code example
public CompletableFuture<String> callLambdaServiceAsync(String payload) {
    return CompletableFuture.supplyAsync(() -> {
        return asyncHttpClient.invoke(payload);
    });
}
Nach dem Login kopieren
Nach dem Login kopieren

Tests und Leistungssteigerungen

Um die Wirksamkeit dieser Änderungen zu überprüfen, habe ich Lasttests mit virtuellen Threads geschrieben, um den maximalen Durchsatz auf einer einzelnen Maschine zu simulieren. Ich habe verschiedene Ebenen der Ausführungszeiten serverloser Funktionen generiert (im Bereich von 1 bis 20 Sekunden) und festgestellt, dass die neue asynchrone, nicht blockierende Implementierung den Durchsatz bei kürzeren Ausführungszeiten um das Achtfache und bei längeren Ausführungszeiten um etwa das Vierfache erhöhte.

Beim Einrichten dieser Lasttests habe ich darauf geachtet, die Verbindungslimits auf Client-Ebene anzupassen, um den Durchsatz zu maximieren, was wichtig ist, um Engpässe in asynchronen Systemen zu vermeiden.

Entdeckung eines versteckten Fehlers im HTTP-Client

Während der Durchführung dieser Stresstests habe ich einen versteckten Fehler in unserem benutzerdefinierten HTTP-Client entdeckt. Der Client verwendete ein Semaphor mit einem auf Integer.MAX_VALUE eingestellten Verbindungszeitlimit. Das heißt, wenn dem Client keine verfügbaren Verbindungen mehr zur Verfügung standen, würde er den Thread auf unbestimmte Zeit blockieren. Die Behebung dieses Fehlers war von entscheidender Bedeutung, um potenzielle Deadlocks in Hochlastszenarien zu verhindern.

Die Wahl zwischen virtuellen Threads und traditionellem asynchronem Code

Man könnte sich fragen, warum wir nicht einfach auf virtuelle Threads umgestiegen sind, die den Bedarf an asynchronem Code reduzieren können, indem Threads ohne nennenswerte Ressourcenkosten blockiert werden können. Derzeit gibt es jedoch eine Einschränkung bei virtuellen Threads: Sie werden während synchronisierter Vorgänge fixiert. Das bedeutet, dass ein virtueller Thread, wenn er in einen synchronisierten Block eintritt, die Bereitstellung nicht aufheben kann, wodurch möglicherweise Betriebssystemressourcen blockiert werden, bis der Vorgang abgeschlossen ist.

Zum Beispiel:

CompletableFuture<String> future1 = callLambdaServiceAsync(payload1);
CompletableFuture<String> future2 = callLambdaServiceAsync(payload2);

CompletableFuture<String> combinedResult = future1.thenCombine(future2, (result1, result2) -> {
    return processResults(result1, result2);
});
Nach dem Login kopieren
Nach dem Login kopieren

Wenn in diesem Code das Lesen blockiert wird, weil keine Daten verfügbar sind, wird der virtuelle Thread an einen Betriebssystem-Thread angeheftet, wodurch verhindert wird, dass er ausgehängt wird und auch der Betriebssystem-Thread blockiert wird.

Glücklicherweise können sich Java-Entwickler mit JEP 491 am Horizont auf ein verbessertes Verhalten für virtuelle Threads freuen, bei dem Blockierungsvorgänge in synchronisiertem Code effizienter gehandhabt werden können, ohne die Plattform-Threads zu erschöpfen.

Abschluss

Durch die Umgestaltung unseres Dienstes auf eine asynchrone, nicht blockierende Architektur haben wir erhebliche Leistungsverbesserungen erzielt. Durch die Implementierung von AsyncHttpClient, die Einführung von SafeAsyncResponse zur Typsicherheit und die Durchführung von Lasttests konnten wir unseren Java-Dienst optimieren und den Durchsatz erheblich verbessern. Dieses Projekt war eine wertvolle Übung zur Modernisierung monolithischer Anwendungen und zeigte die Bedeutung geeigneter asynchroner Praktiken für die Skalierbarkeit.

Im Zuge der Weiterentwicklung von Java können wir möglicherweise in Zukunft virtuelle Threads effektiver nutzen, aber vorerst bleibt die asynchrone und nicht blockierende Architektur ein wesentlicher Ansatz für die Leistungsoptimierung in von Drittanbietern abhängigen Diensten mit hoher Latenz.

Das obige ist der detaillierte Inhalt vonModernisierung von Java-Monolithen für bessere Leistung mit asynchronen und nicht blockierenden Architekturen. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Quelle:dev.to
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Neueste Artikel des Autors
Beliebte Tutorials
Mehr>
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage