Übernehmen Sie die Kontrolle über Threading-Probleme, die sich auf die Leistung Ihrer Java Web API-Anwendung auswirken
Von Sergej Baranow
11. April 2019
7 min lesen
Nur eine einzige Codezeile kann Ihre gesamte Softwareleistung beeinträchtigen, wenn sie nicht rechtzeitig erkannt und korrigiert wird. Sehen Sie sich diesen Beitrag an, um zu erfahren, wie Sie Java-Threads überwachen und die spezifischen Codezeilen in Ihrer Anwendung verstehen, die potenzielle Fehler in Ihrer App-Leistung verursachen könnten.
Zum Abschnitt springen
Threadbezogene Probleme können die Leistung einer Web-API-Anwendung auf eine Weise beeinträchtigen, die oft schwer zu diagnostizieren und schwierig zu lösen ist. Um eine optimale Leistung zu erzielen, ist es wichtig, ein klares Bild des Verhaltens eines Threads zu behalten. In diesem Beitrag zeige ich Ihnen die Verwendung Parasoft SOAtestLoad Test JVM Threads Monitor, um die Threading-Aktivität einer JVM mit Diagrammen wichtiger Statistiken und konfigurierbaren Thread-Dumps anzuzeigen, die auf die Codezeilen verweisen können, die für Leistungsverluste aufgrund ineffizienter Thread-Verwendung verantwortlich sind. Parasoft SOAtests Lastprüfung Mit diesem Modul können Sie alle Funktionstests in Last- und Leistungstests umwandeln.
Wir folgen einem hypothetischen Java-Entwicklungsteam, das beim Erstellen einer Web-API-Anwendung auf einige häufig auftretende Threading-Probleme stößt, und diagnostizieren einige häufig auftretende Thread-bezogene Leistungsprobleme. Danach sehen wir uns komplexere Beispiele für die realen Anwendungen an. (Beachten Sie, dass ein suboptimaler Code in den folgenden Beispielen zu Demonstrationszwecken absichtlich hinzugefügt wurde.)
Die Bankanwendung
Unser hypothetisches Java-Entwicklungsteam hat ein neues Projekt gestartet - eine REST-API-Banking-Anwendung. Das Team richtete zur Unterstützung des neuen Projekts eine CI-Infrastruktur (Continuous Integration) ein, die einen regelmäßigen CI-Job bei Parasoft umfasst SOAtest's Lastprüfung Modul zum kontinuierlichen Testen der Leistung der neuen Anwendung. (Weitere Informationen zum Einrichten automatisierter Leistungstests finden Sie in meinem vorherigen Beitrag. Last- und Leistungstests in einer DevOps Delivery-Pipeline.)
Bankanwendung Version 1: Rennbedingungen in der Erstimplementierung
Der Bank-Anwendungscode wächst und die Tests werden ausgeführt. Das Team bemerkte dies jedoch nach der Implementierung eines neuen transfer
Betrieb, begann die Bank-Anwendung sporadische Fehler unter höherer Last. Der Fehler ist auf die Kontovalidierungsmethode zurückzuführen, bei der gelegentlich ein negativer Saldo in überziehungsgeschützten Konten festgestellt wird. Der Fehler bei der Kontoüberprüfung führt zu einer Ausnahme und einer HTTP 500-Antwort von der API. Die Entwickler vermuten, dass dies durch eine Rennbedingung in der verursacht werden kann IAccount.withdraw
Methode, wenn sie von verschiedenen Threads gleichzeitig aufgerufen wird transfer
Betrieb auf dem gleichen Konto:
13: public boolean transfer(IAccount from, IAccount to, int amount) {
14: if (from.withdraw(amount)) {
15: to.deposit(amount);
16: return true;
17: }
18: return false;
19: }
Bankanwendung Version 2: Hinzufügen der Synchronisation
Die Entwickler beschließen, den Zugriff auf Konten innerhalb des zu synchronisieren transfer
Betrieb zur Verhinderung des vermuteten Rennzustands:
14: public boolean transfer(IAccount from, IAccount to, int amount) {
15: synchronized (to) {
16: synchronized (from) {
17: if (from.withdraw(amount)) {
18: to.deposit(amount);
19: return true;
20: }
21: }
22: }
23: return false;
24: }
Das Team fügt außerdem den JVM-Thread-Monitor zum Lasttestprojekt hinzu, das für die REST-API-Anwendung ausgeführt wird. Der Monitor liefert Diagramme von blockierten, blockierten, geparkten und Gesamt-Threads und zeichnet Dumps von Threads in diesen Zuständen auf.
Die Codeänderung wird in das Repository übertragen und vom CI-Leistungstestprozess erfasst. Am nächsten Tag stellen die Entwickler fest, dass der Leistungstest über Nacht fehlgeschlagen ist. Die Bankanwendung reagierte kurz nach Beginn des Leistungstests für den Übertragungsvorgang nicht mehr. Das Überprüfen der Diagramme des JVM-Threads-Monitors im Lasttestbericht zeigt schnell, dass in der Bank-Anwendung Deadlock-Threads vorhanden sind (siehe Abb. 1.a). Die Deadlock-Details wurden vom JVM-Thread-Monitor als Teil des Berichts gespeichert und zeigen die genauen Codezeilen an, die für den Deadlock verantwortlich sind (siehe Listing 1.b).
Abb. 1.a - Anzahl der festgefahrenen Gewinde in der zu testenden Anwendung (AUT).
DEADLOCKED thread: http-nio-8080-exec-20
com.parasoft.demo.bank.v2.ATM_2.transfer:15
com.parasoft.demo.bank.ATM.transfer:21
...
Blocked by:
DEADLOCKED thread: http-nio-8080-exec-7
com.parasoft.demo.bank.v2.ATM_2.transfer:16
com.parasoft.demo.bank.ATM.transfer:21
com.parasoft.demo.bank.v2.RestController_2.transfer:29
sun.reflect.GeneratedMethodAccessor58.invoke:-1
sun.reflect.DelegatingMethodAccessorImpl.invoke:-1
java.lang.reflect.Method.invoke:-1
org.springframework.web.method.support.InvocableHandlerMethod.doInvoke:209
Listing 1.b - Vom JVM-Threads-Monitor gespeicherte Deadlock-Details
Bankanwendung Version 3: Deadlocks beheben
Die Entwickler von Bankanwendungen beschließen, den Deadlock durch Synchronisieren auf einem einzelnen globalen Objekt zu beheben und den Code der Übertragungsmethode wie folgt zu ändern:
14: public boolean transfer(IAccount from, IAccount to, int amount) {
15: synchronized (Account.class) {
17: if (from.withdraw(amount)) {
18: to.deposit(amount);
19: return true;
20: }
21: }
22: return false;
23: }
Die Änderung behebt das Deadlock-Problem von Version 2 und die Race-Bedingung von Version 1, aber den Durchschnitt transfer
Die Reaktionszeit des Betriebs erhöht sich mehr als um das Fünffache von 30 auf über 150 Millisekunden (siehe Abb. 2.a). Das BlockedRatio-Diagramm des JVM-Thread-Monitors zeigt, dass sich 60 bis 75 Prozent der Anwendungsthreads während der Ausführung des Auslastungstests im Status BLOCKIERT befinden (siehe Abb. 2.b). Die vom Monitor gespeicherten Details zeigen an, dass Anwendungsthreads blockiert sind, während versucht wird, den global synchronisierten Abschnitt in Zeile 15 aufzurufen (siehe Listing 2.c).
BLOCKED thread: http-nio-8080-exec-4
com.parasoft.demo.bank.v3.ATM_3.transfer:15
com.parasoft.demo.bank.ATM.transfer:21
com.parasoft.demo.bank.v3.RestController_3.transfer:29
...
Blocked by:
SLEEPING thread: http-nio-8080-exec-8
java.lang.Thread.sleep:-2
com.parasoft.demo.bank.Account.doWithdraw:64
com.parasoft.demo.bank.Account.withdraw:31
Listing 2.c - Details zum blockierten Thread, die vom JVM-Thread-Monitor gespeichert wurden
Bankanwendung Version 4: Verbesserung der Synchronisationsleistung
Das Entwicklerteam sucht nach einem Fix, der den Race-Zustand löst, ohne Deadlocks einzuführen und die Reaktionsfähigkeit der Anwendung zu beeinträchtigen, und findet nach einigen Recherchen eine vielversprechende Lösung unter Verwendung von java.util.concurrent.locks.ReentrantLock
Klasse:
19: private boolean doTransfer(Account from, Account to, int amount) {
20: try {
21: acquireLocks(from.getReentrantLock(), to.getReentrantLock());
22: if (from.withdraw(amount)) {
23: to.deposit(amount);
24: return true;
25: }
26: return false;
27: } finally {
28: releaseLocks(from.getReentrantLock(), to.getReentrantLock());
29: }
30:
Die Grafiken in Abb. 3a zeigen die Antwortzeiten der Bankanwendung transfer
Betrieb von Version 4 (optimierte Sperrung) im roten Diagramm, Version 3 (globale Objektsynchronisation) im blauen Diagramm und Version 1 (nicht synchronisierter Übertragungsvorgang) im grünen Diagramm. Die Grafiken zeigen, dass die transfer
Die Betriebsleistung hat sich durch die Verriegelungsoptimierung dramatisch verbessert. Der geringfügige Unterschied zwischen dem synchronisierten (rotes Diagramm) und dem nicht synchronisierten (grünes Diagramm) transfer
Der Betrieb ist ein akzeptabler Preis, um die Rennbedingungen zu verhindern.
Abb. 3.a - transfer
Betriebsreaktionszeit der Bankanwendung Version 4 (rot), Version 3 (blau) und Version 1 (grün).
Real World Beispiele
Beispiel 1: Wachsende Reaktionszeit der Anwendung
Die obigen Beispiele für Bankanwendungen dienen dazu, zu demonstrieren, wie typische Einzelfälle von Leistungseinbußen aufgrund von Threading-Problemen behoben werden können. Die Fälle in der realen Welt können komplizierter sein - die Grafiken in Abb. 4 zeigen ein Beispiel für eine Produktions-REST-API-Anwendung, deren Antwortzeit im Verlauf des Leistungstests weiter zunahm. Die Reaktionszeit der Anwendung wuchs in der ersten Hälfte des Tests langsamer und in der zweiten Hälfte schneller (siehe Abb. 4.a). In der ersten Hälfte des Tests korrelierte das Wachstum der Antwortzeit mit der Gesamtzeit, die Anwendungs-Threads im BLOCKED-Zustand verbracht haben (siehe Abb. 4.b). In der zweiten Hälfte des Tests korrelierte das Wachstum der Antwortzeit mit der Anzahl der Anwendungsthreads im Status PARKED. Die vom Load Test JVM Threads Monitor erfassten Stack-Traces enthielten die Details: einer zeigte auf a synchronized
Block, der für übermäßige Zeit im BLOCKED-Zustand verantwortlich war. Der andere zeigte auf die verwendeten Codezeilen java.util.concurrent.locks
Klassen für die Synchronisation, die dafür verantwortlich waren, dass Threads im Status PARKED gehalten wurden. Nachdem diese Codebereiche optimiert wurden, wurden beide Leistungsprobleme behoben.
Beispiel 2: Gelegentliche Spitzen in der Anwendungsantwortzeit
Der Lasttest-JVM-Thread-Monitor kann sehr hilfreich sein, um Details zu seltenen Thread-Problemen zu erfassen, insbesondere wenn Ihre Leistungstests automatisiert sind und regelmäßig ausgeführt werden *. Die Diagramme in Abb. 5 zeigen eine Produktions-REST-API-Anwendung mit intermittierenden Spitzen bei durchschnittlichen und maximalen Antwortzeiten (siehe Abb. 5.a).
Solche Spitzen in der Anwendungsantwortzeit können häufig durch eine suboptimale JVM-Garbage-Collector-Konfiguration verursacht werden. In diesem Fall weist jedoch eine korrelierende Spitze im BlockedTime-Monitor (siehe Abb. 5.b) auf die Thread-Synchronisation als Ursache des Problems hin. Der BlockedThreads-Monitor hilft hier noch mehr, indem er die Stapelspuren der blockierten und blockierenden Threads erfasst. Es ist wichtig, den Unterschied zwischen den Monitoren BlockedTime und BlockedThreads zu verstehen.
Der BlockedTime-Monitor zeigt die akkumulierte Zeit an, die JVM-Threads zwischen den Monitoraufrufen im Status BLOCKED verbracht haben, während der BlockedThreads-Monitor regelmäßig Snapshots von JVM-Threads erstellt und in diesen Snapshots nach blockierten Threads sucht. Aus diesem Grund erkennt der BlockedTime-Monitor die Blockierung von Threads zuverlässiger, weist Sie jedoch nur darauf hin, dass Probleme mit der Blockierung von Threads vorliegen. Der BlockedThreads-Monitor, der regelmäßige Thread-Snapshots erstellt, kann einige Thread-Blockierungsereignisse übersehen. Wenn er jedoch solche Ereignisse erfasst, liefert er detaillierte Informationen darüber, was die Blockierung verursacht. Aus diesem Grund ist es eine Frage der Statistik, ob ein BlockedThreads-Monitor die codebezogenen Details eines blockierten Status erfasst oder nicht. Wenn Ihre Leistungstests jedoch regelmäßig ausgeführt werden, wird das BlockedThreads-Diagramm bald einen Spitzenwert aufweisen (siehe Abb 5.c), was bedeutet, dass blockierte und blockierende Thread-Details erfasst wurden. Diese Details verweisen auf die Codezeilen, die für die seltenen Spitzen in der Antwortzeit der Anwendung verantwortlich sind.
Erstellen von Leistungsregressionssteuerungen
Der Lasttest-JVM-Threads-Monitor ist nicht nur ein effektives Diagnosetool, sondern kann auch zum Erstellen von Leistungsregressionssteuerungen für Thread-bezogene Probleme verwendet werden. Nachdem Sie ein solches Leistungsproblem entdeckt und behoben haben, erstellen Sie einen Leistungsregressionstest dafür. Der Test besteht aus einem bestehenden oder einem neuen Leistungstestlauf und einer neuen Regressionskontrolle. Im Fall von Parasoft Load Test wäre dies eine QoS-Monitor-Metrik für einen relevanten JVM-Threads-Monitor-Kanal. Erstellen Sie beispielsweise für das in Beispiel 1 (Abb. 4) beschriebene Problem eine Lasttest-QoS-Monitor-Metrik, die die Zeit prüft, die Anwendungs-Threads im BLOCKED-Zustand verbracht haben, und eine andere Metrik, die die Anzahl der Threads im PARKED-Zustand prüft. Es ist immer eine gute Idee, benannte Threads in Ihrer Java-Anwendung zu erstellen, damit Sie Leistungsregressionssteuerungen auf einen nach Namen gefilterten Satz von Threads anwenden können.
Verwenden des Java-Threads-Monitors in automatisierten Leistungstests
Die folgende Tabelle enthält eine Zusammenfassung der zu verwendenden Threads Monitor-Kanäle und wann:
Threads-Monitor-Kanal | Wann zu verwenden |
---|---|
Deadlock-Threads | Immer. Deadlocks sind wohl die schwerwiegendsten Thread-Probleme, die die Anwendungsfunktionalität vollständig beeinträchtigen könnten. |
ÜberwachenDeadlockedThreads | |
Blockierte Threads | Immer. Übermäßige Zeit im BLOCKED-Status oder die Anzahl der BLOCKED-Threads führt normalerweise zu Leistungseinbußen. Überwachen Sie mindestens einen dieser Parameter. Wird auch für Steuerelemente zur Leistungsregression verwendet. |
Blockierte Zeit | |
Sperrverhältnis | |
Anzahl blockiert | |
GeparkteThreads | Immer. Eine übermäßige Anzahl von Threads im Status PARKED kann auf eine missbräuchliche Verwendung der Klassen java.util.concurrent.locks und andere Threading-Probleme hinweisen. Wird auch für Steuerelemente zur Leistungsregression verwendet. |
GesamtThreads | Häufig. Verwenden Sie diese Option, um die Anzahl der Threads in BLOCKED, PARKED oder anderen Zuständen mit der Gesamtzahl der Threads zu vergleichen. Wird auch für Steuerelemente zur Leistungsregression verwendet. |
SchlafenThreads | Gelegentlich. Verwendung für Leistungsregressionskontrollen in Bezug auf diese Zustände und für Erkundungstests. |
WartenThreads | |
Wartezeit | |
Wartequote | |
WaitingCount | |
NeueThreads | Selten. Verwendung für Steuerelemente zur Leistungsregression in Bezug auf diese Thread-Zustände. |
Unbekannte Themen |
Schlussfolgerung
Der JVM-Thread-Monitor von Parasoft ist ein effektives Diagnosetool zum Erkennen von Thread-bezogenen JVM-Leistungsproblemen sowie zum Erstellen erweiterter Steuerelemente für die Leistungsregression. In Kombination mit SOAtest's Load Test Continuum hilft der JVM Threads Monitor, den Schritt zur Reproduktion von Leistungsproblemen zu eliminieren, indem relevante Thread-Details aufgezeichnet werden, die auf die Codezeilen hinweisen, die für schlechte Leistung verantwortlich sind, und Ihnen helfen, sowohl die Anwendungsleistung als auch die Entwickler- und QA-Produktivität zu verbessern.