O problema: node morto, tráfego vivo

Você tem um cluster Kubernetes rodando em produção. Um node cai abruptamente. Três segundos depois o Kubernetes detecta a falha e marca o node como NotReady. Até ai tudo certo. Mas o tráfego continua chegando para os pods daquele node por mais 10 a 13 segundos depois disso.

Esse comportamento não e um bug. E uma consequência direta de como o Kubernetes propaga informações entre seus componentes. Entender o mecanismo completo e fundamental para qualquer engenheiro que opera clusters em produção e precisa raciocinar sobre disponibilidade real, não sobre a disponibilidade teórica que o painel promete.

Nos próximos tópicos vamos percorrer o caminho completo: da detecção da falha até o momento em que o kube-proxy para de enviar tráfego para o node morto, mostrando onde cada segundo e gasto.

Como funciona a detecção de node no Kubernetes

Cada node no cluster roda um componente chamado kubelet. Esse kubelet envia heartbeats periódicos para o API server atualizando a condição Ready do node. Por padrão, esse intervalo e de 10 segundos.

O controller manager tem um parâmetro chamado nodeMonitorGracePeriod (padrão: 40 segundos) que define quanto tempo ele espera sem receber heartbeat antes de marcar o node como NotReady. Após isso, outro parâmetro chamado podEvictionTimeout (padrão: 5 minutos) define quanto tempo os pods ficam no node antes de serem evictados.

Em configurações otimizadas para detecção rápida, o ciclo de heartbeat cai para 2 segundos e o nodeMonitorGracePeriod para 6 segundos. E com essa configuração que o benchmark de node detectado em 3 segundos e obtido na prática.

💡
Dica

Nos provedores de nuvem gerenciados como GKE, EKS e AKS, os valores de nodeMonitorGracePeriod já costumam vir otimizados. Em clusters self-managed, verifique as flags do kube-controller-manager antes de confiar nos defaults.

Por que o tráfego continua após a detecção

Detectar que o node esta morto e apenas a primeira etapa. Para parar o tráfego, o Kubernetes precisa propagar essa informação por uma cadeia de componentes, e cada etapa adiciona latência.

O caminho completo e o seguinte: node marcado NotReady, depois o Endpoint controller remove os pods daquele node dos objetos Endpoints, depois o kube-proxy em cada node recebe a atualização e reconfigura as regras de iptables ou IPVS, e apenas então o tráfego para de ser roteado para os pods mortos.

Cada etapa dessa cadeia tem custo. O API server precisa processar a mudança de estado do node. O endpoint controller precisa listar os pods afetados e atualizar cada objeto Endpoints. O kube-proxy em cada node precisa receber o watch event, processar e reconstruir as regras de iptables. Em um cluster com muitos nodes e muitos serviços, esse processo pode facilmente ultrapassar 10 segundos.

⚠️
Atenção

O problema e pior em clusters grandes. Com centenas de nodes e milhares de endpoints, a propagação de uma mudança de estado pode levar dezenas de segundos. Não assuma que o SLA de detecção do node implica um SLA equivalente para o tráfego.

Como diagnosticar o comportamento no seu cluster

Para medir o gap real, você precisa capturar dois timestamps: quando o node virou NotReady e quando as requisições pararam de chegar nos pods afetados. O comando kubectl describe node mostra o histórico de condições do node com timestamps precisos.

Para simular uma falha de node em ambiente de teste, você pode parar o processo kubelet diretamente no node e monitorar o estado a partir do control plane com kubectl get nodes -w. O flag -w mostra as atualizações em tempo real a medida que o estado do node muda.

Para medir o gap de tráfego, configure um loop de requisições HTTP contra o serviço e registre o timestamp de cada resposta com erro. O intervalo entre o primeiro erro e o último erro e o gap real de propagação do seu cluster.

⚠️
Atenção

Nunca rode simulações de falha em clusters de produção sem um plano de rollback. Use sempre um cluster de staging com carga sintética que simule o comportamento real.

Exemplo prático: o gap em números reais

Em um cluster com configuração padrão, a sequência típica e a seguinte. O kubelet para de enviar heartbeat no momento T=0. O controller manager detecta a ausência por volta de T=40s (nodeMonitorGracePeriod padrão). O endpoint controller atualiza os objetos Endpoints por volta de T=42s. O kube-proxy em cada node recebe e aplica as regras atualizadas por volta de T=53s. O tráfego efetivamente para por volta de T=55s.

Com configuração otimizada, esse fluxo inteiro cai para cerca de 15 segundos. A detecção acontece em 3s, mas a propagação até o kube-proxy adiciona mais 10 a 13 segundos. Esse e o gap que aparece nos logs de produção como erros intermitentes logo após uma falha de node.

