Executando verificação de segurança...
4

Rolling Deployments com tempo de inatividade zero no Kubernetes

Kubernetes permite empresas de grande porte operar sistemas robusto em escala. Porém, nem tudo é perfeito e em alguns casos pode acontecer comportamentos inesperados.

Motivação

Uma expectivativa rasoável seria que durante rolling deployment não tivesse nenhum request failure. Maaas, depois de alguns testes descobri que kubernetes vai mandar tráfego para terminating pods, mesmo depois de começar o processo de shutdown de um pod, o que envia um TERM signal para os pods, Kubernetes ainda assim envia novos requests para o pod. Frustante, não? Isso acontece porque não existe uma orquestração entre o Kubernetes enviando um sinal TERM e removendo o pod da lista de endpoints de serviço. Essas duas operações podem acontecer em qualquer sequência, causando um delay entre eles.

Um pod pode ser despejado por vários motivos. O processo de despejo começa quando o servidor API modifica o estado de um pod no etcd para o estado Terminating. O kubelet do nó e o controlador de endpoints monitoram continuamente o estado do pod. Assim que percebem o estado de Termination, eles iniciam o processo de despejo, ambas as operações são assíncronas:

  • Kubelet executa o despejo(pod eviction)
  • O endpoint-controller lida com o processo de remoção do endpoint

SIGTERM

Quando o kubelet reconhece que um pod deve ser encerrado, ele inicia uma sequência de shutdown para cada contêiner no pod.

  1. Executa o pre-stop hook do contaier se tiver
  2. Envia um sinal TERM
  3. Aguarda o encerramento do contêiner

Esse processo sequencial deve levar menos de 30 segundos (ou o valor em segundos especificado no campo spec.terminationGracePeriodSeconds). Se o contêiner ainda estiver em execução além desse tempo, o kubelet aguardará mais 2 segundos e, em seguida, matará o contêiner à força, enviando um sinal KILL.

SIGKILL

Para alcançarmos graceful shutdowns é importante compreender a natureza assíncrona do processo de remoção de pod. Não podemos fazer suposições sobre qual dos processos de despejo será concluído primeiro. Se o processo de remoção do endpoint terminar antes dos contêineres receberem o sinal TERM, nenhuma nova solicitação chegará enquanto os contêineres estiverem encerrando. No entanto, se os contêineres começarem a terminar antes da conclusão do processo de remoção do endpoint, os pods continuarão a receber requests. Nesse caso, os clientes receberão erros de Connection timeout ou Connection refused como respostas. Como a remoção do endpoint deve ser propagada para todos os nós do cluster antes de ser concluída, há uma alta probabilidade de que o processo de remoção do pod seja concluído primeiro.

Solução

Em primeiro lugar, devemos ter certeza de que o aplicativo termina normalmente quando o kubelet envia o sinal TERM para o contêiner. Felizmente, as estruturas de aplicativos da Web geralmente oferecem suporte
graceful shutdown com pouca ou nenhuma configuração. Por exemplo, Spring Boot requer apenas 2 linhas de configuração e aplicativos Express.js precisam de 3 linhas de código JavaScript.

Em seguida, você deve verificar se seu aplicativo está realmente recebendo o sinal TERM o que nem sempre é o caso. Uma mitigação comum é pausar o processo de remoção do pod para aguardar que o processo de remoção do endpoint se propague por todo o cluster Kubernetes. Afinal, não apenas os kube-proxies devem ser notificados, mas também outros componentes, como ingress controllers e load balancers. Para fazer isso, podemos usar duas configurações na especificação do pod: spec.lifecycle.preStop e spec.terminationGracePeriodSeconds.

O pre-stop hook é executado antes que os contêineres recebam o sinal TERM, para que possamos usá-lo para obter um graceful shutdown. Para mitigar o problema, adicionamos um comando sleep no pre-stop hook. Isso atrasará o sinal TERM e criará tempo para a propagação da remoção do ´endpoint´.

Por quanto tempo devemos esperaro pre-stop hook? Depende da latência da sua rede e dos nós. Pode ser necessário realizar alguns testes para descobrir esse valor. No entanto, várias fontes sugerem que um valor entre 5 a 10 segundos deve ser suficiente para a maioria dos casos.

Por exemplo, um atraso de 20 segundos com até 40 segundos de tempo de desligamento do aplicativo resulta na seguinte configuração:

spec:
  terminationGracePeriodSeconds: 60
  containers:
  - name: "{{APP_NAME}}"
    lifecycle:
      preStop:
        exec:
          command: ["/bin/sh","-c","sleep 20"]

Outro approach: Atrase o app shutdown

Em vez de fazer um fazer um sleeping no pre-stop hook, podemos esperar no aplicativo por algum tempo até que nenhum outro request chegue. Application frameworks e runtimes expõem hooks para receber e manipular sinais de processo, por exemplo em aplicativos Express.js:

const server = app.listen(port)

process.on('SIGTERM', () => {
  debug('SIGTERM signal received: closing HTTP server in 10 seconds')

  const closeServer = () => server.close()
  setTimeout(closeServer, 10_000)
})

Este mecanismo de mitigação é baseado na mesma ideia subjacente de esperar com a esperança de que, eventualmente, o Kubernetes não encaminhe novas solicitações para o pod.

Notas finais

Tudo isso é baseado em clusters Kubernetes usando kube-proxy e iptables. O Kubernetes está migrando para implementações CNI que usam eBPF em vez de iptables. Mas o comportamento descrito aqui também se aplica às soluções baseadas em eBPF (ou pelo menos parece aplicar-se a elas) e pode haver mais advertências que você deve ter em mente. Por exemplo, o Cilium parece ter um bug que faz com que as conexões existentes também falhem durante o encerramento do pod, dificultando o desligamento normal.

Referência

Carregando publicação patrocinada...