← Back to list

Silent drops no Kubernetes: TCP keepalive e os 7200 segundos que sua aplicação não vai querer…

Por que o kernel do Linux leva 2h11min pra detectar uma conexão encerrada — e como corrigir isso com TCP keepalive no Kubernetes

Johnny Tardin · 2026-06-01 18:09 · 14 claps · 10.7 min read
#kubernetes #networking #tcp #performance #devops
Open on Medium ↗
Wiki topics: ☁️ · DevOps & Cloud 🔓 · Open Source

Silent drops no Kubernetes: TCP keepalive e os 7200 segundos que sua aplicação não vai querer esperar

Imagem gerada com IA

Imagem gerada com IA

Por que o kernel do Linux leva 2h11min pra detectar uma conexão encerrada — e como corrigir isso com TCP keepalive no Kubernetes

Por duas horas e onze minutos, sua aplicação fica conversando com uma conexão que já foi finalizada. Isso é o padrão. Em qualquer container Linux rodando agora mesmo, em qualquer cluster Kubernetes, em qualquer cloud.

Não é exagero — é literalmente o que vem configurado default. Quando o kernel do Linux abre uma conexão TCP, ele só vai começar a se perguntar “isso aqui ainda tá vivo?” depois de 7200 segundos de silêncio. E mesmo aí, ele faz isso com calma: nove tentativas, uma a cada 75 segundos. Some tudo e dá 2 horas, 11 minutos e 15 segundos entre a interrupção real da conexão e o momento em que o SO admite que ela caiu.

Esse número fazia sentido quando foi escolhido, no fim dos anos 80. Sessões TCP eram caras, raras, longas. Você abria um telnet pra um servidor e ficava lá horas. Um pacote de keepalive era considerado custo desnecessário.

Hoje, esse padrão é catastrófico. Ajustar o TCP keepalive resolve boa parte do problema, mas pra fazer isso direito você precisa entender o que está acontecendo por baixo — por que o kernel age assim, o que mudou na infraestrutura moderna e onde a solução para de funcionar. É sobre isso aqui.

Quando conexões mentem

Pensa numa ligação telefônica em que a outra pessoa desligou, mas a linha ficou aberta. Você continua falando. Ouve silêncio. Acha que ela tá ouvindo. Espera. Quando finalmente cai a ficha, já se passou tempo demais.

É exatamente o que acontece em produção todo dia, em camada TCP. A gente chama isso de silent drop: a conexão foi finalizada silenciosamente — nenhuma das pontas foi notificada. Não veio um FIN avisando o encerramento gracioso, nem um RST. Nenhum erro em camada nenhuma. O netstat continua mostrando ESTABLISHED, mas a conexão do outro lado já não existe.

Pro time que desenvolve a aplicação, isso aparece como pico de latência sem explicação, como timeout intermitente que ninguém consegue reproduzir, como erro que misteriosamente acontece quando o connection pool tenta reciclar, ou no clássico “agora voltou, sei lá por quê”. Parece infraestrutura instável. Não é. É o kernel esperando duas horas e onze minutos pra notar que a conexão foi finalizada há muito tempo.

LINHA DO TEMPO DE UM SILENT DROP
t=0s     │ Cliente ──► Conexão estabelecida ──► Servidor
         │
t=120s   │ Gateway NAT remove o estado (sem tráfego)
         │ Servidor ainda acredita que a conexão está ativa
         │ Cliente ainda acredita que a conexão está ativa
         │ ✂ Conexão encerrada pelo NAT. Ninguém foi notificado.
         │
t=240s   │ Cliente.write() ──X (silêncio)
         │ Aplicação espera...
         │
t=...    │ Eventualmente: timeout da aplicação dispara
         │ Requisição falha. Retry. Nova conexão.
         │ Usuário viu um travamento de 30 segundos.

O problema mora naquele intervalo entre o t=120s (quando a conexão foi finalizada silenciosamente) e o instante em que sua aplicação descobre. Quanto maior esse intervalo, mais sua aplicação age sobre informação obsoleta — e é aí que a coisa fica cara.

O que o keepalive faz, de verdade

Antes de mexer em qualquer coisa, vale entender o mecanismo. O TCP keepalive é o heartbeat que o kernel manda quando uma conexão fica parada. Ele é controlado por três parâmetros que aparecem na fórmula:

detecção = tcp_keepalive_time + (tcp_keepalive_intvl × tcp_keepalive_probes)

O tcp_keepalive_time é o tempo de ociosidade que precisa passar antes do kernel mandar o primeiro probe. O tcp_keepalive_intvl é o intervalo entre probes, caso o primeiro não obtenha resposta. E o tcp_keepalive_probes é quantas tentativas o kernel faz antes de desistir.

Na prática, depois de tcp_keepalive_time segundos parados, o kernel manda um ACK fora de ordem de propósito (com sequence number menor que o esperado) pra forçar uma resposta. Se a outra ponta ainda existe, ela responde — ou com ACK, dizendo "tô aqui", ou com RST, dizendo "não conheço essa conexão". Se vier silêncio, o kernel espera tcp_keepalive_intvl e tenta de novo. Depois de tcp_keepalive_probes falhas seguidas, ele desiste e entrega ETIMEDOUT pra aplicação.

Tem uma sutileza importante: o keepalive só age quando a conexão está idle. Tráfego ativo já funciona como heartbeat implícito, então probes não disparam enquanto você estiver usando a conexão. O mecanismo já está embutido no kernel — não precisa instalar nada, só ajustar os valores.

Com keepalive_time=60, intvl=10, probes=6, o cenário fica assim:

t=0s    ── Conexão idle ────────────────────────
t=60s   ► Probe 1   (sem resposta)
t=70s   ► Probe 2   (sem resposta)
t=80s   ► Probe 3   (sem resposta)
t=90s   ► Probe 4   (sem resposta)
t=100s  ► Probe 5   (sem resposta)
t=110s  ► Probe 6   (sem resposta)
t=120s  ❌ Conexão declarada inativa → ETIMEDOUT

De onde vêm as 2h11min

Se você abrir um terminal em qualquer distribuição Linux mainstream e rodar:

cat /proc/sys/net/ipv4/tcp_keepalive_time      # 7200
cat /proc/sys/net/ipv4/tcp_keepalive_intvl     # 75
cat /proc/sys/net/ipv4/tcp_keepalive_probes    # 9

A conta é a seguinte: 7200 + (75 × 9) = 7875 segundos. Duas horas, onze minutos, quinze segundos.

Esses valores vêm da época do RFC 1122 e dos primeiros tempos do Linux. Eles foram pensados pra sessões de terminal longas, conexões que legitimamente ficavam idle por horas, e redes em que cada pacote tinha custo perceptível. Eram defaults razoáveis pra um mundo em que keepalive era visto como desperdício.

Esse mundo não existe mais. Os defaults foram desenhados antes de containers, antes da pulverização da infraestrutura em microsserviços, antes de NAT em cloud com timeouts de poucos minutos, antes de load balancers fazendo eviction agressiva de conexões, antes do HTTP/2 e do gRPC dependerem de conexões persistentes pra multiplexar streams. A janela em que o kernel espera pra detectar uma interrupção de conexão é hoje muito maior que a janela em que essa interrupção de fato ocorre:

COMPORTAMENTO PADRÃO              REALIDADE EM CLOUD
─────────────────                 ─────────────────────
0────────────────► 2h 11m         0───► 4-5 min: NAT evicta
                                  0───► 60-350s: idle do LB
                                  0───► segundos: blips dropam pacotes

O kernel está fazendo exatamente o que foi programado pra fazer. O problema é que ele foi programado pra uma internet que não é mais a sua.

Por que isso ficou tão comum

Em 1995, silent drops eram raros porque a topologia era simples: uma máquina falava com outra, talvez com um roteador no meio que não mantinha estado. Hoje, entre o cliente e o servidor existem várias camadas que podem finalizar uma conexão silenciosamente.

Quase toda cloud moderna roda algum tipo de connection tracking nos seus gateways NAT, com idle timeouts típicos entre 240 e 350 segundos. Quando o estado expira, a conexão é finalizada silenciosamente — sem notificação pras pontas. Load balancers têm comportamento parecido, e os números variam de provedor pra provedor:

  • AWS NLB → 350 segundos
  • AWS ALB → 60 segundos
  • Google Cloud Load Balancer → 600 segundos
  • Azure Load Balancer → 240 segundos

Some isso a stateful firewalls que descartam entradas quando enchem a connection tracking table, a restarts de pods que matam o listener mas deixam o cliente achando que tudo segue normal, a túneis VPN e redes overlay onde cada camada adiciona um pouco mais de fragilidade. Cada um desses lugares é um candidato a finalizar a conexão silenciosamente, sem FIN nem RST.

Do outro lado, as aplicações modernas dependem cada vez mais de conexões reutilizadas. Connection pools dos drivers de banco mantêm dezenas de conexões idle prontas pra uso. HTTP/2 e gRPC multiplexam tudo numa conexão long-lived — se ela for finalizada silenciosamente, tudo trava junto. Sidecars de service mesh mantêm conexões persistentes pra cada vizinho. Brokers como Kafka rebalanceiam consumers quando uma conexão cai. Quanto mais sua stack se apoia em reuso de conexão, mais ela fica vulnerável a esse problema.

Como aplicar no Kubernetes

A boa notícia é que ajustar TCP keepalive virou trivial. Historicamente, dava trabalho: você teria que mexer no código da aplicação (e em toda biblioteca cliente que ela usa), ou configurar sysctls cluster-wide nos nós, ou rodar um init container privilegiado pra setar isso por pod. Nenhuma dessas opções é boa.

Desde o Kubernetes 1.29 (rodando em kernel 4.5+), os sysctls de TCP keepalive entraram pro conjunto safe. Isso quer dizer que você pode setar eles diretamente no securityContext.sysctls do Pod, sem flag de kubelet, sem habilitação de admin, sem container privilegiado. Sysctls safe são aqueles que ficam isolados no network namespace do Pod — eles não conseguem vazar pra outros pods nem afetar o nó. Antes da 1.29, essa categoria era mais restrita; agora ela inclui o que importa pra esse caso.

Na prática, é isso aqui:

apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      securityContext:
        sysctls:
          - name: net.ipv4.tcp_keepalive_time
            value: "60"
          - name: net.ipv4.tcp_keepalive_intvl
            value: "10"
          - name: net.ipv4.tcp_keepalive_probes
            value: "6"
      containers:
        - name: app
          image: sua-app

Os valores entram em vigor assim que o Pod sobe e valem pra todos os containers dele. Uma coisa pra ficar atento: isso não funciona com hostNetwork: true, porque nesse caso o Pod usa o namespace do host, e você não devia mexer nos sysctls do host por aí mesmo. Se seu cluster tem Pod Security Standards mais restritivos ou OPA Gatekeeper bloqueando sysctls, vale checar antes.

Por que 60, 10 e 6

Cada parâmetro responde a uma restrição concreta de infraestrutura.

Os 60 segundos do tcp_keepalive_time precisam ser menores que o idle timeout mais agressivo no caminho — tipicamente NAT em 4-5 minutos. Mas não pode ser baixo demais, senão o probe dispara em todo pequeno período idle do tráfego normal e você fica gastando ciclo à toa. Sessenta segundos é o ponto em que essas duas pressões se equilibram pra maioria das workloads em cloud.

Os 10 segundos do intervalo entre probes é uma escolha entre detectar rápido e tolerar pequenas flutuações de rede. Curto demais (tipo 2–3 segundos) e qualquer blip de pacote vira false positive. Longo demais e você acaba esperando minutos pra confirmar uma interrupção que já é óbvia. Dez segundos absorve flutuação normal sem arrastar a detecção.

