„Kubernetes hat die Latenz um das Zehnfache erhöht“: Wer ist dafür verantwortlich?

Notiz. übersetzen: Dieser Artikel, verfasst von Galo Navarro, der die Position des Principal Software Engineer beim europäischen Unternehmen Adevinta innehat, ist eine faszinierende und lehrreiche „Untersuchung“ im Bereich des Infrastrukturbetriebs. Der ursprüngliche Titel wurde in der Übersetzung aus einem Grund, den der Autor gleich zu Beginn erklärt, leicht erweitert.

„Kubernetes hat die Latenz um das Zehnfache erhöht“: Wer ist dafür verantwortlich?

Anmerkung des Autors: Sieht aus wie dieser Beitrag angezogen viel mehr Aufmerksamkeit als erwartet. Ich bekomme immer noch wütende Kommentare, dass der Titel des Artikels irreführend sei und dass einige Leser traurig seien. Ich verstehe die Gründe für das, was passiert, deshalb möchte ich Ihnen trotz der Gefahr, die ganze Intrige zu ruinieren, sofort sagen, worum es in diesem Artikel geht. Eine merkwürdige Sache, die ich bei der Migration von Teams zu Kubernetes beobachtet habe, ist, dass immer dann, wenn ein Problem auftritt (z. B. eine erhöhte Latenz nach einer Migration), zunächst Kubernetes dafür verantwortlich gemacht wird, sich dann aber herausstellt, dass der Orchestrator dies nicht wirklich tut beschuldigen. Dieser Artikel erzählt von einem solchen Fall. Sein Name wiederholt den Ausruf eines unserer Entwickler (später werden Sie sehen, dass Kubernetes nichts damit zu tun hat). Sie werden hier keine überraschenden Enthüllungen über Kubernetes finden, aber Sie können ein paar gute Lektionen über komplexe Systeme erwarten.

Vor ein paar Wochen migrierte mein Team einen einzelnen Microservice auf eine Kernplattform, die CI/CD, eine Kubernetes-basierte Laufzeit, Metriken und andere Extras umfasste. Der Umzug hatte Probecharakter: Wir wollten ihn als Grundlage nehmen und in den kommenden Monaten etwa 150 weitere Dienste übertragen. Sie alle sind für den Betrieb einiger der größten Online-Plattformen in Spanien (Infojobs, Fotocasa usw.) verantwortlich.

Nachdem wir die Anwendung auf Kubernetes bereitgestellt und einen Teil des Datenverkehrs dorthin umgeleitet hatten, erwartete uns eine alarmierende Überraschung. Verzögerung (Latenz) Die Anfragen in Kubernetes waren zehnmal höher als in EC10. Im Allgemeinen musste entweder eine Lösung für dieses Problem gefunden oder die Migration des Microservices (und möglicherweise des gesamten Projekts) aufgegeben werden.

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

Um den Engpass zu finden, haben wir Metriken entlang des gesamten Anforderungspfads erfasst. Unsere Architektur ist einfach: Ein API-Gateway (Zuul) leitet Anfragen an Microservice-Instanzen in EC2 oder Kubernetes weiter. In Kubernetes verwenden wir den NGINX Ingress Controller und die Backends sind gewöhnliche Objekte wie Einsatz mit einer JVM-Anwendung auf der Spring-Plattform.

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

Das Problem schien mit der anfänglichen Latenz im Backend zusammenzuhängen (ich habe den Problembereich im Diagramm mit „xx“ markiert). Auf EC2 dauerte die Antwort der Anwendung etwa 20 ms. In Kubernetes erhöhte sich die Latenz auf 100–200 ms.

Wir haben die wahrscheinlichen Verdächtigen im Zusammenhang mit der Laufzeitänderung schnell abgewiesen. Die JVM-Version bleibt gleich. Auch Containerisierungsprobleme hatten damit nichts zu tun: Die Anwendung lief bereits erfolgreich in Containern auf EC2. Wird geladen? Aber selbst bei einer Anfrage pro Sekunde konnten wir hohe Latenzen beobachten. Auch Pausen für die Müllabfuhr könnten vernachlässigt werden.

Einer unserer Kubernetes-Administratoren fragte sich, ob die Anwendung externe Abhängigkeiten habe, da DNS-Abfragen in der Vergangenheit ähnliche Probleme verursacht hätten.

Hypothese 1: DNS-Namensauflösung

Für jede Anfrage greift unsere Anwendung ein- bis dreimal auf eine AWS Elasticsearch-Instanz in einer Domäne zu elastic.spain.adevinta.com. In unseren Containern Es gibt eine Muschel, sodass wir prüfen können, ob die Suche nach einer Domain tatsächlich lange dauert.

DNS-Anfragen vom Container:

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

Ähnliche Anfragen von einer der EC2-Instanzen, auf denen die Anwendung ausgeführt wird:

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

Wenn man bedenkt, dass die Suche etwa 30 ms dauerte, wurde klar, dass die DNS-Auflösung beim Zugriff auf Elasticsearch tatsächlich zur Erhöhung der Latenz beitrug.

