
Hallo zusammen! Mein Name ist Oleg Sidorenkov, ich arbeite als Teamleiter der Infrastruktur bei der Firma DomClick. Wir betreiben den „Kubik“ seit über drei Jahren in der Produktion, und in dieser Zeit haben wir viele verschiedene interessante Momente damit erlebt. Heute werde ich euch erzählen, wie man mit dem richtigen Ansatz noch mehr Leistung aus „vanilla“ Kubernetes für euren Cluster herausholen kann. Bereit, fertig, los!
Ihr wisst alle, dass Kubernetes ein skalierbares Open-Source-System zur Orchestrierung von Containern ist; oder, nun ja, 5 Binärdateien, die Magie bewirken, indem sie den Lebenszyklus eurer Mikrodienste in einer Serverumgebung verwalten. Darüber hinaus ist es ein ziemlich flexibles Werkzeug, das man wie ein Lego-Baukasten für maximale Anpassung an verschiedene Anforderungen zusammenstellen kann.
Und es scheint alles gut zu sein: Werf die Server in den Cluster, wie Holzscheite in den Ofen, und sorge dir nicht um die Hitze. Aber wenn du um die Umwelt besorgt bist, wirst du dir Gedanken machen: "Wie kann ich das Feuer im Ofen am Brennen halten und gleichzeitig den Wald schonen?" Mit anderen Worten, wie kannst du Wege finden, die Infrastruktur zu verbessern und die Kosten zu senken.
1. Achte auf die Ressourcen der Teams und Anwendungen

Eine der banalsten, aber effektivsten Methoden ist die Einführung von requests/limits. Teile die Anwendungen nach Namespaces auf und die Namespaces nach Entwicklungsteams. Setze für die Anwendung vor der Bereitstellung Werte für den Verbrauch von CPU-Zeit, Speicher, ephemeral storage.
resources:
requests:
memory: 2Gi
cpu: 250m
limits:
memory: 4Gi
cpu: 500mDurch Erfahrung sind wir zu dem Schluss gekommen: Man sollte die Requests nicht mehr als doppelt so hoch wie die Limits ansetzen. Das Volumen des Clusters wird auf Grundlage der Requests berechnet, und wenn du deinen Anwendungen einen Unterschied bei den Ressourcen von beispielsweise 5-10 mal gibst, stell dir vor, was mit deinem Node passiert, wenn sie sich mit Pods füllt und plötzlich eine Last erhält. Nichts Gutes. Mindestens throttling, und im schlimmsten Fall verabschiedest du dich von dem Worker und erhältst eine zyklische Last auf den anderen Nodes, nachdem die Pods anfangen umzuziehen.
Außerdem könnt ihr mit limitranges zu Beginn für den Container Werte für die Ressourcen festlegen – minimal, maximal und standardmäßig:
➜ ~ kubectl describe limitranges --namespace ops
Name: limit-range
Namespace: ops
Type Resource Min Max Default Request Default Limit Max Limit/Request Ratio
---- -------- --- --- --------------- ------------- -----------------------
Container cpu 50m 10 100m 100m 2
Container ephemeral-storage 12Mi 8Gi 128Mi 4Gi -
Container memory 64Mi 40Gi 128Mi 128Mi 2Vergessen Sie nicht, die Ressourcen des Namensraums zu begrenzen, damit ein Team nicht alle Ressourcen des Clusters für sich beanspruchen kann:
➜ ~ kubectl describe resourcequotas --namespace ops
Name: resource-quota
Namespace: ops
Resource Used Hard
-------- ---- ----
limits.cpu 77250m 80
limits.memory 124814367488 150Gi
pods 31 45
requests.cpu 53850m 80
requests.memory 75613234944 150Gi
services 26 50
services.loadbalancers 0 0
services.nodeports 0 0Wie aus der Beschreibung hervorgeht resourcequotas, wenn das Team ops Pods bereitstellen möchte, die weitere 10 CPU verbrauchen, wird der Scheduler dies nicht zulassen und einen Fehler ausgeben:
Fehler beim Erstellen: Pods "nginx-proxy-9967d8d78-nh4fs" sind nicht erlaubt: Quota überschritten: resource-quota, angefordert: limits.cpu=5, requests.cpu=5, genutzt: limits.cpu=77250m, requests.cpu=53850m, begrenzt: limits.cpu=10, requests.cpu=10Um ein solches Problem zu lösen, kann man ein Tool schreiben, wie , das in der Lage ist, den Zustand der Ressourcen der Teams zu speichern und zu commiten.
2. Wählen Sie den optimalen Speicherorte für Dateien aus

