Beachten Sie die verringerte Schnittstellenverfügbarkeit, die durch einen asynchronen JSF-Aufruf verursacht wird | JD Cloud Technical Team

Vorwort

In diesem Artikel wird der Fehlerbehebungsprozess für das Problem der verringerten Schnittstellenverfügbarkeit beschrieben, das durch eine Zeitüberschreitung des asynchronen JSF-Aufrufs verursacht wird. Er stellt hauptsächlich die Fehlerbehebungsideen und den Prozess des asynchronen JSF-Aufrufs vor. Ich hoffe, dass er jedem helfen kann, das Prinzip des asynchronen JSF-Aufrufs zu verstehen und einige bereitzustellen Ideen zur Fehlerbehebung. Der in diesem Artikel analysierte JSF-Quellcode basiert auf der Version JSF 1,7.5-HOTFIX-T6.

Herkunft

Problemhintergrund

1. Das Werbebereitstellungssystem ist ein typischer E/A-intensiver (E/A-gebundener) Dienst. Einige Schnittstellen im System können für einen einzelnen Vorgang auf mehr als ein Dutzend externe Schnittstellen angewiesen sein, was dazu führt, dass die Schnittstelle lange dauert Dies beeinträchtigt die Benutzererfahrung erheblich. Daher ist es notwendig, diese externen Aufrufe in den asynchronen Modus umzuschalten, den Gesamtzeitverbrauch durch den gleichzeitigen Modus zu reduzieren und die Reaktionsgeschwindigkeit der Schnittstelle zu verbessern.
2. Im Szenario synchroner Aufrufe dauert die Schnittstelle lange, weist eine schlechte Leistung auf und die Antwortzeit der Schnittstelle ist lang. Um die Antwortzeit der Schnittstelle zu verkürzen, werden derzeit im Allgemeinen Thread-Pools verwendet, um Daten parallel abzurufen. Wenn jedoch Thread-Pools verwendet werden, erfordern unterschiedliche Unternehmen unterschiedliche Thread-Pools, was letztendlich die Wartung erschwert Wenn die Anzahl der CPU-Planungsthreads steigt, führt dies zu ernsthafteren Ressourcenkonflikten, wertvolle CPU-Ressourcen werden durch Kontextwechsel verschwendet und der Thread selbst belegt auch Systemressourcen und kann nicht unendlich erhöht werden.
3. Beim Lesen der JSF-Dokumentation haben wir festgestellt, dass JSF den asynchronen Aufrufmodus unterstützt. Da die Middleware diese Funktion bereits unterstützt, haben wir den von JSF bereitgestellten asynchronen Aufrufmodus übernommen. Derzeit unterstützt JSF drei asynchrone Aufrufmethoden, nämlich die ResponseFuture-Methode und die CompletableFuture-Methode und eine Schnittstellensignaturmethode, die den Rückgabewert als CompletableFuture definiert.

(1) So erhalten Sie ResponseFuture in RpcContext

Für diese Methode muss zunächst das async-Attribut auf der Verbraucherseite auf „true“ gesetzt werden, was bedeutet, dass asynchrone Aufrufe aktiviert werden, und dann die Methode „RpcContext.getContext().getFuture()“ verwendet werden, um ein ResponseFuture abzurufen, in dem der Provider aufgerufen wird. Nachdem die Zukunft abgerufen wurde, Sie können die get-Methode zum Blockieren verwenden. Warten Sie auf die Rückkehr. Diese Methode wird jedoch nicht mehr empfohlen, da der zweite CompletableFuture-Modus leistungsfähiger ist.

Codebeispiel:

asyncHelloService.sayHello("The ResponseFuture One");
ResponseFuture<Object> future1 = RpcContext.getContext().getFuture();
asyncHelloService.sayNoting("The ResponseFuture Two");
ResponseFuture<Object> future2 = RpcContext.getContext().getFuture();
try {
     future1.get();
     future2.get();
} catch (Throwable e) {
    LOGGER.error("catch " + e.getClass().getCanonicalName() + " " + e.getMessage(), e);
}