Dies war jedoch aus zwei Gründen seltsam:

  1. Wir haben bereits eine Menge Kubernetes-Anwendungen, die mit AWS-Ressourcen interagieren, ohne unter hoher Latenz zu leiden. Was auch immer der Grund sein mag, es bezieht sich speziell auf diesen Fall.
  2. Wir wissen, dass die JVM In-Memory-DNS-Caching durchführt. In unseren Bildern ist der TTL-Wert eingeschrieben $JAVA_HOME/jre/lib/security/java.security und auf 10 Sekunden eingestellt: networkaddress.cache.ttl = 10. Mit anderen Worten: Die JVM sollte alle DNS-Abfragen 10 Sekunden lang zwischenspeichern.

Um die erste Hypothese zu bestätigen, haben wir beschlossen, den DNS-Aufruf für eine Weile einzustellen und zu prüfen, ob das Problem behoben ist. Zunächst entschieden wir uns, die Anwendung neu zu konfigurieren, sodass sie direkt über die IP-Adresse und nicht über einen Domänennamen mit Elasticsearch kommuniziert. Dies würde Codeänderungen und eine neue Bereitstellung erfordern, daher haben wir die Domäne einfach ihrer IP-Adresse zugeordnet /etc/hosts:

34.55.5.111 elastic.spain.adevinta.com

Jetzt erhielt der Container fast sofort eine IP. Dies führte zu einer gewissen Verbesserung, aber wir kamen den erwarteten Latenzwerten nur geringfügig näher. Obwohl die DNS-Auflösung lange dauerte, blieb uns der wahre Grund unklar.

Diagnose über Netzwerk

Wir haben uns entschieden, den Verkehr aus dem Container mithilfe von zu analysieren tcpdumpum zu sehen, was genau im Netzwerk passiert:

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

Wir haben dann mehrere Anfragen gesendet und deren Erfassung heruntergeladen (kubectl cp my-service:/capture.pcap capture.pcap) zur weiteren Analyse in Wireshark.

An den DNS-Abfragen gab es nichts Verdächtiges (bis auf eine Kleinigkeit, auf die ich später noch eingehen werde). Es gab jedoch gewisse Kuriositäten in der Art und Weise, wie unser Service jede Anfrage bearbeitete. Unten sehen Sie einen Screenshot der Aufnahme, der zeigt, wie die Anfrage angenommen wird, bevor die Antwort beginnt:

„Kubernetes hat die Latenz um das Zehnfache erhöht“: Wer ist dafür verantwortlich?

Die Paketnummern werden in der ersten Spalte angezeigt. Der Übersichtlichkeit halber habe ich die verschiedenen TCP-Streams farblich gekennzeichnet.

Der grüne Stream beginnend mit Paket 328 zeigt, wie der Client (172.17.22.150) eine TCP-Verbindung zum Container (172.17.36.147) aufgebaut hat. Nach dem ersten Handshake (328-330) wurde Paket 331 gebracht HTTP GET /v1/.. — eine eingehende Anfrage an unseren Dienst. Der gesamte Vorgang dauerte 1 ms.

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

Bisher ist alles in Ordnung und die Zeiten entsprechen in etwa den erwarteten Verzögerungen (20-30 ms, gemessen vom Client aus).

Der blaue Abschnitt dauert jedoch 86 ms. Was ist darin los? Mit Paket 333 hat unser Dienst eine HTTP-GET-Anfrage an gesendet /latest/meta-data/iam/security-credentialsund unmittelbar danach über dieselbe TCP-Verbindung eine weitere GET-Anfrage an /latest/meta-data/iam/security-credentials/arn:...

Wir haben festgestellt, dass sich dies bei jeder Anfrage während der gesamten Ablaufverfolgung wiederholte. Die DNS-Auflösung ist in unseren Containern tatsächlich etwas langsamer (die Erklärung für dieses Phänomen ist recht interessant, aber ich werde sie für einen separaten Artikel aufheben). Es stellte sich heraus, dass die Ursache für die langen Verzögerungen Aufrufe des AWS Instance Metadata Service bei jeder Anfrage waren.

Hypothese 2: unnötige Anrufe bei AWS

Beide Endpunkte gehören zu AWS-Instanzmetadaten-API. Unser Microservice nutzt diesen Dienst, während er Elasticsearch ausführt. Beide Aufrufe sind Teil des grundlegenden Autorisierungsprozesses. Der Endpunkt, auf den bei der ersten Anfrage zugegriffen wird, gibt die mit der Instanz verknüpfte IAM-Rolle aus.

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

Die zweite Anfrage fragt den zweiten Endpunkt 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 einen kurzen Zeitraum nutzen und muss regelmäßig (vorher) neue Zertifikate erwerben Expiration). Das Modell ist einfach: AWS rotiert temporäre Schlüssel aus Sicherheitsgründen häufig, Clients können sie jedoch einige Minuten lang zwischenspeichern, um die mit dem Erhalt neuer Zertifikate verbundenen Leistungseinbußen auszugleichen.

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

