Maison  >  Article  >  développement back-end  >  Comment ajouter l'élection des dirigeants alimentée par Kubernetes à vos applications Go

Comment ajouter l'élection des dirigeants alimentée par Kubernetes à vos applications Go

WBOY
WBOYoriginal
2024-07-20 09:15:39908parcourir

How to add Kubernetes-powered leader election to your Go apps

Publié à l'origine sur par blog

La bibliothèque standard de Kubernetes regorge de joyaux, cachés dans de nombreux sous-paquets qui font partie de l'écosystème. Un exemple que j'ai découvert récemment k8s.io/client-go/tools/leaderelection, qui peut être utilisé pour ajouter un protocole d'élection de leader à toute application exécutée dans un cluster Kubernetes. Cet article expliquera ce qu'est l'élection de leader, comment elle est implémentée dans ce package Kubernetes et fournira un exemple de la façon dont nous pouvons utiliser cette bibliothèque dans nos propres applications.

Élection du chef

L'élection du leader est un concept de systèmes distribués qui constitue un élément essentiel des logiciels hautement disponibles. Il permet à plusieurs processus simultanés de se coordonner et d'élire un seul processus « leader », qui est ensuite chargé d'effectuer des actions synchrones comme l'écriture dans un magasin de données.

Ceci est utile dans les systèmes tels que les bases de données distribuées ou les caches, où plusieurs processus sont en cours d'exécution pour créer une redondance contre les pannes matérielles ou réseau, mais ne peuvent pas écrire simultanément sur le stockage pour garantir la cohérence des données. Si le processus de leadership ne répond plus à un moment donné dans le futur, les processus restants lanceront une nouvelle élection de leader, choisissant finalement un nouveau processus pour agir en tant que leader.

En utilisant ce concept, nous pouvons créer un logiciel hautement disponible avec un seul leader et plusieurs répliques de secours.

Dans Kubernetes, le package d'exécution du contrôleur utilise l'élection du leader pour rendre les contrôleurs hautement disponibles. Dans un déploiement de contrôleur, la réconciliation des ressources se produit uniquement lorsqu'un processus est le leader et que d'autres réplicas sont en attente en attente. Si le pod leader ne répond plus, les répliques restantes éliront un nouveau leader pour effectuer les rapprochements ultérieurs et reprendre le fonctionnement normal.

Baux Kubernetes

Cette bibliothèque utilise un bail Kubernetes, ou verrou distribué, qui peut être obtenu par un processus. Les baux sont des ressources Kubernetes natives détenues par une seule identité, pour une durée donnée, avec une option de renouvellement. Voici un exemple de spécification tiré de la documentation :

apiVersion: coordination.k8s.io/v1
kind: Lease
metadata:
  labels:
    apiserver.kubernetes.io/identity: kube-apiserver
    kubernetes.io/hostname: master-1
  name: apiserver-07a5ea9b9b072c4a5f3d1c3702
  namespace: kube-system
spec:
  holderIdentity: apiserver-07a5ea9b9b072c4a5f3d1c3702_0c8914f7-0f35-440e-8676-7844977d3a05
  leaseDurationSeconds: 3600
  renewTime: "2023-07-04T21:58:48.065888Z"