Os 6 probes seguem a mesma lógica. Mais probes dá mais confiança, menos probes dá detecção mais rápida. Seis é o ponto em que a maioria das redes reais funciona bem.

Tudo isso amarra numa restrição final que precisa ser respeitada: o tempo total de detecção (no caso, 120 segundos) tem que ser menor que o idle timeout do LB/NAT no seu caminho. Se a detecção demora mais que o idle do LB, o silent drop acontece antes do keepalive sequer começar a sondar — e aí o ajuste vira decorativo. Pra um NLB da AWS com 350s, sobra folga confortável. Pra um ALB com 60s de default, você precisaria de tcp_keepalive_time bem menor, ou aumentar o idle do LB, ou partir pra keepalive em camada de aplicação.

A regra prática é simples: olhe o menor idle timeout no caminho da sua conexão, e configure o keepalive pra fechar detecção antes disso, com folga.

Antes e depois

Quando isso entrou em produção, as melhorias apareceram em vários lugares: queda visível de picos em p95 e p99, redução de erros de conexão (broken pipe, connection reset, i/o timeout), menos retry storms, menos connection pool exhaustion. Nada mudou no throughput ou no consumo de recursos — keepalive é barato, são pacotes minúsculos —, e nem uma linha de código de aplicação precisou ser tocada.

A explicação por trás do número é direta: como a detecção da interrupção caiu de horas pra 2 minutos, os connection pools reciclam o socket inativo antes que a aplicação tente reusá-lo em escala. Os retries acontecem cedo, quando o sistema ainda não está degradado. E a cauda de latência encolhe porque menos requisições ficam bloqueadas esperando timeout num socket que já não responde.

p95 latência (ms)
    │
600 ┤        ╱╲    ╱╲              ANTES
    │  ╱╲   ╱  ╲  ╱  ╲     ╱╲
400 ┤ ╱  ╲ ╱    ╲╱    ╲   ╱  ╲
    │╱    ╲              ╲ ╱
200 ┤                     ╳ ← Deploy da config
    │                    ╱ ╲
100 ┤                   ╱   ╲___________ DEPOIS
    │                  ╱
  0 └────────────────────────────────────►
                                 tempo

Vale ser honesto sobre o escopo dessa correção. Ela resolve uma classe específica de problema: timeout causado por conexão idle finalizada silenciosamente. Não vai consertar deadlock na aplicação, downstream lento, fila travada, nem garbage collection mal configurado. Se você aplicar isso e seus timeouts continuarem do mesmo jeito, o gargalo é outro.

A história dos valores configurados

Os valores 60/10/6 não foram a primeira tentativa.

A primeira rodada usou números mais conservadores: tcp_keepalive_time=150, intvl=30, probes=3, com detecção total em 240 segundos. Foi aplicado só em ambientes inferiores, e ainda configurável por aplicação. A lógica era minimizar risco de falsos positivos caso os valores estivessem errados — desconectar conexão ativa por engano teria sido pior do que o problema original.

O que se aprendeu rodando assim por um tempo: quatro minutos de detecção ainda eram lentos demais. Eram melhores que 2h11min, claro, mas no fluxo real a maior parte do dano percebido pelo usuário já tinha ocorrido quando o sistema finalmente detectava que a conexão havia caído. Falso positivos, por outro lado, simplesmente nunca apareceram. Em redes minimamente estáveis, 60/10/6 não causa desconexão indevida. E aquela flexibilidade “configurável por aplicação” que parecia segura no início virou superfície de erro: ninguém usou, ninguém validou, e ela só servia pra que alguém eventualmente colocasse o valor errado.

A segunda iteração foi mais firme. Apertou pra 60/10/6, removeu a configurabilidade, e foi aplicada como default em todos os ambientes e todos os tipos de workload. A decisão foi tratar isso como configuração de plataforma, não como tuning de aplicação — porque a restrição real (idle timeout do LB) é de infraestrutura, não de código.

