Ao lidar com servidores web em produção (Apache, PHP-FPM e Nginx), frequentemente surgem comportamentos que parecem simples na interface da aplicação, mas revelam uma mecânica complexa nas camadas inferiores do sistema operacional.
Recentemente, investiguei como o LiveZilla gerencia o encerramento de sessões e a liberação de conexões ativas. À primeira vista, parece apenas um botão de fechar chat no navegador, mas o ciclo real envolve gatilhos no PHP, transações no MySQL, estados de sockets no kernel Linux e gerenciamento de descritores de arquivo.
Abaixo, compartilho a análise técnica das chamadas de sistema (syscalls), o impacto em memória e I/O, e como diagnosticar conexões fantasmas que se recusam a morrer.
1. Como uma sessão é encerrada na prática#
No LiveZilla, uma sessão pode ser finalizada por três caminhos:
A. Comando explícito do atendente ou cliente#
- O navegador despacha um
POSTparaserver.phpcom parâmetros de ação (comop_act=close_chat). - O PHP processa a instrução e executa um
UPDATEno MySQL (tabelas comolz_chat_postselz_chat_sessions), marcando o atendimento como fechado. - Se a comunicação utilizar Long Polling, a requisição pendente é respondida e o loop no navegador é interrompido via JavaScript.
B. Timeout de inatividade (garbage collection lógico)#
Quando o visitante fecha o navegador sem clicar em sair:
- O servidor LiveZilla possui um timeout configurável (
Configuration -> Server -> Timeout). - Como o PHP é stateless, o processo que atendeu a última requisição já terminou. O registro no banco permanece ativo até que uma rotina de manutenção verifique o timestamp e declare a sessão como finalizada.
C. WebSockets e camada de transporte#
Quando o sistema opera com WebSockets:
- O encerramento envia um frame
CLOSE(Opcode0x8). - O processo no servidor executa a syscall
close()no descritor de arquivo (file descriptor) do socket correspondente.
2. Impacto em i/o, sistema de arquivos e arquivos de sessão#
Durante o encerramento da conversa, ocorrem operações que merecem atenção em ambientes movimentados:
- Gravação de histórico: O buffer de mensagens é serializado e gravado em tabelas de histórico ou enviado por e-mail. Se o nível de depuração estiver ativado, ocorrem chamadas síncronas
fopen()efwrite(). - Arquivos de sessão e Inodes: O
session_destroy()do PHP remove o arquivo temporário de sessão em disco (sess_*). Se o garbage collection do PHP (session.gc_probability) estiver desativado ou mal ajustado, o acúmulo de arquivos antigos pode esgotar a tabela de inodes do disco.
3. Investigando conexões presas em CLOSE_WAIT#
Um dos problemas mais comuns em servidores é quando a aplicação diz que fechou o atendimento, mas a conexão TCP permanece aberta em CLOSE_WAIT.
Por que o estado CLOSE_WAIT acontece?#
Quando o cliente remoto encerra a conexão, o computador dele envia um pacote FIN. O kernel Linux do servidor responde com um ACK e coloca o socket local no estado CLOSE_WAIT. A partir desse momento, o sistema operacional espera que o processo da aplicação (Apache ou PHP-FPM) invoque a syscall close().
Se o PHP travar em uma consulta pesada no banco ou estourar o tempo de execução, o socket nunca é fechado, ocupando slots de conexão no servidor.
Comandos de diagnóstico no terminal#
Para identificar conexões presas em CLOSE_WAIT nas portas web de forma canônica:
ss -tanp state close-wait '( sport = :http or sport = :https )'
Para verificar quais descritores de rede estão abertos por um IP específico (capturando indistintamente processos Apache, PHP-FPM ou Nginx):
lsof -nPi | grep 192.0.2.10
Para inspecionar as chamadas de sistema em tempo real de um worker do PHP-FPM suspeito:
strace -p <PID_DO_PHP_FPM> -e trace=network,file,desc
Na saída do strace, você acompanha o ciclo completo: recvfrom() (recebendo a requisição de fechamento), sendto() (persistindo no MySQL) e o close() final no descritor do socket.
4. Ajustes práticos no servidor web e kernel#
Dois cenários merecem atenção redobrada:
- Locks no MySQL: Se a tabela de sessões travar em
Waiting for table metadata lock, o processo PHP fica preso em espera de socket (estadoS, sono interrompível) enquanto o MySQL não libera o lock — e, enquanto preso, não chamaclose()no socket do visitante nem responde novas requisições. - Proxies reversos e CDNs (Cloudflare): A sessão lógica do chat pode ter terminado, mas o túnel TCP entre a CDN e o servidor de origem permanece aberto devido ao HTTP Keep-Alive.
Ajustes recomendados:#
A. Ajustes na camada da aplicação (o único remédio real para CLOSE_WAIT)#
Como o kernel Linux não possui timer para fechar sockets em CLOSE_WAIT, a liberação depende exclusivamente de a aplicação fechar o descritor de arquivo via close():
- PHP-FPM: Configure
request_terminate_timeout = 30sno pool do PHP-FPM para encerrar workers travados em consultas lentas e liberar o socket. Em caso de processos bloqueados, reiniciar ou matar o worker (kill -9 <PID>) faz o processo master recriá-lo e o kernel desalocar os sockets herdados. - Apache / Nginx: Reduza o
KeepAliveTimeoutpara2a5segundos no servidor web para liberar conexões ociosas rapidamente e mantenha oTimeoutalinhado ao tempo máximo tolerado pela aplicação.
B. Ajuste de TCP no Kernel (sysctl)#
# /etc/sysctl.d/99-tcp-tuning.conf
net.ipv4.tcp_keepalive_time = 600
net.ipv4.tcp_fin_timeout = 30
Aplique as mudanças com sudo sysctl -p /etc/sysctl.d/99-tcp-tuning.conf.
Monitorar esse ciclo com ss, lsof e strace garante que a aplicação encerre conversas sem deixar sockets órfãos acumulando memória no servidor.
Conclusão#
Neste artigo analisei o ciclo completo de encerramento de uma sessão do LiveZilla — do UPDATE no MySQL ao close() no socket — e mostrei que um CLOSE_WAIT persistente quase nunca é problema de kernel, mas de aplicação que não devolve o descritor. Com timeouts bem calibrados no PHP-FPM e monitoramento de sockets ativos, o servidor mantém seu pool saudável mesmo sob tráfego concorrente intenso.
Este artigo foi útil?
Deixe uma reação rápida para apoiar o conteúdo:
Este post está licenciado sob CC BY-NC.



Comentários
Participe da discussão abaixo.
0 comentários