↓Pular para o conteúdo principal
  1. blog/
ARTIGO TÉCNICO

Too many open files, upstream timed out e CPU alta: analisando uma degradação sob concorrência

Caso real de degradação em Nginx e PHP-FPM com Too many open files, upstream timed out, 5xx e CPU/load altos — sem transformar o primeiro erro encontrado em causa raiz.

Tem incidente em que o log praticamente grita uma resposta.

Nesse caso, o Nginx estava registrando ao mesmo tempo:

1
2
3
4
5
accept4() failed (24: Too many open files)
socket() failed (24: Too many open files) while connecting to upstream
upstream timed out while connecting to upstream
recv() failed while reading response header from upstream
upstream prematurely closed connection

Seria muito fácil parar na primeira linha e fechar a análise com:

“faltou file descriptor.”

Só que o cenário tinha mais coisa acontecendo.

A monitoria também mostrava CPU/load elevados, o volume de requisições na origem havia crescido bastante e o impacto estava concentrado em rotas dinâmicas atendidas por FastCGI/PHP-FPM.

Então o trabalho virou separar três perguntas diferentes:

  1. como a degradação se manifestou?
  2. quais recursos estavam sob pressão?
  3. o que as evidências realmente permitem chamar de causa?

O sintoma visto pelo usuário #

Durante a janela crítica, a aplicação apresentou erro ou lentidão principalmente em fluxos dinâmicos, como home, cadastro, login e autenticação.

A origem registrou respostas:

  • HTTP 500;
  • HTTP 502;
  • HTTP 504.

No backend, o Nginx encaminhava as requisições para FastCGI/PHP-FPM em loopback.

Ou seja, o caminho simplificado era:

1
2
3
4
5
6
7
8
9
cliente
  ↓
borda/CDN
  ↓
Nginx na origem
  ↓
FastCGI / PHP-FPM
  ↓
aplicação / banco

Quando a origem começou a degradar, o Nginx passou a falhar tanto ao aceitar novas conexões quanto ao conversar com o upstream.

Primeiro fato forte: file descriptors estavam entrando no modo de falha #

O error log continha centenas de ocorrências relacionadas a descritores e conexão com upstream.

Amostras sanitizadas:

1
2
3
[crit] accept4() failed (24: Too many open files)
[alert] socket() failed (24: Too many open files) while connecting to upstream
[error] upstream timed out while connecting to upstream

Isso prova que, durante a janela, o Nginx encontrou erro de limite de arquivos/descritores ao tentar aceitar ou criar sockets.

Mas vale a distinção:

provar que houve Too many open files não prova, sozinho, que esse foi o único mecanismo responsável pela indisponibilidade.

O mesmo período também tinha sinais claros de pressão de CPU e concorrência.

Segundo fato: a origem estava muito mais carregada #

O access log disponível era global da origem e não registrava o campo Host.

Isso muda como os números precisam ser interpretados.

Não dá para chamar uma contagem daquele arquivo de “visitantes do site”. O que ele mede é requisição HTTP recebida pela origem Nginx.

Na comparação feita durante a análise, a janela crítica apresentou volume de requisições várias vezes maior do que uma janela comparável anterior.

Esse dado não serve para medir audiência.

Serve para sustentar outra afirmação: a origem estava processando uma carga muito acima do padrão comparado naquele intervalo.

A correlação específica com a aplicação foi feita pelo error log, porque ali os campos de host e server estavam presentes.

Essa é uma boa lição de troubleshooting: antes de usar uma métrica, confirme exatamente o que ela mede.

Terceiro fato: CPU e load também subiram #

A monitoria registrou elevação significativa de CPU/load no período.

Em uma coleta posterior do mesmo tipo de ambiente, era possível observar muitos processos PHP-FPM simultâneos, load alto e CPU quase totalmente ocupada.

Isso reforça que o evento não deveria ser tratado apenas como “um número baixo de nofile”.

A capacidade de atender requisições dinâmicas dependia de várias camadas ao mesmo tempo:

1
2
3
4
5
6
sockets / file descriptors
CPU
Nginx
PHP-FPM
aplicação
banco

Quando há concorrência alta, uma limitação pode amplificar a outra.

O erro que eu não queria cometer: culpar PHP-FPM sem evidência #

Como os timeouts aconteciam entre Nginx e FastCGI, PHP-FPM era um suspeito natural.

Então procuramos um sinal bem específico:

1
server reached pm.max_children

Ele não apareceu na busca realizada.

Isso não prova que PHP-FPM estava folgado.

Prova apenas que não havia evidência direta, naquela coleta, de esgotamento explícito de workers pelo mecanismo registrado como pm.max_children.

Os erros de upstream mostram que a comunicação Nginx → PHP-FPM sofreu durante a degradação. Não permitem transformar “PHP-FPM” em causa isolada só porque ele está no caminho.