Les baux sont utilisés par l'écosystème k8s de trois manières :

  1. Node Heartbeats : chaque nœud possède une ressource de location correspondante et met à jour son champ renouvelerTime de manière continue. Si la durée de renouvellement d'un bail n'a pas été mise à jour depuis un certain temps, le nœud sera considéré comme indisponible et aucun autre pod ne lui sera programmé.
  2. Élection du leader : dans ce cas, un bail est utilisé pour coordonner plusieurs processus en demandant à un leader de mettre à jour l'identité du titulaire du bail. Les répliques de secours, avec des identités différentes, sont bloquées en attendant l'expiration du bail. Si le bail expire et n'est pas renouvelé par le leader, une nouvelle élection a lieu au cours de laquelle les répliques restantes tentent de prendre possession du bail en mettant à jour son identité de détenteur avec la leur. Étant donné que le serveur API Kubernetes interdit les mises à jour des objets obsolètes, un seul nœud de secours pourra mettre à jour le bail avec succès, auquel cas il poursuivra son exécution en tant que nouveau leader.
  3. Identité du serveur API : à partir de la v1.26, en tant que fonctionnalité bêta, chaque réplique kube-apiserver publiera son identité en créant un bail dédié. Puisqu'il s'agit d'une nouvelle fonctionnalité relativement mince, il n'y a pas grand-chose d'autre qui puisse être dérivé de l'objet Lease en dehors du nombre de serveurs API en cours d'exécution. Mais cela laisse la possibilité d'ajouter plus de métadonnées à ces baux dans les futures versions de k8.

Explorons maintenant ce deuxième cas d'utilisation des baux en écrivant un exemple de programme pour démontrer comment vous pouvez les utiliser dans des scénarios d'élection de dirigeants.

Exemple de programme

Dans cet exemple de code, nous utilisons le package leaderelection pour gérer les détails de l'élection du leader et de la manipulation du bail.

package main

import (
    "context"
    "fmt"
    "os"
    "time"

    "k8s.io/client-go/tools/leaderelection"
    rl "k8s.io/client-go/tools/leaderelection/resourcelock"
    ctrl "sigs.k8s.io/controller-runtime"
)

var (
    // lockName and lockNamespace need to be shared across all running instances
    lockName      = "my-lock"
    lockNamespace = "default"

    // identity is unique to the individual process. This will not work for anything,
    // outside of a toy example, since processes running in different containers or
    // computers can share the same pid.
    identity      = fmt.Sprintf("%d", os.Getpid())
)

func main() {
    // Get the active kubernetes context
    cfg, err := ctrl.GetConfig()
    if err != nil {
        panic(err.Error())
    }

    // Create a new lock. This will be used to create a Lease resource in the cluster.
    l, err := rl.NewFromKubeconfig(
        rl.LeasesResourceLock,
        lockNamespace,
        lockName,
        rl.ResourceLockConfig{
            Identity: identity,
        },
        cfg,
        time.Second*10,
    )
    if err != nil {
        panic(err)
    }

    // Create a new leader election configuration with a 15 second lease duration.
    // Visit https://pkg.go.dev/k8s.io/client-go/tools/leaderelection#LeaderElectionConfig
    // for more information on the LeaderElectionConfig struct fields
    el, err := leaderelection.NewLeaderElector(leaderelection.LeaderElectionConfig{
        Lock:          l,
        LeaseDuration: time.Second * 15,
        RenewDeadline: time.Second * 10,
        RetryPeriod:   time.Second * 2,
        Name:          lockName,
        Callbacks: leaderelection.LeaderCallbacks{
            OnStartedLeading: func(ctx context.Context) { println("I am the leader!") },
            OnStoppedLeading: func() { println("I am not the leader anymore!") },
            OnNewLeader:      func(identity string) { fmt.Printf("the leader is %s\n", identity) },
        },
    })
    if err != nil {
        panic(err)
    }

    // Begin the leader election process. This will block.
    el.Run(context.Background())

}

Ce qui est bien avec le package leaderelection, c'est qu'il fournit un cadre basé sur le rappel pour gérer les élections des dirigeants. De cette façon, vous pouvez agir de manière granulaire sur des changements d’état spécifiques et libérer correctement des ressources lorsqu’un nouveau leader est élu. En exécutant ces rappels dans des goroutines distinctes, le package profite de la forte prise en charge de la concurrence de Go pour utiliser efficacement les ressources de la machine.

Le tester

Pour tester cela, développons un cluster de test en utilisant kind.

$ kind create cluster

Copiez l'exemple de code dans main.go, créez un nouveau module (go mod init leaderelectiontest) et rangez-le (go mod spice) pour installer ses dépendances. Une fois que vous avez exécuté, lancez main.go, vous devriez voir un résultat comme celui-ci :

