Expert Chapitre 29-30 / 16

Leader election : une seule boucle aux commandes & Concurrence et rate limiting

Comprendre le mécanisme de leader election qui garantit qu'un seul réconciliateur est actif, puis maîtriser la concurrence et le rate limiting pour protéger l'API server et le cluster.

Leader election : une seule boucle aux commandes

L’idée en une phrase

La leader election est le mécanisme par lequel plusieurs répliques d’un même contrôleur désignent un seul leader actif qui exécute la boucle de réconciliation, pendant que les autres attendent en standby — garantissant qu’un unique acteur observe l’écart entre état désiré et état réel et agit pour le combler à un instant donné.

Analogie : Considérons une tour de contrôle aérien : plusieurs contrôleurs sont qualifiés, mais un seul parle aux pilotes à la fois. Les autres surveillent la fréquence et sont prêts à reprendre en quelques secondes si le contrôleur actif devient indisponible. Si deux contrôleurs donnaient des instructions simultanément, les avions recevraient des ordres contradictoires. Le leader election dans Kubernetes joue exactement ce rôle : une seule boucle aux commandes, les autres en veille active.

Points clés

  • En production, un Operator ou un contrôleur est déployé avec plusieurs répliques pour assurer la haute disponibilité. Sans leader election, chaque réplique exécuterait sa propre boucle de réconciliation, provoquant des actions concurrentes et potentiellement contradictoires sur les mêmes ressources.
  • Le mécanisme repose sur un objet de coordination dans le cluster — historiquement un ConfigMap ou un Endpoints, aujourd’hui de préférence un objet Lease (groupe coordination.k8s.io/v1). Le leader acquiert un bail en écrivant son identité dans cet objet ; les autres candidats vérifient périodiquement si le bail est encore valide.
  • Trois paramètres gouvernent le cycle : la lease duration (durée du bail, typiquement 15 s), le renew deadline (délai maximal pour renouveler, typiquement 10 s), et le retry period (intervalle entre les tentatives des candidats, typiquement 2 s). Si le leader ne renouvelle pas avant l’expiration, un candidat prend le relais.
  • Le leader election est elle-même une forme de réconciliation : l’état désiré est « exactement un leader actif », l’état réel est la valeur du Lease dans etcd, et chaque réplique observe → compare → tente d’agir (acquérir ou renouveler le bail).
  • Les contrôleurs natifs de Kubernetes (kube-controller-manager, kube-scheduler) utilisent ce même mécanisme. Lorsque le plan de contrôle comporte trois nœuds, un seul kube-controller-manager est leader à tout instant.

Exemple concret

Un Operator CacheOperator est déployé avec 3 répliques dans le namespace cache-system. Au démarrage, les trois répliques tentent de créer ou acquérir le Lease cache-operator-leader dans le même namespace. La réplique cache-operator-pod-0 acquiert le bail en première — elle inscrit son identité (holderIdentity: cache-operator-pod-0) et le timestamp d’acquisition. Les répliques pod-1 et pod-2 lisent le Lease, constatent qu’il est détenu par pod-0 et que le bail n’a pas expiré : elles se mettent en attente. Toutes les 10 secondes, pod-0 renouvelle le bail. À t₁, pod-0 est tué par un OOMKill. Après 15 secondes (lease duration) sans renouvellement, pod-1 détecte l’expiration et acquiert le Lease. Elle devient le nouveau leader et commence à réconcilier. La transition prend au maximum leaseDuration + retryPeriod — ici 17 secondes. Pendant ce délai, aucune réconciliation ne s’exécute, mais aucune action contradictoire ne survient non plus : la cohérence prime sur la latence.

Mécanismes de coordination pour le leader election

MécanismeObjet utiliséAvantageInconvénient
Lease (recommandé)coordination.k8s.io/v1/LeaseConçu pour cet usage, léger, pas de surcharge sémantiqueNécessite Kubernetes 1.14+
ConfigMapv1/ConfigMapDisponible partout, simpleDétournement d’un objet prévu pour la configuration
Endpointsv1/EndpointsHistoriquement utilisé par les composants du plan de contrôleRisque de collision avec un Service réel, déprécié pour cet usage

Code YAML — inspecter un Lease de leader election

# Exemple de Lease acquis par le leader — observé via kubectl get lease
apiVersion: coordination.k8s.io/v1
kind: Lease
metadata:
  name: cache-operator-leader
  namespace: cache-system
spec:
  holderIdentity: cache-operator-pod-0    # le leader actuel
  leaseDurationSeconds: 15                # durée du bail
  acquireTime: "2026-07-04T08:00:00Z"     # timestamp d'acquisition
  renewTime: "2026-07-04T08:00:30Z"       # dernier renouvellement
  leaseTransitions: 2                     # nombre de changements de leader

Code kubectl — observer le leader election en action

# Lister les Leases dans le namespace du contrôleur
kubectl get lease -n cache-system
# NAME                      HOLDER                    AGE
# cache-operator-leader     cache-operator-pod-0      12m

# Observer le détail du Lease (holderIdentity, timestamps)
kubectl describe lease cache-operator-leader -n cache-system
# Holder Identity:    cache-operator-pod-0
# Lease Duration:     15s
# Renew Time:         2026-07-04T08:00:30Z
# Lease Transitions:  2

# Simuler une perte du leader en supprimant le pod
kubectl delete pod cache-operator-pod-0 -n cache-system
# pod "cache-operator-pod-0" deleted

# Après ~17 secondes, vérifier le nouveau leader
kubectl get lease cache-operator-leader -n cache-system -o jsonpath='{.spec.holderIdentity}'
# cache-operator-pod-1

Code C# — activer le leader election dans un Operator KubeOps

// Program.cs — configuration du leader election dans un Operator KubeOps.
// Une seule réplique exécute la boucle de réconciliation à la fois.
var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddKubernetesOperator(options =>
    {
        // Activer le leader election — indispensable en déploiement multi-répliques.
        // Sans cette option, chaque réplique réconcilie indépendamment.
        options.EnableLeaderElection = true;

        // Nom du Lease utilisé pour la coordination.
        // Chaque Operator doit avoir un nom unique dans son namespace.
        options.LeaderElectionLeaseName = "cache-operator-leader";

        // Namespace du Lease (celui de l'Operator).
        options.LeaderElectionLeaseNamespace = "cache-system";
    })
    .AddController<CacheController>();

var app = builder.Build();
app.Run();

Piège courant : « Mon Operator est déployé avec replicas: 1, donc le leader election est inutile » est inexact. Lors d’un rolling update du Deployment de l’Operator, l’ancienne et la nouvelle réplique coexistent brièvement. Sans leader election, les deux exécutent la boucle simultanément pendant la transition. Le leader election garantit la sécurité même dans ce scénario transitoire.


Concurrence et rate limiting

L’idée en une phrase

Le rate limiting et le contrôle de la concurrence sont les mécanismes qui régulent le débit de la boucle de réconciliation — combien d’objets sont réconciliés simultanément et à quelle vitesse les requêtes sont réémises — pour éviter de submerger l’API server et garantir une convergence stable entre état désiré et état réel.

Analogie : Considérons un barrage hydraulique en amont d’une vallée. L’objectif (état désiré) est d’alimenter les cultures en contrebas avec un débit constant. Si les vannes s’ouvrent toutes en même temps après un orage, le torrent inonde la vallée au lieu de l’irriguer. Le barrage régule le débit : il ouvre quelques vannes à la fois (concurrence) et ralentit le flux quand le niveau monte trop vite (rate limiting). Le contrôleur Kubernetes est ce barrage entre la work queue et l’API server.

Points clés

  • Un contrôleur traite sa work queue (chapitre 15-16) avec un nombre configurable de workers concurrents. Chaque worker dépile une clé, exécute Reconcile, puis passe à la suivante. La valeur par défaut est souvent 1 (un seul objet réconcilié à la fois) ; les contrôleurs qui gèrent des milliers d’objets l’augmentent (le Deployment controller de Kubernetes utilise 5 workers).
  • Le rate limiter contrôle la vitesse à laquelle les éléments sont réémis dans la queue après un échec ou un requeue explicite. Sans rate limiter, un objet en erreur serait retraité immédiatement en boucle, saturant le contrôleur et l’API server.
  • L’algorithme standard combine deux stratégies : un backoff exponentiel par élément (la durée double à chaque échec, typiquement de 5 ms à 1 000 s) et un rate limiter global de type token bucket (par exemple 10 requêtes/s avec un burst de 100). L’élément est retardé par le maximum des deux délais.
  • Augmenter la concurrence accélère la convergence (plus d’objets réconciliés par seconde) mais augmente la charge sur l’API server. Le réglage optimal dépend du nombre d’objets gérés, de la complexité de chaque réconciliation et de la capacité de l’API server.
  • Le rate limiting est un mécanisme de protection mutuelle : il protège l’API server contre la surcharge, et il protège le contrôleur contre l’emballement sur un objet défaillant (qui, sans backoff, consommerait toutes les ressources de la boucle).

Exemple concret

Un Operator gère 500 CR Database. Sa work queue reçoit un événement pour chacune lors d’un resync périodique (chapitre 15-16). Avec un seul worker, les 500 réconciliations s’exécutent séquentiellement — la dernière attend que les 499 précédentes aient terminé. Si chaque réconciliation prend 200 ms, la convergence complète prend 100 secondes. En configurant 5 workers concurrents, cinq réconciliations s’exécutent en parallèle et le temps total tombe à environ 20 secondes. Par ailleurs, la CR database-42 entre dans un état d’erreur (le serveur PostgreSQL cible ne répond pas). Le réconciliateur requeue l’élément avec un backoff exponentiel : premier requeue à 5 ms, puis 10 ms, 20 ms, 40 ms… jusqu’à plafonner à 1 000 s (~16 minutes). Pendant ce temps, les 499 autres CR continuent de se réconcilier normalement — l’élément défaillant ne bloque pas la queue.

Concurrence et rate limiting — paramètres clés