Nachdem wir auf GitHub nach Problemen gesucht hatten, stießen wir auf ein Problem #1921. Sie half uns dabei, die Richtung zu bestimmen, in die wir weiter „graben“ sollten.

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

  • Verfallsdatum (Expiration) Hineinfallen EXPIRATION_THRESHOLD, fest codiert auf 15 Minuten.
  • Seit dem letzten Versuch, Zertifikate zu erneuern, ist mehr Zeit vergangen REFRESH_THRESHOLD, fest codiert für 60 Minuten.

Um das tatsächliche Ablaufdatum der von uns erhaltenen Zertifikate anzuzeigen, haben wir die oben genannten cURL-Befehle sowohl vom Container als auch von der EC2-Instanz aus ausgeführt. Die Gültigkeitsdauer des aus dem Container erhaltenen Zertifikats war deutlich kürzer: genau 15 Minuten.

Jetzt ist alles klar: Auf erste Anfrage erhielt unser Dienst vorläufige Zertifikate. Da sie nicht länger als 15 Minuten gültig waren, beschloss das AWS SDK, sie bei einer späteren Anfrage zu aktualisieren. Und das geschah bei jeder Anfrage.

Warum ist die Gültigkeitsdauer von Zertifikaten kürzer geworden?

AWS-Instanzmetadaten sind für die Verwendung mit EC2-Instanzen und nicht für Kubernetes konzipiert. Andererseits wollten wir die Anwendungsoberfläche nicht ändern. Dafür haben wir verwendet KIAM – ein Tool, das mithilfe von Agenten auf jedem Kubernetes-Knoten Benutzern (Ingenieuren, die Anwendungen in einem Cluster bereitstellen) ermöglicht, Containern in Pods IAM-Rollen zuzuweisen, als wären sie EC2-Instanzen. KIAM fängt Aufrufe an den AWS-Instanzmetadatendienst ab und verarbeitet sie aus seinem Cache, nachdem es sie zuvor von AWS erhalten hat. Aus Anwendungssicht ändert sich nichts.

KIAM liefert kurzfristige Zertifikate für Pods. Dies ist sinnvoll, wenn man bedenkt, dass die durchschnittliche Lebensdauer eines Pods kürzer ist als die einer EC2-Instanz. Standardgültigkeitsdauer für Zertifikate entspricht den gleichen 15 Minuten.

Das hat zur Folge, dass es zu einem Problem kommt, wenn man beide Standardwerte übereinander legt. Jedes für eine Anwendung bereitgestellte Zertifikat läuft nach 15 Minuten ab. Allerdings erzwingt das AWS Java SDK eine Erneuerung jedes Zertifikats, dessen Ablaufdatum weniger als 15 Minuten beträgt.

Dadurch muss das temporäre Zertifikat bei jeder Anfrage erneuert werden, was mehrere Aufrufe der AWS-API erfordert und zu einer erheblichen Erhöhung der Latenz führt. Im AWS Java SDK haben wir gefunden Featureanfrage, in dem ein ähnliches Problem erwähnt wird.

Die Lösung erwies sich als einfach. Wir haben KIAM einfach umkonfiguriert, um Zertifikate mit einer längeren Gültigkeitsdauer anzufordern. Sobald dies geschah, begannen Anfragen ohne die Beteiligung des AWS-Metadatendienstes zu fließen, und die Latenz sank auf ein noch niedrigeres Niveau als in EC2.

Befund

Basierend auf unserer Erfahrung mit Migrationen sind Fehler in Kubernetes oder anderen Elementen der Plattform eine der häufigsten Problemquellen. Außerdem werden keine grundlegenden Mängel in den von uns portierten Microservices behoben. Probleme entstehen oft einfach dadurch, dass wir verschiedene Elemente zusammenfügen.

Wir vermischen komplexe Systeme, die noch nie zuvor miteinander interagiert haben, in der Erwartung, dass sie zusammen ein einziges, größeres 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 Fehlentscheidungen in Kubernetes, KIAM, AWS Java SDK oder unserem Microservice. Es war das Ergebnis der Kombination zweier unabhängiger Standardeinstellungen: eine in KIAM, die andere im AWS Java SDK. Für sich genommen machen beide Parameter Sinn: die aktive Zertifikatserneuerungsrichtlinie im AWS Java SDK und die kurze Gültigkeitsdauer von Zertifikaten in KAIM. Aber wenn man sie zusammenfügt, werden die Ergebnisse unvorhersehbar. Zwei unabhängige und logische Lösungen müssen in ihrer Kombination keinen Sinn ergeben.

PS vom Übersetzer

Weitere Informationen zur Architektur des KIAM-Dienstprogramms zur Integration von AWS IAM mit Kubernetes finden Sie unter Dieser Artikel von seinen Schöpfern.

Lesen Sie auch auf unserem Blog:

Source: habr.com

Kommentar hinzufügen