„Kubernetes hat die Verzögerung verzehnfacht“: Wer ist daran schuld?

Hinweis.: Dieser Artikel, geschrieben von Galo Navarro, der die Position des Principal Software Engineer bei dem europĂ€ischen Unternehmen Adevinta innehat, ist eine faszinierende und lehrreiche „Untersuchung“ im Bereich der Infrastrukturnutzung. Der ursprĂŒngliche Titel wurde im Übersetzungsprozess leicht erweitert, aus GrĂŒnden, die der Autor zu Beginn erklĂ€rt.

„Kubernetes hat die Verzögerung verzehnfacht“: Wer ist daran schuld?

Anmerkung des Autors: Diese Veröffentlichung scheint Aufmerksamkeit erregt zu haben Viel mehr Aufmerksamkeit, als erwartet. Ich erhalte immer noch wĂŒtende Kommentare darĂŒber, dass der Titel des Artikels irrefĂŒhrend ist und dass einige Leser enttĂ€uscht sind. Ich verstehe die GrĂŒnde dafĂŒr, weshalb ich, trotz des Risikos, die gesamte Spannung zu verderben, gleich zu Beginn erzĂ€hlen möchte, worum es in diesem Artikel geht. Bei der Umstellung von Teams auf Kubernetes beobachte ich eine interessante Sache: Jedes Mal, wenn ein Problem auftritt (zum Beispiel steigende Latenzen nach der Migration), wird Kubernetes als erstes beschuldigt, doch spĂ€ter stellt sich heraus, dass der Orchestrator nicht schuld ist. Dieser Artikel erzĂ€hlt von einem solchen Fall. Sein Titel wiederholt den Ausruf eines unserer Entwickler (spĂ€ter werden Sie sehen, dass Kubernetes hier wirklich nichts damit zu tun hat). Sie werden darin keine unerwarteten Offenbarungen ĂŒber Kubernetes finden, aber Sie können sich auf ein paar gute Lektionen ĂŒber komplexe Systeme freuen.

Vor einigen Wochen war mein Team mit der Migration eines Mikrodienstes auf die Hauptplattform beschĂ€ftigt, die CI/CD, eine Kubernetes-basierte Arbeitsumgebung, Metriken und andere nĂŒtzliche Funktionen umfasst. Der Umzug war als Testlauf gedacht: wir planten, ihn als Ausgangspunkt zu verwenden und in den kommenden Monaten etwa 150 weitere Dienste zu migrieren. Diese sind alle fĂŒr den Betrieb einiger der grĂ¶ĂŸten Online-Plattformen in Spanien verantwortlich (Infojobs, Fotocasa usw.).

Nachdem wir die Anwendung in Kubernetes bereitgestellt und einen Teil des Traffics darauf umgeleitet hatten, erwartete uns eine besorgniserregende Überraschung. Die Verzögerung (Latenz) von Anfragen in Kubernetes war zehnmal höher als in EC2. Insgesamt mussten wir entweder eine Lösung fĂŒr dieses Problem finden oder die Migration des Mikrodienstes aufgeben (und möglicherweise das gesamte Projekt).

Warum ist die Verzögerung in Kubernetes so viel höher als in EC2?

Um den Flaschenhals zu finden, haben wir Metriken entlang des gesamten Anfragewegs gesammelt. Unsere Architektur ist einfach: Ein API-Gateway (Zuul) proxied Anfragen zu den Instanzen des Mikrodienstes in EC2 oder Kubernetes. In Kubernetes verwenden wir den NGINX Ingress Controller, und die Backends bestehen aus gewöhnlichen Objekten des Typs Deployment mit einer JVM-Anwendung auf der Spring-Plattform.

                                  EC2
                            +---------------+
                            |  +---------+  |
                            |  |         |  |
                       +-------> BACKEND |  |
                       |    |  |         |  |
                       |    |  +---------+  |                   
                       |    +---------------+
             +------+  |
Public       |      |  |
      -------> ZUUL +--+
traffic      |      |  |              Kubernetes
             +------+  |    +-----------------------------+
                       |    |  +-------+      +---------+ |
                       |    |  |       |  xx  |         | |
                       +-------> NGINX +------> BACKEND | |
                            |  |       |  xx  |         | |
                            |  +-------+      +---------+ |
                            +-----------------------------+

Es schien, dass das Problem mit der Verzögerung in der Anfangsphase der Backend-Arbeit zusammenhing (ich habe den problematischen Bereich im Diagramm als „xx“ markiert). In EC2 betrug die Antwortzeit der Anwendung etwa 20 ms. In Kubernetes stieg die Verzögerung auf 100–200 ms.

Wir schlossen schnell wahrscheinliche VerdĂ€chtige im Zusammenhang mit dem Wechsel der Laufzeit aus. Die JVM-Version blieb gleich. Containerisierungsprobleme waren ebenfalls nicht entscheidend: Die Anwendung lief bereits erfolgreich in Containern in EC2. Last? Aber wir beobachteten hohe Verzögerungen selbst bei 1 Anfrage pro Sekunde. Pausen fĂŒr die MĂŒllsammlung konnten ebenfalls vernachlĂ€ssigt werden.

Einer unserer Kubernetes-Administratoren erkundigte sich, ob es externe AbhĂ€ngigkeiten der Anwendung gibt, da frĂŒhere DNS-Anfragen Ă€hnliche Probleme verursacht haben.

Hypothese 1: DNS-Namensauflösung

Bei jeder Anfrage greift unsere Anwendung ein- bis dreimal auf eine AWS Elasticsearch-Instanz in einer Domain wie elastic.spain.adevinta.com. Innerhalb der Container haben wir eine Shell, daher können wir ĂŒberprĂŒfen, ob die DomĂ€nensuche wirklich viel Zeit in Anspruch nimmt.

DNS-Anfragen aus dem Container:

[root@be-851c76f696-alf8z /]# while true; do dig "elastic.spain.adevinta.com" | grep time; sleep 2; done
;; Abfragezeit: 22 msec
;; Abfragezeit: 22 msec
;; Abfragezeit: 29 msec
;; Abfragezeit: 21 msec
;; Abfragezeit: 28 msec
;; Abfragezeit: 43 msec
;; Abfragezeit: 39 msec

Ähnliche Anfragen von einer der EC2-Instanzen, auf der die Anwendung lĂ€uft:

bash-4.4# while true; do dig "elastic.spain.adevinta.com" | grep time; sleep 2; done
;; Abfragezeit: 77 msec
;; Abfragezeit: 0 msec
;; Abfragezeit: 0 msec
;; Abfragezeit: 0 msec
;; Abfragezeit: 0 msec

Angesichts der Tatsache, dass die Suche etwa 30 ms dauert, wird deutlich, dass die DNS-Auflösung beim Zugriff auf Elasticsearch tatsÀchlich zur Erhöhung der Latenz beitrÀgt.

Das war jedoch aus zwei GrĂŒnden seltsam:

  1. Wir haben bereits zahlreiche Anwendungen in Kubernetes, die mit AWS-Ressourcen interagieren, ohne unter hohen Latenzen zu leiden. Egal aus welchem Grund, es hat direkt mit diesem Fall zu tun.
  2. Wir wissen, dass die JVM DNS im Arbeitsspeicher cached. In unseren Images ist der TTL-Wert festgelegt auf $JAVA_HOME/jre/lib/security/java.security und auf 10 Sekunden eingestellt: networkaddress.cache.ttl = 10. Mit anderen Worten, die JVM sollte alle DNS-Anfragen fĂŒr 10 Sekunden cachen.

Um die erste Hypothese zu bestĂ€tigen, haben wir entschieden, vorĂŒbergehend auf DNS-Anfragen zu verzichten und zu beobachten, ob das Problem verschwindet. ZunĂ€chst haben wir beschlossen, die Anwendung so umzustellen, dass sie sich direkt ĂŒber die IP-Adresse mit Elasticsearch verbindet, anstatt ĂŒber den Domainnamen. Das hĂ€tte CodeĂ€nderungen und ein neues Deployment erfordert, also haben wir einfach die Domain mit ihrer IP-Adresse in /etc/hosts:

34.55.5.111 elastic.spain.adevinta.com

verknĂŒpft. Jetzt erhielt der Container die IP fast sofort. Das fĂŒhrte zu einigen Verbesserungen, aber wir waren nur wenig nĂ€her am erwarteten Latenzniveau. Obwohl die DNS-Auflösung viel Zeit in Anspruch nahm, blieb die wahre Ursache uns weiterhin verborgen.

Diagnose mittels Netzwerk

Wir haben beschlossen, den Datenverkehr aus dem Container zu analysieren mit tcpdump, um nachzuvollziehen, was genau im Netzwerk passiert:

[root@be-851c76f696-alf8z /]# tcpdump -leni any -w capture.pcap

Dann haben wir einige Anfragen gesendet und deren Capture heruntergeladen (kubectl cp my-service:/capture.pcap capture.pcap) fĂŒr eine weitere Analyse in Wireshark.

Es gab nichts VerdĂ€chtiges in den DNS-Anfragen (außer einer Kleinigkeit, ĂŒber die ich spĂ€ter sprechen werde). Aber es gab bestimmte Eigenheiten, wie unser Dienst jede Anfrage bearbeitet hat. Unten sehen Sie einen Screenshot des Captures, der die Annahme der Anfrage vor Beginn der Antwort zeigt:

„Kubernetes hat die Verzögerung verzehnfacht“: Wer ist daran schuld?

Die Paketnummern sind in der ersten Spalte aufgefĂŒhrt. Zur Klarheit habe ich verschiedene TCP-Streams farblich hervorgehoben.

Der grĂŒne Stream, der mit dem 328. Paket beginnt, zeigt, wie der Client (172.17.22.150) eine TCP-Verbindung zum Container (172.17.36.147) herstellt. Nach dem anfĂ€nglichen Handshake (328-330) brachte das Paket 331 HTTP GET /v1/.. — die eingehende Anfrage an unseren Dienst. Der gesamte Prozess dauerte 1 ms.

Der graue Stream (ab Paket 339) zeigt, dass unser Dienst eine HTTP-Anfrage an die Elasticsearch-Instanz gesendet hat (TCP-Handshake fehlt, da die bestehende Verbindung genutzt wird). Dies dauerte 18 ms.

Bisher lĂ€uft alles gut, und die Zeiten entsprechen ungefĂ€hr den erwarteten Verzögerungen (20–30 ms bei Messungen vom Client).

Die blaue Sektion benötigt jedoch 86 ms. Was passiert dort? Mit Paket 333 hat unser Dienst eine HTTP GET-Anfrage an /latest/meta-data/iam/security-credentials, und gleich danach, ĂŒber dieselbe TCP-Verbindung, eine weitere GET-Anfrage an /latest/meta-data/iam/security-credentials/arn:...

Wir haben festgestellt, dass dies bei jeder Anfrage in der gesamten Traceroute wiederholt wird. Die DNS-Auflösung dauert in unseren Containern tatsĂ€chlich etwas lĂ€nger (die ErklĂ€rung fĂŒr dieses PhĂ€nomen ist sehr interessant, aber ich behalte sie mir fĂŒr einen separaten Artikel vor). Es stellte sich heraus, dass die Ursache fĂŒr die hohen Verzögerungen die Zugriffe auf den AWS Instance Metadata-Service bei jeder Anfrage sind.

Hypothese 2: unnötige Zugriffe auf AWS

