Blog

Esgotamento de porta efêmera: quando o servidor para de abrir conexão de saída

O serviço passou a devolver erro de conexão às onze e vinte de uma terça-feira comum, sem pico de tráfego, sem deploy, sem alerta de CPU e com o banco respondendo em dois milissegundos. O log dizia que não foi possível atribuir o endereço solicitado, o time reiniciou o processo, tudo voltou por quarenta minutos e quebrou de novo no mesmo formato. O que estava acabando não era memória, nem conexão de banco, nem descritor de arquivo: era porta de saída, um recurso finito que quase ninguém dimensiona e que ninguém monitora até o dia em que ele acaba. Este artigo mostra por que o esgotamento de porta efêmera é invisível nos painéis usuais e por que o gráfico de tráfego de entrada permanece plano enquanto ele acontece, qual é a tupla que realmente define a capacidade e por que o número de portas é só um dos quatro fatores, por que o estado de espera final existe e o que o encurtamento dele quebra de verdade, por que o cliente HTTP sem reuso de conexão é a causa em oito de cada dez incidentes e o que muda quando existe tradução de endereço no caminho, qual é a sequência de diagnóstico que separa fuga de conexão de demanda legítima, e quais indicadores dão antecedência suficiente para agir antes do primeiro erro.

2026-09-16 / Arquitetura / 18 min

01

O recurso que acaba primeiro não aparece em nenhum painel padrão

Todo painel de serviço mede as mesmas quatro coisas: uso de processador, uso de memória, latência e taxa de erro. Quando a porta efêmera acaba, três dessas quatro permanecem exatamente onde estavam. O processador fica baixo porque o processo não está trabalhando, está falhando cedo. A memória não se move porque nenhuma alocação nova aconteceu. A latência do que ainda passa continua normal porque o serviço que responde está saudável. Apenas a taxa de erro sobe, e ela sobe com uma mensagem que raramente é associada à sua causa real, porque a mensagem fala em endereço e a causa fala em porta.

A mensagem que o sistema operacional devolve é a de que não foi possível atribuir o endereço solicitado. Ela é gerada no momento em que o processo pede uma conexão de saída e o núcleo não encontra nenhuma porta de origem livre para associar àquela conexão. Nada nessa frase menciona porta, esgotamento ou limite, e é por isso que o erro é tão frequentemente diagnosticado como problema de rede, de DNS ou de firewall. O time olha para fora quando o recurso que acabou está dentro da própria máquina.

A segunda propriedade desagradável desse incidente é o formato de recuperação. Reiniciar o processo funciona, porque o reinício fecha todos os soquetes abertos pelo processo e libera as portas associadas. Isso cria uma narrativa enganosa dentro do time: o problema é resolvido por reinício, logo deve ser vazamento de memória, ou algum estado corrompido, ou uma biblioteca com defeito. Na verdade o reinício está apenas devolvendo o recurso ao sistema, e o intervalo entre reinícios é exatamente o tempo que o serviço leva para consumir a faixa inteira de portas de novo. Um intervalo estável de quarenta minutos entre falhas é uma assinatura forte desse esgotamento, porque ele indica consumo linear de um recurso finito e não um defeito aleatório.

Sintoma observadoDiagnóstico usual do timeCausa real quando é porta efêmeraVerificação que separa os dois
Erro de atribuição de endereço nas chamadas de saídaProblema de rede ou de resolução de nomeNenhuma porta de origem livre na faixa configuradaContar soquetes por estado na máquina de origem, não testar conectividade
Reinício resolve por dezenas de minutos e o erro voltaVazamento de memória ou estado corrompidoConsumo linear da faixa até o limite, zerado pelo reinícioMedir o intervalo entre falhas: estável indica recurso finito
Tráfego de entrada plano durante todo o incidenteFalha do serviço de destinoCada requisição de entrada abre várias conexões de saída novasComparar requisições por segundo com conexões de saída por segundo
Milhares de soquetes em espera finalConexões travadas que precisam ser mortasComportamento normal do protocolo após fechamento ativoVerificar se o total se aproxima do tamanho da faixa de portas
Só uma instância falha enquanto as outras seguem bemInstância defeituosa, basta substituirDistribuição desigual de destinos entre instânciasAgrupar conexões por endereço de destino em cada instância

