"Si un trabajador quiere hacer bien su trabajo, primero debe afilar sus herramientas." - Confucio, "Las Analectas de Confucio. Lu Linggong"
Página delantera > Programación > Cómo agregar la elección de líder impulsada por Kubernetes a sus aplicaciones Go

Cómo agregar la elección de líder impulsada por Kubernetes a sus aplicaciones Go

Publicado el 2024-07-30
Navegar:183

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

Publicado originalmente en blog

La biblioteca estándar de Kubernetes está llena de joyas, escondidas en muchos de los diversos subpaquetes que forman parte del ecosistema. Uno de esos ejemplos que descubrí recientemente es k8s.io/client-go/tools/leaderelection, que se puede utilizar para agregar un protocolo de elección de líder a cualquier aplicación que se ejecute dentro de un clúster de Kubernetes. Este artículo analizará qué es la elección de líder, cómo se implementa en este paquete de Kubernetes y brindará un ejemplo de cómo podemos usar esta biblioteca en nuestras propias aplicaciones.

Elección de líder

La elección de líder es un concepto de sistemas distribuidos que es un componente central del software de alta disponibilidad. Permite que múltiples procesos simultáneos se coordinen entre sí y elijan un único proceso "líder", que luego es responsable de realizar acciones sincrónicas como escribir en un almacén de datos.

Esto es útil en sistemas como bases de datos distribuidas o cachés, donde se ejecutan múltiples procesos para crear redundancia contra fallas de hardware o red, pero no se puede escribir en el almacenamiento simultáneamente para garantizar la coherencia de los datos. Si el proceso de líder deja de responder en algún momento en el futuro, los procesos restantes iniciarán una elección de nuevo líder y eventualmente elegirán un nuevo proceso para que actúe como líder.

Utilizando este concepto, podemos crear software de alta disponibilidad con un único líder y múltiples réplicas en espera.

En Kubernetes, el paquete de tiempo de ejecución del controlador utiliza la elección de líder para que los controladores tengan alta disponibilidad. En una implementación de controlador, la reconciliación de recursos solo ocurre cuando un proceso es el líder y otras réplicas están esperando en espera. Si el grupo líder deja de responder, las réplicas restantes elegirán un nuevo líder para realizar conciliaciones posteriores y reanudar el funcionamiento normal.

Arrendamientos de Kubernetes

Esta biblioteca utiliza un arrendamiento de Kubernetes, o bloqueo distribuido, que se puede obtener mediante un proceso. Los arrendamientos son recursos nativos de Kubernetes que están en manos de una única identidad, por un período determinado, con una opción de renovación. Aquí hay una especificación de ejemplo de los documentos:

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"

El ecosistema k8s utiliza los arrendamientos de tres maneras:

  1. Latidos del nodo: cada nodo tiene un recurso de arrendamiento correspondiente y actualiza su campo renewTime de forma continua. Si el tiempo de renovación de un contrato de arrendamiento no se ha actualizado por un tiempo, el nodo se considerará no disponible y no se programarán más pods para él.
  2. Elección de líder: En este caso, se utiliza un contrato de arrendamiento para coordinar varios procesos haciendo que un líder actualice la identidad del titular del contrato de arrendamiento. Las réplicas en espera, con identidades diferentes, están bloqueadas esperando que caduque el contrato de arrendamiento. Si el contrato de arrendamiento expira y el líder no lo renueva, se lleva a cabo una nueva elección en la que las réplicas restantes intentan tomar posesión del contrato de arrendamiento actualizando su identidad de titular con la suya propia. Dado que el servidor API de Kubernetes no permite actualizaciones de objetos obsoletos, solo un nodo en espera podrá actualizar con éxito el contrato de arrendamiento, momento en el cual continuará la ejecución como el nuevo líder.
  3. Identidad del servidor API: a partir de la versión 1.26, como función beta, cada réplica de kube-apiserver publicará su identidad mediante la creación de un contrato de arrendamiento dedicado. Dado que se trata de una característica nueva y relativamente reducida, no se puede derivar mucho más del objeto Lease aparte de cuántos servidores API se están ejecutando. Pero esto deja espacio para agregar más metadatos a estos arrendamientos en futuras versiones de k8.

Ahora exploremos este segundo caso de uso de arrendamientos escribiendo un programa de muestra para demostrar cómo se pueden utilizar en escenarios de elección de líderes.

Programa de ejemplo

En este ejemplo de código, utilizamos el paquete leaderelection para manejar los detalles específicos de la elección del líder y la manipulación del arrendamiento.

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())

}

Lo bueno del paquete de elección de líderes es que proporciona un marco basado en devolución de llamadas para manejar las elecciones de líderes. De esta manera, puede actuar sobre cambios de estado específicos de forma granular y liberar recursos adecuadamente cuando se elige un nuevo líder. Al ejecutar estas devoluciones de llamada en gorutinas separadas, el paquete aprovecha el sólido soporte de concurrencia de Go para utilizar de manera eficiente los recursos de la máquina.

Probandolo

Para probar esto, activemos un grupo de prueba usando kind.

$ kind create cluster

