Detaillierte Cloud-native Analyse zum Debuggen von Netzwerklatenzproblemen in Kubernetes-Clustern

I. Einleitung

  • Da die Größe der Kubernetes-Cluster weiter wächst, werden die Anforderungen an die Dienstlatenz immer strenger. Manchmal wird beobachtet, dass einige Dienste, die auf der Kubernetes-Plattform ausgeführt werden, gelegentlich mit Latenzproblemen konfrontiert sind. Diese zeitweiligen Probleme sind nicht auf Leistungsprobleme zurückzuführen die Anwendung selbst. verursacht.
  • Nach und nach stellte ich fest, dass das Verzögerungsproblem, das durch Anwendungen im Kubernetes-Cluster verursacht wurde, zufällig zu sein schien. Der Aufbau einiger Netzwerkverbindungen kann mehr als 100 ms dauern, was dazu führt, dass nachgelagerte Dienste eine Zeitüberschreitung erleiden oder es erneut versuchen. Diese Dienste selbst verarbeiten Geschäftsantworten. Die Zeit kann zwar innerhalb von 100 ms gehalten werden, der Verbindungsaufbau dauert aber länger als 100 ms, was nicht tolerierbar ist. Darüber hinaus beträgt die Zeit für einige SQL-Abfragen, die sehr schnell (in der Größenordnung von Millisekunden) ausgeführt werden sollten, aus Anwendungssicht tatsächlich mehr als 100 ms. Aus Sicht der MySQL-Datenbank ist dies jedoch völlig normal und möglicherweise nicht langsam Es wurden Abfrageprobleme gefunden.
  • Durch Fehlerbehebung kann das Problem auf den Zusammenhang zwischen dem Verbindungsaufbau mit dem Kubernetes-Knoten eingegrenzt werden, einschließlich Anfragen innerhalb des Clusters oder Anfragen mit externen Ressourcen und externen Besuchern. Der einfachste Weg, dieses Problem zu reproduzieren, besteht darin, Vegeta auf einem beliebigen internen Knoten zu verwenden, um einen HTTP-Stresstest für einen mit NodePort bereitgestellten Dienst zu starten. Sie können beobachten, dass von Zeit zu Zeit einige Anfragen mit hoher Latenz generiert werden. Wie kann man dieses Problem verfolgen und lokalisieren?

2. Problemanalyse

  • Wenn Sie versuchen, das Problem anhand eines einfachen Beispiels zu reproduzieren, hoffen Sie, den Umfang des Problems einzugrenzen und unnötige Komplexität zu beseitigen. Zunächst waren zu viele Komponenten am Datenfluss zwischen Vegeta und Kubernetes Pods beteiligt. Es war schwierig festzustellen, ob es sich hierbei um ein tieferes Netzwerkproblem handelte, sodass eine Subtraktion erforderlich war:

Fügen Sie hier eine Bildbeschreibung ein

  • Der Vegeta-Client initiiert eine TCP-Anfrage an einen Kube-Knoten im Cluster. Der Kubernetes-Cluster im Rechenzentrum nutzt das Overlay-Netzwerk (läuft auf dem vorhandenen Rechenzentrumsnetzwerk) und kapselt die IP-Pakete des Overlay-Netzwerks im Rechenzentrum. innerhalb des IP-Pakets. Wenn die Anfrage den ersten Kube-Knoten erreicht, führt sie eine NAT-Übersetzung durch, um die IP und den Port des Kube-Knotens in die Netzwerkadresse des Overlays umzuwandeln, insbesondere die IP und den Port des Pods, auf dem die Anwendung ausgeführt wird. Bei der Anforderung einer Antwort erfolgt die entsprechende Rücktransformation (SNAT/DNAT). Dabei handelt es sich um ein sehr komplexes System, das eine große Menge veränderlicher Zustände verwaltet, die bei der Bereitstellung von Diensten ständig aktualisiert werden.
  • Als wir Vegeta zum ersten Mal für Stresstests verwendeten, stellten wir fest, dass es in der TCP-Handshake-Phase (zwischen SYN und SYN-ACK) zu einer Verzögerung kam. Um die durch HTTP und Vegeta verursachte Komplexität zu vereinfachen, verwenden Sie hping3 zum Senden von SYN-Paketen und beobachten Sie, ob die Antwortpakete verzögert sind. Schließen Sie dann die Verbindung, um diese Pakete mit Verzögerungen von mehr als 100 ms herauszufiltern und Vegeta einfach zu reproduzieren. Stress der Schicht 7 Testen oder Simulieren eines Dienstes, der einem SYN-Angriff ausgesetzt ist. Das folgende Protokoll zeigt das Ergebnis des Sendens von TCP-SYN/SYN-ACK-Paketen an Port 30927 des Kube-Knotens in 10-ms-Intervallen und des Herausfilterns langsamer Anfragen:
theojulienne@shell ~ $ sudo hping3 172.16.47.27 -S -p 30927 -i u10000 | egrep --line-buffered 'rtt=[0-9]{
    
    3}\.'
