Seidor
StatefulSets

19 de março de 2024

StatefulSets, gestão otimizada de aplicações com estado

O que é um StatefulSet?

Podemos defini-lo como o objeto API de carga de trabalho e gerenciamento de aplicações com estado ou também conhecidas como “stateful applications”. Ele gerencia a implementação e o escalonamento de um conjunto de pods, oferecendo garantias sobre a ordem e singularidade dos pods que gerencia.

Semelhante aos deployments, um StatefulSet gerencia pods baseados em especificações de contêiner idênticas entre si, mas, ao contrário dos deployments, um StatefulSet mantém uma identidade fixa para cada um de seus pods. Ou seja, esses pods, embora tenham sido criados a partir das mesmas especificações, não são intercambiáveis entre si, possuindo um identificador persistente que se mantém em qualquer reprogramação ((re)scheduling).

Se desejarmos utilizar volumes de armazenamento para alcançar persistência em nossa carga de trabalho, os StatefulSets podem fazer parte de uma boa solução. Embora esses pods que pertencem a um StatefulSet sejam suscetíveis a falhas, seus identificadores persistentes nos facilitarão a união dos volumes já existentes com os novos pods que substituirão os pods falhados.

Quando utilizar um StatefulSet?

Os StatefulSets são especialmente úteis nos seguintes casos ou requisitos.

  • Identificadores de rede únicos e estáveis.
  • Armazenamento estável e persistente.
  • Implantações e escalonamentos ordenados e sem afetação.
  • Atualizações automáticas, contínuas e ordenadas.

Quando nos referimos a aplicações que não requerem um identificador persistente, implementação ordenada, eliminações ou escalonamentos.
Devemos, nesse caso, implementar nossa aplicação mediante objetos de carga de trabalho que forneçam um conjunto de réplicas sem estado. Por exemplo, um ReplicaSet ou Deployment que seria mais eficaz dado tal caso.

Limitações a ter em conta

  • O armazenamento para um pod determinado deve ser provisionado mediante um “PersistentVolume” ou volume persistente em função da classe de armazenamento solicitada, ou bem, previamente de mão do administrador.
  • Eliminar e/ou reduzir um StatefulSet não eliminará os volumes associados a este. Devido a que sempre tratamos de garantir a segurança e persistência dos dados, normalmente mais valiosos que uma eliminação automática de todos os recursos associados ao StatefulSet.
  • Atualmente, os StatefulSets precisam de um serviço “Headless” ou sem cabeça que deve ser o responsável pela identidade de rede dos pods implantados. Nesse caso devemos criar esse serviço por nosso lado.
  • Os StatefulSets não oferecem nenhuma garantia em termos de finalização dos pods devido à eliminação do StatefulSet. Para poder garantir uma finalização ordenada e sem afetação dos pods devemos utilizar a possibilidade de escalar o StatefulSet a 0 antes de eliminá-lo.
  • Quando trabalhamos com atualizações contínuas (rolling updates) sobre as políticas de administração de pods predefinidas (OrderedReady), é possível receber um estado de interrupção ou quebra que requeira intervenção manual para sua reparação.

Componentes de um Kubernetes StatefulSet

Vamos analisar o seguinte exemplo de um StatefulSet.

O seguinte exemplo foi extraído da documentação oficial do Kubernetes e, portanto, não revela nenhum tipo de configuração ou dado sensível.

statefulset

Observações:

  • Linha 1-13: O serviço "headless" ou sem cabeça, chamado "nginx" e utilizado para controlar o domínio da rede. Podemos abrir uma porta específica e atribuir-lhe um alias.
  • Linha 15-49: O StatefulSet chamado “web” tem uma “spec” (especificação) na linha 24 que nos indica o número de réplicas do contêiner de “nginx” que serão lançadas em pods únicos.
  • Linha 41-49: O “volumeClaimTemplate” nos facilitará o armazenamento estável (persistente) utilizando um “PersistentVolume” provisionado pelo “PersistentVolume Provisioner”.

