
Dieser Artikel hilft Ihnen zu verstehen, wie die Lastverteilung in Kubernetes funktioniert, was beim Skalieren von langlebigen Verbindungen passiert und warum es sinnvoll ist, die Client-seitige Lastverteilung in Betracht zu ziehen, wenn Sie HTTP/2, gRPC, RSockets, AMQP oder andere langlebige Protokolle verwenden.
Ein wenig darüber, wie der Traffic in Kubernetes umverteilt wird
Kubernetes bietet zwei praktische Abstraktionen für das Deployment von Anwendungen: Services und Deployments.
Deployments beschreiben, wie viele Instanzen Ihrer Anwendung zu jedem Zeitpunkt ausgeführt werden sollen. Jede Anwendung wird als Pod ausgeführt und erhält eine IP-Adresse.
Services ähneln funktionell einem Lastverteiler. Sie dienen der Verteilung des Traffics auf viele Pods.
Schauen wir uns an, wie das aussieht.
- Im folgenden Diagramm sehen Sie drei Instanzen einer Anwendung und einen Lastverteiler:

- Der Lastverteiler wird als Service bezeichnet, dem eine IP-Adresse zugewiesen ist. Jede eingehende Anfrage wird an einen der Pods weitergeleitet:

- Das Deployment-Szenario definiert die Anzahl der Instanzen der Anwendung. Sie müssen praktisch nie einen Pod direkt bereitstellen:

- Jeder Pod erhält seine eigene IP-Adresse:

Es ist nützlich, Services als ein Set von IP-Adressen zu betrachten. Jedes Mal, wenn Sie auf einen Service zugreifen, wird eine der IP-Adressen aus der Liste ausgewählt und als Zieladresse verwendet.
So sieht das aus.
- Eine curl-Anfrage an den Service 10.96.45.152 kommt an:

- Der Service wählt eine der drei Pod-Adressen als Ziel:

- Der Traffic wird zu einem bestimmten Pod umgeleitet:

Wenn Ihre Anwendung aus Frontend und Backend besteht, haben Sie sowohl einen Service als auch ein Deployment für jedes.
Wenn das Frontend eine Anfrage an das Backend stellt, muss es nicht wissen, wie viele Pods das Backend tatsächlich bedient: das können eins, zehn oder hundert sein.
Das Frontend weiß auch nichts über die Adressen der Pods, die das Backend bedienen.
Wenn das Frontend eine Anfrage an das Backend stellt, verwendet es die IP-Adresse des Backend-Services, die sich nicht ändert.
So sieht das aus.
- Pod 1 fragt eine interne Komponente des Backends an. Anstatt einen bestimmten Pod des Backends auszuwählen, stellt er eine Anfrage an den Service:

- Der Service wählt einen der Backend-Pods als Zieladresse aus:

- Der Traffic läuft von Pod 1 zu Pod 5, das vom Service ausgewählt wurde:

- Pod 1 weiß nicht, wie viele ähnliche Pods wie Pod 5 hinter dem Service verborgen sind:

Aber wie genau verteilt der Service die Anfragen? Es scheint, als würde Round-Robin-Balancing verwendet? Lassen Sie uns das klären.
Lastverteilung in Kubernetes-Services
Kubernetes-Services existieren nicht. Für einen Service gibt es keinen Prozess, dem eine IP-Adresse und ein Port zugewiesen sind.
Sie können dies überprüfen, indem Sie auf jeden Knoten des Clusters gehen und den Befehl netstat -ntlp ausführen.
Sie werden nicht einmal die IP-Adresse finden, die dem Service zugewiesen ist.
Die IP-Adresse des Services befindet sich auf der Steuerebene, im Controller, und ist in der Datenbank — etcd — gespeichert. Diese Adresse wird auch von einem weiteren Bestandteil — kube-proxy — verwendet.
Kube-proxy erhält eine Liste von IP-Adressen für alle Services und erstellt eine Menge von iptables-Regeln auf jedem Knoten des Clusters.
Diese Regeln sagen: „Wenn wir die IP-Adresse des Services sehen, ändern wir die Zieladresse der Anfrage und senden sie an einen der Pods.“
Die IP-Adresse des Services wird nur als Eingangspunkt verwendet und wird von keinem Prozess, der diese IP-Adresse und den Port abhört, betreut.
Lassen Sie uns das ansehen.
- Betrachten wir einen Cluster mit drei Knoten. Auf jedem Knoten befinden sich Pods:

- Die miteinander verbundenen Pods, die beige gefärbt sind, sind Teil des Services. Da der Service nicht als Prozess existiert, wird er grau dargestellt:

- Der erste Pod fragt den Service an und soll zu einem der verbundenen Pods gelangen:

- Aber der Service existiert nicht, es gibt keinen Prozess. Wie funktioniert das?

- Bevor die Anfrage den Knoten verlässt, durchläuft sie die iptables-Regeln:

- Die iptables-Regeln wissen, dass es den Service nicht gibt, und ersetzen die IP-Adresse durch eine der IP-Adressen der Pods, die mit diesem Service verbunden sind:

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

- Je nach Netzwerktopologie erreicht die Anfrage schließlich den Pod:

Können iptables die Last verteilen?
Nein, iptables werden zur Filterung verwendet und wurden nicht für das Lastenausgleichsdesign entwickelt.
Es besteht jedoch die Möglichkeit, eine Regelmenge zu schreiben, die wie ein .
Und genau das wird in Kubernetes umgesetzt.
Wenn Sie drei Pods haben, wird kube-proxy folgende Regeln schreiben:
- Wählen Sie den ersten Pod mit einer Wahrscheinlichkeit von 33 %, andernfalls fahren Sie mit der nächsten Regel fort.
- Wählen Sie den zweiten Pod mit einer Wahrscheinlichkeit von 50 %, andernfalls fahren Sie mit der nächsten Regel fort.
- Wählen Sie den dritten Pod.
Ein solches System führt dazu, dass jeder Pod mit einer Wahrscheinlichkeit von 33 % ausgewählt wird.

Und es gibt keine Garantie, dass Pod 2 als nächstes nach Pod 1 gewählt wird.
Hinweis: iptables verwendet ein statistisches Modul mit zufälliger Verteilung. Somit basiert der Lastenausgleichsalgorithmus auf einer zufälligen Auswahl.
Jetzt, wo Sie verstehen, wie die Dienste funktionieren, schauen wir uns interessantere Nutzungsszenarien an.
Langsame Verbindungen in Kubernetes skalieren standardmäßig nicht.
Jede HTTP-Anfrage vom Frontend an das Backend wird durch eine separate TCP-Verbindung bedient, die geöffnet und geschlossen wird.
Wenn das Frontend 100 Anfragen pro Sekunde an das Backend sendet, werden 100 verschiedene TCP-Verbindungen geöffnet und geschlossen.
Die Bearbeitungszeit einer Anfrage kann verkürzt und die Last verringert werden, wenn eine einzige TCP-Verbindung geöffnet wird, die für alle folgenden HTTP-Anfragen verwendet wird.
Im HTTP-Protokoll gibt es eine Funktion, die als HTTP Keep-Alive oder Wiederverwendung der Verbindung bezeichnet wird. In diesem Fall wird eine einzige TCP-Verbindung verwendet, um viele HTTP-Anfragen und -Antworten zu senden und zu empfangen:

Diese Funktion ist standardmäßig nicht aktiviert: sowohl der Server als auch der Client müssen entsprechend konfiguriert sein.
Die Einstellung selbst ist einfach und für die meisten Programmiersprachen und Umgebungen verfügbar.
Hier sind einige Links zu Beispielen in verschiedenen Sprachen:
Was passiert, wenn wir Keep-Alive im Kubernetes-Dienst verwenden?
Nehmen wir an, dass sowohl das Frontend als auch das Backend Keep-Alive unterstützen.
Wir haben eine Kopie des Frontends und drei Instanzen des Backends. Das Frontend sendet die erste Anfrage und eröffnet eine TCP-Verbindung zum Backend. Die Anfrage erreicht den Dienst, und einer der Pods des Backends wird als Zieladresse ausgewählt. Der Pod des Backends sendet eine Antwort, und das Frontend erhält sie.
Im Gegensatz zur üblichen Situation, bei der die TCP-Verbindung nach dem Empfang der Antwort geschlossen wird, bleibt sie jetzt für die nächsten HTTP-Anfragen offen.
Was passiert, wenn das Frontend weitere Anfragen an das Backend sendet?
Für die Weiterleitung dieser Anfragen wird die offene TCP-Verbindung verwendet, alle Anfragen gelangen an denselben Pod des Backends, an den die erste Anfrage ging.
Sollte iptables den Datenverkehr nicht umleiten?
Nicht in diesem Fall.
Wenn eine TCP-Verbindung hergestellt wird, durchläuft sie die iptables-Regeln, die das spezifische Backend auswählen, an das der Datenverkehr gesendet wird.
Da alle folgenden Anfragen über die bereits geöffnete TCP-Verbindung laufen, werden die iptables-Regeln nicht mehr aufgerufen.
Schauen wir uns an, wie das aussieht.
- Das erste Pod sendet eine Anfrage an den Dienst:

- Sie wissen bereits, was als Nächstes kommt. Der Dienst existiert nicht, aber es gibt iptables-Regeln, die die Anfrage bearbeiten:

- Eines der Backend-Pods wird als Zieladresse ausgewählt:

- Die Anfrage erreicht das Pod. In diesem Moment wird eine dauerhafte TCP-Verbindung zwischen den beiden Pods hergestellt:

- Jede nachfolgende Anfrage vom ersten Pod wird über die bereits etablierte Verbindung gesendet:

Infolgedessen haben Sie eine schnellere Antwortzeit und höhere Durchsatzraten, aber die Möglichkeit zur Skalierung des Backends verloren.
Selbst wenn Sie zwei Pods im Backend haben, wird der Datenverkehr durch die ständige Verbindung immer zu einem von ihnen geleitet.
Kann man das beheben?
Da Kubernetes nicht weiß, wie man dauerhafte Verbindungen ausbalanciert, obliegt diese Aufgabe Ihnen.
Dienste sind eine Menge von IP-Adressen und Ports, die Endpunkte genannt werden.
Ihre Anwendung kann eine Liste von Endpunkten vom Dienst erhalten und entscheiden, wie die Anfragen zwischen ihnen verteilt werden. Sie können eine dauerhafte Verbindung mit jedem Pod öffnen und die Anfragen über diese Verbindungen im Round-Robin-Verfahren ausbalancieren.
Oder komplexere .
Der Client-seitige Code, der für die Lastverteilung verantwortlich ist, sollte folgender Logik folgen:
- Eine Liste von Endpunkten vom Dienst abrufen.
- Für jeden Endpunkt eine dauerhafte Verbindung öffnen.
- Wenn eine Anfrage gestellt werden muss, eine der offenen Verbindungen verwenden.
- Regelmäßig die Liste der Endpunkte aktualisieren, neue dauerhafte Verbindungen erstellen oder alte schliessen, falls sich die Liste ändert.
So wird es aussehen:.
- Anstatt dass das erste Pod die Anfrage an den Dienst sendet, können Sie die Anfragen auf der Client-Seite ausbalancieren:

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

- Sobald Sie die Liste erhalten, speichern Sie sie auf der Client-Seite und verwenden Sie sie für die Verbindung mit den Pods:

- Sie sind selbst für den Lastenausgleichsalgorithmus verantwortlich:

Jetzt stellt sich die Frage: Betrifft dieses Problem nur HTTP Keep-Alive?
Lastenausgleich auf der Client-Seite
HTTP ist nicht das einzige Protokoll, das dauerhafte TCP-Verbindungen verwenden kann.
Wenn Ihre Anwendung eine Datenbank nutzt, wird die TCP-Verbindung nicht jedes Mal geöffnet, wenn Sie eine Abfrage ausführen oder ein Dokument aus der Datenbank abrufen müssen.
Stattdessen wird eine permanente TCP-Verbindung zur Datenbank geöffnet und genutzt.
Wenn Ihre Datenbank in Kubernetes bereitgestellt wird und der Zugriff als Dienst bereitgestellt wird, werden Sie mit den gleichen Problemen konfrontiert, die im vorherigen Abschnitt beschrieben sind.
Eine Replik der Datenbank wird stärker belastet als die anderen. Kube-proxy und Kubernetes helfen nicht beim Lastenausgleich der Verbindungen. Sie müssen sich um den Lastenausgleich der Abfragen an Ihre Datenbank kümmern.
Je nach der Bibliothek, die Sie für die Verbindung zur Datenbank verwenden, können Sie verschiedene Optionen zur Lösung dieses Problems haben.
Im Folgenden finden Sie ein Beispiel für den Zugriff auf das MySQL-Datenbankcluster aus Node.js:
var mysql = require('mysql');
var poolCluster = mysql.createPoolCluster();
var endpoints = /* Endpunkte vom Dienst abrufen */
for (var [index, endpoint] of endpoints) {
poolCluster.add(`mysql-replica-${index}`, endpoint);
}
// Abfragen an die clusterfähige MySQL-Datenbank stellenEs gibt viele andere Protokolle, die dauerhafte TCP-Verbindungen nutzen:
- WebSockets und gesicherte WebSockets
- HTTP/2
- , würden Sie den Port
- RSockets
- ), das einen offenen Standard für den Austausch von Geschäftsnachrichten zwischen Anwendungen oder Organisationen darstellt.
Sie sollten mit den meisten dieser Protokolle bereits vertraut sein.
Aber wenn diese Protokolle so beliebt sind, warum gibt es dann keine standardisierte Lösung für den Lastenausgleich? Warum ist eine Änderung der Client-Logik erforderlich? Gibt es eine native Lösung in Kubernetes?
Kube-proxy und iptables wurden entwickelt, um die meisten Standardnutzungsfälle bei der Bereitstellung in Kubernetes zu schließen. Das wurde zur Vereinfachung getan.
Wenn Sie einen Webdienst nutzen, der eine REST-API bereitstellt, haben Sie Glück — in diesem Fall werden keine dauerhaften TCP-Verbindungen verwendet, und Sie können jeden Kubernetes-Dienst nutzen.
Aber sobald Sie beginnen, dauerhafte TCP-Verbindungen zu verwenden, müssen Sie herausfinden, wie Sie die Last gleichmäßig auf die Backends verteilen können. Kubernetes enthält dafür keine fertigen Lösungen.
Es gibt jedoch sicherlich Optionen, die helfen können.
Lastenausgleich für langlebige Verbindungen in Kubernetes
In Kubernetes gibt es vier Arten von Diensten:
- ClusterIP
- NodePort
- LoadBalancer
- Headless
Die ersten drei Dienste basieren auf einer virtuellen IP-Adresse, die von kube-proxy zur Erstellung von iptables-Regeln verwendet wird. Aber die fundamentale Grundlage aller Dienste ist der headless-Dienst.
Ein headless-Dienst ist nicht mit einer IP-Adresse verbunden und bietet lediglich einen Mechanismus zum Abrufen der Liste von IP-Adressen und Ports, die mit den zugehörigen Pods (Endpunkten) verbunden sind.
Alle Dienste basieren auf dem headless-Dienst.
Der ClusterIP-Dienst ist ein headless-Dienst mit zusätzlichen Funktionen:
- Ebene der Verwaltung weist ihm eine IP-Adresse zu.
- Kube-proxy formt die notwendigen iptables-Regeln.
So können Sie kube-proxy ignorieren und die Liste der Endpunkte, die aus dem headless-Dienst abgerufen wurde, direkt für das Lastenmanagement in Ihrer Anwendung verwenden.
Aber wie fügen Sie eine solche Logik zu allen in einem Cluster bereitgestellten Anwendungen hinzu?
Wenn Ihre Anwendung bereits bereitgestellt ist, kann dies unlösbar erscheinen. Es gibt jedoch eine alternative Option.
Service Mesh wird Ihnen helfen.
Sie haben wahrscheinlich bereits bemerkt, dass die Client-seitige Lastenverteilung eine recht standardmäßige Strategie ist.
Wenn die Anwendung startet, dann:
- Erhält eine Liste von IP-Adressen vom Dienst.
- Öffnet und hält einen Pool von Verbindungen.
- Aktualisiert den Pool regelmäßig, indem sie Endpunkte hinzufügt oder entfernt.
Sobald die Anwendung eine Anfrage stellen möchte, dann:
- Wählt eine verfügbare Verbindung aus, indem sie eine Logik anwendet (zum Beispiel Round-Robin).
- Führt die Anfrage aus.
Diese Schritte gelten sowohl für WebSocket-Verbindungen als auch für gRPC und AMQP.
Sie können diese Logik in eine separate Bibliothek auslagern und in Ihren Anwendungen verwenden.
Anstelle dessen können Sie Servicenetzwerke wie Istio oder Linkerd verwenden.
Service Mesh ergänzt Ihre Anwendung um einen Prozess, der:
- Automatisch die IP-Adressen der Dienste sucht.
- Verbindungen wie WebSockets und gRPC überprüft.
- Anfragen anhand des richtigen Protokolls ausbalanciert.
Service Mesh hilft, den Datenverkehr im Cluster zu verwalten, ist jedoch recht ressourcenintensiv. Weitere Optionen sind die Verwendung von Drittanbieter-Bibliotheken wie Netflix Ribbon oder programmierbaren Proxys wie Envoy.
Was passiert, wenn die Lastenverteilung ignoriert wird?
Sie können die Lastenverteilung möglicherweise nicht nutzen und dennoch keine Veränderungen bemerken. Lassen Sie uns einige Szenarien untersuchen.
Wenn Sie mehr Kunden als Server haben, ist das nicht so ein großes Problem.
Angenommen, es gibt fünf Kunden, die sich mit zwei Servern verbinden. Selbst ohne Lastverteilung werden beide Server genutzt:

Verbindungen können ungleichmäßig verteilt sein: Möglicherweise haben sich vier Kunden mit demselben Server verbunden, aber es besteht eine gute Chance, dass beide Server verwendet werden.
Was problematischer ist, ist das Gegenteil.
Wenn Sie weniger Kunden und mehr Server haben, könnten Ihre Ressourcen nicht ausreichend genutzt werden und es könnte ein potenzieller Engpass entstehen.
Angenommen, es gibt zwei Kunden und fünf Server. Im besten Fall gibt es zwei permanente Verbindungen zu zwei der fünf Server.
Die übrigen Server werden untätig sein:

Wenn diese beiden Server die Anfragen der Kunden nicht bewältigen können, wird horizontale Skalierung nicht helfen.
Fazit
Kubernetes-Dienste sind für den Einsatz in den meisten gängigen Szenarien von Webanwendungen konzipiert.
Sobald Sie jedoch mit Anwendungsprotokollen arbeiten, die dauerhafte TCP-Verbindungen verwenden, wie z. B. Datenbanken, gRPC oder WebSockets, sind die Dienste nicht mehr geeignet. Kubernetes bietet keine internen Mechanismen zur Lastverteilung dauerhafter TCP-Verbindungen.
Das bedeutet, dass Sie Anwendungen unter Berücksichtigung der Möglichkeit der Lastverteilung auf der Client-Seite schreiben müssen.
Übersetzung erstellt von .
Was gibt es noch zu lesen:
- .
- .
- .
Quelle: habr.com




























