Tinder-Übergang zu Kubernetes

Notiz. übersetzen: Mitarbeiter des weltberühmten Tinder-Dienstes teilten kürzlich einige technische Details zur Migration ihrer Infrastruktur auf Kubernetes mit. Der Prozess dauerte fast zwei Jahre und führte zum Start einer sehr großen Plattform auf K8s, die aus 200 Diensten bestand, die auf 48 Containern gehostet wurden. Auf welche interessanten Schwierigkeiten stießen die Tinder-Ingenieure und zu welchen Ergebnissen kamen sie? Lesen Sie diese Übersetzung.

Tinder-Übergang zu Kubernetes

Warum?

Vor fast zwei Jahren beschloss Tinder, seine Plattform auf Kubernetes umzustellen. Kubernetes würde es dem Tinder-Team ermöglichen, Container zu erstellen und mit minimalem Aufwand durch unveränderliche Bereitstellung in die Produktion überzugehen (unveränderliche Bereitstellung). In diesem Fall würden die Zusammenstellung von Anwendungen, ihre Bereitstellung und die Infrastruktur selbst eindeutig durch Code definiert.

Wir suchten auch nach einer Lösung für das Problem der Skalierbarkeit und Stabilität. Wenn die Skalierung kritisch wurde, mussten wir oft mehrere Minuten warten, bis neue EC2-Instanzen hochgefahren wurden. Die Idee, Container zu starten und den Verkehr in Sekunden statt in Minuten zu bedienen, gefiel uns sehr.

Der Prozess erwies sich als schwierig. Während unserer Migration Anfang 2019 erreichte der Kubernetes-Cluster eine kritische Masse und wir begannen, auf verschiedene Probleme aufgrund des Verkehrsaufkommens, der Clustergröße und des DNS zu stoßen. Dabei haben wir viele interessante Probleme im Zusammenhang mit der Migration von 200 Diensten und der Wartung eines Kubernetes-Clusters bestehend aus 1000 Knoten, 15000 Pods und 48000 laufenden Containern gelöst.

Wie?

Seit Januar 2018 haben wir verschiedene Phasen der Migration durchlaufen. Wir begannen mit der Containerisierung aller unserer Dienste und deren Bereitstellung in Kubernetes-Test-Cloud-Umgebungen. Ab Oktober haben wir damit begonnen, alle bestehenden Dienste systematisch auf Kubernetes zu migrieren. Im März des folgenden Jahres haben wir die Migration abgeschlossen und nun läuft die Tinder-Plattform ausschließlich auf Kubernetes.

Erstellen von Images für Kubernetes

Wir verfügen über über 30 Quellcode-Repositorys für Microservices, die auf einem Kubernetes-Cluster ausgeführt werden. Der Code in diesen Repositorys ist in verschiedenen Sprachen (z. B. Node.js, Java, Scala, Go) mit mehreren Laufzeitumgebungen für dieselbe Sprache geschrieben.

Das Build-System ist so konzipiert, dass es für jeden Microservice einen vollständig anpassbaren „Build-Kontext“ bereitstellt. Es besteht normalerweise aus einer Docker-Datei und einer Liste von Shell-Befehlen. Ihr Inhalt ist vollständig anpassbar und gleichzeitig sind alle diese Build-Kontexte nach einem standardisierten Format geschrieben. Durch die Standardisierung von Build-Kontexten kann ein einziges Build-System alle Microservices verarbeiten.

Tinder-Übergang zu Kubernetes
Abbildung 1-1. Standardisierter Build-Prozess über Builder-Container

Um maximale Konsistenz zwischen den Laufzeiten zu erreichen (Laufzeitumgebungen) Während der Entwicklung und beim Testen wird derselbe Build-Prozess verwendet. Wir standen vor einer sehr interessanten Herausforderung: Wir mussten eine Möglichkeit entwickeln, die Konsistenz der Build-Umgebung auf der gesamten Plattform sicherzustellen. Um dies zu erreichen, werden alle Montagevorgänge in einem speziellen Container durchgeführt. Baumeister.

