Pular para o conteúdo

Sinais e exit codes

fundamentos
Sinal O que é Pode ser capturado?
SIGHUP 1 Terminal fechou; por convenção, recarregar config sim
SIGINT 2 Ctrl+C sim
SIGQUIT 3 Ctrl+, gera core dump sim
SIGKILL 9 Encerramento imediato não
SIGSEGV 11 Violação de memória sim (raramente útil)
SIGPIPE 13 Escreveu em pipe fechado sim
SIGTERM 15 Pedido educado de encerramento sim
SIGSTOP 19 Suspende não
SIGCONT 18 Retoma sim

SIGTERM é o que o Docker e o Kubernetes enviam primeiro. SIGKILL vem depois, se o processo não sair no prazo.

Janela do terminal
kill -TERM <pid> # peça primeiro
kill -9 <pid> # force só se não houver saída
kill -l # lista todos os sinais
trap 'echo saindo; limpar' TERM INT # capture em shell script

Quando o processo termina por causa de um sinal, o código de saída é 128 + número do sinal. Daí os valores que aparecem em log de container:

Código Sinal Causa usual
0 Terminou normalmente
1 Erro genérico da aplicação
2 Uso incorreto do comando (convenção)
126 Arquivo encontrado, sem permissão de execução
127 Comando não encontrado
129 SIGHUP Terminal encerrado
130 SIGINT Ctrl+C
137 SIGKILL Quase sempre OOM, ou grace period estourado
139 SIGSEGV Segfault: arquitetura errada, libc incompatível, bug nativo
143 SIGTERM Encerramento pedido (deploy, escala, drenagem)

137 — o mais comum e o mais mal interpretado. Duas causas:

Janela do terminal
# foi OOM?
docker inspect <ct> --format '{{.State.OOMKilled}}'
kubectl describe pod <pod> -n <ns> | grep -i -A2 'last state'
dmesg -T | grep -i 'killed process'

Se OOMKilled for verdadeiro, aumente o limite de memória ou reduza o consumo. Se for falso, o processo ignorou o SIGTERM e foi morto ao fim do terminationGracePeriodSeconds — corrija o encerramento gracioso.

139 — o binário quebrou. Em container, a causa número um é imagem de arquitetura errada (arm64 em host amd64) ou binário dinâmico rodando em base sem a libc esperada (o caso Alpine/musl).

143 — normalmente não é erro: o orquestrador pediu para sair, em deploy, escala ou drenagem de nó. Só investigue se estiver acontecendo fora desses momentos.

A sequência no Kubernetes, e onde cada coisa quebra:

1. Pod marcado para remoção
2. Em paralelo:
a. Removido dos EndpointSlices (para de receber tráfego novo)
b. preStop executa
c. SIGTERM enviado ao processo
3. Espera terminationGracePeriodSeconds (padrão 30s)
4. SIGKILL, se ainda estiver vivo → exit 137
lifecycle:
preStop:
exec: { command: ["sh", "-c", "sleep 10"] } # deixa o balanceador perceber a saída
terminationGracePeriodSeconds: 45 # > preStop + requisição mais longa

Do lado da aplicação, capture o sinal e encerre limpo:

import signal, sys
def encerrar(sig, frame):
servidor.parar_de_aceitar() # não aceite requisição nova
servidor.aguardar_em_andamento(timeout=25)
sys.exit(0)
signal.signal(signal.SIGTERM, encerrar)

Em container, o processo principal é o PID 1 — e o PID 1 não recebe os handlers padrão de sinal. Se a aplicação não trata SIGTERM explicitamente, ela simplesmente ignora e morre com 137 ao fim do prazo.

Outro caso: ENTRYPOINT ["sh", "-c", "meu-app"] faz o shell ser o PID 1, e ele pode não repassar o sinal ao filho.

ENTRYPOINT ["/bin/app"] # forma exec: o app é o PID 1 e recebe o sinal
# se precisar de shell ou de reaping de zumbis:
# ENTRYPOINT ["tini", "--", "/bin/app"]

Use a forma exec (lista JSON), não a forma shell (string).

Janela do terminal
docker inspect <ct> --format '{{.State.ExitCode}} {{.State.OOMKilled}} {{.State.Error}}'
kubectl get pod <pod> -n <ns> -o jsonpath='{.status.containerStatuses[0].lastState}' | jq
kubectl describe pod <pod> -n <ns> | sed -n '/Last State/,/Ready/p'
journalctl -k | grep -i 'out of memory'