E o banco? O Slow Query Log estava desligado #

Outra hipótese óbvia era banco lento.

Ao verificar a configuração, o Slow Query Log estava desabilitado.

Em termos práticos:

1
slow_query_log = OFF

Isso cria uma limitação forense importante.

Depois do evento, sem histórico de slow query para aquela janela, não havia evidência suficiente para confirmar ou descartar queries lentas como fator contribuinte.

A conclusão correta não era:

O banco não teve slow query.

Era:

Não havia histórico adequado para responder essa pergunta com segurança.

Incidente também ensina pela evidência que faltou.

O que o conjunto de evidências sustentava #

A leitura consolidada ficou mais forte quando deixamos de procurar “o erro vencedor”.

O que estava comprovado:

  • houve crescimento forte de carga HTTP na origem na janela analisada;
  • as rotas dinâmicas concentraram grande parte do impacto;
  • apareceram respostas 500, 502 e 504;
  • o Nginx registrou Too many open files e falha ao criar sockets;
  • houve upstream timed out e outras falhas na comunicação com FastCGI/PHP-FPM;
  • a monitoria registrou pressão de CPU/load;
  • não foi encontrado server reached pm.max_children na busca realizada;
  • o Slow Query Log não estava habilitado para permitir análise histórica de queries lentas.

O comportamento era compatível com degradação da origem sob alta concorrência, envolvendo pressão de processamento e dificuldade de manter/encaminhar conexões para o backend.

Isso é mais preciso do que escolher uma linha do log e transformá-la em RCA inteira.

Como eu investigaria esse tipo de incidente #

A sequência que mais ajuda é começar pelo estado e ir reduzindo o problema por camada.

1. Confirmar pressão de CPU, memória e load #

1
2
3
uptime
free -m
ps -eo pid,ppid,user,stat,%cpu,%mem,etime,cmd --sort=-%cpu | head -40

Se houver histórico de monitoramento, correlacionar exatamente com a janela do erro.

2. Procurar os modos de falha no Nginx #

1
2
grep -Ei 'too many open files|socket\(\) failed|upstream timed out|prematurely closed|recv\(\) failed' \
  /var/log/nginx/error.log

O objetivo não é só contar mensagens. É ver quando começam, quais rotas aparecem e para qual upstream a requisição estava indo.

3. Ver limites de descritores #

1
2
cat /proc/$(cat /run/nginx.pid)/limits | grep -i 'open files'
systemctl show nginx -p LimitNOFILE

E comparar com a quantidade de FDs realmente abertos pelo processo quando possível.

4. Conferir PHP-FPM #

1
2
ps -ef | grep '[p]hp-fpm'
grep -R 'pm.max_children\|pm.max_requests\|pm =' /etc/php/*/fpm/pool.d/ 2>/dev/null

Se status do pool estiver habilitado, melhor ainda: ele dá uma visão mais útil do que simplesmente contar processos no ps.

5. Verificar evidência de banco sem inventar histórico #

No MySQL/MariaDB:

1
2
3
SHOW VARIABLES LIKE 'slow_query_log';
SHOW VARIABLES LIKE 'long_query_time';
SHOW FULL PROCESSLIST;

Se o Slow Query Log estava desligado durante a falha, registrar essa limitação explicitamente.

Correção e prevenção são outra etapa #

Este artigo não pretende transformar o caso em receita universal de tuning.

A correção correta depende do que cada ambiente mostrar: limites de nofile, configuração do serviço, capacidade de CPU, comportamento do pool PHP-FPM, custo das rotas dinâmicas, banco e estratégia de cache.

A parte reproduzível é o método:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
sintoma
→ linha do tempo
→ carga
→ recursos do host
→ Nginx
→ upstream
→ PHP-FPM
→ banco
→ evidências ausentes
→ conclusão proporcional aos dados

O aprendizado mais útil #

Too many open files era uma evidência real e importante.

upstream timed out também.

CPU/load altos também.

O erro seria escolher qualquer um deles isoladamente e fingir que o restante não existia.

Em incidentes de concorrência, o comportamento costuma emergir da interação entre limites e recursos.

Por isso a conclusão técnica ficou menos dramática e mais útil:

a origem degradou sob carga concentrada, com pressão de processamento e falhas operacionais na manutenção e encaminhamento de conexões para o backend.

E duas coisas ficaram deliberadamente sem virar culpadas: esgotamento explícito de workers PHP-FPM e slow queries, porque a evidência disponível não sustentava essas afirmações.

Sem floreio. E, principalmente, sem fechar a causa que o log não fechou.

TEM UM CENÁRIO PARECIDO?

Me chama.

Manda o contexto, os sintomas e o que já foi testado. Bora organizar as evidências antes de sair mexendo.

Falar com Castro →
Sem enrolação. Com evidência.