len=46 ip=172.16.47.27 ttl=59 DF id=0 sport=30927 flags=SA seq=1485 win=29200 rtt=127.1 ms
len=46 ip=172.16.47.27 ttl=59 DF id=0 sport=30927 flags=SA seq=1486 win=29200 rtt=117.0 ms
len=46 ip=172.16.47.27 ttl=59 DF id=0 sport=30927 flags=SA seq=1487 win=29200 rtt=106.2 ms
len=46 ip=172.16.47.27 ttl=59 DF id=0 sport=30927 flags=SA seq=1488 win=29200 rtt=104.1 ms
len=46 ip=172.16.47.27 ttl=59 DF id=0 sport=30927 flags=SA seq=5024 win=29200 rtt=109.2 ms
len=46 ip=172.16.47.27 ttl=59 DF id=0 sport=30927 flags=SA seq=5231 win=29200 rtt=109.2 ms
  • Anhand der Sequenznummer und der Zeit im Protokoll ist zunächst zu beobachten, dass es sich bei dieser Verzögerung nicht um einen einzelnen Vorfall handelt, sondern häufig in Clustern auftritt, als ob der Rückstand an Anforderungen schließlich auf einmal verarbeitet würde.
  • Als Nächstes möchten wir gezielt ermitteln, bei welcher Komponente möglicherweise eine Ausnahme vorliegt. Sind das die NAT-Regeln für Kube-Proxy, da sie Hunderte von Zeilen lang sind? Oder funktioniert der IPIP-Tunnel oder eine ähnliche Netzwerkkomponente schlecht? Eine Möglichkeit zur Fehlerbehebung besteht darin, jeden Schritt im System zu testen. Was passiert, wenn Sie die NAT-Regeln und die Firewall-Logik entfernen und nur IPIP-Tunnel verwenden?

Fügen Sie hier eine Bildbeschreibung ein

  • Befindet es sich ebenfalls auf einem Kube-Knoten, ermöglicht Linux die direkte Kommunikation mit dem Pod, was sehr einfach ist:
theojulienne@kube-node-client ~ $ sudo hping3 10.125.20.64 -S -i u10000 | egrep --line-buffered 'rtt=[0-9]{
    
    3}\.'
len=40 ip=10.125.20.64 ttl=64 DF id=0 sport=0 flags=RA seq=7346 win=0 rtt=127.3 ms
len=40 ip=10.125.20.64 ttl=64 DF id=0 sport=0 flags=RA seq=7347 win=0 rtt=117.3 ms
len=40 ip=10.125.20.64 ttl=64 DF id=0 sport=0 flags=RA seq=7348 win=0 rtt=107.2 ms
  • Wie Sie anhand der Ergebnisse sehen können, besteht das Problem weiterhin, sodass Probleme mit iptables und NAT ausgeschlossen sind. Liegt ein Problem mit TCP vor? Werfen wir einen Blick darauf, was passiert, wenn wir ICMP-Anfragen verwenden.
theojulienne@kube-node-client ~ $ sudo hping3 10.125.20.64 --icmp -i u10000 | egrep --line-buffered 'rtt=[0-9]{
    
    3}\.'
len=28 ip=10.125.20.64 ttl=64 id=42594 icmp_seq=104 rtt=110.0 ms
len=28 ip=10.125.20.64 ttl=64 id=49448 icmp_seq=4022 rtt=141.3 ms
len=28 ip=10.125.20.64 ttl=64 id=49449 icmp_seq=4023 rtt=131.3 ms
len=28 ip=10.125.20.64 ttl=64 id=49450 icmp_seq=4024 rtt=121.2 ms
len=28 ip=10.125.20.64 ttl=64 id=49451 icmp_seq=4025 rtt=111.2 ms
len=28 ip=10.125.20.64 ttl=64 id=49452 icmp_seq=4026 rtt=101.1 ms
len=28 ip=10.125.20.64 ttl=64 id=50023 icmp_seq=4343 rtt=126.8 ms
len=28 ip=10.125.20.64 ttl=64 id=50024 icmp_seq=4344 rtt=116.8 ms
len=28 ip=10.125.20.64 ttl=64 id=50025 icmp_seq=4345 rtt=106.8 ms
len=28 ip=10.125.20.64 ttl=64 id=59727 icmp_seq=9836 rtt=106.1 ms
  • Die Ergebnisse zeigen, dass ICMP das Problem immer noch reproduzieren kann. Verursacht der IPIP-Tunnel das Problem? Vereinfachen wir das Problem noch weiter:

Fügen Sie hier eine Bildbeschreibung ein

  • Ist es also möglich, dass irgendeine Kommunikation zwischen diesen Knoten dieses Problem verursacht?
theojulienne@kube-node-client ~ $ sudo hping3 172.16.47.27 --icmp -i u10000 | egrep --line-buffered 'rtt=[0-9]{
    
    3}\.'
len=46 ip=172.16.47.27 ttl=61 id=41127 icmp_seq=12564 rtt=140.9 ms
len=46 ip=172.16.47.27 ttl=61 id=41128 icmp_seq=12565 rtt=130.9 ms
len=46 ip=172.16.47.27 ttl=61 id=41129 icmp_seq=12566 rtt=120.8 ms
len=46 ip=172.16.47.27 ttl=61 id=41130 icmp_seq=12567 rtt=110.8 ms
len=46 ip=172.16.47.27 ttl=61 id=41131 icmp_seq=12568 rtt=100.7 ms
len=46 ip=172.16.47.27 ttl=61 id=9062 icmp_seq=31443 rtt=134.2 ms
len=46 ip=172.16.47.27 ttl=61 id=9063 icmp_seq=31444 rtt=124.2 ms
len=46 ip=172.16.47.27 ttl=61 id=9064 icmp_seq=31445 rtt=114.2 ms
len=46 ip=172.16.47.27 ttl=61 id=9065 icmp_seq=31446 rtt=104.2 ms
  • Hinter dieser Komplexität steht vereinfacht ausgedrückt jede Netzwerkkommunikation zwischen zwei Kube-Knoten, einschließlich ICMP. Wenn dieser Zielknoten „abnormal“ ist (einige Knoten sind schlechter als andere, z. B. höhere Latenz und häufigere Probleme), werden Sie beim Auftreten des Problems immer noch ähnliche Verzögerungen feststellen.
  • Die Frage ist nun: Offensichtlich tritt dieses Problem nicht auf allen Computern auf. Warum tritt dieses Problem nur auf den Servern mit Kube-Knoten auf? Tritt es auf, wenn der Kube-Knoten als Anforderungssender oder Anforderungsempfänger fungiert? Glücklicherweise ist es jetzt einfach, den Umfang des Problems einzugrenzen: Sie können einen Computer außerhalb des Clusters als Absender verwenden und denselben „bekannten Fehler“-Computer als Ziel der Anforderung verwenden, aber feststellen, dass die Anforderung darin liegt Richtung Problem besteht weiterhin:
theojulienne@shell ~ $ sudo hping3 172.16.47.27 -p 9876 -S -i u10000 | egrep --line-buffered 'rtt=[0-9]{
    
    3}\.'
len=46 ip=172.16.47.27 ttl=61 DF id=0 sport=9876 flags=RA seq=312 win=0 rtt=108.5 ms
len=46 ip=172.16.47.27 ttl=61 DF id=0 sport=9876 flags=RA seq=5903 win=0 rtt=119.4 ms
len=46 ip=172.16.47.27 ttl=61 DF id=0 sport=9876 flags=RA seq=6227 win=0 rtt=139.9 ms
len=46 ip=172.16.47.27 ttl=61 DF id=0 sport=9876 flags=RA seq=7929 win=0 rtt=131.2 ms
  • Wiederholen Sie dann den obigen Vorgang und senden Sie dieses Mal die Anfrage vom Kube-Knoten an den externen Knoten:
theojulienne@kube-node-client ~ $ sudo hping3 172.16.33.44 -p 9876 -S -i u10000 | egrep --line-buffered 'rtt=[0-9]{
    
    3}\.'
^C
--- 172.16.33.44 hping statistic ---
22352 packets transmitted, 22350 packets received, 1% packet loss
round-trip min/avg/max = 0.2/7.6/1010.6 ms
  • Wenn Sie sich die Verzögerungsdaten bei der Paketerfassung ansehen, können Sie weitere Informationen erhalten. Insbesondere wird die Latenz auf der sendenden Seite beobachtet (unten), der empfangende Server sieht jedoch keine Latenz (oben). Beachten Sie die Delta-Spalte in der Abbildung (in Sekunden):

Fügen Sie hier eine Bildbeschreibung ein

  • Wenn man sich außerdem den Unterschied in der Reihenfolge von TCP- und ICMP-Netzwerkpaketen auf der Empfängerseite ansieht (basierend auf der Sequenz-ID), kann man feststellen, dass ICMP-Pakete immer in der Reihenfolge ankommen, in der sie gesendet werden, aber die Die Lieferzeit ist unregelmäßig und die Sequenz-ID von TCP-Paketen ist manchmal gestaffelt und ein Teil davon wird angehalten. Wenn Sie insbesondere die Ports zählen, an denen SYN-Pakete gesendet/empfangen werden, sind diese Ports auf der Empfangsseite nicht sequentiell, auf der Sendeseite jedoch sequentiell.
  • Die derzeit von Servern verwendeten Netzwerkkarten, wie sie beispielsweise in Ihrem eigenen Rechenzentrum verwendet werden, weisen einige subtile Unterschiede bei der Verarbeitung von TCP- und ICMP-Netzwerknachrichten auf. Wenn ein Datagramm eintrifft, hasht die Netzwerkkarte das auf jeder Verbindung weitergeleitete Paket und versucht, verschiedene Verbindungen verschiedenen Empfangswarteschlangen zuzuweisen, wobei sie (vermutlich) jeder Warteschlange einen CPU-Kern zuweist. Bei TCP-Paketen enthält dieser Hashwert sowohl die Quell-IP und den Quell-Port als auch die Ziel-IP und den Ziel-Port. Mit anderen Worten: Der Hashwert jeder Verbindung ist wahrscheinlich unterschiedlich. Bei ICMP-Paketen enthält der Hashwert nur die Quell-IP und Ziel-IP, da kein Port vorhanden ist, was den obigen Befund erklärt.
  • Eine weitere neue Entdeckung war, dass ICMP-Pakete zwischen den beiden Hosts für einen bestimmten Zeitraum blockiert waren, während TCP-Pakete für denselben Zeitraum in Ordnung waren. Dies scheint uns zu sagen, dass es sich um den Hash der empfangenden Netzwerkkartenwarteschlange handelt, der ein „Scherz“ ist, was fast sicher ist, dass die Pause während der Verarbeitung von RX-Paketen auf der empfangenden Seite auftritt und kein Problem auf der sendenden Seite ist. Dadurch werden Übertragungsprobleme zwischen Kube-Knoten ausgeschlossen, sodass jetzt bekannt ist, dass es sich um einen Stillstand in der Verarbeitungsphase des Pakets handelt und dass es sich um einen Kube-Knoten handelt, der sich auf der Empfängerseite befindet.

3. Netzwerkpaketverarbeitungsprozess des Linux-Kernels

  • Um zu verstehen, warum das Problem auf der Empfangsseite des Kube-Knotendienstes auftritt, werfen wir einen Blick darauf, wie Linux mit Netzwerkpaketen umgeht. In der einfachsten und primitivsten Implementierung sendet die Netzwerkkarte nach dem Empfang eines Netzwerkpakets einen Interrupt an den Linux-Kernel, um ihn darüber zu informieren, dass ein Netzwerkpaket verarbeitet werden muss. Der Kernel stoppt andere Arbeiten, die er gerade ausführt, schaltet den Kontext auf den Interrupt-Handler um, verarbeitet das Netzwerkpaket und wechselt dann zurück zur vorherigen Arbeitsaufgabe.

Fügen Sie hier eine Bildbeschreibung ein

  • Der Kontextwechsel wird sehr langsam sein. Diese Methode stellte in den 1990er Jahren möglicherweise kein Problem für 10-Mbit-Netzwerkkarten dar, aber viele Server verfügen jetzt über 10G-Netzwerkkarten, und die maximale Paketverarbeitungsgeschwindigkeit kann möglicherweise 15 Millionen Pakete pro Sekunde erreichen: auf a kleiner 8-Kerner Auf dem Server bedeutet das Millionen von Interrupts pro Sekunde.
  • Vor vielen Jahren hat Linux eine neue NAPI hinzugefügt. Die Netzwerk-API wurde in der Vergangenheit verwendet, um die traditionelle Methode zu ersetzen. Moderne Netzwerkkartentreiber nutzen diese neue API, um die Leistung der Hochgeschwindigkeits-Paketverarbeitung erheblich zu verbessern. Bei niedrigen Geschwindigkeiten akzeptiert der Kernel weiterhin Interrupts von der Netzwerkkarte, wie zuvor beschrieben. Sobald Pakete eintreffen, die den Schwellenwert überschreiten, deaktiviert der Kernel Interrupts und beginnt mit der Abfrage der Netzwerkkarte, um Netzwerkpakete stapelweise zu erfassen. Dieser Vorgang wird in einem „Softirq“ abgeschlossen oder kann auch als Software-Interrupt-Kontext bezeichnet werden. Dies geschieht am Ende des Systemaufrufs, wenn das Programm den Kernel-Space anstelle des User-Space betreten hat.

Fügen Sie hier eine Bildbeschreibung ein

  • Diese Methode ist viel schneller als die herkömmliche Methode, bringt aber auch ein weiteres Problem mit sich. Wenn die Anzahl der Pakete so groß ist, dass die gesamte CPU-Zeit für die Verarbeitung der von der Netzwerkkarte empfangenen Pakete aufgewendet wird, das Benutzermodusprogramm jedoch nicht in der Lage ist, diese Netzwerkanforderungen in der Warteschlange (z. B. von einer TCP-Verbindung) tatsächlich zu verarbeiten (z. B. beim Abrufen von Daten usw.), irgendwann füllt sich die Warteschlange und es werden Pakete verworfen. Um die Laufzeit zwischen Benutzermodus und Kernelmodus auszugleichen, begrenzt der Kernel die Anzahl der Verarbeitungspakete für einen bestimmten Software-Interrupt-Kontext und legt ein „Budget“ fest. Sobald dieser „Budget“-Wert überschritten wird, wird ein weiterer Thread namens „ksoftiqrd“ aktiviert (oder Sie können diesen Thread im ps-Befehl sehen), der die Software außerhalb des normalen Systemaufrufpfads weiter verarbeitet. Dieser Thread wird den Kontext unterbrechen Verwenden Sie den Standard-Prozessplaner, um eine faire Planung zu erreichen.

Fügen Sie hier eine Bildbeschreibung ein

  • Indem wir den Pfad herausfinden, den der Linux-Kernel zur Verarbeitung von Netzwerkpaketen verwendet, können wir feststellen, dass dieser Verarbeitungsprozess tatsächlich angehalten werden kann. Wenn das Intervall zwischen Softirq-Verarbeitungsaufrufen länger wird, befindet sich das Netzwerkpaket möglicherweise eine Zeit lang in der RX-Warteschlange der Netzwerkkarte. Dies kann an einem CPU-Kern-Deadlock oder langsameren Verarbeitungsaufgaben liegen, die den Kernel daran hindern, Softirqs zu verarbeiten . .

4. Grenzen Sie das Problem auf einen bestimmten Kern oder eine bestimmte Methode ein

  • Bisher glauben wir, dass diese Verzögerung tatsächlich möglich ist, und scheinen einige sehr ähnliche Anzeichen zu beobachten. Der nächste Schritt besteht darin, die Theorie zu bestätigen und zu versuchen, die Ursache des Problems zu verstehen.
  • Werfen wir einen Blick auf die Netzwerkanfrage, die das Problem verursacht hat:
len=46 ip=172.16.53.32 ttl=61 id=29573 icmp_seq=1953 rtt=99.3 ms
len=46 ip=172.16.53.32 ttl=61 id=29574 icmp_seq=1954 rtt=89.3 ms
len=46 ip=172.16.53.32 ttl=61 id=29575 icmp_seq=1955 rtt=79.2 ms
len=46 ip=172.16.53.32 ttl=61 id=29576 icmp_seq=1956 rtt=69.1 ms
len=46 ip=172.16.53.32 ttl=61 id=29577 icmp_seq=1957 rtt=59.1 ms
len=46 ip=172.16.53.32 ttl=61 id=29790 icmp_seq=2070 rtt=75.7 ms
len=46 ip=172.16.53.32 ttl=61 id=29791 icmp_seq=2071 rtt=65.6 ms
len=46 ip=172.16.53.32 ttl=61 id=29792 icmp_seq=2072 rtt=55.5 ms
  • Wie bereits erwähnt, werden diese ICMP-Pakete in eine bestimmte RX-Warteschlange einer Netzwerkkarte gehasht und dann von einem bestimmten CPU-Kern verarbeitet. Wenn Sie verstehen möchten, was der Kernel tut, müssen Sie zunächst wissen, um welchen CPU-Kern es sich handelt und wie softirq und ksoftiqrd diese Pakete verarbeiten. Dies wird bei der Lokalisierung des Problems sehr hilfreich sein.
  • Es gibt Tools, mit denen man den Laufstatus des Linux-Kernels in Echtzeit verfolgen kann. Zu diesem Zweck kann bcc verwendet werden. Mit bcc können Sie ein kleines C-Programm schreiben und es in jede Funktion des Kernels einbinden. Anschließend können Ereignisse zwischengespeichert und an ein Python-Programm im Benutzermodus übertragen werden. Dieses Python-Programm führt eine zusammenfassende Analyse dieser Ereignisse und dann die Ergebnisse durch werden Ihnen zurückgegeben. Das oben erwähnte „Einhängen in jede Funktion im Kernel“ ist eigentlich ein schwieriger Punkt, wurde jedoch so sicher wie möglich verwendet, da es darauf ausgelegt ist, solche Probleme in der Produktionsumgebung zu verfolgen. Probleme können im Allgemeinen nicht einfach in einem Test oder einer Entwicklung reproduziert werden Umfeld.
  • Wir wissen, dass der Kernel diese IMCP-Ping-Pakete verarbeitet, also fangen wir die icmp_echo-Methode des Kernels ab. Diese Methode akzeptiert ein eingehendes ICMP-Paket „Echo Request“ und initiiert eine ICMP-Antwort „Echo Response“. Sie können diese Pakete identifizieren durch die in hping3 angezeigte icmp_seq-Sequenznummer. Der Code für dieses BCC-Skript mag kompliziert erscheinen, aber wenn man ihn aufschlüsselt, klingt er nicht so beängstigend. Der Funktion icmp_echo wird ein Zeiger auf die Struktur sk_buff * skb übergeben, bei der es sich um das Datenpaket handelt, das die ICMP-Echo-Anfrage enthält. Wir können etwas recherchieren, echo.sequence extrahieren (entsprechend icmp_seq, das oben von hping3 gezeigt wurde) und es zurück an den Benutzerbereich senden. Gleichzeitig können Sie auch problemlos den aktuellen Prozessnamen oder die Prozess-ID abrufen.
  • Wenn der Kernel diese Pakete verarbeitet, können Sie die folgenden Ergebnisse sehen:
TGID    PID     PROCESS NAME    ICMP_SEQ
0       0       swapper/11      770
0       0       swapper/11      771
0       0       swapper/11      772
0       0       swapper/11      773
0       0       swapper/11      774
20041   20086   prometheus      775
0       0       swapper/11      776
0       0       swapper/11      777
0       0       swapper/11      778
4512    4542   spokes-report-s  779
  • Was hier zum Prozessnamen zu beachten ist, ist, dass Sie im Kontext von Softirq, der nach dem Systemaufruf auftritt, sehen können, dass der Prozess, der diesen Systemaufruf initiiert hat, als „Prozess“ angezeigt wird, obwohl dies der Kernel ist, der ihn verarbeitet im Kontext des Kernels.
  • Durch Ausführen ist es nun möglich, von hping3 beobachtete blockierte Pakete mit dem Prozess zu korrelieren, der sie verarbeitet hat. Ein einfacher grep der erfassten icmp_seq-Werte bietet Kontext, um zu sehen, was passiert ist, bevor diese Pakete verarbeitet wurden. Pakete, die den in hping3 oben angezeigten icmp_seq-Werten entsprechen, wurden markiert, und die beobachteten RTT-Werte werden ebenfalls angezeigt (in Klammern gehen wir davon aus, dass Anfragen mit RTT<50 ms nicht herausgefiltert werden):
