Kubernetes Load Balancing und Skalierung langlebiger Verbindungen


Dieser Artikel hilft Ihnen zu verstehen, wie der Lastausgleich in Kubernetes funktioniert, was beim Skalieren langlebiger Verbindungen passiert und warum Sie den Ausgleich auf der Clientseite in Betracht ziehen sollten, wenn Sie HTTP / 2, gRPC, RSockets, AMQP oder andere langlebige Protokolle verwenden. 

Ein bisschen darüber, wie der Verkehr in Kubernetes umverteilt wird 


Kubernetes bietet zwei praktische Abstraktionen für die Einführung von Anwendungen: Dienste und Bereitstellungen.

Bereitstellungen beschreiben, wie und wie viele Kopien Ihrer Anwendung zu einem bestimmten Zeitpunkt ausgeführt werden sollen. Jede Anwendung wird wie unter (Pod) bereitgestellt und erhält eine IP-Adresse.

Feature-Services ähneln einem Load Balancer. Sie sind so konzipiert, dass sie den Verkehr auf mehrere Herde verteilen.

Mal sehen, wie es aussieht .

  1. In der folgenden Abbildung sehen Sie drei Instanzen derselben Anwendung und einen Load Balancer:

  2. Der Load Balancer heißt Service, ihm wird eine IP-Adresse zugewiesen. Jede eingehende Anfrage wird an einen der Pods umgeleitet:

  3. Das Bereitstellungsskript bestimmt die Anzahl der Anwendungsinstanzen. Sie müssen fast nie direkt unter bereitstellen:

  4. Jedem Pod wird eine eigene IP-Adresse zugewiesen:



Es ist nützlich, Dienste als eine Reihe von IP-Adressen zu betrachten. Bei jedem Zugriff auf den Dienst wird eine der IP-Adressen aus der Liste ausgewählt und als Zieladresse verwendet.

Dies ist wie folgt .

  1. Es gibt eine Curl-Anfrage 10.96.45.152 an den Service:

  2. Der Dienst wählt eine der drei Pod-Adressen als Ziel aus:

  3. Der Datenverkehr wird zu einem bestimmten Pod umgeleitet:



Wenn Ihre Anwendung aus einem Frontend und einem Backend besteht, verfügen Sie jeweils über einen Service und eine Bereitstellung.

Wenn das Frontend die Anforderung an das Backend erfüllt, muss es nicht genau wissen, wie viele Herde das Backend bedient: Es kann einen, zehn oder einhundert geben.

Außerdem weiß das Frontend nichts über die Adressen der Herde, die das Backend bedienen.

Wenn das Frontend eine Anfrage an das Backend sendet, verwendet es die IP-Adresse des Backend-Dienstes, die sich nicht ändert.

So sieht es aus .

  1. Unter 1 fordert die interne Backend-Komponente an. Anstatt eine bestimmte für das Backend auszuwählen, wird eine Serviceanforderung ausgeführt:

  2. Der Dienst wählt einen der Backend-Pods als Zieladresse aus:

  3. Der Verkehr geht von Herd 1 zu Herd 5, der vom Dienst ausgewählt wurde:

  4. Unter 1 weiß es nicht genau, wie viele solcher Herde wie unter 5 hinter dem Dienst versteckt sind:



Aber wie genau verteilt der Dienst Anfragen? Scheint Round-Robin-Balancing verwendet zu werden? Lass es uns richtig machen. 

Balancing in Kubernetes Services


Kubernetes-Dienste existieren nicht. Es gibt keinen Prozess für den Dienst, dem eine IP-Adresse und ein Port zugewiesen wurden.

Sie können dies überprüfen, indem Sie zu einem beliebigen Knoten im Cluster gehen und den Befehl netstat -ntlp ausführen.

Sie können nicht einmal die dem Dienst zugewiesene IP-Adresse finden.

Die IP-Adresse des Dienstes befindet sich in der Steuerungsschicht, im Controller und wird in der Datenbank - etcd. Dieselbe Adresse wird von einer anderen Komponente verwendet - kube-proxy.
Kube-Proxy empfängt eine Liste von IP-Adressen für alle Dienste und bildet eine Reihe von Iptables-Regeln für jeden Knoten des Clusters.

