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.
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.
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.
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.
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.
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
terminationGracePeriodSecondsadequado 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
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.
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.
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.
Comentários
Deixar um comentárioVocê precisa ter uma conta no CuritibaBlog para comentar.