A lição transferível é essa: comece conservador quando o risco for desconhecido, mas não fique conservador depois que os dados mostram que dá pra apertar. E desconfie de configurabilidade que ninguém pediu — ela quase sempre é dívida disfarçada de flexibilidade.

Como verificar que funcionou

Setar o sysctl no Pod spec não é garantia de que ele realmente está ativo no container — vale a pena confirmar.

Dentro do pod, você consegue ler direto do /proc:

cat /proc/sys/net/ipv4/tcp_keepalive_time
cat /proc/sys/net/ipv4/tcp_keepalive_intvl
cat /proc/sys/net/ipv4/tcp_keepalive_probes

Se os valores aparecerem como 7200, 75 e 9, o sysctl não foi aplicado e algo está bloqueando (versão do K8s, política de admission, hostNetwork). Se aparecerem os valores configurados, está ativo.

Pra inspecionar conexões reais e ver o timer do keepalive em ação:

ss -o state established

Conexões com keepalive ativo mostram algo tipo timer:(keepalive,55sec,0), indicando quanto falta pro próximo probe.

E, no nível da aplicação, o que vale a pena observar é a redução em métricas de erro de conexão, a estabilidade do connection pool (razão idle/active ao longo do dia), e o comportamento das caudas de p95/p99.

Quando keepalive não basta

TCP keepalive resolve um problema bem específico, e vale saber onde ele para.

Ele só age em conexões idle. Se a conexão está mandando dados ativamente mas o receptor está travado (processo em deadlock, GC longo, fila cheia), o keepalive nem entra em cena — você precisa de TCP_USER_TIMEOUT (que controla quanto tempo o socket espera por ACK de dados enviados) ou de timeouts em camada de aplicação.

Se o protocolo já tem heartbeat próprio em camada superior, geralmente é melhor usar ele. HTTP/2 tem PING frames. gRPC tem keepalive embutido no client e server, com defaults razoáveis. A maioria dos drivers modernos de banco (pgx, HikariCP e outros) também tem keepalive próprio que vale configurar. Service meshes como Envoy expõem controles de keepalive no proxy.

TCP keepalive cobre justamente o caso de conexões finalizadas silenciosamente que nada mais detecta. Mas não substitui timeouts de aplicação, circuit breakers, nem heartbeats de protocolo. O ideal é usar em conjunto.

Fechando

Defaults de kernel são artefatos da época em que foram escritos. Os valores de TCP keepalive refletem uma internet de 1989 — sessões longas, redes confiáveis, custo por pacote. Hoje você opera com NAT em cloud, load balancers evictando conexões a cada poucos minutos, e toda uma stack de microsserviços que depende de reuso de conexão pra funcionar direito. O descompasso entre o que o kernel assume e o que a sua infraestrutura faz é o que gera conexões finalizadas silenciosamente sem que ninguém perceba.

Se você está caçando timeout intermitente, retry storm sem origem clara ou pico de latência que aparece e some sem explicação, olhe pro keepalive antes de culpar a aplicação ou o downstream. Com alguma frequência, o sistema operacional está esperando pacientemente — por duas horas — pra te contar que uma conexão caiu faz tempo.

Referências


메타데이터
post_id
a371dea1be2d
slug
silent-drops-no-kubernetes-tcp-keepalive-e-os-7200-segundos-que-sua-aplicação-não-vai-quere-a371dea1be2d
url
https://medium.com/@johnnytardin/silent-drops-no-kubernetes-tcp-keepalive-e-os-7200-segundos-que-sua-aplica%C3%A7%C3%A3o-n%C3%A3o-vai-quere-a371dea1be2d
canonical_url
https://medium.com/@johnnytardin/silent-drops-no-kubernetes-tcp-keepalive-e-os-7200-segundos-que-sua-aplica%C3%A7%C3%A3o-n%C3%A3o-vai-quere-a371dea1be2d
author_url
https://medium.com/@johnnytardin
status
ok
fetched_at
2026-06-09 15:37:30