Diese Regeln lauten: "Wenn die IP-Adresse des Dienstes angezeigt wird, müssen wir die Zieladresse der Anforderung ändern und an einen der Pods senden."

Die IP-Adresse des Dienstes wird nur als Einstiegspunkt verwendet und von keinem Prozess bedient, der diese IP-Adresse und diesen Port abhört.

Schauen wir uns das an

  1. Stellen Sie sich einen Cluster aus drei Knoten vor. Auf jedem Knoten befinden sich Pods:

  2. Strickherde in Beige sind Teil des Service. Da der Dienst nicht als Prozess vorhanden ist, ist er ausgegraut:

  3. Der erste fragt nach dem Service und sollte auf einen der zugehörigen Herde fallen:

  4. Aber der Dienst existiert nicht, es gibt keinen Prozess. Wie funktioniert es?

  5. Bevor die Anforderung den Knoten verlässt, durchläuft sie die iptables-Regeln:

  6. Die iptables-Regeln wissen, dass kein Dienst vorhanden ist, und ersetzen die IP-Adresse durch eine der IP-Adressen der diesem Dienst zugeordneten Pods:

  7. Die Anfrage erhält eine gültige IP-Adresse als Zieladresse und wird normalerweise verarbeitet:

  8. Abhängig von der Netzwerktopologie erreicht die Anforderung schließlich den Herd:



Können iptables die Last ausgleichen?


Nein, iptables werden zum Filtern verwendet und wurden nicht zum Ausgleichen entwickelt.

Es ist jedoch möglich, eine Reihe von Regeln zu schreiben, die wie ein Pseudo-Balancer funktionieren .

Und genau das macht Kubernetes.

Wenn Sie drei Pods haben, schreibt kube-proxy die folgenden Regeln:

  1. Wählen Sie die erste mit einer Wahrscheinlichkeit von 33%, andernfalls fahren Sie mit der nächsten Regel fort.
  2. Wählen Sie die zweite mit einer Wahrscheinlichkeit von 50%, andernfalls fahren Sie mit der nächsten Regel fort.
  3. Wählen Sie die dritte unter.

Ein solches System führt dazu, dass jedes Sub mit einer Wahrscheinlichkeit von 33% ausgewählt wird.



Es gibt keine Garantie dafür, dass unter 2 die nächste Datei nach Datei 1 ausgewählt wird.

Hinweis : iptables verwendet ein statistisches Modul für die zufällige Verteilung. Somit basiert der Ausgleichsalgorithmus auf einer zufälligen Auswahl.

Nachdem Sie nun verstanden haben, wie Services funktionieren, schauen wir uns interessantere Arbeitsszenarien an.

Langlebige Verbindungen in Kubernetes werden standardmäßig nicht skaliert


Jede HTTP-Anforderung vom Front-End zum Back-End wird von einer separaten TCP-Verbindung bedient, die geöffnet und geschlossen wird.

Wenn das Frontend 100 Anforderungen pro Sekunde an das Backend sendet, werden 100 verschiedene TCP-Verbindungen geöffnet und geschlossen.

Sie können die Verarbeitungszeit der Anforderung reduzieren und die Last verringern, wenn Sie eine TCP-Verbindung öffnen und für alle nachfolgenden HTTP-Anforderungen verwenden.

Das HTTP-Protokoll enthält eine Funktion namens HTTP Keep-Alive oder Wiederverwendung der Verbindung. In diesem Fall wird eine TCP-Verbindung zum Senden und Empfangen vieler HTTP-Anforderungen und -Antworten verwendet:



Diese Funktion ist standardmäßig nicht aktiviert: Sowohl der Server als auch der Client müssen entsprechend konfiguriert werden.

Das Setup selbst ist für die meisten Programmiersprachen und -umgebungen einfach und zugänglich.

Hier einige Links zu Beispielen in verschiedenen Sprachen:


Was passiert, wenn wir Keep-Alive in Kubernetes verwenden?
Nehmen wir an, dass sowohl die Frontend- als auch die Backend-Unterstützung am Leben bleiben.