Copie el código de muestra en main.go, cree un nuevo módulo (go mod init leaderelectiontest) y ordénelo (go mod tidy) para instalar sus dependencias. Una vez que ejecute go run main.go, debería ver un resultado como este:

$ 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!

La identidad exacta del líder será diferente de la que aparece en el ejemplo (138), ya que este es solo el PID del proceso que se estaba ejecutando en mi computadora al momento de escribir este artículo.

Y aquí está el contrato de arrendamiento que se creó en el grupo de prueba:

$ kubectl describe lease/my-lock
Name:         my-lock
Namespace:    default
Labels:       
Annotations:  
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:                    

Ver que la "Identidad del Titular" sea la misma que el PID del proceso, 138.

Ahora, abramos otra terminal y ejecutemos el mismo archivo main.go en un proceso separado:

$ 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

Este segundo proceso esperará para siempre, hasta que el primero no responda. Terminemos el primer proceso y esperemos unos 15 segundos. Ahora que el primer proceso no renueva su reclamo sobre el contrato de arrendamiento, el campo .spec.renewTime ya no se actualizará. Esto eventualmente hará que el segundo proceso desencadene una elección de nuevo líder, ya que el tiempo de renovación del contrato de arrendamiento es anterior a su duración. Debido a que este proceso es el único que está en marcha, se elegirá a sí mismo como el nuevo líder.

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

Si todavía hubiera varios procesos en ejecución después de que el líder inicial saliera, el primer proceso en adquirir el contrato de arrendamiento sería el nuevo líder y el resto continuaría en espera.

No hay garantías de un solo líder

Este paquete no es infalible, ya que "no garantiza que solo un cliente actúe como líder (también conocido como esgrima)". Por ejemplo, si un líder está en pausa y deja que su contrato de arrendamiento caduque, otra réplica en espera adquirirá el contrato de arrendamiento. Luego, una vez que el líder original reanude la ejecución, pensará que sigue siendo el líder y continuará trabajando junto al líder recién elegido. De esta manera, puedes terminar con dos líderes ejecutándose simultáneamente.

Para solucionar este problema, se debe incluir un token de valla que haga referencia al contrato de arrendamiento en cada solicitud al servidor. Una ficha de vallado es efectivamente un número entero que aumenta en 1 cada vez que un contrato de arrendamiento cambia de manos. Por lo tanto, el servidor rechazará las solicitudes de un cliente con un token de vallado antiguo. En este escenario, si un líder antiguo se despierta del sueño y un líder nuevo ya ha incrementado el token de cercado, todas las solicitudes del líder anterior serán rechazadas porque está enviando un token más antiguo (más pequeño) que el que el servidor ha visto desde el líder más nuevo.

Implementar vallas en Kubernetes sería difícil sin modificar el servidor API central para tener en cuenta los tokens de vallas correspondientes para cada arrendamiento. Sin embargo, el riesgo de tener varios controladores líderes se ve mitigado en cierta medida por el propio servidor API de k8s. Debido a que se rechazan las actualizaciones de objetos obsoletos, sólo los controladores con la versión más actualizada de un objeto pueden modificarlo. Entonces, si bien podríamos tener varios controladores líderes ejecutándose, el estado de un recurso nunca regresaría a versiones anteriores si un controlador omite un cambio realizado por otro líder. En cambio, el tiempo de reconciliación aumentaría ya que ambos líderes necesitan actualizar sus propios estados internos de recursos para asegurarse de que están actuando según las versiones más recientes.

Aún así, si estás usando este paquete para implementar la elección de líder usando un almacén de datos diferente, esta es una advertencia importante que debes tener en cuenta.

Conclusión

La elección de líder y el bloqueo distribuido son componentes fundamentales de los sistemas distribuidos. Al intentar crear aplicaciones tolerantes a fallos y de alta disponibilidad, es fundamental tener herramientas como estas a su disposición. La biblioteca estándar de Kubernetes nos brinda un contenedor probado en batalla alrededor de sus primitivos para permitir a los desarrolladores de aplicaciones integrar fácilmente la elección de líder en sus propias aplicaciones.

Si bien el uso de esta biblioteca en particular lo limita a implementar su aplicación en Kubernetes, esa parece ser la forma en que va el mundo recientemente. Si de hecho eso es un factor decisivo, por supuesto, puede bifurcar la biblioteca y modificarla para que funcione con cualquier almacén de datos compatible con ACID y de alta disponibilidad.

¡Estén atentos para más análisis profundos de las fuentes de k8!

Declaración de liberación Este artículo se reproduce en: https://dev.to/sklarsa/how-to-add-kubernetes-powered-leader-election-to-your-go-apps-57jh?1 Si hay alguna infracción, comuníquese con Study_golang @163.com eliminar
Último tutorial Más>

Descargo de responsabilidad: Todos los recursos proporcionados provienen en parte de Internet. Si existe alguna infracción de sus derechos de autor u otros derechos e intereses, explique los motivos detallados y proporcione pruebas de los derechos de autor o derechos e intereses y luego envíelos al correo electrónico: [email protected]. Lo manejaremos por usted lo antes posible.

Copyright© 2022 湘ICP备2022001581号-3