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.
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.
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:
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.
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.
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.
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.
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!
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