Ursprünglich auf meinem Blog gepostet
Standardmäßig verarbeiten Operatoren, die mit Kubebuilder und Controller-Runtime erstellt wurden, jeweils eine einzelne Abgleichsanforderung. Dies ist eine sinnvolle Einstellung, da es für Betreiberentwickler einfacher ist, über die Logik in ihren Anwendungen nachzudenken und diese zu debuggen. Es schränkt auch den Durchsatz vom Controller zu den Kubernetes-Kernressourcen wie ectd und dem API-Server ein.
Aber was passiert, wenn sich Ihre Arbeitswarteschlange staut und sich die durchschnittlichen Abstimmungszeiten verlängern, weil Anfragen in der Warteschlange stehen bleiben und auf ihre Bearbeitung warten? Zum Glück für uns enthält eine Controller-Laufzeit-Controller-Struktur ein MaxConcurrentReconciles-Feld (wie ich bereits in meinem Kubebuilder-Tipps-Artikel erwähnt habe). Mit dieser Option können Sie die Anzahl gleichzeitiger Abgleichsschleifen festlegen, die in einem einzelnen Controller ausgeführt werden. Mit einem Wert über 1 können Sie also mehrere Kubernetes-Ressourcen gleichzeitig abgleichen.
Zu Beginn meiner Operator-Reise hatte ich eine Frage: Wie können wir garantieren, dass dieselbe Ressource nicht gleichzeitig von zwei oder mehr Goroutinen abgeglichen wird? Wenn MaxConcurrentReconciles auf über 1 gesetzt ist, könnte dies zu allen möglichen Race-Conditions und unerwünschtem Verhalten führen, da sich der Zustand eines Objekts innerhalb einer Abgleichsschleife durch einen Nebeneffekt einer externen Quelle (eine Abgleichsschleife, die in einem anderen Thread läuft) ändern könnte. .
Ich habe eine Weile darüber nachgedacht und sogar einen sync.Map-basierten Ansatz implementiert, der es einer Goroutine ermöglichen würde, eine Sperre für eine bestimmte Ressource zu erhalten (basierend auf ihrem Namespace/Namen).
Es stellte sich heraus, dass all dieser Aufwand umsonst war, da ich kürzlich (in einem k8s-Slack-Kanal) erfahren habe, dass die Controller-Workqueue diese Funktion bereits enthält! Allerdings mit einer einfacheren Implementierung.
Dies ist eine kurze Geschichte darüber, wie die Arbeitswarteschlange eines K8S-Controllers garantiert, dass eindeutige Ressourcen nacheinander abgeglichen werden. Selbst wenn MaxConcurrentReconciles auf über 1 eingestellt ist, können Sie sicher sein, dass jeweils nur eine einzige Abstimmungsfunktion auf eine bestimmte Ressource wirkt.
Controller-Runtime verwendet die Bibliothek client-go/util/workqueue, um die zugrunde liegende Abstimmungswarteschlange zu implementieren. In der doc.go-Datei des Pakets heißt es in einem Kommentar, dass die Arbeitswarteschlange diese Eigenschaften unterstützt:
Moment mal... Meine Antwort ist genau hier im zweiten Aufzählungspunkt, der „Geizigen“-Eigenschaft! Laut diesen Dokumenten wird die Warteschlange dieses Parallelitätsproblem automatisch für mich lösen, ohne dass ich eine einzige Codezeile schreiben muss. Lassen Sie uns die Implementierung durchgehen.
Die Arbeitswarteschlangenstruktur verfügt über drei Hauptmethoden: Hinzufügen, Abrufen und Fertig. Innerhalb eines Controllers würde ein Informer der Arbeitswarteschlange Abstimmungsanforderungen (Namespace-Namen generischer k8s-Ressourcen) hinzufügen. Eine in einer separaten Goroutine ausgeführte Abgleichsschleife würde dann die nächste Anforderung aus der Warteschlange abrufen (blockieren, wenn sie leer ist). Die Schleife würde die benutzerdefinierte Logik ausführen, die in den Controller geschrieben ist, und dann würde der Controller „Done“ in der Warteschlange aufrufen und die Abgleichsanforderung als Argument übergeben. Dadurch würde der Prozess erneut beginnen und die Abgleichsschleife würde Get aufrufen, um das nächste Arbeitselement abzurufen.
Dies ähnelt der Verarbeitung von Nachrichten in RabbitMQ, wo ein Mitarbeiter ein Element aus der Warteschlange entfernt, es verarbeitet und dann eine „Bestätigung“ an den Nachrichtenbroker zurücksendet, die angibt, dass die Verarbeitung abgeschlossen ist und das Element sicher aus der Warteschlange entfernt werden kann die Warteschlange.
Dennoch habe ich einen Operator in der Produktion, der die Infrastruktur von QuestDB Cloud betreibt, und wollte sicherstellen, dass die Arbeitswarteschlange wie angekündigt funktioniert. Also hat a einen Schnelltest geschrieben, um sein Verhalten zu validieren.
Hier ist ein einfacher Test, der die Eigenschaft „Geizig“ validiert:
package main_test import ( "testing" "github.com/stretchr/testify/assert" "k8s.io/client-go/util/workqueue" ) func TestWorkqueueStingyProperty(t *testing.T) { type Request int // Create a new workqueue and add a request wq := workqueue.New() wq.Add(Request(1)) assert.Equal(t, wq.Len(), 1) // Subsequent adds of an identical object // should still result in a single queued one wq.Add(Request(1)) wq.Add(Request(1)) assert.Equal(t, wq.Len(), 1) // Getting the object should remove it from the queue // At this point, the controller is processing the request obj, _ := wq.Get() req := obj.(Request) assert.Equal(t, wq.Len(), 0) // But re-adding an identical request before it is marked as "Done" // should be a no-op, since we don't want to process it simultaneously // with the first one wq.Add(Request(1)) assert.Equal(t, wq.Len(), 0) // Once the original request is marked as Done, the second // instance of the object will be now available for processing wq.Done(req) assert.Equal(t, wq.Len(), 1) // And since it is available for processing, it will be // returned by a Get call wq.Get() assert.Equal(t, wq.Len(), 0) }
Da die Arbeitswarteschlange unter der Haube einen Mutex verwendet, ist dieses Verhalten threadsicher. Selbst wenn ich also mehr Tests schreiben würde, bei denen mehrere Goroutinen gleichzeitig mit hoher Geschwindigkeit aus der Warteschlange lesen und schreiben würden, um sie zu durchbrechen, wäre das tatsächliche Verhalten der Arbeitswarteschlange dasselbe wie das unseres Single-Threaded-Tests.
In den Kubernetes-Standardbibliotheken verstecken sich viele kleine Juwelen wie dieses, von denen sich einige an nicht so offensichtlichen Orten befinden (wie eine Controller-Laufzeit-Arbeitswarteschlange im Go-Client-Paket). Trotz dieser und ähnlicher Entdeckungen, die ich in der Vergangenheit gemacht habe, habe ich immer noch das Gefühl, dass meine früheren Versuche, diese Probleme zu lösen, keine völlige Zeitverschwendung sind. Sie zwingen Sie dazu, kritisch über grundlegende Probleme beim Rechnen mit verteilten Systemen nachzudenken, und helfen Ihnen, besser zu verstehen, was unter der Haube vor sich geht. Wenn ich also herausgefunden habe, dass „Kubernetes es geschafft hat“, bin ich erleichtert, dass ich meine Codebasis vereinfachen und vielleicht einige unnötige Komponententests entfernen kann.
Das obige ist der detaillierte Inhalt vonWie gehen Kubernetes-Operatoren mit Parallelität um?. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!