Hier möchte ich auf das Thema persistente Volumes und das Speichersystem der Worker-Knoten von Kubernetes eingehen. Ich hoffe, dass niemand „Cube“ auf HDD in der Produktion verwendet, aber manchmal reicht auch ein gewöhnliches SSD nicht mehr aus. Wir sind auf das Problem gestoßen, dass die Protokolle die Festplatte aufgrund von Ein-/Ausgaben überlasteten, und die Lösungsmöglichkeiten sind nicht sehr vielfältig:
Verwenden Sie Hochleistungs-SSDs oder wechseln Sie zu NVMe (wenn Sie selbst über Ihre Hardware verfügen).
Verringern Sie das Protokollierungslevel.
Führen Sie ein "intelligentes" Balancing der Pods durch, die die Festplatte stark beanspruchen (
podAntiAffinity).
Der Screenshot oben zeigt, was mit der Festplatte unter nginx-ingress-controller passiert, wenn das Protokollieren von access_logs (~12.000 Protokolle/Sek.) aktiviert ist. Solch ein Zustand kann natürlich zur Verschlechterung aller Anwendungen auf diesem Knoten führen.
Was die PV betrifft, so habe ich leider nicht alle Persistente Volumes. Nutzen Sie die beste Option, die genau zu Ihnen passt. Historisch gesehen benötigen einige wenige Dienste RWX-Volumes, und dafür wurde schon lange NFS-Speicher verwendet. Günstig und… ausreichend. Natürlich haben wir damit schon so manche Erfahrung gemacht — seien Sie gewarnt, aber wir haben gelernt, es zu optimieren, und die Kopfschmerzen gehören der Vergangenheit an. Und wenn möglich, wechseln Sie zu einem objektbasierten Speicher S3.
3. Optimierte Images erstellen

Es ist am besten, optimierte Images für Container zu verwenden, damit Kubernetes sie schneller abrufen und effizienter ausführen kann.
Optimiert bedeutet, dass die Images:
nur eine Anwendung enthalten oder nur eine Funktion ausführen;
klein sind, da größere Images schlechter über das Netzwerk übertragen werden;
über Endpunkte zur Überprüfung der Betriebsbereitschaft und Verfügbarkeit verfügen, mit denen Kubernetes Maßnahmen bei Ausfällen ergreifen kann;
betriebssystemfreundlich für Container sind (wie Alpine oder CoreOS), die weniger anfällig für Konfigurationsfehler sind;
mehrstufige Builds verwenden, damit Sie nur die kompilierten Anwendungen und nicht die zugehörigen Quelltexte bereitstellen können.
Es gibt viele Tools und Dienstleistungen, die es ermöglichen, Images im laufenden Betrieb zu überprüfen und zu optimieren. Es ist wichtig, sie immer auf dem aktuellen Stand und sicher zu halten. Am Ende erzielen Sie:
Reduzierung der Netzwerkbelastung des gesamten Clusters.
Verringerung der Startzeit des Containers.
Kleineren Speicherbedarf für Ihr gesamtes Docker-Registry.
4. Verwenden Sie den DNS-Cache

Wenn es um hohe Lasten geht, ist das Leben ohne Optimierung des DNS-Systems des Clusters ziemlich belastend. Vor langer Zeit unterstützten die Entwickler von Kubernetes ihre Lösung kube-dns. Diese wurde auch bei uns implementiert, aber diese Software wurde nicht besonders optimiert und erzielte nicht die erforderliche Leistung, obwohl die Aufgabe scheinbar einfach war. Dann kam coredns, zu dem wir übergegangen sind und keine Probleme hatten; es wurde schließlich zum Standard-DNS-Dienst in K8s. Irgendwann erreichten wir 40.000 rps beim DNS-System, und auch dieses Lösung reichte nicht mehr aus. Aber aus glücklichen Umständen kam Nodelocaldns heraus, auch bekannt als node local cache, auch bekannt als .
Warum verwenden wir das? Im Linux-Kernel gibt es einen Fehler, der bei mehrfachen Anfragen über conntrack NAT über UDP zu einem Race Condition beim Schreiben in die conntrack-Tabellen führt, und ein Teil des Traffics über NAT geht verloren (jeder Zugriff über den Service ist NAT). Nodelocaldns löst dieses Problem, indem es NAT eliminiert und die Verbindung zu den Upstream-DNS auf TCP aufrüstet sowie DNS-Anfragen an die Upstreams lokal cached (einschließlich eines kurzen 5-sekündigen negativen Caches).
5. Skalieren Sie Pods automatisch horizontal und vertikal

Können Sie mit Zuversicht sagen, dass alle Ihre Mikrodienste für eine zwei- bis dreifache Erhöhung der Last bereit sind? Wie weisen Sie Ihren Anwendungen die richtigen Ressourcen zu? Ein paar Pods über der Arbeitslast laufen zu lassen, kann überflüssig sein, und sie am Limit zu halten, birgt das Risiko, bei plötzlichem Anstieg des Traffics auf den Dienst Ausfallzeiten zu erfahren. Der goldene Mittelweg wird durch den Zauber der Multiplikation mit Diensten wie erreicht, die und .
VPA ermöglicht es, automatisch die Requests/Limits Ihrer Container im Pod basierend auf der tatsächlichen Nutzung zu erhöhen. Wie kann das nützlich sein? Wenn Sie Pods haben, die aus irgendeinem Grund nicht horizontal skaliert werden können (was nicht ganz zuverlässig ist), können Sie versuchen, die Anpassung ihrer Ressourcen dem VPA zu überlassen. Sein Hauptmerkmal ist ein Empfehlungssystem auf der Grundlage historischer und aktueller Daten vom metric-server, daher können Sie, wenn Sie nicht möchten, dass die Requests/Limits automatisch geändert werden, einfach die empfohlenen Ressourcen für Ihre Container beobachten und die Einstellungen zur Einsparung von CPU und Speicher im Cluster optimieren.
Das Bild wurde von https://levelup.gitconnected.com/kubernetes-autoscaling-101-cluster-autoscaler-horizontal-pod-autoscaler-and-vertical-pod-2a441d9ad231 entnommen
Der Scheduler in Kubernetes basiert immer auf Requests. Welchen Wert Sie auch verwenden, der Scheduler sucht einen geeigneten Knoten, basierend darauf. Die Limits-Werte benötigt der Kubelet, um zu wissen, wann er einen Pod drosseln oder killen soll. Und da der einzige wichtige Parameter der Request-Wert ist, wird der VPA damit arbeiten. Jedes Mal, wenn Sie vertikales Scaling für eine Anwendung festlegen, definieren Sie, wie die Requests beschaffen sein sollten. Und was passiert dann mit den Limits? Dieser Parameter wird ebenfalls proportional skaliert.
Zum Beispiel, hier sind die typischen Einstellungen für einen Pod:
Ressourcen:
Anfragen:
Arbeitsspeicher: 250Mi
CPU: 200m
Limits:
Arbeitsspeicher: 500Mi
CPU: 350mDer Empfehlungssystem bestimmt, dass Ihre Anwendung für einen normalen Betrieb 300m CPU und 500Mi benötigt. Sie erhalten folgende Einstellungen:
Ressourcen:
Anfragen:
Arbeitsspeicher: 500Mi
CPU: 300m
Limits:
Arbeitsspeicher: 1000Mi
CPU: 525mWie oben erwähnt, handelt es sich um eine proportionale Skalierung basierend auf dem Verhältnis von Anfragen/Limits im Manifest:
CPU: 200m → 300m: Verhältnis 1:1.75;
Arbeitsspeicher: 250Mi → 500Mi: Verhältnis 1:2.
Was das betrifft HPA, hier ist der Mechanismus transparenter. Es werden Schwellenwerte für Metriken festgelegt, zum Beispiel für CPU und Arbeitsspeicher. Wenn der Durchschnittswert aller Replikate den Schwellenwert überschreitet, wird die Anwendung um +1 Pod skaliert, bis der Wert unter dem Schwellenwert liegt oder die maximale Anzahl an Replikaten erreicht ist.
Das Bild wurde von https://levelup.gitconnected.com/kubernetes-autoscaling-101-cluster-autoscaler-horizontal-pod-autoscaler-and-vertical-pod-2a441d9ad231 entnommen
Neben den üblichen Metriken wie CPU und Arbeitsspeicher können Sie Schwellenwerte für Ihre benutzerdefinierten Metriken aus Prometheus festlegen und mit ihnen arbeiten, wenn Sie dies als die genaueste Betrachtungsweise erachten, wann Ihre Anwendung skaliert werden sollte. Sobald die Anwendung sich unter den festgelegten Schwellenwert für die Metrik stabilisiert, beginnt die HPA, die Pods nach unten bis zur minimalen Anzahl an Replikaten oder bis zu dem Zustand zu skalieren, in dem die Last dem festgelegten Schwellenwert entspricht.
6. Vergessen Sie nicht die Node Affinity und Pod Affinity