O nome do objeto “StatefulSet” deve ser um nome válido para o subdomínio especificado no DNS.

  • Linha 20-22: Devemos especificar uma etiqueta, mas levando em consideração que deve ser a mesma que a etiqueta especificada no campo “app” na linha 29. Não etiquetar corretamente o campo do “Pod selector” resultará em um erro de validação durante o processo de criação do StatefulSet.
  • Linha 25: Podemos especificar “minReadySeconds” dentro de “specs” (especificações), o qual é um campo opcional e determina os segundos que devem passar desde a criação de um novo pod em execução e no estado pronto ou “ready” sem que nenhum de seus contêineres se quebre para ser considerado no estado disponível ou “available”. Esta opção é especialmente útil quando aplicamos atualizações contínuas (rolling updates) para verificar o progresso dessa atualização.

Identidade do pod

Os pods que pertencem a um StatefulSet possuem um identificador único composto por um ordinal (número ordinal), uma identidade de rede persistente e armazenamento persistente também. A identidade ou identificador é associada ao pod independentemente de em qual nó ele seja programado((re)schedule).

Índice ordinal

Para um StatefulSet com n réplicas, a cada pod será atribuído um número inteiro ordinal de 0 a n-1, único em seu conjunto.

Identificador Persistente de Rede

Cada pod de um StatefulSet deriva seu nome do nome do StatefulSet e do ordinal do pod. O padrão para o nome do host(pod) construído é $(statefulset name)-$(ordinal).

O StatefulSet pode utilizar um serviço "headless" ou sem cabeça para controlar o domínio de seus pods. Esse domínio tem a seguinte nomenclatura $(nome do serviço).$(namespace).svc.cluster.local, onde “cluster.local” é o domínio do cluster.

À medida que cada pod é criado, obtemos um subdomínio DNS correspondente, com o seguinte formato: $(podname).$(domínio do serviço), onde o domínio do serviço é definido pelo campo “serviceName” no StatefulSet.

Dependendo da configuração do DNS no seu cluster, é possível que você não consiga resolver o nome DNS para um pod recém-executado. Esse comportamento ocorre quando outros clientes no cluster já enviaram consultas para esse nome de host do pod antes de ele ser criado. O armazenamento em cache negativo significa que os resultados de buscas anteriores que falharam são lembrados e reutilizados, mesmo após o pod ter sido executado.

Para que esse problema não ocorra e não tenhamos alguns segundos de queda ou de erros após criar um pod, podemos utilizar algumas das soluções descritas a seguir.

  • Lançar as consultas diretamente através da API do Kubernetes (por exemplo, utilizando um relógio) em vez de confiar nas buscas de DNS.
  • Reduzir o tempo de armazenamento em cache no seu provedor de DNS no Kubernetes (normalmente configurar o “configmap” para CoreDNS, geralmente com valor em 30s).

"Lembrar que, como mencionado na seção de limitações, nós mesmos somos responsáveis por criar o serviço headless (sem cabeça) responsável pela identidade de rede dos pods."

A seguir, alguns exemplos de como o Domínio do cluster, o nome do serviço e o nome do StatefulSet afetam o DNS dos pods.

statefulset

Armazenamento persistente

Para cada entrada “VolumeClaimTemplate” definida em um StatefulSet, cada pod recebe um “PersistentVolumeClaim”. No exemplo de cada “nginx” mencionado anteriormente, cada pod recebe um único PersistentVolume com StorageClass “my-storage-class“ e 1Gib de armazenamento provisionado.

Quando um pod é reprogramado ((re)schedule) em um nó, seus “VolumeMounts” montam os “PersistentVolumes” associados com seus respectivos “PersistentVolumesClaims”.

Etiqueta de nome do pod

Quando o “Controller” do StatefulSet cria um pod, adiciona uma etiqueta, “statefulset.kubernetes.io/pod-name”, que é definida com o nome do pod. Essa mesma etiqueta nos permite associar um serviço a um pod específico no StatefulSet.

Garantias de Implementação e Escalabilidade