A última linha da tabela é a que mais atrasa diagnóstico em ambiente com várias réplicas. Como o esgotamento depende da combinação entre origem e destino, e não apenas do volume total, é perfeitamente possível que uma instância que conversa predominantemente com um único destino quebre enquanto as vizinhas, com a mesma carga mas destinos mais espalhados, continuem saudáveis. Substituir a instância faz o sintoma sumir por alguns minutos e reforça a conclusão errada de que o problema era da máquina.

02

A capacidade real é uma tupla de quatro elementos, não um número de portas

A intuição de que existem sessenta e poucos mil portas e que portanto cabem sessenta e poucos mil conexões é errada em duas direções ao mesmo tempo, e entender por quê é o que permite dimensionar corretamente. Uma conexão é identificada por quatro valores: endereço de origem, porta de origem, endereço de destino e porta de destino. O que precisa ser único no sistema é a combinação dos quatro, e não a porta de origem isoladamente.

Isso significa que a mesma porta de origem pode ser reutilizada para destinos diferentes sem nenhum conflito. Uma máquina com trinta mil portas efêmeras disponíveis pode manter trinta mil conexões com um banco de dados e outras trinta mil com um serviço de pagamento simultaneamente, porque as tuplas diferem no endereço de destino. O limite prático, portanto, não é por máquina: é por par de origem e destino. Um serviço que fala com um único destino tem a capacidade mais baixa possível, e um serviço que espalha chamadas entre muitos destinos tem capacidade muito maior sem mudar nada na configuração.

Na direção contrária, a capacidade efetiva é menor do que a faixa sugere por causa do tempo de retenção. Uma porta liberada não volta imediatamente ao conjunto disponível: ela fica retida durante o estado de espera final, que em sistemas derivados de Linux dura sessenta segundos por padrão. A conta que interessa não é quantas portas existem, e sim quantas conexões novas por segundo podem ser abertas para o mesmo destino sem que a taxa de criação supere a taxa de liberação. O número é simples e costuma surpreender: a faixa dividida pelo tempo de retenção.

// Capacidade de conexoes novas por segundo para um mesmo destino.
// O limite nao e o numero de portas, e a taxa de reciclagem delas.

/**
 * @param {number} portaInicial primeiro valor da faixa efemera
 * @param {number} portaFinal   ultimo valor da faixa efemera
 * @param {number} retencaoSeg  segundos em espera final apos o fechamento
 * @param {number} destinos     quantos pares endereco:porta distintos recebem trafego
 */
export function capacidadeDeConexoesNovas({
  portaInicial = 32768,
  portaFinal = 60999,
  retencaoSeg = 60,
  destinos = 1,
}) {
  const faixa = portaFinal - portaInicial + 1;

  // Por destino distinto a faixa inteira volta a ficar disponivel, porque a
  // unicidade exigida e a da tupla de quatro elementos e nao a da porta.
  const portasUteis = faixa * destinos;

  // Uma porta fechada de forma ativa so retorna ao conjunto depois da
  // espera final. Em regime permanente, a taxa sustentavel e a razao
  // entre o estoque e o tempo que cada unidade fica indisponivel.
  const novasPorSegundo = Math.floor(portasUteis / retencaoSeg);

  return {
    faixa,
    portasUteis,
    novasPorSegundo,
    // Ponto de saturacao: acima disso o estoque encolhe a cada segundo
    // ate zerar, e o erro aparece quando ele zera, nao quando a taxa sobe.
    observacao: `Acima de ${novasPorSegundo} conexoes novas por segundo para ${destinos} destino(s), o estoque de portas encolhe de forma monotona.`,
  };
}