Nicht alle Knoten arbeiten mit derselben Hardware, nicht alle Pods müssen rechenintensive Anwendungen ausführen. Kubernetes ermöglicht es, Spezialisierungen für Knoten und Pods über Node Affinity und Pod Affinity.
Wenn Sie Knoten haben, die für rechenintensive Operationen geeignet sind, ist es besser, Anwendungen entsprechend diesen Knoten zuzuordnen, um maximale Effizienz zu erzielen. Verwenden Sie dazu nodeSelector mit dem Knotenlabel.
Angenommen, Sie haben zwei Knoten: einen mit CPUType=HIGHFREQ und einer großen Anzahl schneller Kerne, und einen anderen mit MemoryType=HIGHMEMORY einer großen Menge Arbeitsspeicher und höherer Leistung. Am einfachsten ist es, das Deployment des Pods dem Knoten HIGHFREQ, indem Sie in den Abschnitt spec diesen Selector hinzufügen:
…
nodeSelector:
CPUType: HIGHFREQEin kostspieligerer und spezifischer Ansatz, dies zu tun, ist die Verwendung von nodeAffinity im Feld Affinity im Abschnitt. specEs gibt zwei Optionen:
requiredDuringSchedulingIgnoredDuringExecution: harte Einschränkung (der Scheduler wird Pods nur auf bestimmten Knoten bereitstellen (und nirgendwo anders));preferredDuringSchedulingIgnoredDuringExecution: weiche Konfiguration (der Scheduler wird versuchen, auf bestimmten Knoten bereitzustellen, und wenn das nicht funktioniert, wird er versuchen, auf dem nächsten verfügbaren Knoten bereitzustellen).
Sie können eine bestimmte Syntax zur Steuerung der Knotentags festlegen, zum Beispiel In, NotIn, Exists, DoesNotExist, Gt oder Lt. Denken Sie jedoch daran, dass komplexe Methoden in langen Listen von Tags die Entscheidungsfindung in kritischen Situationen verlangsamen. Mit anderen Worten, machen Sie es nicht komplizierter.
Wie oben erwähnt, erlaubt Kubernetes die Bindung aktueller Pods. Das heißt, Sie können bestimmte Pods so konfigurieren, dass sie zusammen mit anderen Pods in derselben Verfügbarkeitszone (für Clouds relevant) oder auf Knoten arbeiten.
Im podAffinity Felder Affinity im Abschnitt. spec stehen die gleichen Felder zur Verfügung wie bei nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution und preferredDuringSchedulingIgnoredDuringExecution. Der einzige Unterschied besteht darin, dass matchExpressions Pods an den Knoten bindet, auf dem bereits ein Pod mit diesem Tag ausgeführt wird.
Außerdem bietet Kubernetes das Feld podAntiAffinity, das im Gegensatz dazu keinen Pod an einen Knoten mit bestimmten Pods bindet.
Was die Ausdrücke betrifft, nodeAffinity kann der gleiche Rat gegeben werden: Bemühen Sie sich um Einfachheit und Klarheit der Regeln, und versuchen Sie nicht, die Podspezifikation mit komplexen Regelwerken zu überladen. Es ist sehr einfach, eine Regel zu erstellen, die den Clusterbedingungen nicht entspricht, was zusätzliche Last auf den Scheduler erzeugt und die Gesamtleistung verringert.
7. Taints & Tolerations
Es gibt noch eine weitere Methode zur Steuerung des Schedulers. Wenn Sie einen großen Cluster mit Hunderten von Knoten und Tausenden von Mikrodiensten haben, ist es sehr schwierig, bestimmten Pods die Bereitstellung auf bestimmten Knoten zu verwehren.
Hierbei hilft der Mechanismus der Taints – verbotene Regeln. Zum Beispiel kann man in bestimmten Szenarien bestimmten Knoten verbieten, Pods bei sich zu starten. Um einen Taint auf einen bestimmten Knoten anzuwenden, muss die Option taint in kubectl verwendet werden. Geben Sie den Schlüssel und den Wert an und dann einen Taint wie NoSchedule oder NoExecute:
$ kubectl taint nodes node10 node-role.kubernetes.io/ingress=true:NoScheduleEs ist auch erwähnenswert, dass der Mechanismus für Taints drei Hauptwirkungen unterstützt: NoSchedule, NoExecute und PreferNoSchedule.
NoSchedulebedeutet, dass solange in der Podspezifikation kein entsprechender Eintrag vorhanden isttolerations, wird er nicht auf dem Knoten bereitgestellt (in diesem Beispielnode10).PreferNoSchedule— eine vereinfachte VersionNoSchedule. In diesem Fall wird der Scheduler versuchen, Pods, die keinen entsprechenden Eintrag haben, nicht auf den Knoten zu verteilen, aber das ist keine strikte Einschränkung. Wenn im Cluster keine Ressourcen vorhanden sind, beginnen die Pods, auf diesem Knoten bereitgestellt zu werden.tolerationsauf den Knoten, aber das ist keine strikte Einschränkung. Wenn im Cluster nicht genügend Ressourcen vorhanden sind, beginnen die Pods, sich auf diesem Knoten zu messen.NoExecute— dieser Effekt löst eine sofortige Evakuierung von Pods aus, für die kein entsprechender Eintrag vorhanden isttolerations.
Interessanterweise lässt sich dieses Verhalten durch ein Toleranzmechanismus rückgängig machen. Dies ist praktisch, wenn es einen "verbotenen" Knoten gibt und Sie nur Infrastruktur-Services darauf platzieren möchten. Wie macht man das? Erlauben Sie nur die Pods, für die eine entsprechende Toleranz vorhanden ist.
So könnte die Pod-Spezifikation aussehen:
spec:
tolerations:
- key: "node-role.kubernetes.io\/ingress"
operator: "Equal"
value: "true"
effect: "NoSchedule"Das bedeutet nicht, dass der Pod beim nächsten Redeployment genau auf diesem Knoten landet, das ist kein Node Affinity Mechanismus und nodeSelector. Aber durch die Kombination mehrerer Funktionen können Sie eine sehr flexible Planer-Konfiguration erreichen.
8. Konfigurieren Sie die Bereitstellungspriorität von Pods
Dass Sie die Bindung von Pods an Knoten konfiguriert haben, bedeutet nicht, dass alle Pods mit derselben Priorität behandelt werden müssen. Beispielsweise möchten Sie möglicherweise bestimmte Pods früher als andere bereitstellen.
Kubernetes bietet verschiedene Möglichkeiten zur Konfiguration der Priorität von Pods (Pod Priority und Preemption). Die Konfiguration besteht aus mehreren Teilen: dem Objekt PriorityClass und der Beschreibung des Feldes priorityClassName in der Pod-Spezifikation. Sehen wir uns ein Beispiel an:
apiVersion: scheduling.k8s.io\/v1
kind: PriorityClass
metadata:
name: high-priority
value: 99999
globalDefault: false
description: "Diese Prioritätsklasse sollte nur für sehr wichtige Pods verwendet werden"Wir erstellen PriorityClass, geben ihm einen Namen, eine Beschreibung und einen Wert. Je höher value, desto höher die Priorität. Der Wert kann jede 32-Bit-Ganzzahl sein, die kleiner oder gleich 1.000.000.000 ist. Höhere Werte sind für systemkritische Pods reserviert, die in der Regel nicht verdrängt werden können. Die Verdrängung erfolgt nur, wenn der hochpriorisierte Pod keinen Platz zum Bereitstellen hat; dann werden einige Pods von einem bestimmten Knoten evakuiert. Wenn Ihnen dieser Mechanismus zu streng ist, können Sie die Option preemptionPolicy: Never, hinzufügen, und dann wird es keine Verdrängung geben, der Pod wird an erster Stelle in der Warteschlange stehen und warten, bis der Scheduler freie Ressourcen für ihn findet.
Dann erstellen wir einen Pod, in dem wir den Namen angeben priorityClassName:
apiVersion: v1
kind: Pod
metadata:
name: static-web
labels:
role: myrole
spec:
containers:
- name: web
image: nginx
ports:
- name: web
containerPort: 80
protocol: TCP
priorityClassName: high-priority
Es können beliebig viele Prioritätsklassen erstellt werden, obwohl empfohlen wird, sich dafür nicht zu sehr zu begeistern (zum Beispiel auf niedrig, mittel und hoch zu beschränken).
So können Sie im Bedarfsfall die Effizienz der Bereitstellung kritischer Dienste wie nginx-ingress-controller, coredns usw. steigern.
9. Optimieren Sie den ETCD-Cluster

