Percio Castelo.
EN Currículo
Mitigando exaustão de conexões MariaDB sob ataque L7 no CloudLinux e cPanel
← Voltar para blog

Mitigando exaustão de conexões MariaDB sob ataque L7 no CloudLinux e cPanel

05/11/2026 · 6 min · Banco de Dados · 215 visualizações

Quando você mantém servidores com cPanel e hospeda dezenas de sites na mesma máquina, incidentes de lentidão costumam seguir um padrão previsível. No entanto, quando um ambiente compartilhado sobre CloudLinux sofre ataques na camada de aplicação (Layer 7), a interação entre Apache, PHP-FPM, Kernel e MariaDB cria um efeito cascata que exige intervenção cirúrgica.

Recentemente passei por um caso crítico de esgotamento total de recursos no MariaDB. O incidente bloqueou até as ferramentas padrão de terminal e quase derrubou o servidor inteiro. Documentei neste artigo o comportamento de baixo nível do sistema operacional, os binários envolvidos e as estratégias práticas que utilizei para conter o problema sem reiniciar o banco de dados.


1. O sintoma: thread pool saturado e o falso positivo do sending data#

O incidente começou com alertas de monitoramento indicando que o MariaDB havia esgotado todos os trabalhadores disponíveis. Uma consulta rápida na lista de processos revelou o seguinte cenário:

+----------+--------------+-----------+--------------+---------+-------+--------------+------------------------------------------------------------------------------------------------------+
| Id       | User         | Host      | db           | Command | Time  | State        | Info                                                                                                 |
+----------+--------------+-----------+--------------+---------+-------+--------------+------------------------------------------------------------------------------------------------------+
| 59634010 | user_dbuser  | localhost | user_chatdb  | Query   | 124   | Sending data | SELECT COUNT(*) AS `total`,(SELECT COUNT(*) FROM (SELECT `lz_chat_archive`.`chat_id` FROM ...        |
| 59649250 | user_dbuser  | localhost | user_chatdb  | Query   | 3     | Sending data | SELECT COUNT(*) AS `total`,(SELECT COUNT(*) FROM (SELECT `lz_tickets`.`id` FROM `lz_tickets`) AS ... |
+----------+--------------+-----------+--------------+---------+-------+--------------+------------------------------------------------------------------------------------------------------+

O que o estado sending data significa#

À primeira vista, o estado "Sending data" parece indicar lentidão de rede, sugerindo pacotes TCP represados no buffer de saída. No entanto, na arquitetura interna do MariaDB e MySQL (especificamente na função sql_select.cc), esse rótulo engloba toda a fase de execução da consulta, leitura de linhas e envio dos resultados.

Como as consultas da aplicação continham subconsultas encadeadas (SELECT ... FROM (SELECT ...)), a engine de armazenamento InnoDB foi forçada a gerar tabelas temporárias.

O MariaDB tenta inicialmente criar tabelas temporárias na memória RAM usando a engine MEMORY. Quando o volume de dados ultrapassa os parâmetros tmp_table_size ou max_heap_table_size, o motor despeja essa tabela temporária no disco (utilizando Aria ou o tablespace temporário do InnoDB). Isso gerou um gargalo massivo de operações de entrada e saída (IOPS), retendo as threads do banco presas em chamadas de sistema read() e pread64() no kernel.


2. Lockout do servidor: falha de handshake TLS e o erro 1040#

Minha primeira tentativa de conter a crise foi limpar a fila de processos diretamente pela linha de comando:

mysql -e "SELECT concat('KILL ', id, ';') FROM information_schema.processlist WHERE user = 'user_dbuser' AND time > 100" | grep -v "concat" | mysql

O comando retornou uma falha imediata:

ERROR 2002 (HY000): Received error packet before completion of TLS handshake. The authenticity of the following error cannot be verified: 1040 - Too many connections

Arquitetura de conexões e rejeição prévia ao TLS#

O daemon mariadbd mantém um laço principal que escuta a porta 3306 usando as chamadas de sistema accept() e epoll(). Quando uma nova solicitação de conexão chega, o daemon confere o contador global max_connections antes de repassar a sessão para a biblioteca de criptografia SSL.

Como o pool de conexões estava completamente ocupado pelas threads travadas em I/O, o daemon rejeitou a conexão imediatamente enviando um pacote TCP com código de erro em formato bruto. O cliente de terminal mysql, esperando a mensagem inicial de negociação do protocolo TLS (Server Hello), não conseguiu completar o aperto de mão e gerou a mensagem de erro acima.


3. Rastreabilidade de rede: socket Unix vs. TCP/IP#

Para identificar de onde vinham as conexões, inspecionei a pilha de rede com ferramentas do sistema:

ss -atpn | grep :3306
# Retorno:
# LISTEN     0      80         0.0.0.0:3306      0.0.0.0:*      users:(("mariadbd",pid=3312899,fd=20))
# TIME-WAIT  0      0        127.0.0.1:2077    127.0.0.1:33064

lsof -i :3306
# Retorno:
# COMMAND      PID  USER   FD   TYPE    DEVICE SIZE/OFF NODE NAME
# mariadbd 3312899 mysql   20u  IPv4 497374505      0t0  TCP *:mysql (LISTEN)

O utilitário ss mostrou apenas o descritor de arquivo aberto do listener e conexões residuais na interface local (127.0.0.1). Não existia tráfego externo acessando a porta 3306 diretamente da internet.

O ataque acontecia na camada HTTP/HTTPS (porta 443). As requisições atingiam o servidor web, que as repassava aos trabalhadores do PHP (lsphp no CloudLinux). O PHP, funcionando como intermediário da aplicação, abria conexões locais com o banco através do socket /var/lib/mysql/mysql.sock ou loopback, ocultando a origem sob o identificador localhost.


4. O contra-ataque: estratégias de mitigação e isolamento#

Aumentar o parâmetro max_connections global ou reiniciar o serviço MariaDB durante o pico traria dois prejuízos sérios: interromperia transações de outros clientes hospedados e consumiria ainda mais memória RAM. A saída foi aplicar contenção isolada.

A. Limitando o usuário no nível do MariaDB#

O MySQL e o MariaDB reservam uma vaga extra de conexão para contas com privilégio administrativo SUPER (como o root do banco). Conectando via socket local direto:

mysql -u root --socket=/var/lib/mysql/mysql.sock -e "ALTER USER 'user_dbuser'@'localhost' WITH MAX_USER_CONNECTIONS 30; FLUSH PRIVILEGES;"

Essa diretiva atualizou a tabela interna mysql.user. Caso os scripts PHP da conta atingissem a 31ª conexão concorrente, o mariadbd passava a recusar novas tentativas com o erro 1226 (42000): User has exceeded the 'max_user_connections', resguardando o pool global para os outros sites da máquina.

B. Confinando processos web via cgroups do LVE#

Para impedir que as requisições PHP continuassem sobrecarregando o servidor antes mesmo de abrir sockets com o banco, atuei no Kernel Linux utilizando o módulo LVE:

lvectl set user --maxEntryProcs=15 --nproc=50
lvectl apply all
pkill -u user

O parâmetro maxEntryProcs limita o total de requisições web concorrentes que o servidor web pode injetar no ambiente do usuário. Ao restringir o valor para 15, qualquer requisição excedente foi bloqueada pelo kernel, forçando o retorno de um erro HTTP 508 Resource Limit Is Reached para o visitante e poupando a CPU da máquina. O comando pkill finalizou os processos órfãos que ainda aguardavam em segundo plano.


5. Caça ao IP atacante: parsing de logs POSIX#

Com o banco estabilizado, precisei identificar e bloquear a origem do ataque no firewall do servidor (CSF). Ao tentar analisar o tráfego do Apache com o utilitário tail, surgiu uma falha de sintaxe comum em diretórios extensos:

tail -5000 /usr/local/apache/domlogs/user/* | awk '{print $1}' | sort | uniq -c | sort -nr | head -n 10
# Retorno:
# tail: option used in invalid context -- 5

O caractere curinga * expandiu dezenas de arquivos, misturando pastas e links simbólicos. O utilitário tail perdeu o contexto do argumento diante da quantidade excessiva de descritores de arquivo.

A solução assertiva foi canalizar o conteúdo via cat, redirecionando mensagens de erro para descartar avisos de diretórios:

cat /usr/local/apache/domlogs/user/* 2>/dev/null | awk '{print $1, $7}' | sort | uniq -c | sort -nr | head -n 20

Esse comando expôs imediatamente o IP de origem e a URL requisitada em massa (/chat/server.php). Com a evidência em mãos, apliquei o bloqueio definitivo no firewall:

csf -d 198.51.100.45 "Bloqueio de ataque Layer 7 contra chat"

6. Hipóteses alternativas e cenários de borda#

Em produção, não dá para apostar em uma única hipótese. Durante a investigação, mantive dois cenários alternativos no radar:

  1. Conexões persistentes em segundo plano (PConnect):

Se o PHP estivesse configurado com conexões persistentes (PDO::ATTR_PERSISTENT), os processos web poderiam ter respondido à requisição HTTP e deixado o descritor de arquivo aberto no banco em estado ocioso (Sleep). Descartei essa possibilidade porque a lista de processos mostrava as threads em execução ativa com estado Sending data.

  1. Ausência de índices em tabelas com milhões de linhas:

Se uma tabela como lz_chat_archive acumula gigabytes de dados sem índices adequados na coluna chat_id, uma única consulta com contagem força uma varredura completa da tabela (Full Table Scan). Para confirmar esse comportamento, executei:

EXPLAIN SELECT COUNT(*) FROM (SELECT `lz_chat_archive`.`chat_id` FROM `lz_chat_archive`) AS tmp;

Se a coluna type indicar ALL com contagem elevada de linhas analisadas, parte do incidente decorre de modelagem deficiente do banco de dados, exigindo criação de índices apropriados além dos limites operacionais.


7. O toolkit compilado para resposta a incidentes#

Para consultas rápidas durante incidentes similares, reuni os comandos fundamentais em ordem prática de execução:

Conexão de resgate via socket local#

mysql -u root --socket=/var/lib/mysql/mysql.sock -e "SHOW PROCESSLIST;"

Injeção de emergência via GDB (caso o root seja bloqueado)#

Se até a vaga administrativa do root for comprometida, é possível injetar uma nova cota de conexões direto na memória do processo via GDB sem reiniciar o serviço:

gdb -p $(pgrep -x mariadbd) -ex "set max_connections=2000" -ex "detach" -ex "quit"

Interrupção forçada de conexões na interface local#

tcpkill -i lo port 3306

Confinamento da conta no CloudLinux#

lvectl set user --maxEntryProcs=15 --nproc=50
lvectl apply all
pkill -u user

Extração do IP atacante nos logs do Apache#

cat /usr/local/apache/domlogs/user/* 2>/dev/null | awk '{print $1, $7}' | sort | uniq -c | sort -nr | head -n 20

O que fica de aprendizado na contenção de incidentes#

Gerenciar incidentes sob estresse exige observar a infraestrutura em camadas. Um erro de banco de dados nem sempre decorre de limites do MariaDB; frequentemente, reflete requisições agressivas na camada web que inundam os trabalhadores de aplicação.

Isolar o processo no nível do kernel através do LVE, usar o socket local para contornar falhas de rede e aplicar bloqueios cirúrgicos no firewall resolvem crises em servidores compartilhados mantendo a estabilidade de todas as outras contas ativas.

Este artigo foi útil?

Deixe uma reação rápida para apoiar o conteúdo:

215 leituras
Creative Commons Atribuição-NãoComercial

Este post está licenciado sob CC BY-NC.

Comentários

Participe da discussão abaixo.

0 comentários