Home  >  Article  >  Backend Development  >  How do Kubernetes Operators Handle Concurrency?

How do Kubernetes Operators Handle Concurrency?

Barbara Streisand
Barbara StreisandOriginal
2024-10-10 06:07:29281browse

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.

client-go/util

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 prend en charge ces propriétés :

  • Équitable : éléments traités dans l'ordre dans lequel ils sont ajoutés.
  • Avare : un seul élément ne sera pas traité plusieurs fois simultanément, et si un élément est ajouté plusieurs fois avant de pouvoir être traité, il ne sera traité qu'une seule fois.
  • Plusieurs consommateurs et producteurs. En particulier, il est permis qu'un élément soit remis en file d'attente pendant son traitement.
  • Notifications d'arrêt.

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.

Comment fonctionne la file d'attente ?

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.

Un petit test

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 rompre, le comportement réel de la file d'attente serait le même que celui de notre test monothread.

Tout n'est pas perdu

How do Kubernetes Operators Handle Concurrency?

There are a lot of little gems like this hiding in the Kubernetes standard libraries, some of which are in not-so-obvious places (like a controller-runtime workqueue found in the go client package). Despite this discovery, and others like it that I've made in the past, I still feel that my previous attempts at solving these issues are not complete time-wasters. They force you to think critically about fundamental problems in distributed systems computing, and help you to understand more of what is going on under the hood. So that by the time I've discovered that "Kubernetes did it", I'm relieved that I can simplify my codebase and perhaps remove some unnecessary unit tests.

The above is the detailed content of How do Kubernetes Operators Handle Concurrency?. For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn