Em servidores compartilhados com dezenas ou centenas de contas, um único site desregulado pode comprometer a estabilidade de todos os demais. Um cenário clássico ocorre quando uma aplicação de atendimento em tempo real (como o LiveZilla) ou um plugin mal otimizado começa a abrir dezenas de conexões concorrentes no banco de dados.
Em uma situação recente, um único usuário (user_cliente) enfileirou 88 conexões simultâneas no MariaDB 10.11, levando o serviço ao teto global de max_connections e travando as consultas de todas as outras contas hospedadas na máquina.
O detalhe intrigante foi que o CloudLinux LVE Manager já estava ativado com limites restritivos para o usuário:
- SPEED: 200% (2 núcleos de CPU)
- SPEED MYSQL: 400%
- EP (Entry Processes): 20
- NPROC: 50
Mesmo com essas travas ativas, o comando mysqladmin proc stat continuava registrando 88 conexões ativas pertencentes ao mesmo usuário.
Neste artigo, vamos entender por que os limites do LVE no painel não contiveram a sobrecarga sozinhos, o que acontece no kernel durante esse tipo de travamento e como estabilizar o banco a quente, sem causar downtime em horário de pico.
1. Por que os limites do LVE pareceram não funcionar#
Para compreender a causa desse comportamento, precisamos olhar como o kernel Linux enxerga o agendamento de processos e a comunicação entre o servidor web e o banco de dados.
O LVE monitora e restringe processos que rodam sob o UID do usuário (user_cliente), como os workers do PHP-FPM ou processos CGI. No entanto, quando o script PHP chama mysqli_connect() ou instancia um objeto PDO, ele abre um descritor de arquivo de rede ou socket apontando para o binário do MariaDB (mysqld).
Assim que a conexão é aceita, o processamento interno da consulta ocorre no contexto do processo mysqld, que executa sob o UID de sistema mysql. Ou seja, a execução da query acontece fora da jaula do usuário, a menos que o MySQL Governor esteja totalmente acoplado ao binário do banco por meio de hooks específicos no kernel.
A armadilha do estrangulamento de CPU#
Nesse cenário, limitar a CPU do usuário pelo LVE para 200% produziu um efeito colateral inesperado:
- Com pouca CPU disponível, os scripts PHP do LiveZilla demoravam muito mais para processar cada requisição web.
- Como os scripts ficavam lentos ou atingiam limites de tempo de execução, eles nunca chegavam a invocar a função nativa de encerramento (
mysql_close) para enviar o pacote de término (QUIT). - No lado do MariaDB, as conexões permaneciam abertas em estado de espera (Sleep) ou executando subqueries pesadas, acumulando conexões zumbis na memória até bater no limite global do servidor.
O próprio estrangulamento da CPU no PHP alimentou o represamento de conexões no motor de banco de dados.
2. Diagnóstico no terminal e descarte de hipóteses#
Antes de aplicar qualquer medida corretiva, é fundamental inspecionar a lista de processos do MariaDB para verificar o tempo de atividade das threads:
mysqladmin proc v | grep user_cliente
A listagem expôs dezenas de consultas ativas marcando tempos entre 380 e 2.377 segundos (quase 40 minutos rodando em segundo plano).
Para isolar a causa exata, convém descartar os principais cenários de borda:
- Travamentos de tabela em engines legadas (MyISAM): se a tabela utilizasse MyISAM, uma instrução
INSERTbloquearia todas as outras leituras na mesma tabela.
- Verificação:
mysql -e "SHOW OPEN TABLES WHERE In_use > 0;".
- Deadlocks no InnoDB: transações concorrentes disputando os mesmos registros.
- Verificação:
mysql -e "SHOW ENGINE INNODB STATUS\G" | grep -A 20 "TRANSACTIONS".
- Esgotamento de portas TCP (TIME_WAIT): acúmulo de sockets pendentes de liberação na pilha de rede.
- Verificação:
netstat -nat | awk '/:3306/ {print $6}' | sort | uniq -c.
- Intervenção do OOM Killer: se as dezenas de conexões abertas estivessem consumindo memória acima do limite físico da máquina.
- Verificação:
dmesg -T | grep -i oom-killer.
O resultado confirmou que a maioria das conexões estava presa em tabelas temporárias ou aguardando respostas de scripts PHP que haviam congelado.
Se quisermos observar como o MariaDB aceita novas conexões em nível de chamadas de sistema, podemos usar o strace:
strace -fp $(pidof mysqld) -e trace=accept,accept4
3. Investigando o MySQL governor#
A reação lógica seria usar o utilitário dbctl para aplicar um teto de conexões por usuário:
dbctl set user_cliente --max_user_connections 30
Porém, ao inspecionar os processos em execução no servidor:
ps faux | grep dbgovernor
O retorno mostrava apenas o coletor auxiliar sentry_daemon.py. O daemon principal dbgovernor não estava rodando.
Ao checar a instalação:
yum install governor-mysql -y
O pacote básico já constava instalado, mas o binário do MariaDB em execução no sistema era a versão padrão da distribuição, e não o binário compilado pelo CloudLinux (cl-mariadb1011):
mysql -V
Para que o CloudLinux consiga interceptar chamadas internas do banco e aplicar regras automáticas de estrangulamento, é necessário substituir o pacote oficial pelos pacotes específicos do CloudLinux:
/usr/share/lve/dbgovernor/mysqlgovernor.py --set-mysql-version=mariadb1011
/usr/share/lve/dbgovernor/mysqlgovernor.py --install
O comando --install remove os pacotes do MariaDB padrão e instala a versão com suporte a LVE. No entanto, fazer essa troca durante o dia derruba o banco de dados temporariamente, inviabilizando a operação em horário comercial.
4. Plano de contenção a quente (sem reiniciar o MariaDB)#
Para restaurar a estabilidade imediata do servidor sem derrubar as conexões dos demais clientes, aplicamos três medidas defensivas em tempo de execução.
Passo 1: Limite rígido de conexões por usuário via SQL#
Em vez de depender de ferramentas externas, configuramos o próprio motor de autenticação do MariaDB para rejeitar novas conexões desse usuário assim que ele atingir 20 conexões simultâneas:
ALTER USER 'user_cliente'@'localhost' WITH MAX_USER_CONNECTIONS 20;
ALTER USER 'user_cliente'@'127.0.0.1' WITH MAX_USER_CONNECTIONS 20;
FLUSH PRIVILEGES;
Com essa diretiva ativa na memória, qualquer tentativa da aplicação de abrir a 21ª conexão é rejeitada com o erro 1226 (User has exceeded the 'max_user_connections' resource). O site do cliente passa a conter suas próprias requisições sem afetar nenhum outro inquilino do servidor.
Passo 2: Redução dinâmica dos tempos limite de inatividade#
Para encerrar conexões que ficaram órfãs após o encerramento dos scripts PHP, reduzimos os parâmetros de timeout globalmente sem reiniciar o daemon:
SET GLOBAL wait_timeout = 60;
SET GLOBAL interactive_timeout = 60;
Para garantir que a regra persista após futuros reinícios, incluímos as diretivas no /etc/my.cnf:
[mysqld]
wait_timeout = 60
interactive_timeout = 60
max_statement_time = 120
A flag max_statement_time = 120 define um teto máximo em segundos para consultas SELECT, cancelando automaticamente buscas que demorem mais de dois minutos para concluir.
Passo 3: Watchdog defensivo via crontab#
Como camada complementar de segurança, criamos um script leve para monitorar e encerrar consultas anormalmente longas do usuário problemático:
Crie o arquivo /usr/local/bin/mysql_limit_user.sh:
#!/bin/bash
# Localiza threads do usuario especifico rodando ha mais de 120 segundos
THREADS=$(mysql -Ne "SELECT id FROM information_schema.processlist WHERE user='user_cliente' AND time > 120")
if [ -n "$THREADS" ]; then
for ID in $THREADS; do
mysql -e "KILL $ID"
logger -t MYSQL_WATCHDOG "Encerrada thread $ID do usuario user_cliente por excesso de tempo"
done
fi
Dê permissão de execução ao script:
chmod +x /usr/local/bin/mysql_limit_user.sh
E configure a execução a cada dois minutos no cron do root:
(crontab -l 2>/dev/null; echo "*/2 * * * * /usr/local/bin/mysql_limit_user.sh >/dev/null 2>&1") | crontab -
5. Boas práticas para estabilidade contínua do banco de dados#
Após controlar o incidente sem paradas não planejadas, algumas providências definitivas garantem a tranquilidade do ambiente:
- Agende a instalação do MySQL Governor para uma janela de manutenção: em horário de baixo tráfego (geralmente de madrugada), execute a troca para a versão
cl-mariadb1011e inicie o serviçodbgovernorpara que o CloudLinux passe a monitorar consultas no kernel de forma nativa. - Revise as consultas da aplicação ofensora: em casos com LiveZilla ou fóruns legados, consultas de contagem (
SELECT COUNT(*)) sobre tabelas históricas sem índices adequados são as principais vilãs. Criar índices em colunas de busca e arquivar conversas antigas alivia a carga de I/O em disco. - Mantenha o teto de MAX_USER_CONNECTIONS em contas com histórico de abuso: estabelecer um limite individual de 20 ou 30 conexões para aplicações de chat ou e-commerce movimentados evita que surtos inesperados de tráfego consumam todo o pool global de conexões do servidor.
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