// Caso tipico de servico que fala com um unico balanceador interno:
// 28232 portas / 60s = 470 conexoes novas por segundo.
// Um servico que atende 500 req/s e abre uma conexao nova por requisicao
// ja esta acima do ponto de saturacao, com trafego de entrada considerado
// baixo por qualquer painel.

O resultado desse cálculo é o número mais útil do incidente inteiro, porque ele transforma uma discussão vaga sobre carga em um limite concreto que pode ser comparado com a métrica de requisições. Uma faixa padrão de vinte e oito mil portas com sessenta segundos de retenção sustenta pouco menos de quinhentas conexões novas por segundo para um mesmo destino. Um serviço que recebe quinhentas requisições por segundo e abre uma conexão nova a cada uma delas está exatamente no ponto de virada, e qualquer crescimento de cinco por cento no tráfego passa a consumir estoque em vez de reciclar.

ESTOQUE DE PORTAS EM REGIME PERMANENTE

  faixa = 28.232 portas    retencao = 60s
  taxa sustentavel = 28.232 / 60 = 470 conexoes novas/s

  taxa de abertura < 470/s          taxa de abertura > 470/s
  -----------------------           -----------------------
  disponivel  ~~~~~~~~~~~~~         disponivel  \
              estavel                            \
                                                  \
  espera final ~~~~~~~~~~~                          \____ 0
              plato                    erro aparece aqui,
                                       minutos depois da
                                       mudanca de taxa

  A defasagem entre a mudanca de taxa e o erro e o que faz o
  incidente parecer desconectado de qualquer alteracao recente.

A defasagem representada no diagrama é a razão pela qual esse incidente quase nunca é associado ao deploy que o causou. Se a faixa tem vinte e oito mil portas e o excesso é de cem conexões por segundo acima da taxa sustentável, o estoque leva quase cinco minutos para zerar. Se o excesso for de dez conexões por segundo, leva quase cinquenta minutos. Um deploy feito às dez horas produz erro às dez e cinquenta, e a essa altura ninguém mais está olhando para ele.

03

A espera final não é desperdício, e encurtá-la tem preço

A primeira reação de quase todo time diante de milhares de soquetes em espera final é tratá-los como lixo acumulado e procurar a configuração que os elimina. Vale entender antes por que esse estado existe, porque ele resolve dois problemas concretos e desativá-lo sem saber disso troca um incidente visível por um incidente silencioso e muito pior de diagnosticar.

O primeiro problema é o do pacote atrasado. A rede pode entregar fora de ordem e com atraso arbitrário, então um segmento da conexão antiga pode chegar depois que ela foi encerrada. Se a mesma tupla de quatro elementos já tiver sido reutilizada por uma conexão nova, esse segmento atrasado é entregue à conexão errada, e o resultado é corrupção de dados no nível da aplicação, não erro de rede. A espera final mantém a tupla reservada por tempo suficiente para que qualquer pacote antigo já tenha expirado na rede.

O segundo problema é o da confirmação perdida. Quem fecha ativamente precisa garantir que a confirmação final chegue ao outro lado, e se ela se perder o outro lado retransmite o pedido de fechamento. Sem a espera final, essa retransmissão chega a uma tupla que não existe mais e recebe uma recusa, o que faz o lado remoto encerrar a conexão de forma abrupta em vez de ordenada. Em um servidor que mantém estado por conexão, isso vira acúmulo de conexões meio abertas do outro lado do fio.