Seine Container-Implementierung erforderte fortgeschrittene Docker-Techniken. Builder erbt die lokale Benutzer-ID und Geheimnisse (wie SSH-Schlüssel, AWS-Anmeldeinformationen usw.), die für den Zugriff auf private Tinder-Repositorys erforderlich sind. Es stellt lokale Verzeichnisse bereit, die Quellen enthalten, um Build-Artefakte auf natürliche Weise zu speichern. Dieser Ansatz verbessert die Leistung, da er das Kopieren von Build-Artefakten zwischen dem Builder-Container und dem Host überflüssig macht. Gespeicherte Build-Artefakte können ohne zusätzliche Konfiguration wiederverwendet werden.

Für einige Dienste mussten wir einen weiteren Container erstellen, um die Kompilierungsumgebung der Laufzeitumgebung zuzuordnen (z. B. generiert die bcrypt-Bibliothek von Node.js während der Installation plattformspezifische Binärartefakte). Während des Kompilierungsprozesses können die Anforderungen zwischen den Diensten variieren und die endgültige Docker-Datei wird im Handumdrehen kompiliert.

Kubernetes-Cluster-Architektur und -Migration

Verwaltung der Clustergröße

Wir haben uns für die Verwendung entschieden kube-aws für die automatisierte Clusterbereitstellung auf Amazon EC2-Instanzen. Ganz am Anfang funktionierte alles in einem gemeinsamen Knotenpool. Wir erkannten schnell die Notwendigkeit, Arbeitslasten nach Größe und Instanztyp zu trennen, um die Ressourcen effizienter zu nutzen. Die Logik bestand darin, dass sich die Ausführung mehrerer geladener Multithread-Pods hinsichtlich der Leistung als vorhersehbarer erwies als deren Koexistenz mit einer großen Anzahl von Single-Thread-Pods.

Am Ende haben wir uns für Folgendes entschieden:

  • m5.4xgroß — zur Überwachung (Prometheus);
  • c5.4xgroß – für Node.js-Workload (Single-Threaded-Workload);
  • c5.2xgroß - für Java und Go (Multithread-Workload);
  • c5.4xgroß — für das Bedienfeld (3 Knoten).

Migration

Einer der vorbereitenden Schritte für die Migration von der alten Infrastruktur auf Kubernetes war die Umleitung der bestehenden direkten Kommunikation zwischen Diensten auf die neuen Load Balancer (Elastic Load Balancer (ELB)). Sie wurden in einem bestimmten Subnetz einer Virtual Private Cloud (VPC) erstellt. Dieses Subnetz war mit einer Kubernetes-VPC verbunden. Dadurch konnten wir Module schrittweise migrieren, ohne die spezifische Reihenfolge der Dienstabhängigkeiten zu berücksichtigen.

Diese Endpunkte wurden mithilfe gewichteter Sätze von DNS-Einträgen erstellt, deren CNAMEs auf jeden neuen ELB verweisen. Zur Umstellung haben wir einen neuen Eintrag hinzugefügt, der auf den neuen ELB des Kubernetes-Dienstes mit einer Gewichtung von 0 zeigt. Anschließend haben wir die Time To Live (TTL) des Eintrags auf 0 gesetzt. Danach wurden die alten und neuen Gewichte geändert langsam angepasst und schließlich wurden 100 % der Last auf einen neuen Server übertragen. Nachdem die Umschaltung abgeschlossen war, kehrte der TTL-Wert auf ein angemesseneres Niveau zurück.

Die Java-Module, die wir hatten, kamen mit DNS mit niedriger TTL zurecht, die Node-Anwendungen jedoch nicht. Einer der Ingenieure hat einen Teil des Verbindungspoolcodes neu geschrieben und ihn in einen Manager eingebunden, der die Pools alle 60 Sekunden aktualisiert. Der gewählte Ansatz funktionierte sehr gut und ohne spürbare Leistungseinbußen.

Unterricht

Die Grenzen der Netzwerkstruktur

Am frühen Morgen des 8. Januar 2019 stürzte die Tinder-Plattform unerwartet ab. Als Reaktion auf einen unabhängigen Anstieg der Plattformlatenz am frühen Morgen erhöhte sich die Anzahl der Pods und Knoten im Cluster. Dies führte dazu, dass der ARP-Cache auf allen unseren Knoten erschöpft war.

Es gibt drei Linux-Optionen im Zusammenhang mit dem ARP-Cache:

Tinder-Übergang zu Kubernetes
(Quelle)

gc_thresh3 - Das ist eine harte Grenze. Das Auftreten von „Neighbor Table Overflow“-Einträgen im Protokoll führte dazu, dass selbst nach der synchronen Garbage Collection (GC) nicht genügend Platz im ARP-Cache vorhanden war, um den benachbarten Eintrag zu speichern. In diesem Fall hat der Kernel das Paket einfach vollständig verworfen.