(2) So erhalten Sie CompletableFuture in RpcContext (unterstützt von Version 1.7.5 und höher)

Für diese Methode muss zunächst das async-Attribut auf der Verbraucherseite auf „true“ gesetzt werden, was bedeutet, dass asynchrone Aufrufe aktiviert werden. Anschließend wird die Methode RpcContext.getContext().getCompletableFuture() verwendet, bei der der Provider aufgerufen wird, um eine CompletableFuture für nachfolgende Vorgänge abzurufen. CompletableFuture erweitert Future und kann Berechnungsergebnisse durch Festlegen von Rückrufen verarbeiten. Es unterstützt Kombinationsoperationen und weitere Orchestrierung, wodurch das Problem der Rückrufhölle bis zu einem gewissen Grad gelöst wird.

Codebeispiel:

asyncHelloService.sayHello("The CompletableFuture One");
CompletableFuture<String> cf1 = RpcContext.getContext().getCompletableFuture();
asyncHelloService.sayNoting("The CompletableFuture Two");
CompletableFuture<String> cf2 = RpcContext.getContext().getCompletableFuture();

CompletableFuture<String> cf3 = RpcContext.getContext().asyncCall(() -> {
    asyncHelloService.sayHello("The CompletableFuture Three");
});
try {
    cf1.get();
    cf2.get();
    cf3.get();
} catch (Throwable e) {
    LOGGER.error("catch " + e.getClass().getCanonicalName() + " " + e.getMessage(), e);
}

(3) Verwenden Sie die von CompletableFuture signierte Schnittstelle (unterstützt von Version 1.7.5 und höher).

Dieser Modus erfordert eine Codeänderung und der Dienstanbieter muss die Rückgabewertsignatur der Methode im Voraus als CompletableFuture definieren. Diese Art von Aufrufer kann asynchron ohne Konfiguration verwendet werden.

Codebeispiel:

CompletableFuture<String> cf4 = asyncHelloService.sayHelloAsync("The CompletableFuture Fore");
cf4.whenComplete((res, err) -> {
    if (err != null) {
        LOGGER.error("interface async cf4 now complete error " + err.getClass().getCanonicalName() + " " + err.getMessage(), err);
    } else {
        LOGGER.info("interface async cf4 now complete : {}", res);
    }
});
CompletableFuture<Void> cf5 = asyncHelloService.sayNotingAsync("The CompletableFuture Five");

try {
    LOGGER.info("interface async cf1 now is : {}", cf4.get());
    LOGGER.info("interface async cf2 now is : {}", cf5.get());
} catch (Throwable e) {
    LOGGER.error("catch " + e.getClass().getCanonicalName() + " " + e.getMessage(), e);
}

Durch die Analyse der oben genannten drei asynchronen Aufrufmodi erfordert der dritte, dass der Anbieter die Methodensignatur ändert, um asynchron zu unterstützen, was schwierig zu implementieren ist. Um Änderungen zu minimieren und die API-Nutzung zu optimieren, haben wir uns schließlich für die zweite Methode entschieden besteht darin, das async-Attribut am Ende auf true zu setzen und nach dem Initiieren des Aufrufs ein CompletableFuture-Objekt von RpcContext für nachfolgende Vorgänge abzurufen.

Problemphänomen

Nach der Transformation des asynchronen Modus wurde der Zeitverbrauch einiger Schnittstellen, die auf viele externe Dienste angewiesen sind, erheblich reduziert. Oberflächlich betrachtet sieht das System friedlich aus, aber der gelegentliche Rückgang der Schnittstellenverfügbarkeit ist ein sehr gefährliches Signal. Das Folgende ist eine Schnittstelle das asynchrone Aufrufe verwendet. Verfügbarkeitsüberwachung





 