Ajuste consideradoO que ele realmente fazRisco que introduzQuando é defensável
Ampliar a faixa de portas efêmerasAumenta o estoque, elevando a taxa sustentável na mesma proporçãoConflito com portas de serviço fixas acima de trinta e dois milSempre, desde que a faixa não invada portas já usadas por serviços locais
Reutilizar soquete em espera final para conexão de saídaPermite reusar a tupla quando a nova conexão é comprovadamente posteriorBaixo em saída, exige marcação de tempo ativa nas duas pontasAjuste padrão recomendado antes de qualquer outro
Reciclagem agressiva por origemDescarta conexões com marcação de tempo considerada antigaQuebra clientes atrás de tradução de endereço compartilhadaPraticamente nunca: removido de núcleos recentes por causar mais dano que benefício
Reduzir o tempo de espera finalEncurta a janela de proteção contra pacote atrasadoCorrupção silenciosa em rede com reordenação realRede interna controlada e de latência baixa, como último recurso
Reusar conexão em vez de abrir novaElimina a criação de tuplas, atacando a causaNenhum, além de exigir configuração correta de tempo ociosoSempre: é a correção real, os demais são mitigação

A hierarquia da tabela é deliberada. A última linha é a única correção que remove a causa, e as quatro anteriores compram tempo com graus diferentes de risco. Um time que aplica apenas as mitigações resolve o incidente de hoje e reencontra o mesmo problema quando o tráfego dobrar, com a diferença de que não sobra mais nenhum parâmetro para ajustar.

04

O cliente que abre conexão nova a cada chamada é a causa em oito de cada dez casos

Quando a investigação chega ao código, o achado quase sempre é o mesmo: um cliente HTTP instanciado dentro da função que faz a chamada. Cada requisição cria um cliente, o cliente cria um pool próprio, o pool abre uma conexão, a resposta é lida e o cliente é descartado junto com a conexão. Do ponto de vista do desenvolvedor não há erro nenhum: o código é limpo, não tem estado global e não vaza memória. Do ponto de vista do sistema operacional, cada requisição de entrada acabou de consumir uma porta que ficará indisponível pelo próximo minuto.

Existe uma variação mais sutil que aparece em serviços que já usam cliente compartilhado e mesmo assim esgotam. O pool tem um limite de conexões ociosas mantidas, e quando esse limite é menor do que a concorrência real, as conexões que excedem o limite são fechadas em vez de devolvidas ao pool. O comportamento observado é um reuso parcial, com uma fração estável do tráfego abrindo conexão nova, e essa fração é suficiente para esgotar a faixa se a concorrência for alta. O sintoma distintivo é que o número de conexões em espera final cresce proporcionalmente ao tráfego, mas com um coeficiente menor que um.

// Cliente HTTP com reuso real de conexao em Node.js.
// O ponto critico nao e criar o agente, e dimensionar as conexoes
// ociosas para acima da concorrencia esperada.

import http from 'node:http';
import https from 'node:https';

// Concorrencia media esperada por instancia contra este destino.
// Abaixo dela o pool fecha conexoes que seriam reutilizadas, e a
// fracao fechada vira consumo continuo de portas efemeras.
const CONCORRENCIA_ESPERADA = 64;

const opcoesDeAgente = {
  keepAlive: true,

  // Conexoes ociosas mantidas. Se ficar abaixo da concorrencia, o
  // excedente e fechado ao inves de devolvido, e cada fechamento
  // custa uma porta retida por toda a espera final.
  maxSockets: CONCORRENCIA_ESPERADA * 2,
  maxFreeSockets: CONCORRENCIA_ESPERADA,

  // Tempo ocioso antes de fechar. Precisa ser menor que o tempo de
  // ociosidade aceito pelo servidor remoto, senao o cliente reusa uma
  // conexao que a outra ponta ja fechou e recebe erro de socket morto.
  keepAliveMsecs: 15_000,
  timeout: 30_000,

  // Distribui as requisicoes entre as conexoes livres em vez de
  // empilhar tudo na primeira, o que mantem o pool aquecido por igual.
  scheduling: 'lifo',
};

export const agenteHttp = new http.Agent(opcoesDeAgente);
export const agenteHttps = new https.Agent(opcoesDeAgente);

/**
 * Cliente de modulo, criado uma vez. Criar por requisicao e o erro que
 * produz esgotamento de porta sem nenhum sintoma no codigo.
 */
export async function chamarServicoInterno(caminho, corpo) {
  const resposta = await fetch(`https://servico-interno.local${caminho}`, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify(corpo),
    // Em Node, fetch usa o dispatcher global; para http/https nativos
    // o agente acima e passado diretamente nas opcoes da requisicao.
    dispatcher: undefined,
  });

  if (!resposta.ok) {
    // Ler o corpo mesmo em erro. Uma resposta cujo corpo nunca e
    // consumido pode manter a conexao ocupada ate o timeout, o que
    // reduz o pool efetivo e empurra o excedente para conexao nova.
    await resposta.text().catch(() => '');
    throw new Error(`servico interno respondeu ${resposta.status}`);
  }

  return resposta.json();
}

O comentário sobre consumir o corpo da resposta mesmo em caso de erro não é detalhe de estilo. Uma conexão cujo corpo não foi lido até o fim não pode ser devolvida ao pool, porque o protocolo não sabe onde termina a mensagem anterior. Muitos clientes tratam isso fechando a conexão, e o resultado é que o caminho de erro do serviço consome portas em um ritmo muito maior do que o caminho de sucesso. Isso produz um comportamento de realimentação cruel: uma degradação parcial no destino aumenta a taxa de erro, a taxa de erro aumenta o consumo de portas, e o esgotamento de portas transforma a degradação parcial em indisponibilidade total.

O outro cuidado é o tempo ocioso. Se o cliente mantém a conexão por mais tempo do que o servidor remoto aceita, o servidor fecha primeiro e o cliente descobre isso apenas ao tentar usar a conexão, recebendo um erro de soquete encerrado. Times que encontram esse erro costumam desligar o reuso inteiro para fazê-lo sumir, o que reintroduz o esgotamento. A correção correta é manter o tempo ocioso do cliente confortavelmente abaixo do tempo aceito pelo servidor, com uma diferença de pelo menos cinco segundos para cobrir variação de relógio e atraso de rede.

05

Tradução de endereço muda o dono do problema e o lugar onde ele aparece

Em ambiente de contêiner e de nuvem, o tráfego de saída raramente vai direto da máquina para o destino. Ele passa por uma camada de tradução de endereço, seja o gateway da rede virtual, seja a regra local que mascara o endereço do contêiner pelo endereço do nó. Essa camada precisa alocar uma porta própria para cada conexão traduzida, e é essa alocação, não a do processo, que passa a ser o recurso escasso.

A consequência prática é que o esgotamento deixa de ser um problema de uma máquina e passa a ser um problema compartilhado por todos os contêineres que saem pelo mesmo endereço traduzido. Um serviço barulhento pode consumir a faixa do gateway e derrubar conexões de serviços vizinhos que não mudaram nada, o que é exatamente o padrão de dano colateral que aparece em limite de taxa global. O diagnóstico fica mais difícil porque o erro aparece em um processo que não é o causador, e a métrica que explicaria o fenômeno está no gateway, geralmente fora do alcance do time de aplicação.

Existe ainda uma armadilha de contagem. Muitos gateways gerenciados publicam um número de portas por instância traduzida, e esse número é alocado em blocos por destino. Um serviço que conversa com poucos destinos, o caso comum de uma aplicação que fala com um banco gerenciado e um cache, consome um bloco inteiro por destino e atinge o teto com uma fração pequena do total anunciado. A leitura ingênua do número publicado leva o time a concluir que sobra capacidade enquanto o gateway já está recusando.

  • Mapeie por onde o tráfego de saída sai de fato: direto, por gateway gerenciado ou por mascaramento no nó, porque o recurso finito fica em lugares diferentes em cada caso.
  • Prefira ponto de acesso privado ao destino quando ele existir, porque isso remove a tradução do caminho e devolve a contagem de portas para a própria máquina.
  • Trate a faixa do gateway como recurso compartilhado com orçamento por serviço, não como capacidade infinita, e monitore a alocação por destino e não só o total.
  • Distribua os destinos quando possível, porque cada endereço de destino distinto multiplica a capacidade de tuplas disponível sem nenhuma mudança de configuração.
  • Verifique se a tradução está fazendo conexão de longa duração sobreviver ao tempo ocioso do próprio gateway, porque uma conexão derrubada silenciosamente vira reabertura e consumo de porta.