ParamètreValeur typiqueEffetRisque si trop élevé
MaxConcurrentReconciles1 à 10Nombre de workers parallèles par contrôleurSurcharge de l’API server, conflits d’écriture
Base delay (backoff)5 msDélai initial après un premier échec(rarement trop élevé)
Max delay (backoff)1 000 sPlafond du backoff exponentielConvergence trop lente sur erreurs transitoires
Bucket rate (global)10 req/sDébit global maximal de la queueSurcharge de l’API server
Bucket size (burst)100Nombre de requêtes autorisées en rafalePic de charge instantané

Code YAML — configuration d’un controller-manager

# controller-config.yaml — ConfigMap référencée par le Deployment de l'Operator
apiVersion: v1
kind: ConfigMap
metadata:
  name: operator-config
  namespace: database-system
data:
  # Nombre de workers concurrents pour le contrôleur Database
  MAX_CONCURRENT_RECONCILES: "5"
  # Backoff exponentiel : délai initial et plafond
  RATE_LIMIT_BASE_DELAY_MS: "5"
  RATE_LIMIT_MAX_DELAY_MS: "300000"

Code kubectl — diagnostiquer la concurrence et le rate limiting

# Observer le nombre de workers en cours via les métriques du contrôleur
# (nécessite un endpoint /metrics exposé par l'Operator)
kubectl port-forward -n database-system svc/operator-metrics 8080:8080
# curl localhost:8080/metrics | grep workqueue

# Métriques clés de la work queue (format Prometheus) :
# workqueue_depth — nombre d'éléments en attente dans la queue
# workqueue_adds_total — nombre total d'ajouts
# workqueue_retries_total — nombre de requeues (indicateur de rate limiting actif)
# workqueue_queue_duration_seconds — temps d'attente avant traitement

# Identifier un objet en boucle d'erreur (retries élevés)
kubectl get events -n database-system --field-selector reason=ReconcileError --sort-by='.lastTimestamp'
# LAST SEEN   TYPE      REASON           OBJECT              MESSAGE
# 2s          Warning   ReconcileError   database/db-42      connection refused (attempt 8, next retry in 1280ms)

Code C# — configurer concurrence et rate limiting en KubeOps

// Configuration du contrôleur avec concurrence et rate limiting.
// Le rate limiter protège l'API server et évite l'emballement sur erreur.
builder.Services
    .AddKubernetesOperator()
    .AddController<DatabaseController>(options =>
    {
        // 5 workers réconcilent en parallèle —
        // adapté à un parc de ~500 CR avec réconciliation rapide.
        options.MaxConcurrentReconciles = 5;
    });

// Le pattern de requeue avec backoff dans le reconciler.
public class DatabaseController : IEntityController<V1Database>
{
    private static readonly TimeSpan BaseDelay = TimeSpan.FromMilliseconds(500);
    private static readonly TimeSpan MaxDelay = TimeSpan.FromMinutes(5);

    public async Task ReconcileAsync(V1Database entity, CancellationToken token)
    {
        try
        {
            // 1. Observer l'état réel de la base de données cible
            var status = await CheckDatabaseHealth(entity, token);

            // 2. Comparer avec l'état désiré (spec)
            if (status.NeedsReconciliation(entity.Spec))
            {
                // 3. Agir pour combler l'écart
                await ApplyDesiredState(entity, token);
            }

            // Réconciliation réussie — réinitialiser le compteur d'erreurs.
            entity.Status.ConsecutiveErrors = 0;
            entity.Status.LastReconciled = DateTime.UtcNow;
        }
        catch (DatabaseConnectionException ex)
        {
            // Incrémenter le compteur pour le backoff exponentiel.
            entity.Status.ConsecutiveErrors++;

            // Calculer le délai avec backoff : base × 2^(erreurs-1), plafonné.
            var delay = TimeSpan.FromMilliseconds(
                Math.Min(
                    BaseDelay.TotalMilliseconds
                        * Math.Pow(2, entity.Status.ConsecutiveErrors - 1),
                    MaxDelay.TotalMilliseconds));

            // Requeue avec le délai calculé — la work queue
            // replanifie la réconciliation après ce délai.
            throw new RequeueException(delay, ex.Message);
        }
    }
}

Piège courant : « Augmenter MaxConcurrentReconciles à 50 accélérera la convergence » est une simplification dangereuse. Au-delà d’un certain seuil, les workers concurrents saturent l’API server en requêtes GET/UPDATE, provoquent des erreurs 429 Too Many Requests ou des timeouts, et déclenchent en cascade des requeues qui engorgent la work queue. L’effet est inverse de celui recherché : la convergence ralentit. Le réglage se fait par mesure — augmenter progressivement en surveillant les métriques workqueue_depth et apiserver_request_duration_seconds.


Quiz — validation des acquis
Expert 7 questions Objectif : 5/7 minimum
0/7
bonnes reponses
Objectif non atteint (minimum 5/7 requis).
Relire la fiche memo ci-dessus en pretant attention aux points manques, puis cliquer sur « Recommencer » pour retenter.