Wir haben eine Kopie des Frontends und drei Kopien des Backends. Das Frontend stellt die erste Anfrage und öffnet eine TCP-Verbindung zum Backend. Die Anfrage erreicht den Dienst, einer der Backend-Pods wird als Zieladresse ausgewählt. Es sendet eine Antwort an das Backend und das Frontend empfängt sie.

Im Gegensatz zur üblichen Situation wird die TCP-Verbindung, wenn sie nach dem Empfang der Antwort geschlossen wird, jetzt für die folgenden HTTP-Anforderungen geöffnet.

Was passiert, wenn das Frontend mehr Backend-Anfragen sendet?

Um diese Anforderungen weiterzuleiten, wird eine offene TCP-Verbindung verwendet. Alle Anforderungen werden an dieselbe im Backend gesendet, wo die erste Anforderung eingegangen ist.

Sollten iptables den Verkehr nicht umverteilen?

Nicht in diesem Fall.

Wenn eine TCP-Verbindung erstellt wird, werden die iptables-Regeln durchlaufen, die eine bestimmte für das Backend auswählen, in das der Datenverkehr geleitet wird.

Da alle folgenden Anforderungen über eine bereits offene TCP-Verbindung ausgeführt werden, werden iptables-Regeln nicht mehr aufgerufen.

Mal sehen, wie es aussieht .

  1. Das erste Sub sendet eine Anfrage an den Dienst:

  2. Sie wissen bereits, was als nächstes passieren wird. Der Dienst existiert nicht, aber es gibt iptables-Regeln, die die Anforderung verarbeiten:

  3. Einer der Backend-Pods wird als Zieladresse ausgewählt:

  4. Die Anfrage erreicht den Herd. Zu diesem Zeitpunkt wird eine permanente TCP-Verbindung zwischen den beiden Pods hergestellt:

  5. Jede nächste Anfrage vom ersten Pod geht über eine bereits hergestellte Verbindung:



Infolgedessen haben Sie eine schnellere Antwort und eine höhere Bandbreite erhalten, aber die Fähigkeit zur Skalierung des Backends verloren.

Selbst wenn Sie zwei Pods im Backend mit einer konstanten Verbindung haben, wird der Datenverkehr immer zu einem von ihnen geleitet.

Kann das behoben werden?

Da Kubernetes nicht weiß, wie dauerhafte Verbindungen ausgeglichen werden können, liegt diese Aufgabe in Ihrer Verantwortung.

Dienste sind eine Reihe von IP-Adressen und Ports, die als Endpunkte bezeichnet werden.

Ihre Anwendung kann eine Liste der Endpunkte vom Dienst abrufen und entscheiden, wie Anforderungen zwischen ihnen verteilt werden sollen. Sie können mit Round-Robin eine dauerhafte Verbindung zu jedem Herd herstellen und Anforderungen zwischen diesen Verbindungen ausgleichen.

Oder wenden Sie ausgefeiltere Ausgleichsalgorithmen an .

Der clientseitige Code, der für den Ausgleich verantwortlich ist, sollte dieser Logik folgen:

  1. Rufen Sie die Liste der Endpunkte vom Dienst ab.
  2. Öffnen Sie für jeden Endpunkt eine dauerhafte Verbindung.
  3. Wenn Sie eine Anfrage stellen müssen, verwenden Sie eine der offenen Verbindungen.
  4. Aktualisieren Sie regelmäßig die Liste der Endpunkte, erstellen Sie neue oder schließen Sie alte dauerhafte Verbindungen, wenn sich die Liste ändert.

So wird es aussehen .

  1. Anstatt die erste Anforderung an den Dienst zu senden, können Sie Anforderungen auf der Clientseite ausgleichen:

  2. Sie müssen Code schreiben, der fragt, welche Pods Teil des Dienstes sind:

  3. Sobald Sie die Liste erhalten haben, speichern Sie sie auf der Clientseite und stellen Sie damit eine Verbindung zu den Pods her:

  4. Sie selbst sind für den Lastausgleichsalgorithmus verantwortlich:



Die Frage ist nun: Gilt dieses Problem nur für HTTP Keep-Alive?

Clientseitiger Lastausgleich


HTTP ist nicht das einzige Protokoll, das dauerhafte TCP-Verbindungen verwenden kann.

Wenn Ihre Anwendung eine Datenbank verwendet, wird die TCP-Verbindung nicht jedes Mal geöffnet, wenn Sie eine Anforderung ausführen oder ein Dokument aus der Datenbank abrufen müssen. 

Stattdessen wird eine permanente TCP-Verbindung zur Datenbank geöffnet und verwendet.

Wenn Ihre Datenbank in Kubernetes bereitgestellt wird und der Zugriff als Dienst bereitgestellt wird, treten dieselben Probleme wie im vorherigen Abschnitt beschrieben auf.

Ein Datenbankreplikat wird mehr als der Rest geladen. Kube-Proxy und Kubernetes helfen nicht, Verbindungen auszugleichen. Sie sollten sich darum kümmern, Abfragen in Ihrer Datenbank auszugleichen.

Je nachdem, mit welcher Bibliothek Sie eine Verbindung zur Datenbank herstellen, stehen Ihnen möglicherweise verschiedene Optionen zur Lösung dieses Problems zur Verfügung.

Das folgende Beispiel zeigt den Zugriff auf einen MySQL-Datenbankcluster von Node.js aus:

var mysql = require('mysql');
var poolCluster = mysql.createPoolCluster();

var endpoints = /* retrieve endpoints from the Service */

for (var [index, endpoint] of endpoints) {
  poolCluster.add(`mysql-replica-${index}`, endpoint);
}

// Make queries to the clustered MySQL database

Es gibt unzählige andere Protokolle, die dauerhafte TCP-Verbindungen verwenden:

  • WebSockets und gesicherte WebSockets
  • HTTP / 2
  • gRPC
  • RSockets
  • AMQP

Sie sollten bereits mit den meisten dieser Protokolle vertraut sein.

Aber wenn diese Protokolle so beliebt sind, warum gibt es keine standardisierte Ausgleichslösung? Warum ist eine Änderung der Clientlogik erforderlich? Gibt es eine native Kubernetes-Lösung?

Kube-Proxy und iptables dienen zum Schließen der meisten Standardbereitstellungsszenarien für Kubernetes. Dies dient der Bequemlichkeit.

Wenn Sie einen Webdienst verwenden, der eine REST-API bereitstellt, haben Sie Glück. In diesem Fall werden keine permanenten TCP-Verbindungen verwendet. Sie können einen beliebigen Kubernetes-Dienst verwenden.

Sobald Sie jedoch dauerhafte TCP-Verbindungen verwenden, müssen Sie herausfinden, wie Sie die Last gleichmäßig auf die Backends verteilen können. Kubernetes enthält für diesen Fall keine vorgefertigten Lösungen.

Natürlich gibt es Optionen, die helfen können.

Ausgleich langlebiger Verbindungen in Kubernetes


Kubernetes bietet vier Arten von Diensten an:

  1. Clusterip
  2. NodePort
  3. Lastenausgleicher
  4. Kopflos

Die ersten drei Dienste basieren auf der virtuellen IP-Adresse, die vom kube-proxy zum Erstellen von iptables-Regeln verwendet wird. Die grundlegende Grundlage aller Dienste ist jedoch ein kopfloser Dienst.

Dem kopflosen Dienst ist keine IP-Adresse zugeordnet, und er bietet nur einen Mechanismus zum Abrufen einer Liste von IP-Adressen und Ports der zugeordneten Herde (Endpunkte).

Alle Dienste basieren auf dem kopflosen Dienst.

Der ClusterIP-Dienst ist ein kopfloser Dienst mit einigen Ergänzungen: 

  1. Die Verwaltungsschicht weist ihr eine IP-Adresse zu.
  2. Kube-Proxy bildet die notwendigen iptables-Regeln.

Auf diese Weise können Sie kube-proxy ignorieren und die Liste der vom kopflosen Dienst empfangenen Endpunkte direkt verwenden, um die Last in Ihrer Anwendung auszugleichen.

Aber wie kann man allen in einem Cluster bereitgestellten Anwendungen eine ähnliche Logik hinzufügen?

Wenn Ihre Anwendung bereits bereitgestellt ist, scheint eine solche Aufgabe unmöglich zu sein. Es gibt jedoch eine Alternative.

Service Mesh wird Ihnen helfen


Sie haben wahrscheinlich bereits bemerkt, dass die clientseitige Lastausgleichsstrategie Standard ist.

Wenn die Anwendung gestartet wird, gilt Folgendes:

  1. Ruft eine Liste der IP-Adressen vom Dienst ab.
  2. Öffnet und verwaltet einen Verbindungspool.
  3. Aktualisiert den Pool regelmäßig und fügt Endpunkte hinzu oder entfernt sie.

Sobald die Anwendung eine Anfrage stellen möchte, gilt Folgendes:

  1. Wählt eine verfügbare Verbindung unter Verwendung einer Logik aus (z. B. Round-Robin).
  2. Erfüllt die Anfrage.

Diese Schritte funktionieren für WebSockets, gRPC und AMQP.

Sie können diese Logik in eine separate Bibliothek aufteilen und in Ihren Anwendungen verwenden.

Stattdessen können jedoch Service-Grids wie Istio oder Linkerd verwendet werden.

Service Mesh ergänzt Ihre Anwendung mit einem Prozess, der:

  1. Sucht automatisch nach IP-Adressen von Diensten.
  2. Überprüft Verbindungen wie WebSockets und gRPC.
  3. Gleicht Anforderungen mit dem richtigen Protokoll aus.

Service Mesh hilft bei der Verwaltung des Datenverkehrs innerhalb des Clusters, ist jedoch recht ressourcenintensiv. Andere Optionen verwenden Bibliotheken von Drittanbietern wie Netflix Ribbon oder programmierbare Proxys wie Envoy.

Was passiert, wenn Sie Ausgleichsprobleme ignorieren?


Sie können den Lastausgleich nicht verwenden und bemerken keine Änderungen. Schauen wir uns einige Arbeitsszenarien an.

Wenn Sie mehr Clients als Server haben, ist dies kein so großes Problem.

Angenommen, es gibt fünf Clients, die eine Verbindung zu zwei Servern herstellen. Auch wenn kein Ausgleich besteht, werden beide Server verwendet:



Verbindungen können ungleich verteilt sein: Möglicherweise sind vier Clients mit demselben Server verbunden, aber es besteht eine gute Chance, dass beide Server verwendet werden.

Problematischer ist das umgekehrte Szenario.

Wenn Sie weniger Clients und mehr Server haben, werden Ihre Ressourcen möglicherweise nicht ausreichend genutzt und es tritt ein potenzieller Engpass auf.

Angenommen, es gibt zwei Clients und fünf Server. Bestenfalls gibt es zwei permanente Verbindungen zu zwei von fünf Servern.

Andere Server sind inaktiv:



Wenn diese beiden Server die Verarbeitung von Clientanforderungen nicht verarbeiten können, hilft die horizontale Skalierung nicht.

Fazit


Kubernetes-Dienste funktionieren in den meisten Standard-Webanwendungsszenarien.

Sobald Sie jedoch mit Anwendungsprotokollen arbeiten, die dauerhafte TCP-Verbindungen verwenden, wie z. B. Datenbanken, gRPC oder WebSockets, sind Dienste nicht mehr geeignet. Kubernetes bietet keine internen Mechanismen zum Ausgleichen persistenter TCP-Verbindungen.

Dies bedeutet, dass Sie Anwendungen mit der Möglichkeit des Balancierens auf der Clientseite schreiben müssen.

Übersetzung vorbereitet von einem Team Kubernetes aaS aus der Mail.ru .

Was gibt es sonst noch zu diesem Thema zu lesen :

  1. Drei Ebenen der automatischen Skalierung in Kubernetes und deren effektive Verwendung
  2. Kubernetes im Geiste der Piraterie mit einer Implementierungsvorlage .
  3. Kubernetes .

All Articles