06

A sequência de diagnóstico que separa fuga de demanda legítima

Diante do erro, a tentação é ampliar a faixa de portas imediatamente. Isso funciona e é a coisa certa a fazer para conter o incidente, mas se for a única ação o problema volta com tráfego maior. A sequência abaixo é a que produz, no mesmo incidente, tanto a contenção quanto a evidência que orienta a correção definitiva.

  1. Confirme o recurso que acabou contando soquetes por estado na máquina que emitiu o erro, não na máquina de destino. Um total em espera final próximo do tamanho da faixa confirma o diagnóstico em segundos e elimina toda a investigação de rede.
  2. Agrupe as conexões por endereço de destino. Se um destino concentra a maior parte, a correção é reuso de conexão naquele cliente específico; se estão espalhadas, o caminho é ampliar faixa e revisar o tempo de retenção.
  3. Compare a taxa de conexões novas por segundo com a taxa de requisições recebidas. Uma razão próxima de um é a assinatura de cliente sem reuso, e uma razão muito maior que um indica que cada requisição abre várias conexões, tipicamente por chamada em série a serviços distintos.
  4. Contenha ampliando a faixa efêmera e habilitando o reuso de soquete em espera final para conexões de saída, que é o ajuste de menor risco e devolve capacidade imediata sem tocar na janela de proteção do protocolo.
  5. Corrija a causa movendo o cliente para escopo de módulo com reuso de conexão habilitado, e dimensione as conexões ociosas acima da concorrência esperada para evitar o reuso parcial que engana o diagnóstico.
  6. Feche o ciclo instrumentando a métrica de portas em uso como fração da faixa, porque é ela que transforma o próximo episódio em alerta com antecedência de minutos em vez de erro sem aviso.
#!/usr/bin/env bash
# Diagnostico de esgotamento de porta efemera.
# Rode na maquina que emitiu o erro de atribuicao de endereco.
set -euo pipefail

FAIXA=$(cat /proc/sys/net/ipv4/ip_local_port_range)
INICIO=$(echo "$FAIXA" | awk '{print $1}')
FIM=$(echo "$FAIXA" | awk '{print $2}')
TOTAL=$(( FIM - INICIO + 1 ))

echo "Faixa efemera: $INICIO-$FIM ($TOTAL portas)"
echo

# 1) Quantos soquetes existem em cada estado. Um valor de TIME-WAIT
#    proximo de TOTAL confirma o esgotamento sem ambiguidade.
echo "== Soquetes por estado =="
ss -tan | awk 'NR > 1 { print $1 }' | sort | uniq -c | sort -rn
echo

# 2) Para onde eles estao indo. Um destino dominante aponta o cliente
#    que precisa de reuso de conexao.
echo "== Top 10 destinos em TIME-WAIT =="
ss -tan state time-wait | awk 'NR > 1 { print $5 }' \
  | sed 's/:[0-9]*$//' | sort | uniq -c | sort -rn | head -10
echo

# 3) Ocupacao efetiva da faixa. Acima de 80 por cento ja e alerta,
#    porque a margem restante some em segundos sob rajada.
EM_USO=$(ss -tan | awk 'NR > 1 { print $4 }' \
  | sed 's/.*://' | awk -v i="$INICIO" -v f="$FIM" \
    '$1 >= i && $1 <= f { c++ } END { print c + 0 }')