Wir gebrauchen Flanell als Netzwerkstruktur in Kubernetes. Pakete werden über VXLAN übertragen. VXLAN ist ein L2-Tunnel, der auf einem L3-Netzwerk aufgebaut ist. Die Technologie nutzt die MAC-in-UDP-Kapselung (MAC Address-in-User Datagram Protocol) und ermöglicht die Erweiterung von Layer-2-Netzwerksegmenten. Das Transportprotokoll im physischen Rechenzentrumsnetzwerk ist IP plus UDP.

Tinder-Übergang zu Kubernetes
Abbildung 2–1. Flanelldiagramm (Quelle)

Tinder-Übergang zu Kubernetes
Abbildung 2–2. VXLAN-Paket (Quelle)

Jeder Kubernetes-Worker-Knoten weist einen virtuellen Adressraum mit einer /24-Maske aus einem größeren /9-Block zu. Für jeden Knoten ist dies der Fall Mittel ein Eintrag in der Routing-Tabelle, ein Eintrag in der ARP-Tabelle (auf der flannel.1-Schnittstelle) und ein Eintrag in der Switching-Tabelle (FDB). Sie werden beim ersten Start eines Worker-Knotens oder jedes Mal, wenn ein neuer Knoten erkannt wird, hinzugefügt.

Darüber hinaus erfolgt die Kommunikation zwischen Knoten und Pod (oder Pod-Pod) letztendlich über die Schnittstelle eth0 (wie im Flanelldiagramm oben gezeigt). Dies führt zu einem zusätzlichen Eintrag in der ARP-Tabelle für jeden entsprechenden Quell- und Zielhost.

In unserem Umfeld ist diese Art der Kommunikation sehr verbreitet. Für Dienstobjekte in Kubernetes wird ein ELB erstellt und Kubernetes registriert jeden Knoten beim ELB. Der ELB weiß nichts über Pods und der ausgewählte Knoten ist möglicherweise nicht das endgültige Ziel des Pakets. Der Punkt ist, dass ein Knoten, wenn er ein Paket vom ELB empfängt, es unter Berücksichtigung der Regeln berücksichtigt iptables für einen bestimmten Dienst und wählt zufällig einen Pod auf einem anderen Knoten aus.

Zum Zeitpunkt des Ausfalls befanden sich 605 Knoten im Cluster. Aus den oben genannten Gründen reichte dies aus, um die Bedeutung zu überwinden gc_thresh3, was die Standardeinstellung ist. Wenn dies geschieht, werden nicht nur Pakete verworfen, sondern der gesamte virtuelle Adressraum von Flannel mit einer /24-Maske verschwindet aus der ARP-Tabelle. Die Knoten-Pod-Kommunikation und DNS-Abfragen werden unterbrochen (DNS wird in einem Cluster gehostet; Einzelheiten finden Sie weiter unten in diesem Artikel).

Um dieses Problem zu lösen, müssen Sie die Werte erhöhen gc_thresh1, gc_thresh2 и gc_thresh3 und starten Sie Flannel neu, um die fehlenden Netzwerke neu zu registrieren.

Unerwartete DNS-Skalierung

Während des Migrationsprozesses haben wir DNS aktiv genutzt, um den Datenverkehr zu verwalten und Dienste schrittweise von der alten Infrastruktur auf Kubernetes zu übertragen. Wir legen in Route53 relativ niedrige TTL-Werte für zugehörige RecordSets fest. Als die alte Infrastruktur auf EC2-Instanzen lief, zeigte unsere Resolver-Konfiguration auf Amazon DNS. Wir hielten dies für selbstverständlich und die Auswirkungen der niedrigen TTL auf unsere Dienste und Amazon-Dienste (wie DynamoDB) blieben weitgehend unbemerkt.

Als wir Dienste auf Kubernetes migrierten, stellten wir fest, dass DNS 250 Anfragen pro Sekunde verarbeitete. Infolgedessen kam es bei Anwendungen zu ständigen und schwerwiegenden Zeitüberschreitungen bei DNS-Abfragen. Dies geschah trotz unglaublicher Bemühungen zur Optimierung und Umstellung des DNS-Anbieters auf CoreDNS (das bei Spitzenlast 1000 Pods erreichte, die auf 120 Kernen liefen).

