Publié à l'origine sur mon blog
Par défaut, les opérateurs créés à l'aide de Kubebuilder et du contrôleur d'exécution traitent une seule demande de réconciliation à la fois. Il s'agit d'un paramètre judicieux, car il est plus facile pour les développeurs d'opérateurs de raisonner et de déboguer la logique de leurs applications. Cela limite également le débit du contrôleur vers les ressources principales de Kubernetes telles que ectd et le serveur API.
Mais que se passe-t-il si votre file d'attente de travail commence à être sauvegardée et que les délais moyens de rapprochement augmentent en raison de demandes laissées dans la file d'attente, en attente d'être traitées ? Heureusement pour nous, une structure Controller d'exécution de contrôleur inclut un champ MaxConcurrentReconciles (comme je l'ai mentionné précédemment dans mon article Kubebuilder Tips). Cette option vous permet de définir le nombre de boucles de réconciliation simultanées exécutées dans un seul contrôleur. Ainsi, avec une valeur supérieure à 1, vous pouvez réconcilier plusieurs ressources Kubernetes simultanément.
Au début de mon parcours d'opérateur, une question que je me posais était de savoir comment pouvons-nous garantir que la même ressource n'est pas rapprochée en même temps par 2 goroutines ou plus ? Avec MaxConcurrentReconciles défini au-dessus de 1, cela pourrait conduire à toutes sortes de conditions de concurrence et de comportements indésirables, car l'état d'un objet à l'intérieur d'une boucle de réconciliation pourrait changer via un effet secondaire provenant d'une source externe (une boucle de réconciliation s'exécutant dans un thread différent). .
J'y ai réfléchi pendant un moment et j'ai même implémenté une approche basée sur sync.Map qui permettrait à une goroutine d'acquérir un verrou pour une ressource donnée (en fonction de son espace de noms/nom).
Il s'avère que tous ces efforts ont été vains, puisque j'ai récemment appris (dans un canal Slack de K8s) que la file d'attente du contrôleur inclut déjà cette fonctionnalité ! Mais avec une mise en œuvre plus simple.
Voici une brève histoire sur la façon dont la file d'attente de travail d'un contrôleur K8s garantit que les ressources uniques sont réconciliées séquentiellement. Ainsi, même si MaxConcurrentReconciles est défini au-dessus de 1, vous pouvez être sûr qu'une seule fonction de réconciliation agit sur une ressource donnée à la fois.
Controller-runtime utilise la bibliothèque client-go/util/workqueue pour implémenter sa file d'attente de réconciliation sous-jacente. Dans le fichier doc.go du package, un commentaire indique que la file d'attente de travail prend en charge ces propriétés :
Attendez une seconde... Ma réponse est ici, dans le deuxième point, la propriété "Stingy" ! Selon ces documents, la file d'attente gérera automatiquement ce problème de concurrence pour moi, sans avoir à écrire une seule ligne de code. Passons en revue la mise en œuvre.
La structure workqueue comporte 3 méthodes principales : Add, Get et Done. À l'intérieur d'un contrôleur, un informateur ajouterait des demandes de réconciliation (noms d'espace de noms des ressources k8s génériques) à la file d'attente de travail. Une boucle de réconciliation exécutée dans une goroutine distincte obtiendrait alors la requête suivante de la file d'attente (bloquante si elle est vide). La boucle exécuterait toute la logique personnalisée écrite dans le contrôleur, puis le contrôleur appellerait Done dans la file d'attente, en transmettant la demande de réconciliation comme argument. Cela recommencerait le processus et la boucle de réconciliation appellerait Get pour récupérer l'élément de travail suivant.
Cela est similaire au traitement des messages dans RabbitMQ, où un travailleur retire un élément de la file d'attente, le traite, puis renvoie un « Ack » au courtier de messages indiquant que le traitement est terminé et qu'il est possible de supprimer l'élément en toute sécurité. la file d'attente.
Pourtant, j'ai un opérateur en production qui alimente l'infrastructure de QuestDB Cloud, et je voulais être sûr que la file d'attente de travail fonctionne comme annoncé. J'ai donc écrit un test rapide pour valider son comportement.
Voici un test simple qui valide la propriété "Stingy" :
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) }
Étant donné que la file d'attente utilise un mutex sous le capot, ce comportement est threadsafe. Ainsi, même si j'écrivais plus de tests utilisant plusieurs goroutines lisant et écrivant simultanément à partir de la file d'attente à grande vitesse pour tenter de la briser, le comportement réel de la file d'attente serait le même que celui de notre test à thread unique.
Il y a beaucoup de petits joyaux comme celui-ci cachés dans les bibliothèques standard de Kubernetes, dont certains se trouvent dans des endroits pas si évidents (comme une file d'attente d'exécution du contrôleur trouvée dans le package client go). Malgré cette découverte, et d'autres similaires que j'ai faites dans le passé, j'ai toujours le sentiment que mes tentatives précédentes pour résoudre ces problèmes ne sont pas une perte de temps totale. Ils vous obligent à réfléchir de manière critique aux problèmes fondamentaux de l’informatique des systèmes distribués et vous aident à mieux comprendre ce qui se passe sous le capot. Ainsi, au moment où j'ai découvert que "Kubernetes l'a fait", je suis soulagé de pouvoir simplifier ma base de code et peut-être supprimer certains tests unitaires inutiles.
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!