TGID    PID     PROCESS NAME    ICMP_SEQ ** RTT
--
10137   10436   cadvisor        1951
10137   10436   cadvisor        1952
76      76      ksoftirqd/11    1953 ** 99ms
76      76      ksoftirqd/11    1954 ** 89ms
76      76      ksoftirqd/11    1955 ** 79ms
76      76      ksoftirqd/11    1956 ** 69ms
76      76      ksoftirqd/11    1957 ** 59ms
76      76      ksoftirqd/11    1958 ** (49ms)
76      76      ksoftirqd/11    1959 ** (39ms)
76      76      ksoftirqd/11    1960 ** (29ms)
76      76      ksoftirqd/11    1961 ** (19ms)
76      76      ksoftirqd/11    1962 ** (9ms)
--
10137   10436   cadvisor        2068
10137   10436   cadvisor        2069
76      76      ksoftirqd/11    2070 ** 75ms
76      76      ksoftirqd/11    2071 ** 65ms
76      76      ksoftirqd/11    2072 ** 55ms
76      76      ksoftirqd/11    2073 ** (45ms)
76      76      ksoftirqd/11    2074 ** (35ms)
76      76      ksoftirqd/11    2075 ** (25ms)
76      76      ksoftirqd/11    2076 ** (15ms)
76      76      ksoftirqd/11    2077 ** (5ms)
  • Aus den obigen Ergebnissen geht hervor, dass diese Pakete zunächst vom Prozess ksoftirqd/11 verarbeitet werden, der uns bequem mitteilt, dass dieser bestimmte Computer seine ICMP-Pakete an den CPU-Kern 11 des Empfängers gehasht hat. Wir können das auch jedes Mal sehen, wenn Sie es sehen blockiert, sehen Sie immer, dass einige Pakete im Kontext des Systemaufrufs softirq des Cadvisors verarbeitet werden, und dann übernimmt ksoftirqd und verarbeitet den Rückstand, und dies entspricht zufällig den blockierten Paketen, die wir gefunden haben.
  • Die Tatsache, dass cAdvisor immer unmittelbar vor der blockierten Anfrage ausgeführt wird, deutet ebenfalls darauf hin, dass dies möglicherweise mit dem Problem zusammenhängt, das wir beheben. Ironischerweise verursachte die Verwendung von cAdvisor genau zur „Analyse der Ressourcennutzung und Leistungsmerkmale laufender Container“, wie auf der Homepage von cAdvisor beschrieben, dieses Leistungsproblem. Wie bei vielen Dingen im Zusammenhang mit Containern handelt es sich hierbei um relativ hochmoderne Tools und es gibt Situationen, die zu unerwarteten Leistungseinbußen führen.

5. Was hat cAdvisor getan, um die Pause zu verursachen?

  • Nachdem Sie nun verstanden haben, wie ein Stillstand auftritt, welchen Prozess er verursacht und welchen CPU-Kern er verursacht, haben Sie nun ein gutes Verständnis davon. Damit der Kernel ksoftirqd hart blockiert, anstatt ksoftirqd im Voraus zu planen, und nachdem wir auch gesehen haben, dass die Pakete im Softirq-Kontext von cAdvisor verarbeitet werden, gehen wir davon aus, dass cAdvisor möglicherweise sehr langsam beim Aufrufen des Systemaufrufs ist und danach den Rest des Netzwerks abschließt Pakete können normal verarbeitet werden:

Fügen Sie hier eine Bildbeschreibung ein

  • Dies ist nur eine Theorie. Wie können Sie also überprüfen, ob dies tatsächlich geschieht? Wir können während des gesamten Prozesses verfolgen, was auf dem CPU-Kern ausgeführt wird, den Punkt finden, an dem Pakete das „Budget“ überschreiten, die ksoftirqd-Verarbeitung aktivieren und dann zurückgehen und sehen, was auf dem CPU-Kern ausgeführt wird. Stellen Sie sich das so vor, als würden Sie alle paar Millisekunden eine Röntgenaufnahme der CPU machen. Es sieht aus wie das:

Fügen Sie hier eine Bildbeschreibung ein

  • Deep Traversal besteht darin, dass die meisten dieser Anforderungen implementiert wurden. Das Perf-Record-Tool kann bestimmte CPU-Kerne mit einer bestimmten Frequenz abtasten und Echtzeit-Aufrufdiagramme generieren (einschließlich Benutzerraum und Kernel). Echtzeitaufrufe können mit einem von Brendan Gregg entwickelten FlameGraph-Programm aufgezeichnet und manipuliert werden. Dieses Tool behält die Reihenfolge des Stack-Trace bei und kann alle 1 ms eine einzelne Zeile des Stack-Trace abtasten und dann vor ksoftirqd eine Stichprobe von 100 ms erhalten wird ausgeführt. :
# record 999 times a second, or every 1ms with some offset so not to align exactly with timers
sudo perf record -C 11 -g -F 999
# take that recording and make a simpler stack trace.
sudo perf script 2>/dev/null | ./FlameGraph/stackcollapse-perf-ordered.pl | grep ksoftir -B 100
  • Die Ergebnisse sind wie folgt: (Hunderte von Spuren, die ähnlich aussehen)