ETCD kann als das Gehirn des gesamten Clusters bezeichnet werden. Es ist sehr wichtig, dass diese Datenbank auf hohem Niveau funktioniert, da die Geschwindigkeit der Operationen im 'Kube' von ihr abhängt. Eine durchaus gängige und gleichzeitig gute Lösung ist es, den ETCD-Cluster auf Master-Nodes zu halten, um eine minimale Verzögerung zum kube-apiserver zu haben. Wenn das nicht möglich ist, sollten Sie ETCD so nah wie möglich platzieren, wobei eine gute Bandbreite zwischen den Teilnehmern gewährleistet sein sollte. Achten Sie auch darauf, wie viele Nodes aus ETCD ausfallen können, ohne dem Cluster zu schaden.

Bedenken Sie, dass eine übermäßige Anzahl von Teilnehmern im Cluster die Ausfallsicherheit auf Kosten der Leistung erhöhen kann, alles sollte in Maßen geschehen.
Wenn es um die Konfiguration des Dienstes geht, sind die Empfehlungen spärlich:
Haben Sie gute Hardware, abhängig von der Größe des Clusters (Sie können lesen ).
Passen Sie einige Parameter an, wenn Sie den Cluster über mehrere Rechenzentren verteilt haben oder Ihr Netzwerk und die Festplatten verbesserungswürdig sind (Sie können lesen ).
Fazit
In diesem Artikel werden Punkte beschrieben, die unser Team zu befolgen versucht. Dies ist kein schrittweiser Aktionsplan, sondern Optionen, die zur Optimierung der Betriebskosten des Clusters nützlich sein können. Klar ist, dass jeder Cluster einzigartig ist und die Anpassungslösungen stark variieren können. Daher wäre es interessant, Ihr Feedback zu erhalten: Wie überwachen Sie Ihren Kubernetes-Cluster, mit welchen Mitteln optimieren Sie seine Leistung? Teilen Sie Ihre Erfahrungen in den Kommentaren, es wird interessant sein, diese zu erfahren.
Quelle: habr.com