PCT=$(awk -v e="$EM_USO" -v t="$TOTAL" 'BEGIN { printf "%.1f", (e / t) * 100 }')
echo "== Ocupacao =="
echo "Portas em uso na faixa: $EM_USO de $TOTAL ($PCT%)"
echo

# 4) Ajustes vigentes que mudam a taxa de reciclagem.
echo "== Parametros relevantes =="
echo "tcp_tw_reuse: $(cat /proc/sys/net/ipv4/tcp_tw_reuse)"
echo "tcp_fin_timeout: $(cat /proc/sys/net/ipv4/tcp_fin_timeout)"
echo "somaxconn: $(cat /proc/sys/net/core/somaxconn)"

O terceiro bloco desse script é o que costuma ser adaptado para virar métrica permanente. Exportar a ocupação da faixa como um valor percentual e alertar em oitenta por cento dá uma antecedência que varia de minutos a dezenas de minutos, dependendo da velocidade de consumo, e é sempre maior que zero, que é a antecedência que o time tem hoje. Vale exportar junto a contagem agrupada por destino, porque quando o alerta disparar a primeira pergunta será para onde as conexões estão indo, e ter a resposta pronta economiza a parte mais demorada do diagnóstico.

Uma observação sobre o segundo passo que evita uma conclusão errada frequente. Um total alto em espera final não é por si só um problema: ele é o resultado esperado de um serviço que fecha muitas conexões ativamente e indica apenas que o fechamento está acontecendo do lado certo. O que caracteriza o incidente é a proporção entre esse total e o tamanho da faixa. Vinte mil soquetes em espera final com uma faixa de sessenta mil é operação normal, e os mesmos vinte mil com uma faixa de vinte e dois mil é um serviço a minutos de parar.

FAQ

Perguntas frequentes

Ampliar a faixa de portas efêmeras para o máximo possível tem alguma desvantagem?

Tem uma, e ela é concreta o suficiente para merecer verificação antes de aplicar o ajuste em produção. A faixa efêmera é o conjunto de portas que o núcleo escolhe automaticamente quando ninguém pede uma porta específica, e ampliar o início dela para valores baixos faz com que o núcleo possa atribuir a uma conexão de saída exatamente a porta que algum serviço local pretende usar como porta de escuta. O resultado é um serviço que falha ao iniciar dizendo que o endereço já está em uso, de forma intermitente e dependente do momento do reinício, que é um dos modos de falha mais difíceis de reproduzir que existem. O procedimento seguro tem três partes. A primeira é inventariar todas as portas fixas usadas na máquina, incluindo as de agentes de monitoramento, as de ferramentas de depuração e as de qualquer serviço auxiliar que costuma ficar acima de trinta mil, porque a colisão quase sempre acontece nessa faixa alta e não nas portas conhecidas abaixo de mil. A segunda é declarar essas portas como reservadas no parâmetro apropriado do núcleo, o que faz a alocação automática pular exatamente esses valores mesmo que eles estejam dentro da faixa ampliada, resolvendo o conflito sem abrir mão da capacidade. A terceira é ampliar a faixa começando pelo limite superior, que costuma ser seguro por padrão, antes de mexer no limite inferior, onde mora o risco. Com esses cuidados, uma faixa que vai de dez mil a sessenta e cinco mil é perfeitamente operável e mais que dobra a taxa sustentável de conexões novas. Vale lembrar que essa ampliação continua sendo mitigação: ela empurra o ponto de saturação para frente, e o serviço que abre uma conexão nova por requisição vai reencontrá-lo com o dobro do tráfego.

Como isso aparece em um cluster de contêineres, onde cada pod tem a própria pilha de rede?