cadvisor;[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];entry_SYSCALL_64_after_swapgs;do_syscall_64;sys_read;vfs_read;seq_read;memcg_stat_show;mem_cgroup_nr_lru_pages;mem_cgroup_node_nr_lru_pages
cadvisor;[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];entry_SYSCALL_64_after_swapgs;do_syscall_64;sys_read;vfs_read;seq_read;memcg_stat_show;mem_cgroup_nr_lru_pages;mem_cgroup_node_nr_lru_pages
cadvisor;[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];entry_SYSCALL_64_after_swapgs;do_syscall_64;sys_read;vfs_read;seq_read;memcg_stat_show;mem_cgroup_iter
cadvisor;[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];entry_SYSCALL_64_after_swapgs;do_syscall_64;sys_read;vfs_read;seq_read;memcg_stat_show;mem_cgroup_nr_lru_pages;mem_cgroup_node_nr_lru_pages
cadvisor;[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];[cadvisor];entry_SYSCALL_64_after_swapgs;do_syscall_64;sys_read;vfs_read;seq_read;memcg_stat_show;mem_cgroup_nr_lru_pages;mem_cgroup_node_nr_lru_pages
ksoftirqd/11;ret_from_fork;kthread;kthread;smpboot_thread_fn;smpboot_thread_fn;run_ksoftirqd;__do_softirq;net_rx_action;ixgbe_poll;ixgbe_clean_rx_irq;napi_gro_receive;netif_receive_skb_internal;inet_gro_receive;bond_handle_frame;__netif_receive_skb_core;ip_rcv_finish;ip_rcv;ip_forward_finish;ip_forward;ip_finish_output;nf_iterate;ip_output;ip_finish_output2;__dev_queue_xmit;dev_hard_start_xmit;ipip_tunnel_xmit;ip_tunnel_xmit;iptunnel_xmit;ip_local_out;dst_output;__ip_local_out;nf_hook_slow;nf_iterate;nf_conntrack_in;generic_packet;ipt_do_table;set_match_v4;ip_set_test;hash_net4_kadt;ixgbe_xmit_frame_ring;swiotlb_dma_mapping_error;hash_net4_test
ksoftirqd/11;ret_from_fork;kthread;kthread;smpboot_thread_fn;smpboot_thread_fn;run_ksoftirqd;__do_softirq;net_rx_action;gro_cell_poll;napi_gro_receive;netif_receive_skb_internal;inet_gro_receive;__netif_receive_skb_core;ip_rcv_finish;ip_rcv;ip_forward_finish;ip_forward;ip_finish_output;nf_iterate;ip_output;ip_finish_output2;__dev_queue_xmit;dev_hard_start_xmit;dev_queue_xmit_nit;packet_rcv;tpacket_rcv;sch_direct_xmit;validate_xmit_skb_list;validate_xmit_skb;netif_skb_features;ixgbe_xmit_frame_ring;swiotlb_dma_mapping_error;__dev_queue_xmit;dev_hard_start_xmit;__bpf_prog_run;__bpf_prog_r
  • Es gibt viele Protokolle, aber wenn Sie vorsichtig sind, haben Sie vielleicht ein festes Muster entdeckt: cAdvisor und dann ksoftirqd, was bedeutet das also? Jede Zeile ist ein Trace-Datensatz zu einem bestimmten Zeitpunkt, und die Methoden in jedem aufgerufenen Methodenstapel werden durch Semikolons getrennt. In der Mitte der Zeile können Sie sehen, dass der aufgerufene Systemaufruf read() ist: ...;dosyscall_64;sys_read;... Daher verbringt cAdvisor viel Zeit damit, den Systemaufruf read() aufzurufen, der mit der mem_cgroup zusammenhängt * Funktion, weil es sich um die Methode handelt (die Methode am unteren Rand des Stapels). Der Methoden-Stack-Trace kann den spezifischen Inhalt des Lesevorgangs nicht einfach anzeigen. Daher können Sie Strace verwenden, um zu sehen, was cAdvisor tut, und um Systemaufrufe zu finden, die länger als 100 ms dauern.
theojulienne@kube-node-bad ~ $ sudo strace -p 10137 -T -ff 2>&1 | egrep '<0\.[1-9]'
[pid 10436] <... futex resumed> )       = 0 <0.156784>
[pid 10432] <... futex resumed> )       = 0 <0.258285>
[pid 10137] <... futex resumed> )       = 0 <0.678382>
[pid 10384] <... futex resumed> )       = 0 <0.762328>
[pid 10436] <... read resumed> "cache 154234880\nrss 507904\nrss_h"..., 4096) = 658 <0.179438>
[pid 10384] <... futex resumed> )       = 0 <0.104614>
[pid 10436] <... futex resumed> )       = 0 <0.175936>
[pid 10436] <... read resumed> "cache 0\nrss 0\nrss_huge 0\nmapped_"..., 4096) = 577 <0.228091>
[pid 10427] <... read resumed> "cache 0\nrss 0\nrss_huge 0\nmapped_"..., 4096) = 577 <0.207334>
[pid 10411] <... epoll_ctl resumed> )   = 0 <0.118113>
[pid 10382] <... pselect6 resumed> )    = 0 (Timeout) <0.117717>
[pid 10436] <... read resumed> "cache 154234880\nrss 507904\nrss_h"..., 4096) = 660 <0.159891>
[pid 10417] <... futex resumed> )       = 0 <0.917495>
[pid 10436] <... futex resumed> )       = 0 <0.208172>
[pid 10417] <... futex resumed> )       = 0 <0.190763>
[pid 10417] <... read resumed> "cache 0\nrss 0\nrss_huge 0\nmapped_"..., 4096) = 576 <0.154442>
  • An diesem Punkt können Sie ziemlich sicher sein, dass der Systemaufruf read() sehr langsam ist. Gemessen an dem von read gelesenen Inhalt und dem Kontext von mem_cgroup lesen diese read()-Aufrufe die Datei „memory.state“, die zur Beschreibung der Speichernutzung des Systems und der cgroup-Einschränkungen verwendet wird. cAdvisor fragt diese Datei ab, um Details zu den vom Container verwendeten Ressourcen zu erhalten, und ruft diese Methode manuell auf, um zu überprüfen, ob das Problem beim Kernel oder bei cAdvisor liegt:
theojulienne@kube-node-bad ~ $ time cat /sys/fs/cgroup/memory/memory.stat >/dev/null

real    0m0.153s
user    0m0.000s
sys    0m0.152s
theojulienne@kube-node-bad ~ $
  • Da dieses Problem reproduziert werden kann, deutet es darauf hin, dass es sich um eine „pathologische“ Methode handelt, die vom Kernel ausgelöst wird.