Durch Überwachung können wir feststellen, dass die Verfügbarkeit dieser Schnittstelle gelegentlich abnimmt. Im Allgemeinen kann die Verringerung der Verfügbarkeit der Schnittstelle durch eine Zeitüberschreitung oder das Auslösen einiger versteckter Probleme verursacht werden. Die Logik dieser Schnittstelle ist jedoch sehr einfach, nämlich Fragen Sie die Datenbank anhand der ID ab. Die Geschäftslogik ist sehr einfach. Theoretisch sollte die Verfügbarkeit nicht so stark reduziert werden. Durch die Protokollprüfung haben wir festgestellt, dass eine TimeOutException-Ausnahme aufgetreten ist, als die get-Methode von CompletableFuture asynchron aufgerufen wurde und blockiert und gewartet wurde. Das aktuelle Zeitlimit für die Schnittstellenkonfiguration beträgt 5 Sekunden. Ursprünglich ist das Schnittstellen-Zeitlimit ein Problem, auf das wir häufig stoßen, aber wir sind zu dem gegangen Anbieter, um das Protokoll abzufragen. Es wurde festgestellt, dass diese Anfrage nur wenige Millisekunden dauerte. Der Anbieter kehrte offensichtlich in wenigen Millisekunden oder Dutzenden von Millisekunden zurück. Warum kam es beim Verbraucher immer noch zu einer Zeitüberschreitung? Mit dieser Frage haben wir weiter analysiert, ob dies der Fall war verursacht durch JSF-Asynchronität. .

Beheben Sie Positionierungsgründe

Durch das Lesen des Quellcodes von JSF verstehen wir, dass der grundlegende Prozess des asynchronen JSF-Aufrufs darin besteht, dass der Client, bevor er eine Anforderung an den Server sendet, zunächst ermittelt, ob die Anforderung einen asynchronen Aufruf erfordert. Bei Bedarf wird ein JSFCompletableFuture-Objekt erstellt generiert. Diese Klasse wird seit CompletableFuture geerbt. Ein FutureMap-Objekt wird verwendet, um die eindeutige msgId der Anforderung und ein MsgFuture-Objekt zwischenzuspeichern. Das MsgFuture-Objekt enthält den Kanal, die Nachricht, das Timeout, kompatibelFuture und andere in diesem Aufruf verwendete Attribute. Nach dem Server Beim Rückruf können Sie die msgId übergeben, um das entsprechende MsgFuture-Objekt für die spätere Verarbeitung zu finden.

Generieren Sie zunächst die Zuordnung zwischen MsgId- und MsgFuture-Objekten in der doSendAsyn-Methode, serialisieren Sie dann die Daten und schreiben Sie schließlich die Daten, die über die lange Verbindung von netty an den Kanal gesendet werden sollen.

(1) Generieren Sie JSFCompletableFuture





 

(2) Behalten Sie die Beziehung zwischen msgId und MsgFuture bei





 

(3) Behalten Sie die Beziehung zwischen msgId und MsgFuture bei





 

(4) Einen Anruf einleiten





 

Nachdem der Server die Anforderung empfangen hat, löst er die Methode „channelRead“ der Klasse „ServerChannelHandler“ auf dem Server aus, die zurückgerufen werden soll. Diese Methode überprüft das Serialisierungsprotokoll, generiert dann eine JSFTask-Aufgabe und sendet die Aufgabe an den JSF-Business-Thread-Pool Ausführung und warten Sie auf das Geschäft. Nachdem die Aufgabe im Thread-Pool ausgeführt wurde, wird die Schreibmethode aufgerufen, um den Rückgabewert über den Kanal an den Client zurückzuschreiben.

(1) Der Server empfängt die Antwortverarbeitung





 

(2) Der Server schreibt die Antwort zurück





 

Nachdem der Client die Antwort erhalten hat, löst er die Methode „channelRead“ der Klasse „ClientChannelHandler“ des Clients aus. Diese Methode findet das vom Client zwischengespeicherte MsgFuture-Objekt anhand der vom Server zurückgegebenen msgId und bestimmt dann, ob das Attribut „CompatibleFuture“ im Objekt nicht vorhanden ist -null. Wenn es nicht leer ist, wird eine Aufgabe an den Callback-Thread-Pool gesendet. Die Hauptfunktion dieser Aufgabe besteht darin, die Methoden „completeExceptionally“ und „complete“ von CompletableFuture auszuführen, die zum Auslösen der nächsten Phase der Ausführung von CompletableFuture verwendet werden .

