Technische Überprüfung für die dritte Dezemberwoche 2022

In dieser Woche haben wir vor allem an der Optimierung eines bestimmten Java-Dienstes gearbeitet. Der Dienst hatte Probleme mit der CPU-Auslastung, die nicht hochgefahren werden konnte. Die erste Frage, die sich stellte, war, ob der Dienst ein Problem mit zu wenig Arbeits-Threads hat. Später stellte sich heraus, dass es nicht daran lag, dass die CPU-Auslastung nicht erhöht werden konnte, sondern dass die erhöhte CPU-Auslastung zu mehr Timeout-Problemen führen würde. Der Dienst hatte schon vor langer Zeit die Rückmeldung erhalten, dass die Leistung unzureichend war und es nicht empfohlen wurde, ihn weiter zu nutzen. Ich habe also das Gefühl, dass das Problem vom Framework ausgeht, nicht vom Geschäftscode.

Nachdem ich den Code des Frameworks gelesen und durchforstet habe, verwendet das Framework netty als NIO-Server-Framework und verteilt bei der Ausführung der Geschäftslogik die Geschäftsverarbeitungsaufgaben an den Worker-Thread. Der Worker-Thread verarbeitet dann die Geschäftslogik. Ein Problem des vorherigen Frameworks war, dass die Anzahl der Worker-Threads zu gering war, um mit hohen IO-Situationen fertig zu werden. In diesem Fall handelt es sich nicht um das gleiche Problem wie beim letzten Mal, und die Anzahl der Worker-Threads ist den Protokollen zufolge ausreichend. Könnte es ein client-seitiges Problem sein. In der gesamten Microservice-Architektur fungiert der Dienst als Client, der die Schnittstelle anderer Dienste aufruft.

Dies könnte ein Einstiegspunkt sein. Beim Durchlesen des Codes habe ich festgestellt, dass das Service-Framework durch die Implementierung der Java-Schnittstelle InvocationHandler eine Proxy-Klasse ObjectProxy erzeugt, die die RPC-Aufrufe an andere Dienste übernimmt. Im Geschäftscode erhält ObjectProxy bei der Initiierung von Aufrufen zu anderen Dienstschnittstellen über RPC eine Liste gültiger Knoten, die dem Zieldienst entsprechen (diese Liste gültiger Knoten wird alle 30s aktualisiert), und zwar mit Hilfe des zugehörigen ProtocolInvoker. Anschließend übergibt er die Liste an den Lastausgleicher LoadBalancer, um den Zielknoten dieses Aufrufs zu ermitteln. Der Aufruf an den Zielknoten erfolgt dann über die protokollspezifische Invoker-Klasse, die für die Verwaltung langer Verbindungen zum Zieldienst zuständig ist und zum Zeitpunkt des Aufrufs eine Verbindung auswählt, an die eine Anfrage gesendet und von der eine Antwort empfangen wird. Die spezifische Anfragemethode hängt davon ab, ob der Aufruf asynchron oder synchron ist.

Jeder Zieldienst, den der Client aufrufen muss, besteht aus mehreren Knotenpunkten. Für jeden Knoten erstellt das Framework standardmäßig zwei E/A-Threads für Netzwerk-E/A-Übertragungen (NIO-Modus). Darüber hinaus erstellt das Framework standardmäßig für jeden Knoten eine TCP-Verbindung, die der Anzahl der Prozessoren entspricht. Jeder E/A-Thread enthält einen Selektor, der nach Ereignissen im Zusammenhang mit der Verbindung fragt. Für jede TCP-Verbindung wird eine TCPSession erstellt, und jedes Mal, wenn eine Anfrage gesendet wird, wird ein Ticket erstellt, um die Anfrage und die zugehörige Antwort zu verfolgen. Bei synchronen Anfragen wird die Anfrage gesendet und blockiert, bis die Antwort eintrifft. Bei asynchronen Anfragen wird das Ticket mit einer Callback-Funktion gefüllt. Wenn die Antwort eintrifft, wird die TicketNumber (der eindeutige Index des Tickets) verwendet, um das entsprechende Ticket zu finden und die vorausgefüllte Callback-Funktion zur weiteren Verarbeitung aufzurufen.

Das Framework für NIO-Operationen, die zugrundeliegende Verwendung von Java bietet die Möglichkeit der NIO-Bibliothek. Die oben erwähnte TCP-Verbindung, in der Tat ist die NIO-Bibliothek in der SocketChannel. dann das Gerüst, wie das Paket zu teilen, wie es ist, das Gerüst durch den Puffer, um die aktuellen Daten zu speichern wurde gelesen, und wir verwenden in der Regel das RPC-Protokoll, wird das Paket die Anzahl der Bytes in das erste Byte des Pakets, müssen nur die Puffer mit der Anzahl der Bytes der Größe des Pakets können Sie wissen, ob die volle lesen Wenn das Paket nicht gelesen wird, wird es weiter gelesen. Wenn das Paket nicht vollständig gelesen wurde, warten Sie weiter auf die nachfolgenden Daten. Wenn das Paket gelesen wurde, werden die Daten aus dem Puffer entsprechend der Größe der Byteanzahl aufgeteilt und entsprechend verarbeitet. In diesem Framework wird ein zusätzlicher Worker-Thread aus einem Thread-Pool genommen, um die anschließende Verarbeitung des Tickets durchzuführen. Derzeit wird die Arbeit des Thread-Pools nur für diese Aufgabe verwendet, seine Standardanzahl von Threads und die gleiche Anzahl von Kernen, die maximale Anzahl von Threads ist doppelt so hoch wie die Anzahl der Kerne.

Gemäß der Definition der Java NIO-Bibliothek gibt es mehrere IO-Operationen, die im Channel spezifiziert sind, und der Selector fragt ab, ob diese Operationen bereit sind, und wenn sie bereit sind, gibt er den SelectionKey zurück. Der SelectionKey enthält die notwendigen Parameter für die richtige Operation, die von dem bereiten Channel ausgeführt werden soll.

Nach der Lektüre des Codes gibt es bisher keine offensichtlichen Probleme auf der Client-Seite. Die verwendete Lösung ist im Grunde genommen ausgereift und stabil, so dass es möglich ist, dass das Problem auf der Serverseite liegt, was noch behoben werden muss.