Wöchentlicher technischer Bericht für die vierte Woche im November 2022

Diese Woche habe ich mich hauptsächlich mit der Optimierung eines Dienstes beschäftigt. Dieser Dienst ist in Java geschrieben. Wenn in der Produktionsumgebung nicht viel Verkehr herrscht, gibt es auch eine Zeitüberschreitung für den Aufruf von Batches. Und wenn die Zeitüberschreitung gesendet wird, ist die CPU-Auslastung niedrig. Bei der Beobachtung stieg die CPU-Auslastung nie an. Zu diesem Zeitpunkt wurde spekuliert, dass alle Threads bei einer bestimmten Operation blockiert waren und das Problem verursachten. Die meisten der Dienste, mit denen ich gearbeitet habe, einschließlich dieses Dienstes, sind IO-intensiv. Diese Art von Diensten beinhaltet viele RPC-Aufrufe, und wenn RPC-Aufrufe erfolgen, blockieren die Worker-Threads und machen es unmöglich, andere Anfragen zu bearbeiten. Daher wird die Anzahl der Worker-Threads für diese Art von Diensten sehr hoch angesetzt, um sicherzustellen, dass zusätzliche Threads für die Bearbeitung von IO-Anfragen zur Verfügung stehen, so dass nachfolgende Anfragen nicht bearbeitet werden können, weil die Mehrheit der Threads blockiert, was letztendlich zu einer großen Anzahl von Timeouts führt.

Das ist tatsächlich problematisch, obwohl Java das NIO-Modell beim Umgang mit Sockets und beim Parsen von Anfragen übernommen hat, aber die logische Verarbeitung der Anfrage verwendet immer noch einen Pool von Worker-Threads. Wenn ein Worker-Thread bei der IO-Anfrage blockiert ist, kann der Worker-Thread nur warten, bis die IO-Anfrage abgeschlossen ist oder eine Zeitüberschreitung eintritt. Wenn das von der IO-Anfrage zurückgegebene Ergebnis nicht vom Endergebnis der Anfrage abhängt, wird die Anfrage an einen anderen Thread-Pool zur Verarbeitung weitergeleitet. Diesmal ist die Verarbeitung für diesen Worker-Thread asynchron.

Nach der Fehlersuche stellte sich schließlich heraus, dass dieser Dienst eine Konfiguration über die Anzahl der Worker-Threads hatte, die aufgrund eines Framework-Problems nicht gelesen wurde. Dies führte dazu, dass der Dienst mit der Standardeinstellung für die Anzahl der Worker-Threads startete, die der Anzahl der CPU-Kerne entspricht. Wenn die Worker-Threads also nachgelagerte RPC-Aufrufe verarbeiteten, führte jede kleine Schwankung in der verstrichenen Zeit der nachgelagerten Schnittstelle dazu, dass eine große Anzahl von Anfragen einen Timeout verursachte. Zu diesem Zeitpunkt ist auch die CPU-Auslastung gering, da die Worker-Threads die meiste Zeit blockiert sind und auf die Rückkehr der IO-Operation warten.

Wie Sie das Problem beheben können, müssen Sie zunächst im Protokollrahmen den Namen des aktuellen Threads für den Logback-Protokollrahmen ausgeben lassen, d.h. in den Einstellungen für das Protokollformat % t hinzufügen. Dann einen einzelnen Knoten in der Testumgebung für Drucktests, um in einer bestimmten QPS den Dienst für die Anfrageverarbeitung zu sehen. Am besten fügen Sie StopWatch zum logischen Code der Anfrage hinzu, um die Analyse zu unterstützen. Schließlich stellen Sie fest, dass das Ausgabeprotokoll nur von einigen wenigen Threads stammt, was nicht stimmt. Verwenden Sie dann jstack, um die Anzahl der Threads herauszufiltern. Sie stellen fest, dass es tatsächlich nur vier sind.

Dies lässt sich damit erklären, dass das von diesem Java-Dienst verwendete Framework das netty-Framework für die Verarbeitung von Anfragen verwendet und diese schließlich an den netty-Worker-Thread-Pool weitergeleitet werden, um die Anfragelogik zu verarbeiten. In diesem Fall entspricht der netty worker thread pool dem Präfix nioEventLoopGroup-5. Darüber hinaus kann anhand der kumulativen CPU-Zeit nachgewiesen werden, dass es wirklich nur diese vier Threads gibt, die die gesamte Hauptlogik der Anfrage verarbeiten. Für diese Art von Dienst wird in der Produktionsumgebung die Anzahl der Worker-Threads in der Regel auf 800~1000 festgelegt und die Zeitüberschreitung für RPC-Aufrufe wird streng eingestellt, um zu verhindern, dass eine große Anzahl von Worker-Threads blockiert wird, was letztendlich zu einem drastischen Rückgang des Durchsatzes des Knotens führt.

Nachdem ich das Framework aktualisiert hatte, war das Problem gelöst. Außerdem entdeckte ich, dass eine der wichtigsten Schnittstellen des Dienstes einen nachgelagerten Aufruf auf der Grundlage des http-Protokolls enthielt. Der Aufruf erfolgte durch Manipulation der OkHttp-Bibliothek, und ich stellte fest, dass der Aufruf kein Timeout setzte. Das ist falsch, denn in einer extremen Situation, in der der Downstream verzögert zurückkehrt, wird der Worker-Thread lange Zeit blockiert. Daher müssen Sie eine angemessene Timeout-Zeit für Downstream-Aufrufe festlegen, um den reibungslosen Ablauf zwischen Upstream- und Downstream-Aufrufen zu gewährleisten und das Auftreten von Lawinen zu verhindern. Die Timeout-Zeit wird im Allgemeinen in drei Arten unterteilt: Verbindungs-Timeout, Lese-Timeout und Schreib-Timeout, die alle festgelegt werden müssen. Außerdem habe ich für die mögliche Existenz einer großen Anzahl von Http-Aufrufen den OkHttp ConnectionPool aktiviert.

Laut der Dokumentation besteht der Vorteil von ConnectionPool darin, dass sich mehrere http- oder http/2-Anfragen an dieselbe Adresse dieselbe Verbindung teilen können. Es sei jedoch darauf hingewiesen, dass die Voraussetzung für diese gemeinsame Nutzung ist, dass die Serverseite eine lange http-Verbindung unterstützt.

Außerdem haben wir uns in dieser Woche hauptsächlich mit den TCP-Inhalten des Tencent Cloud’s Advanced Architect beschäftigt, da die Prüfung am Wochenende angesetzt ist. Diese TCP-Prüfung ist etwas schwieriger als die ursprünglichen Architect- und Practitioner-Prüfungen, so dass Sie sich trotzdem darauf vorbereiten müssen. Ich hatte auf der Arbeit nicht viel Zeit, um mir den Stoff anzusehen, also habe ich den schweren Stoff direkt bis 5 Uhr morgens am Samstag durchgearbeitet und zum Glück habe ich die Prüfung am Ende bestanden. Ich sollte einen Artikel über diese Prüfung schreiben.