(1) Der Kunde erhält die Antwort





 

(2) Finden Sie die lokale MsgFuture





 

(3) Fügen Sie MsgFuture zum Thread-Pool hinzu





 

(4) Lösen Sie die Methode „complete“ oder „completeExceptionally“ von CompletableFuture aus





 

Obwohl wir durch die Analyse des Quellcodes den gesamten Prozess des asynchronen JSF-Aufrufs kennen, können wir immer noch nicht erklären, warum gelegentlich Zeitüberschreitungen auftreten, die nicht ablaufen sollten (hier bedeutet dies, dass der Server eindeutig keine Zeitüberschreitung aufweist, der Client dies jedoch weiterhin anzeigt). Durch das Ausschließen verschiedener Prozesse haben wir schließlich herausgefunden, dass es möglicherweise mit dem Hinzufügen der Aufgabe zum Callback-Thread-Pool zusammenhängt, um die vollständige Methode von CompletableFuture nach dem asynchronen JSF-Rückruf auszuführen, da diese Methode weiterhin ausgeführt wird Nachfolgende Phasen von CompletableFuture und unser Geschäftscode erhalten den RpcContext. Nachdem das CompletableFuture-Objekt darin zurückgegeben wurde, wird im Allgemeinen die unäre Abhängigkeitsmethode ThenApply von CompletableFuture verwendet, um einige nachfolgende Verarbeitungen durchzuführen. Die vollständige Methode von CompletableFuture wird verwendet, um die Ausführung dieser nachfolgenden Phasen auszulösen.

Geschäftscode asynchron aufrufen:



 

Lassen Sie uns das Grundwissen von CompletableFuture vorstellen. Jedes CompletableFuture kann als Beobachter betrachtet werden. Im Inneren befindet sich ein verknüpfter Listenmitgliedsvariablenstapel vom Typ „Completion“, der zum Speichern aller darin registrierten Beobachter verwendet wird. Wenn die Ausführung des Beobachters abgeschlossen ist, wird das Stapelattribut entfernt und die registrierten Beobachter werden der Reihe nach benachrichtigt. Daher wird zu diesem Zeitpunkt die ThenApply-Methode in unserem Programm aufgerufen. Die folgende Abbildung zeigt die darin enthaltenen Schlüsselattribute Abschließbare Zukunft.



Abbildung 12, dann Diagramm anwenden



 

Wenn der oben beschriebene asynchrone Aufrufprozess nicht klar ist, können Sie sich das folgende Aufrufbeziehungsdiagramm ansehen.





 



Bei der Betrachtung der Standardkonfiguration des Callack-Thread-Pools haben wir festgestellt, dass die Anzahl der Kernthreads 20, die Warteschlangenlänge 256 und die maximale Anzahl von Threads 200 beträgt. Angesichts dessen haben wir vermutet, dass die Anzahl der Kernthreads möglicherweise nicht ausreicht, was dazu führt, dass einige Rückrufaufgaben in der Warteschlange zurückbleiben und nicht rechtzeitig ausgeführt werden, was zu einer Zeitüberschreitung führt. Da es zu diesem Zeitpunkt keine andere Möglichkeit gibt, den Betriebsstatus des CallBack-Thread-Pools zu ermitteln, haben wir unsere Vermutung überprüft, indem wir den Geschäftscode geändert und den aktuellen Status des Callback-Thread-Pools abgerufen haben, als eine Timeout-Ausnahme aufgetreten ist.

(1) Rufen Sie den Thread-Pool-Statuscode ab





 

Nachdem wir den Code geändert und online gegangen waren, lief das System eine Zeit lang und die Schnittstellenverfügbarkeit nahm ab. Dann haben wir das Protokoll abgefragt. Aus dem Protokoll ist ersichtlich, dass bei Auftreten einer Timeout-Ausnahme die Anzahl der Kernthreads in der JSF Der Callback-Thread-Pool war voll. Gleichzeitig sind 71 Aufgaben in der Warteschlange zurückgeblieben. Anhand dieses Protokolls kann festgestellt werden, dass die Zeitüberschreitung der Aufgabenwarteschlange auftritt, weil die Anzahl der Kernthreads im JSF-Callback-Thread-Pool voll ist.





 

Problemanalyse

1. Aus dem obigen Protokoll wissen wir, dass die Ursache darin liegt, dass der asynchrone Thread-Pool voll ist. Theoretisch sollten normale Anforderungen schnell verarbeitet werden, auch wenn einige Warteschlangen vorhanden sind. Nachdem wir jedoch den Geschäftscode überprüft hatten, stellten wir fest, dass einige Unser Geschäft wurde in ThenApply erledigt. Einige zeitaufwändige Vorgänge, und eine andere asynchrone Methode wird in ThenApply aufgerufen.

2. Der erste Fall führt dazu, dass der Thread-Pool-Thread ständig belegt ist und andere Aufgaben in die Warteschlange gestellt werden. Dies ist tatsächlich akzeptabel, aber im zweiten Fall kann es aufgrund von Zirkelverweisen im Thread-Pool zu einem Deadlock kommen. Der Grund dafür ist dass die übergeordnete Aufgabe den asynchronen Rückruf zur Ausführung in den Thread-Pool stellt und die Unteraufgaben der übergeordneten Aufgabe den asynchronen Rückruf ebenfalls zur Ausführung in den Thread-Pool stellen. Die Kern-Thread-Größe des Callback-Thread-Pools beträgt 20. Wann Wenn 20 Anforderungen gleichzeitig eintreffen, wird der Rückruf-Kernthread ausgeführt. Wenn der Thread voll ist, wird die Unteraufgabe beim Anfordern des Threads in die Blockierwarteschlange eingegeben. Der Abschluss der übergeordneten Aufgabe hängt jedoch von der Unteraufgabe ab. Zu diesem Zeitpunkt kann die Unteraufgabe nicht abgerufen werden Thread, die übergeordnete Aufgabe kann nicht abgeschlossen werden. Der Hauptthread führt get aus, wechselt in den Blockierungsstatus und kann nie wiederhergestellt werden.

Lösung

Kurzfristige Lösung: Da die Thread-Pool-Kernthreads voll sind und Warteschlangen verursachen, wird die Anzahl der JSF-Callback-Thread-Pool-Kernthreads von 20 auf 200 angepasst.

Langfristiger Plan: Optimieren Sie den Code, sodass die zeitaufwändigen Vorgänge in ThenApply nicht im Callback-Thread-Pool ausgeführt werden. Optimieren Sie gleichzeitig die Codelogik und entfernen Sie den Prozess des erneuten Startens asynchroner Aufrufe innerhalb der ThenApply-Methode.

Vergleich vor und nach der Anpassung:





 

Durch Überprüfung der Überwachung kann festgestellt werden, dass die Schnittstellenverfügbarkeitsrate nach der Optimierung bei 100 % geblieben ist.

Autor: JD Retail Song Weifei

Quelle: JD Cloud Developer Community Bitte geben Sie beim Nachdruck die Quelle an

Broadcom kündigt die Beendigung des bestehenden Deepin-IDE-Versionsupdates des VMware-Partnerprogramms an und ersetzt das alte Erscheinungsbild durch ein neues Erscheinungsbild Zhou Hongyi: Der gebürtige Hongmeng wird definitiv erfolgreich sein WAVE SUMMIT begrüßt seine zehnte Sitzung, Wen Xinyiyan wird die neueste Enthüllung haben! Yakult Company bestätigt, dass 95 G-Daten durchgesickert sind Die beliebteste Lizenz unter den Programmiersprachen im Jahr 2023 „2023 China Open Source Developer Report“ offiziell veröffentlicht Julia 1.10 offiziell veröffentlicht Fedora 40 plant die Vereinheitlichung von /usr/bin und /usr/sbin Rust 1.75 .0-Version
{{o.name}}
{{m.name}}

Supongo que te gusta

Origin my.oschina.net/u/4090830/blog/10456180
Recomendado
Clasificación