Aparece de três formas distintas, e confundi-las é o que faz o diagnóstico demorar. A primeira forma é a mais parecida com o caso tradicional: quando o pod tem o próprio espaço de rede com endereço roteável, a faixa efêmera é a do espaço de rede dele e o esgotamento é local, o que na prática é uma boa notícia porque isola o dano ao serviço causador e mantém o diagnóstico dentro do alcance do time. A segunda forma acontece quando a saída passa por mascaramento no nó, que é o padrão de muitas instalações: aí o recurso finito é a faixa do nó, compartilhada por todos os pods que saem por ele, e um serviço barulhento derruba vizinhos que não mudaram nada. O sintoma característico é o erro aparecer em pods de aplicações diferentes ao mesmo tempo, sem correlação entre elas a não ser o nó em que rodam, e a confirmação vem de contar as conexões no espaço de rede do nó em vez do pod. A terceira forma é a do gateway gerenciado de saída da nuvem, onde a alocação é feita em blocos por destino e o teto anunciado nunca é atingível na prática por um serviço que fala com poucos destinos; nesse caso a métrica que importa não existe dentro do cluster e precisa vir do provedor. As correções acompanham a forma: para a primeira, ajuste da faixa no espaço de rede do pod e reuso de conexão no cliente; para a segunda, a mesma coisa no nó mais um orçamento de conexões por pod para que o isolamento não dependa de boa vontade; para a terceira, ponto de acesso privado ao destino, que remove a tradução do caminho e é quase sempre também mais barato e mais rápido do que sair pela internet.

O mesmo problema existe em conexões de banco de dados com pool, ou o pool já resolve?

O pool resolve o caso normal e deixa passar três situações que produzem exatamente o mesmo esgotamento, e todas as três são comuns o bastante para valer a verificação. A primeira é o pool com validação agressiva, configurado para descartar e recriar conexões ociosas em intervalos curtos como forma de evitar conexões mortas. Se o intervalo for de poucos minutos e o pool tiver dezenas de conexões por instância, com dezenas de instâncias, a taxa de recriação vira um fluxo constante de tuplas novas para o mesmo destino, que é a pior configuração possível para porta efêmera. A segunda é o pool com tempo de vida máximo por conexão, um ajuste legítimo para permitir que mudanças de destino sejam absorvidas depois de um failover, mas que precisa vir com dispersão aleatória: sem ela todas as conexões criadas no mesmo momento do deploy expiram no mesmo momento, e a instância recria o pool inteiro de uma vez em uma rajada que pode consumir uma fração relevante da faixa em um segundo. A terceira, e a mais frequente, é o pool que não cobre todo o tráfego: consultas passam pelo pool, mas a migração, o script de manutenção, o processo de relatório e a verificação de saúde abrem conexões próprias fora dele, e uma verificação de saúde que abre e fecha uma conexão a cada cinco segundos por instância produz, com cem instâncias, vinte conexões novas por segundo para o mesmo destino sem que nenhuma métrica de aplicação registre isso. A recomendação prática é fazer a verificação de saúde reutilizar uma conexão do próprio pool em vez de abrir a sua, dispersar o tempo de vida máximo com uma variação aleatória de pelo menos dez por cento, e contar as conexões por destino em vez de confiar na métrica de conexões ativas do pool, que só enxerga o que passa por ele.

Porta efêmera é capacidade, e capacidade que ninguém mede é capacidade que acaba sem aviso

O esgotamento de porta de saída é o incidente que quebra um serviço saudável sem mover nenhum dos quatro indicadores que o painel mostra, e que se disfarça de problema de rede porque a mensagem de erro fala em endereço quando a causa é taxa de reciclagem. Entender que a capacidade é a faixa dividida pelo tempo de retenção, e multiplicada pelo número de destinos distintos, transforma uma discussão vaga em um limite comparável com a métrica de requisições. Ampliar a faixa contém, reusar conexão corrige, e medir a ocupação como fração da faixa é o que troca um erro sem aviso por um alerta com minutos de antecedência. Posso levantar onde o seu tráfego de saída realmente sai, calcular a taxa sustentável por destino, corrigir os clientes que abrem conexão por requisição e instrumentar os indicadores que dão antecedência antes do próximo episódio.