6. Warum ist das Lesen so langsam?

  • In diesem Stadium ist es leicht, ähnliche Probleme zu finden, die von anderen gemeldet wurden. Wie sich herausstellte, wurde dieses Problem bereits an cAdvisor gemeldet, wobei sich herausstellte, dass es sich um ein Problem mit hoher CPU-Auslastung handelte, und es wurde einfach nicht bemerkt, dass die Latenz zufällig auch den Netzwerkstapel beeinträchtigte. Tatsächlich haben einige interne Entwickler festgestellt, dass cAdvisor mehr CPU verbraucht als erwartet, aber es scheint kein Problem zu sein, da unsere Server über ausreichend CPU-Leistung verfügen, sodass die CPU-Auslastung nicht untersucht wurde.
  • Bei dieser Frage geht es hauptsächlich um die Speicher-Cgroup, die für die Verwaltung und Zählung der Speichernutzung innerhalb des Namespace (Containers) verantwortlich ist. Die Speicher-Cgroup wird von Docker freigegeben, wenn alle Prozesse in der Cgroup beendet werden. Allerdings ist „Speicher“ nicht nur der Speicher des Prozesses, und während die Nutzung des Prozessspeichers wegfällt, stellt sich heraus, dass der Kernel auch Speicher für Cache-Speicherplatz zuweist, wie z. B. Dentries und Inodes (Verzeichnis- und Dateimetadaten), die werden in der Speicher-Cgroup zwischengespeichert. Wie aus dieser Frage hervorgeht: „Zombie“-Cgroups: Cgroups, auf denen keine Prozesse laufen und die gelöscht wurden, belegen noch eine gewisse Menge an Speicherplatz (in unserem Fall handelt es sich bei diesen Cache-Objekten um Verzeichnisdaten, es kann sich aber auch um die Seite handeln). Cache oder tmpfs).
  • Anstatt alle Cache-Seiten zu durchlaufen, wenn die Kontrollgruppe freigegeben wird, was auch sehr langsam sein kann, wartet der Kernel träge darauf, dass der Speicher verwendet wird, bevor er ihn zurückfordert. Wenn alle Speicherseiten gelöscht sind, wird die entsprechende Kontrollgruppe schließlich recycelt. Gleichzeitig werden diese Gruppen weiterhin in der Statistik gezählt.
  • Aus Leistungssicht amortisieren sie den enormen Zeitaufwand des direkten Blockrecyclings, indem sie jede Seite in Raten recyceln und sich dafür entscheiden, die anfängliche Bereinigung schnell durchzuführen, aber auf diese Weise etwas Cache im Speicher verbleibt. Aber das ist in Ordnung, die cgroup wird schließlich bereinigt, wenn der Kernel die letzte Seite des Speichers im Cache zurückgewinnt, es handelt sich also nicht um ein „Leck“. Leider liegt das Problem in der Art und Weise, wie „memory.stat“ die Suche durchführt. Auf einigen unserer Server ist der Kernel beispielsweise noch in der Version 4.9. Die Implementierung dieser Version ist problematisch. Außerdem verfügen unsere Server generell über viel Speicherplatz Dies bedeutet, dass die letzte Wiederherstellung des Speichercaches und die Bereinigung von Zombie-Gruppen lange dauern kann.
  • Es stellte sich heraus, dass die Knoten eine große Anzahl von Zombie-Gruppen hatten und einige Lese-/Pausenzeiten von mehr als einer Sekunde aufwiesen. Die vorübergehende Problemumgehung für dieses cAdvisor-Problem besteht darin, den systemweiten Verzeichnis-/Inode-Cache sofort freizugeben, wodurch die Leselatenz sofort beseitigt wird und die Netzwerklatenz ebenfalls behoben wird, da die Cache-Entfernung auch die von „Zombie“-Cgroups belegten zwischengespeicherten Seiten umfasst. Sie werden ebenfalls freigegeben gleichzeitig. Dies ist keine endgültige Lösung, sondern überprüft die Ursache des Problems.
  • Es stellt sich heraus, dass neuere Kernel-Versionen (4.19+) die Leistung von Memory.stat-Aufrufen verbessern, sodass dies nach dem Update auf diese Kernel-Version kein Problem mehr darstellt. In der Zwischenzeit können Sie vorhandene Tools verwenden, um Probleme mit Knoten im Kubernetes-Cluster zu erkennen und sie ordnungsgemäß zu entfernen und neu zu starten: Diese Tools werden verwendet, um Latenzsituationen zu erkennen, die hoch genug sind, um Probleme auszulösen. Anschließend wird normal damit umgegangen Neustart. Dies verschaffte uns eine Atempause, in der wir die Systeme und Kernel der verbleibenden Server aktualisieren konnten.

7. Zusammenfassung

  • Da sich dieses Problem darin äußert, dass die NIC-RX-Warteschlange einige hundert Millisekunden lang anhält, verursacht es eine hohe Latenz bei kurzen Verbindungen sowie Verzögerungen in der Mitte der Verbindung (z. B. zwischen MySQL-Abfragen und Antwortpaketen). Das Verständnis und die Aufrechterhaltung der Leistung unserer grundlegendsten Systeme wie Kubernetes ist entscheidend für die Zuverlässigkeit und Geschwindigkeit aller darauf aufbauenden Dienste.

Supongo que te gusta

Origin blog.csdn.net/Forever_wj/article/details/134948470
Recomendado
Clasificación