Originalmente publicado em por blog
A biblioteca padrão do Kubernetes está cheia de joias, escondidas em muitos dos vários subpacotes que fazem parte do ecossistema. Um exemplo que descobri recentemente é k8s.io/client-go/tools/leaderelection, que pode ser usado para adicionar um protocolo de eleição de líder a qualquer aplicativo em execução dentro de um cluster Kubernetes. Este artigo discutirá o que é a eleição de líder, como ela é implementada neste pacote Kubernetes e fornecerá um exemplo de como podemos usar esta biblioteca em nossos próprios aplicativos.
A eleição de líder é um conceito de sistemas distribuídos que é um alicerce central de software altamente disponível. Ele permite que vários processos simultâneos se coordenem entre si e elejam um único processo "líder", que é então responsável por executar ações síncronas, como gravar em um armazenamento de dados.
Isso é útil em sistemas como bancos de dados distribuídos ou caches, onde vários processos estão em execução para criar redundância contra falhas de hardware ou rede, mas não podem gravar no armazenamento simultaneamente para garantir a consistência dos dados. Se o processo do líder deixar de responder em algum momento no futuro, os processos restantes darão início a uma eleição de novo líder, eventualmente escolhendo um novo processo para atuar como líder.
Usando esse conceito, podemos criar software altamente disponível com um único líder e múltiplas réplicas em espera.
No Kubernetes, o pacote controller-runtime usa a eleição de líder para tornar os controladores altamente disponíveis. Em uma implantação de controlador, a reconciliação de recursos ocorre apenas quando um processo é o líder e outras réplicas estão aguardando em espera. Se o pod líder não responder, as réplicas restantes elegerão um novo líder para realizar reconciliações subsequentes e retomar a operação normal.
Esta biblioteca usa um Kubernetes Lease, ou bloqueio distribuído, que pode ser obtido por um processo. Locações são recursos nativos do Kubernetes mantidos por uma única identidade, por um determinado período, com opção de renovação. Aqui está um exemplo de especificação dos 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"
Os arrendamentos são usados pelo ecossistema k8s de três maneiras:
Agora vamos explorar este segundo caso de uso de arrendamentos escrevendo um programa de exemplo para demonstrar como você pode usá-los em cenários de eleição de líder.
Neste exemplo de código, estamos usando o pacote leaderelection para lidar com as especificidades da eleição do líder e da manipulação do arrendamento.
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()) }
O que há de bom no pacote leaderelection é que ele fornece uma estrutura baseada em retorno de chamada para lidar com eleições de líderes. Dessa forma, você pode agir de maneira granular em mudanças de estado específicas e liberar recursos adequadamente quando um novo líder for eleito. Ao executar esses retornos de chamada em goroutines separadas, o pacote aproveita o forte suporte de simultaneidade do Go para utilizar com eficiência os recursos da máquina.
Para testar isso, vamos criar um cluster de teste usando kind.
$ kind create cluster
Copie o código de exemplo em main.go, crie um novo módulo (go mod init leaderelectiontest) e arrume-o (go mod tidy) para instalar suas dependências. Depois de executar go run main.go, você deverá ver uma saída como esta:
$ 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!
A identidade exata do líder será diferente da que está no exemplo (138), pois este é apenas o PID do processo que estava sendo executado no meu computador no momento da escrita.
E aqui está o Lease que foi criado no cluster de teste:
$ 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:
Veja que a "Identidade do Titular" é igual ao PID do processo, 138.
Agora, vamos abrir outro terminal e executar o mesmo arquivo main.go em um processo 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 processo irá esperar para sempre, até que o primeiro não responda. Vamos encerrar o primeiro processo e esperar cerca de 15 segundos. Agora que o primeiro processo não está renovando sua reivindicação no Lease, o campo .spec.renewTime não será mais atualizado. Isto acabará por fazer com que o segundo processo desencadeie uma eleição de novo líder, uma vez que o tempo de renovação do arrendamento é mais antigo que a sua duração. Como este processo é o único em execução, ele se elegerá como o novo 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!
Se houvesse vários processos ainda em execução após a saída do líder inicial, o primeiro processo a adquirir o arrendamento seria o novo líder e o restante continuaria em espera.
Este pacote não é infalível, pois "não garante que apenas um cliente atue como líder (também conhecido como esgrima)". Por exemplo, se um líder estiver pausado e deixar seu Lease expirar, outra réplica em espera adquirirá o Lease. Então, quando o líder original retomar a execução, ele pensará que ainda é o líder e continuará trabalhando ao lado do líder recém-eleito. Dessa forma, você pode acabar com dois líderes concorrendo simultaneamente.
Para corrigir isso, um token de isolamento que faça referência ao Lease precisa ser incluído em cada solicitação ao servidor. Um token de esgrima é efetivamente um número inteiro que aumenta em 1 cada vez que um arrendamento muda de mãos. Portanto, um cliente com um token de esgrima antigo terá suas solicitações rejeitadas pelo servidor. Neste cenário, se um líder antigo acordar do sono e um novo líder já tiver incrementado o token de esgrima, todas as solicitações do antigo líder seriam rejeitadas porque ele está enviando um token mais antigo (menor) do que o que o servidor viu no líder mais novo.
Implementar o isolamento no Kubernetes seria difícil sem modificar o servidor API principal para contabilizar os tokens de isolamento correspondentes para cada arrendamento. No entanto, o risco de ter vários controladores líderes é um tanto mitigado pelo próprio servidor API k8s. Como as atualizações em objetos obsoletos são rejeitadas, somente os controladores com a versão mais atualizada de um objeto podem modificá-lo. Portanto, embora pudéssemos ter vários controladores líderes em execução, o estado de um recurso nunca regrediria para versões mais antigas se um controlador perdesse uma alteração feita por outro líder. Em vez disso, o tempo de reconciliação aumentaria, uma vez que ambos os líderes precisariam de actualizar os seus próprios estados internos de recursos para garantir que estão a agir de acordo com as versões mais recentes.
Ainda assim, se você estiver usando este pacote para implementar a eleição de líderes usando um armazenamento de dados diferente, esta é uma advertência importante a ser observada.
A eleição do líder e o bloqueio distribuído são blocos de construção críticos de sistemas distribuídos. Ao tentar construir aplicativos tolerantes a falhas e altamente disponíveis, é fundamental ter ferramentas como essas à sua disposição. A biblioteca padrão do Kubernetes nos fornece um wrapper testado em batalha em torno de seus primitivos para permitir que os desenvolvedores de aplicativos criem facilmente a eleição de líderes em seus próprios aplicativos.
Embora o uso desta biblioteca específica limite você à implantação de seu aplicativo no Kubernetes, parece ser assim que o mundo está indo recentemente. Se de fato isso for um obstáculo, é claro que você pode bifurcar a biblioteca e modificá-la para funcionar em qualquer armazenamento de dados altamente disponível e compatível com ACID.
Fique ligado para mais detalhes sobre fontes k8s!
Isenção de responsabilidade: Todos os recursos fornecidos são parcialmente provenientes da Internet. Se houver qualquer violação de seus direitos autorais ou outros direitos e interesses, explique os motivos detalhados e forneça prova de direitos autorais ou direitos e interesses e envie-a para o e-mail: [email protected]. Nós cuidaremos disso para você o mais rápido possível.
Copyright© 2022 湘ICP备2022001581号-3