Bei der Recherche nach anderen möglichen Ursachen und Lösungen haben wir herausgefunden Artikel, beschreibt Race-Bedingungen, die sich auf das Paketfilter-Framework auswirken Netfilter unter Linux. Die von uns beobachteten Zeitüberschreitungen gepaart mit einem steigenden Zähler insert_failed in der Flannel-Schnittstelle stimmten mit den Ergebnissen des Artikels überein.

Das Problem tritt in der Phase der Quell- und Ziel-Netzwerkadressübersetzung (SNAT und DNAT) und der anschließenden Eintragung in die Tabelle auf Conntrack. Einer der intern diskutierten und von der Community vorgeschlagenen Workarounds bestand darin, den DNS auf den Worker-Knoten selbst zu verschieben. In diesem Fall:

  • SNAT ist nicht erforderlich, da der Datenverkehr innerhalb des Knotens bleibt. Es muss nicht über die Schnittstelle geleitet werden eth0.
  • DNAT ist nicht erforderlich, da die Ziel-IP lokal für den Knoten ist und kein zufällig ausgewählter Pod gemäß den Regeln iptables.

Wir haben uns entschieden, bei diesem Ansatz zu bleiben. CoreDNS wurde als DaemonSet in Kubernetes bereitgestellt und wir haben einen lokalen Knoten-DNS-Server implementiert aufgelöst.conf jeden Pod durch Setzen eines Flags --cluster-dns команды Kubelet . Diese Lösung erwies sich bei DNS-Timeouts als wirksam.

Wir sahen jedoch immer noch einen Paketverlust und einen Anstieg des Zählers insert_failed in der Flannel-Schnittstelle. Dies blieb auch nach der Implementierung der Problemumgehung bestehen, da wir SNAT und/oder DNAT nur für den DNS-Verkehr eliminieren konnten. Für andere Verkehrsarten blieben die Rennbedingungen erhalten. Glücklicherweise handelt es sich bei den meisten unserer Pakete um TCP-Pakete, und wenn ein Problem auftritt, werden sie einfach erneut übertragen. Wir versuchen weiterhin, für alle Verkehrsarten eine passende Lösung zu finden.

Verwendung von Envoy für einen besseren Lastausgleich

Als wir die Backend-Dienste auf Kubernetes migrierten, begannen wir unter einer ungleichmäßigen Last zwischen den Pods zu leiden. Wir haben festgestellt, dass HTTP Keepalive dazu führte, dass ELB-Verbindungen bei den ersten bereitstehenden Pods jeder bereitgestellten Bereitstellung hängen blieben. Somit lief der Großteil des Datenverkehrs über einen kleinen Prozentsatz der verfügbaren Pods. Die erste von uns getestete Lösung bestand darin, MaxSurge bei neuen Bereitstellungen für Worst-Case-Szenarien auf 100 % zu setzen. Der Effekt erwies sich im Hinblick auf größere Einsätze als unbedeutend und wenig aussichtsreich.

Eine andere von uns verwendete Lösung bestand darin, die Ressourcenanforderungen für kritische Dienste künstlich zu erhöhen. In diesem Fall hätten in der Nähe platzierte Pods mehr Spielraum als andere schwere Pods. Auch das würde auf Dauer nicht funktionieren, weil es eine Verschwendung von Ressourcen wäre. Darüber hinaus waren unsere Node-Anwendungen Single-Threaded und konnten dementsprechend nur einen Kern nutzen. Die einzige wirkliche Lösung bestand darin, einen besseren Lastausgleich zu verwenden.

Wir wollten es schon lange voll und ganz schätzen Gesandte. Die aktuelle Situation ermöglichte es uns, es in sehr begrenztem Umfang einzusetzen und sofortige Ergebnisse zu erzielen. Envoy ist ein leistungsstarker Open-Source-Layer-XNUMX-Proxy, der für große SOA-Anwendungen entwickelt wurde. Es kann erweiterte Lastausgleichstechniken implementieren, einschließlich automatischer Wiederholungsversuche, Leistungsschalter und globaler Ratenbegrenzung. (Notiz. übersetzen: Mehr dazu können Sie in lesen Dieser Artikel über Istio, das auf Envoy basiert.)

Wir haben uns die folgende Konfiguration ausgedacht: Wir haben einen Envoy-Sidecar für jeden Pod und eine einzelne Route und verbinden den Cluster lokal über den Port mit dem Container. Um eine mögliche Kaskadierung zu minimieren und einen kleinen Trefferradius aufrechtzuerhalten, haben wir eine Flotte von Envoy-Front-Proxy-Pods verwendet, einen pro Availability Zone (AZ) für jeden Dienst. Sie verließen sich auf eine einfache Service-Discovery-Engine, die von einem unserer Ingenieure geschrieben wurde und einfach eine Liste von Pods in jeder AZ für einen bestimmten Service zurückgab.

Service-Front-Envoys nutzten dann diesen Service-Discovery-Mechanismus mit einem Upstream-Cluster und einer Upstream-Route. Wir haben angemessene Zeitüberschreitungen festgelegt, alle Leistungsschaltereinstellungen erhöht und eine minimale Wiederholungskonfiguration hinzugefügt, um bei einzelnen Ausfällen zu helfen und reibungslose Bereitstellungen sicherzustellen. Wir haben vor jedem dieser Service-Front-Envoys einen TCP-ELB platziert. Selbst wenn das Keepalive von unserer Haupt-Proxy-Ebene auf einigen Envoy-Pods hängen blieb, konnten sie die Last immer noch viel besser bewältigen und waren so konfiguriert, dass sie den Ausgleich über least_request im Backend vornehmen.

Für die Bereitstellung haben wir den preStop-Hook sowohl für Anwendungs-Pods als auch für Sidecar-Pods verwendet. Der Hook löste einen Fehler bei der Überprüfung des Status des Admin-Endpunkts im Sidecar-Container aus und ging für eine Weile in den Ruhezustand, um die Beendigung aktiver Verbindungen zu ermöglichen.

Einer der Gründe, warum wir so schnell vorankommen konnten, sind die detaillierten Metriken, die wir problemlos in eine typische Prometheus-Installation integrieren konnten. Dadurch konnten wir genau sehen, was passierte, während wir Konfigurationsparameter anpassten und den Datenverkehr neu verteilten.

Die Ergebnisse waren unmittelbar und offensichtlich. Wir haben mit den am stärksten unausgeglichenen Diensten begonnen und im Moment ist es vor den 12 wichtigsten Diensten im Cluster tätig. In diesem Jahr planen wir den Übergang zu einem Full-Service-Mesh mit erweiterter Serviceerkennung, Schaltkreisunterbrechung, Ausreißererkennung, Ratenbegrenzung und Nachverfolgung.

Tinder-Übergang zu Kubernetes
Abbildung 3–1. CPU-Konvergenz eines Dienstes während des Übergangs zu Envoy

Tinder-Übergang zu Kubernetes

Tinder-Übergang zu Kubernetes

Endergebnis

Durch diese Erfahrung und zusätzliche Forschung haben wir ein starkes Infrastrukturteam mit ausgeprägten Fähigkeiten im Entwerfen, Bereitstellen und Betreiben großer Kubernetes-Cluster aufgebaut. Alle Tinder-Ingenieure verfügen nun über das Wissen und die Erfahrung, Container zu packen und Anwendungen auf Kubernetes bereitzustellen.

Als in der alten Infrastruktur Bedarf an zusätzlicher Kapazität entstand, mussten wir mehrere Minuten auf den Start neuer EC2-Instanzen warten. Jetzt starten Container und verarbeiten den Datenverkehr innerhalb von Sekunden statt Minuten. Das Planen mehrerer Container auf einer einzelnen EC2-Instanz sorgt auch für eine verbesserte horizontale Konzentration. Daher prognostizieren wir für 2019 eine deutliche Reduzierung der EC2-Kosten im Vergleich zum Vorjahr.

Die Migration dauerte fast zwei Jahre, wir haben sie jedoch im März 2019 abgeschlossen. Derzeit läuft die Tinder-Plattform ausschließlich auf einem Kubernetes-Cluster, der aus 200 Diensten, 1000 Knoten, 15 Pods und 000 laufenden Containern besteht. Die Infrastruktur ist nicht länger die alleinige Domäne der Betriebsteams. Alle unsere Ingenieure tragen diese Verantwortung und steuern den Prozess der Erstellung und Bereitstellung ihrer Anwendungen ausschließlich mithilfe von Code.

PS vom Übersetzer

Lesen Sie auch eine Reihe von Artikeln auf unserem Blog:

Source: habr.com

Kommentar hinzufügen