Conteúdos

Graceful Shutdown

TL;DR: Este artigo aborda a importância do Graceful Shutdown nos microsserviços, os sinais de desligamento (SIGTERM, SIGINT e SIGKILL) e três visões sobre esse desafio: Go, Kubernetes e Istio.

Introdução

A escalabilidade horizontal é uma das principais vantagens das arquiteturas de softwares modernas para que os sistemas continuem responsivos sob variações de carga.

Ou seja, réplicas dos microsserviços são escaladas quando a carga aumenta, e desligadas quando a carga diminui.

Para que essa elasticidade (aumento e diminuição de réplicas) seja natural e transparente, é importante que o tempo de inicialização dos microsserviços seja baixo. Quanto mais rápido um serviço inicializar, mais rápido estará disponível para atender requisições e dividir a carga com as outras réplicas.

Pergunta

E sobre o desligamento dos microsserviços?

O que acontece se uma réplica recebe um sinal para ser desligada no momento em que está processando requisições?

Algumas ocasiões em que uma réplica pode receber um sinal de desligamento, são:

  1. diminuição de carga;
  2. rolling update;
  3. rolling restart.

Normalmente, ao receber um sinal, o processamento dessas requisições seriam interrompidos e os clientes receberiam erros.

A não ser que esse serviço tenha um processo de desligamento mais inteligente: graceful shutdown.

Entendendo os sinais

Os sinais são usados principalmente em sistemas do tipo Unix, e são enviados pelo Kernel ou de algum outro programa.

Os principais sinais de desligamento/encerramento de um programa, são SIGTERM, SIGINT e SIGKILL.

SIGTERM é um sinal genérico usado para causar o encerramento do programa. É o sinal gerado pelo comando kill.

Info
O Kubernetes envia o sinal SIGTERM para matar um Pod.

O sinal SIGINT é enviado quando o usuário digita CTRL-c.

SIGKILL é usado para causar o encerramento imediato do programa. Não pode ser interceptado ou ignorado e, portanto, é sempre fatal.

Graceful shutdown

Graceful Shutdown significa um desligamento inteligente, agradável, sem danos ao sistema.

Para que microsserviços tenham esse desligamento inteligente, eles precisam lidar com os sinais SIGTERM e SIGINT listados acima.

O comportamento padrão da maioria das tecnologias é interromper o processamento do programa, de forma que, muitas vezes, é prejudicial para o funcionamento.

Em Go, por exemplo, um sinal síncrono é convertido em panic em tempo de execução.

Uma forma simples de lidar com esses sinais, é esperar alguns segundos para que o processamento seja finalizado. Mas pode ser necessário fechar conexões com banco de dados, redis ou um message broker, por exemplo.

O Graceful Shutdown pode ser implementado diretamente no código do serviço. Porém, o Kubernetes e Istio possuem configurações que podem ajudar nessa tarefa.

Go

A implementação mais comum de graceful shutdown em Go, é usando Goroutines e Channels, como o exemplo abaixo.

Dessa forma, o servidor HTTP é inicializado numa nova goroutine, enquanto a principal espera por um sinal no channel quit.

Assim que um sinal é recebido, o servidor é desligado com um timeout de 5 segundos. Ou seja, se em 5 segundos ainda existir alguma conexão ativa, a função Shutdown() retorna um erro.

Nota
A função Shutdown() foi introduzida no Go1.8

Os principais frameworks web de Go sugerem implementações nesse padrão, com Goroutines e Channels:

Pra quem prefere usar bibliotecas de terceiros criadas exclusivamente para isso, eu recomendaria a ory/graceful. Foi a que mais me chamou atenção pela simplicidade e possibilidade de customizar a função de desligamento.

Dica
Uma adaptação do exemplo acima utilizando a biblioteca ory/graceful, está disponível no meu GitHub.

Kubernetes

Nos Pods do Kubernetes, que representam réplicas de um Deployment, é possível configurar um hook chamado preStop, que é invocado antes do sinal SIGTERM ser enviado.

Configurando um sleep neste hook, podemos ter um graceful shutdown, como no exemplo abaixo.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  containers:
    - name: web
      image: nginx
      ports:
        - name: web
          containerPort: 80
      lifecycle:
        preStop:
          exec:
            command: ["sleep", "15"]

O intervalo do sleep deve ser o suficiente para que a alteração de endpoint do Kubernetes seja propagada ao kube-proxy, Ingress Controller, CoreDNS, etc. Para mais detalhes, veja esse artigo.

Por padrão, o Kubernetes espera até 30 segundos no processo de desligamento de um Pod antes de forçar o encerramento do processo (SIGKILL, que não pode ser interceptado).

Dica
Recomendo fortemente a leitura deste artigo para maiores detalhes sobre o Graceful Shutdown no Kubernetes.
Aviso
A grande desvantagem dessa abordagem é que a imagem Docker precisa ter o comando sleep, dificultando o uso de imagens Distroless.

Istio

O Istio possui uma configuração chamada TerminationDrainDuration em que é possível definir uma pausa antes do desligamento do sidecar.

Info

Sidecar é um conceito comum nas implementações de Service Mesh: é um container que acompanha a aplicação (que também e um container) dentro do Pod do Kubernetes. Assim, temos 2 containers dentro do Pod: app + sidecar.

O sidecar é um proxy (no caso do Istio, é o Envoy) que faz a intermediação de todo tráfego do Pod para que tenhamos todas as vantagens do Service Mesh.

Quando o proxy recebe SIGTERM ou SIGINT, ele começa a drenar as conexões, impedindo novas conexões e permitindo que as conexões existentes sejam concluídas.

Dica
Lembrando que o SIGTERM é enviado após a execução do hook preStop

A duração desse processo de drenagem é configurável tanto globalmente:

1
2
3
4
5
6
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
spec:
  meshConfig:
    defaultConfig:
      terminationDrainDuration: 50s

quanto por workload (por Pod):

1
2
annotations:
  proxy.istio.io/config: '{ "terminationDrainDuration": 50s }'

A duração padrão é 5 segundos.

Conclusão

A melhor abordagem para habilitar Graceful Shutdown depende do cenário de cada projeto/sistema.

Existem projetos em que:

  • alterar o código dos microsserviços é muito trabalhoso;
  • não usa Service Mesh (Istio);
  • o uso de imagens Distroless é prioridade;
  • não estão no Kubernetes;
  • que estão no Kubernetes, usando Istio, e tem agilidade para alterar o código dos microsserviços. Nesse caso é possível conciliar mais de uma estratégia para garantir zero downtime.
Pergunta

Compartilhe nos comentários os desafios e aprendizados do seu projeto! 😉

  • Qual é o cenário do seu projeto?
  • Qual abordagem é utilizada para graceful shutdown?
  • Como é a implementação na sua linguagem de programação e framework preferido?

Referências

  1. Termination Signals
  2. Graceful shutdown and zero downtime deployments in Kubernetes
  3. signal package
  4. Challenges of running Istio distroless images
  5. Graceful shutdown in Go http server