Se tivermos um StatefulSet com n réplicas, quando os pods estão sendo implantados, eles o fazem de forma sequencial na seguinte ordem {0..N-1}.

Quando os pods estão sendo eliminados, eles o fazem de forma reversa, ou seja, do {N-1..0}.

Antes que uma operação de escalonamento seja aplicada a um pod, todos os predecessores deste devem estar em estado “Running” e “Ready”.

Antes que um pod seja terminado, todos os seus sucessores devem ser completamente encerrados.

Nunca devemos especificar em “pod.Spec.TerminationGracePeriodSeconds” um valor de 0. Esta prática é insegura e nunca recomendada.

Estratégias de Atualização

O campo .spec.updateStrategy nos permite configurar ou desativar atualizações contínuas (rolling updates) automáticas para os contêineres, etiquetas, recursos requests e limits e também annotations para os pods em um StatefulSet. Temos dois possíveis valores.

OnDelete: Quando estabelecemos o tipo de “updateStrategy” para “OnDelete”, o controller do StatefulSet automaticamente não atualizará os pods que pertencem aos pods. Os usuários deverão excluir manualmente os pods para forçar o controller a criar novos pods que reflitam as modificações realizadas no template do StatefulSet.

RollingUpdate: Este valor implementa as atualizações automáticas, por padrão este é o valor estabelecido para a estratégia de atualização.

Atualizações Contínuas

Quando a estratégia de atualizações (spec.updateStrategy.type) de um StatefulSet é configurada como RollingUpdate, o controlador (controller) do StatefulSet excluirá e recriará cada pod no StatefulSet. O processo segue na mesma ordem que quando terminamos pods (do ordinal maior para o menor). Atualizando cada pod um por um.

O controlplane do Kubernetes espera até que um pod atualizado esteja em execução e pronto antes de atualizar seu predecessor. O controlplane respeita o valor que atribuímos a "spec.minReadySeconds" desde que o pod muda seu estado para "pronto" antes de continuar atualizando o restante dos pods.

Partições

A estratégia de atualização “rolling update” nos permite fazer a atualização de forma parcial, se especificarmos uma partição (spec.updaterStrategy.rollingUpdate.partition) todos os pods que tiverem um ordinal maior que o valor da partição serão atualizados quando o template (spec.template) do StatefulSet for modificado.

Os pods que possuírem um ordinal menor que o valor da partição não serão atualizados e, mesmo que sejam excluídos e reimplantados, o serão com a versão anterior do template do StatefulSet. Se o valor da partição for maior que o número de réplicas (spec.replicas), nenhum pod será atualizado.

Retrocesso forçado

Quando utilizamos a estratégia de atualização padrão “OrderedReady”, é possível entrar em um estado de falha em que seja necessária intervenção manual para repará-lo. Se atualizarmos o template de um pod e nossa configuração nunca mudar seu estado para “running” ou “ready”, o StatefulSet deixará de ser atualizado e ficará em espera.

Este estado não é suficiente para reverter o pod template para uma boa configuração. Devido a um problema conhecido, o StatefulSet continuará esperando que o pod quebrado mude seu estado para "ready", o que nunca acontecerá, antes de tentar reverter para a configuração funcional.

Uma vez revertido o template, devemos eliminar os pods do StatefulSet que tenham sido tentados a serem implantados com a configuração incorreta. Feito isso, o próprio StatefulSet começará a recriar os pods utilizando o template revertido para a configuração correta.

Réplicas

O campo opcional “spec.replicas” especifica o número desejado de pods, o valor padrão é 1.

Se precisarmos escalar automaticamente uma implantação, podemos usar diretamente o comando kubectl ou modificar o arquivo ".yml" e reimplantar o StatefulSet. Com o comando kubectl:

"kubectl scale statefulset statefulset –replicas=X"

Se tivermos um “HorizontalPodAutoscaler” ou uma API similar para escalar automaticamente, não devemos especificar este campo, pois o control plane do Kubernetes agirá por nós.