Beide Endpoints gehören zu AWS Instance Metadata API. Unser Mikrodienst nutzt diesen Dienst beim Arbeiten mit Elasticsearch. Beide Aufrufe sind Teil des Basis-Autorisierungsprozesses. Der Endpoint, auf den bei der ersten Anfrage zugegriffen wird, gibt die IAM-Rolle zurĂŒck, die mit der Instanz verknĂŒpft ist.

/ # curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
arn:aws:iam::<account_id>:role/some_role

Die zweite Anfrage fragt beim zweiten Endpoint nach temporĂ€ren Berechtigungen fĂŒr diese Instanz:

/ # curl http://169.254.169.254/latest/meta-data/iam/security-credentials/arn:aws:iam::<account_id>:role/some_role`
{
    "Code" : "Success",
    "LastUpdated" : "2012-04-26T16:39:16Z",
    "Type" : "AWS-HMAC",
    "AccessKeyId" : "ASIAIOSFODNN7EXAMPLE",
    "SecretAccessKey" : "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
    "Token" : "token",
    "Expiration" : "2017-05-17T15:09:54Z"
}

Der Kunde kann sie fĂŒr eine kurze Zeit nutzen und muss regelmĂ€ĂŸig neue Zertifikate erhalten (bis zu ihrem Ablauf). Das Modell ist einfach: AWS rotiert temporĂ€re SchlĂŒssel hĂ€ufig aus SicherheitsgrĂŒnden, aber die Kunden können sie fĂŒr einige Minuten cachen, um die Leistungseinbußen bei der Erlangung neuer Zertifikate auszugleichen.

Das AWS Java SDK sollte die Verantwortung fĂŒr die Organisation dieses Prozesses ĂŒbernehmen, jedoch geschieht dies aus irgendeinem Grund nicht.

Durch die Suche nach Problemen auf GitHub sind wir auf ein Problem gestoßen #1921. Dieses hat uns geholfen, die Richtung zu bestimmen, in die wir weiter "graben" sollten.

AWS SDK aktualisiert Zertifikate, wenn eine der folgenden Bedingungen eintritt:

  • Das Ablaufdatum (Ablauf) fĂ€llt auf EXPIRATION_THRESHOLD, das fest im Code auf 15 Minuten gesetzt ist.
  • Seit dem letzten Versuch, die Zertifikate zu aktualisieren, ist mehr Zeit vergangen als REFRESH_THRESHOLD, das auf 60 Minuten festgelegt ist.

Um die tatsĂ€chliche GĂŒltigkeitsdauer der von uns erhaltenen Zertifikate zu ĂŒberprĂŒfen, haben wir die oben genannten cURL-Befehle sowohl aus dem Container als auch aus einer EC2-Instanz ausgefĂŒhrt. Die GĂŒltigkeitsdauer des Zertifikats, das aus dem Container erhalten wurde, war deutlich kĂŒrzer: genau 15 Minuten.

Jetzt ist alles klar: FĂŒr die erste Anfrage erhielt unser Dienst temporĂ€re Zertifikate. Da ihre GĂŒltigkeit 15 Minuten nicht ĂŒberschritt, entschied sich das AWS SDK bei der nĂ€chsten Anfrage, diese zu erneuern. Und das geschah bei jeder Anfrage.

Warum wurde die GĂŒltigkeitsdauer der Zertifikate kĂŒrzer?

Der AWS Instance Metadata Service ist fĂŒr die Arbeit mit EC2-Instanzen gedacht, nicht fĂŒr Kubernetes. Andererseits wollten wir die AnwendungsoberflĂ€che nicht Ă€ndern. Dazu haben wir KIAM — ein Tool, das es Benutzern (Ingenieuren, die Anwendungen in einem Cluster bereitstellen) ermöglicht, IAM-Rollen Container in Pods zuzuweisen, als wĂ€ren sie EC2-Instanzen, mithilfe von Agenten auf jedem Kubernetes-Knoten. KIAM fĂ€ngt Aufrufe an den AWS Instance Metadata Service ab und verarbeitet diese aus seinem Cache, nachdem es sie zuvor von AWS bezogen hat. Aus der Sicht der Anwendung Ă€ndert sich nichts.

KIAM stellt kurzfristige Zertifikate fĂŒr Pods bereit. Das ist sinnvoll, wenn man bedenkt, dass die durchschnittliche Lebensdauer eines Pods kĂŒrzer ist als die eines EC2-Instances. StandardmĂ€ĂŸig hat das Zertifikat eine GĂŒltigkeitsdauer von 15 Minuten..

Wenn man also beide Standardwerte ĂŒbereinanderlegt, ergibt sich ein Problem. Jedes Zertifikat, das der Anwendung bereitgestellt wird, lĂ€uft nach 15 Minuten ab. Das AWS Java SDK zwingt ein Update jedes Zertifikats, wenn weniger als 15 Minuten bis zum Ablauf verbleiben.

Infolgedessen wird das temporÀre Zertifikat mit jeder Anfrage aktualisiert, was mehrere API-Aufrufe bei AWS nach sich zieht und die Latenz erheblich erhöht. Im AWS Java SDK haben wir festgestellt, Feature-Anfrage, in dem ein Àhnliches Problem erwÀhnt wird.

Die Lösung war einfach. Wir haben KIAM einfach so konfiguriert, dass Zertifikate mit einer lĂ€ngeren GĂŒltigkeitsdauer angefordert werden. Sobald das geschah, wurden die Anfragen ohne den AWS Metadata-Service verarbeitet, und die Latenz sank sogar auf ein niedrigeres Niveau als bei EC2.

Fazit

Basierend auf unserer Erfahrung mit Migrationen lĂ€sst sich sagen, dass eine der hĂ€ufigsten Ursachen fĂŒr Probleme nicht Fehler in Kubernetes oder anderen Plattformkomponenten sind. Auch ist es nicht mit grundlegenden MĂ€ngeln in den Mikrodiensten verbunden, die wir ĂŒbertragen. Probleme entstehen hĂ€ufig einfach, weil wir verschiedene Elemente zusammenfĂŒgen.

Wir mischen komplexe Systeme, die zuvor nie interagiert haben, in der Erwartung, dass sie zusammen ein grĂ¶ĂŸeres, einheitliches System bilden. Leider gilt: Je mehr Elemente, desto mehr Raum fĂŒr Fehler, desto höher die Entropie.

In unserem Fall war die hohe Latenz nicht das Ergebnis von Fehlern oder schlechten Entscheidungen in Kubernetes, KIAM, dem AWS Java SDK oder unserem Mikrodienst. Sie war das Resultat der Kombination von zwei unabhĂ€ngigen, standardmĂ€ĂŸig festgelegten Parametern: einem in KIAM und einem im AWS Java SDK. FĂŒr sich genommen sind beide Parameter sinnvoll: sowohl die aktive Zertifikataktualisierungspolitik im AWS Java SDK als auch die kurze GĂŒltigkeitsdauer der Zertifikate in KIAM. Doch wenn man sie zusammenfĂŒhrt, werden die Ergebnisse unvorhersehbar. Zwei unabhĂ€ngige und logische Lösungen mĂŒssen nicht zwangslĂ€ufig Sinn ergeben, wenn sie kombiniert werden.

P.S. vom Übersetzer

Erfahren Sie mehr ĂŒber die Architektur des KIAM-Tools zur Integration von AWS IAM mit Kubernetes in in diesem Artikel von den Erstellern.

Lesen Sie auch in unserem Blog:

Quelle: habr.com

Erwerben Sie zuverlĂ€ssiges Hosting fĂŒr Websites mit DDoS-Schutz, VPS VDS-Server đŸ”„ Kaufen Sie zuverlĂ€ssiges Hosting fĂŒr Websites mit DDoS-Schutz, VPS VDS-Server | ProHoster