O número exato varia muito com o tamanho do cluster, a quantidade de services e endpoints, e a carga no API server no momento da falha. Em clusters de larga escala com dezenas de nodes e centenas de services, o gap pode ser consideravelmente maior.

🚀
Pro tip

Ative o recurso EndpointSlices (padrão desde o Kubernetes 1.21) e verifique se o kube-proxy do seu cluster esta configurado para usa-lo. EndpointSlices propagam atualizações de forma muito mais eficiente do que o objeto Endpoints clássico em clusters grandes.

Comparação com outras abordagens de alta disponibilidade

O Kubernetes não e a única camada que pode lidar com falhas de node. Veja como ele se compara a outras abordagens:

  • Load balancer externo com health check ativo: pode detectar e remover um backend em menos de 5 segundos com intervalos agressivos, sem depender da cadeia interna do Kubernetes. E a camada de proteção mais rápida disponível.
  • Service mesh (Istio, Linkerd): o proxy sidecar detecta conexões TCP fechadas quase instantaneamente e pode fazer retry automático em outro pod sem esperar o kube-proxy atualizar.
  • Circuit breaker na aplicação: padrões como Resilience4j ou implementações próprias podem absorver o período de falha sem propagar erros para o usuário final.

A estratégia mais robusta combina as três camadas. O Kubernetes cobre a recuperação automática com re-schedulamento dos pods, mas a responsabilidade de absorver o período de transição e das camadas acima.

Pontos positivos e limitações

Por que o Kubernetes funciona assim: o modelo baseado em reconciliação e eventual consistency e deliberado. Ele prioriza consistência e simplicidade operacional em vez de latência mínima de failover. Para a maioria dos workloads, 10 a 15 segundos de tráfego degradado e aceitável.

Limitações reais: para serviços com SLA de disponibilidade muito alto, o comportamento padrão do Kubernetes pode ser insuficiente. Serviços financeiros, de saúde e de comunicação crítica precisam das camadas adicionais para absorver o gap de propagação.

🔴
Cuidado

Reduzir agressivamente o nodeMonitorGracePeriod e o ciclo de heartbeat pode causar falsos positivos em nodes com carga alta de CPU que ficam lentos para enviar heartbeat. Ajuste esses valores com cuidado e monitore o comportamento em staging primeiro.

Casos de uso reais

Entender esse comportamento e crítico em vários cenários do dia a dia de operações:

  • Manutenção planejada de node: antes de drenar um node com kubectl drain, valide que o PodDisruptionBudget dos serviços críticos esta configurado corretamente para evitar indisponibilidade durante a redistribuição.
  • Auto-scaling agressivo: clusters com scale-down frequente de nodes precisam de terminationGracePeriodSeconds adequado para que os pods terminem graciosamente antes do node ser removido.
  • Deployments de alta frequência: equipes que fazem dezenas de deploys por dia precisam de readiness probes bem configuradas para que o tráfego só chegue em pods prontos, não em pods ainda inicializando.
  • Janelas de manutenção curtas: SREs que planejam janelas de manutenção precisam considerar o gap de propagação no calculo do tempo total de indisponibilidade esperado.

Dicas e boas práticas

💡
Dica

Configure readiness probes com failureThreshold: 1 e periodSeconds: 2 para que pods com problemas sejam removidos dos endpoints mais rapidamente, sem esperar os defaults mais conservadores.

💡
Dica

Use preStop hooks com um sleep de 5 a 10 segundos para dar tempo ao kube-proxy de remover o pod dos endpoints antes de o processo principal encerrar. Isso evita que o pod receba tráfego durante o shutdown.

🚀
Pro tip

Configure Pod Disruption Budgets (PDB) para garantir que o Kubernetes nunca evicte pods de um serviço crítico abaixo de um número mínimo de replicas. Isso não resolve o gap de detecção, mas garante que sempre haja pods saudáveis prontos para absorver o tráfego redistribuído.

Vale a pena otimizar o gap?

Para a maioria dos times, a resposta e sim, mas com as ferramentas certas. Otimizar os parâmetros do controller manager e do kube-proxy para detecção mais rápida faz sentido, mas o ganho real vem das camadas complementares: health check no load balancer e retry no service mesh.

O gap de 10 a 15 segundos que o Kubernetes tem por padrão não e um problema para workloads tolerantes a falhas. Mas entender onde esse tempo e gasto e o primeiro passo para decidir conscientemente se sua arquitetura precisa de mais proteção, ou se o comportamento padrão já e suficiente para o seu SLA.

O próximo passo prático: meça o gap real do seu cluster usando kubectl describe node combinado com um loop de requisições HTTP em paralelo. Você pode se surpreender tanto para melhor quanto para pior com o número que encontrar.