$ go run main.go
I0716 11:43:50.337947     138 leaderelection.go:250] attempting to acquire leader lease default/my-lock...
I0716 11:43:50.351264     138 leaderelection.go:260] successfully acquired lease default/my-lock
the leader is 138
I am the leader!

The exact leader identity will be different from what's in the example (138), since this is just the PID of the process that was running on my computer at the time of writing.

And here's the Lease that was created in the test cluster:

$ kubectl describe lease/my-lock
Name:         my-lock
Namespace:    default
Labels:       <none>
Annotations:  <none>
API Version:  coordination.k8s.io/v1
Kind:         Lease
Metadata:
  Creation Timestamp:  2024-07-16T15:43:50Z
  Resource Version:    613
  UID:                 1d978362-69c5-43e9-af13-7b319dd452a6
Spec:
  Acquire Time:            2024-07-16T15:43:50.338049Z
  Holder Identity:         138
  Lease Duration Seconds:  15
  Lease Transitions:       0
  Renew Time:              2024-07-16T15:45:31.122956Z
Events:                    <none>

See that the "Holder Identity" is the same as the process's PID, 138.

Now, let's open up another terminal and run the same main.go file in a separate process:

$ go run main.go
I0716 11:48:34.489953     604 leaderelection.go:250] attempting to acquire leader lease default/my-lock...
the leader is 138

This second process will wait forever, until the first one is not responsive. Let's kill the first process and wait around 15 seconds. Now that the first process is not renewing its claim on the Lease, the .spec.renewTime field won't be updated anymore. This will eventually cause the second process to trigger a new leader election, since the Lease's renew time is older than its duration. Because this process is the only one now running, it will elect itself as the new leader.

the leader is 604
I0716 11:48:51.904732     604 leaderelection.go:260] successfully acquired lease default/my-lock
I am the leader!

If there were multiple processes still running after the initial leader exited, the first process to acquire the Lease would be the new leader, and the rest would continue to be on standby.

No single-leader guarantees

This package is not foolproof, in that it "does not guarantee that only one client is acting as a leader (a.k.a. fencing)". For example, if a leader is paused and lets its Lease expire, another standby replica will acquire the Lease. Then, once the original leader resumes execution, it will think that it's still the leader and continue doing work alongside the newly-elected leader. In this way, you can end up with two leaders running simultaneously.

To fix this, a fencing token which references the Lease needs to be included in each request to the server. A fencing token is effectively an integer that increases by 1 every time a Lease changes hands. So a client with an old fencing token will have its requests rejected by the server. In this scenario, if an old leader wakes up from sleep and a new leader has already incremented the fencing token, all of the old leader's requests would be rejected because it is sending an older (smaller) token than what the server has seen from the newer leader.

Implementing fencing in Kubernetes would be difficult without modifying the core API server to account for corresponding fencing tokens for each Lease. However, the risk of having multiple leader controllers is somewhat mitigated by the k8s API server itself. Because updates to stale objects are rejected, only controllers with the most up-to-date version of an object can modify it. So while we could have multiple controller leaders running, a resource's state would never regress to older versions if a controller misses a change made by another leader. Instead, reconciliation time would increase as both leaders need to refresh their own internal states of resources to ensure that they are acting on the most recent versions.

Still, if you're using this package to implement leader election using a different data store, this is an important caveat to be aware of.

Conclusion

Leader election and distributed locking are critical building blocks of distributed systems. When trying to build fault-tolerant and highly-available applications, having tools like these at your disposal is critical. The Kubernetes standard library gives us a battle-tested wrapper around its primitives to allow application developers to easily build leader election into their own applications.

While use of this particular library does limit you to deploying your application on Kubernetes, that seems to be the way the world is going recently. If in fact that is a dealbreaker, you can of course fork the library and modify it to work against any ACID-compliant and highly-available datastore.

Stay tuned for more k8s source deep dives!

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!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn