5 June 2009 - 17:28Múltiplas conexões no CodeIgniter: Cuidado!
Não estou escrevendo aqui para explicar como se utilizar múltiplas conexões no CodeIgniter, para isso, acredito que a documentação esteja suficientemente clara (http://codeigniter.com/user_guide/database/index.html).
Vim falar dos perigos de utilizar conexão a mais de um banco de dados no CI:
Recentemente, comecei a ter problemas em um portal de grande acesso. O servidor começava a ficar cada vez mais lento, até crashar com um erro do MySQL 1040 (too many connections [Muitas conexões]). Abri o “Process List” do MySQL para investigar (Você pode acessar via phpmyadmin, outras IDEs ou direto pela query “show processlist”).
Não foi difícil perceber o problema: Existiam dezenas ou centenas de conexões com tempos cada vez maiores e com o status “Sleep”.
Sleep significa que existe uma conexão com o banco, porém o usuário (no caso minha aplicação PHP) não está mandando nem recebendo nenhum dado, simplesmente está parado esperando que algo aconteça. Se você não utiliza conexão persistente (O que era meu caso), isso pode acontecer por uma série de razões:
- Alguma de suas páginas está demorando muito para ser carregada (Ou algo no código está gerando um loop infinito).
- Algum Web Service externo está muito lento ou não está online e não foi setado um timeout adequado.
- Existem muitas conexões ao Banco, enviando ou recebendo dados, o que gera uma fila de espera para as novas conexões que ficam aguardando.
De cara, a solução mais fácil (O que infelizmente não foi meu caso), é que eu estivesse utilizando conexão persistente nas configurações do meu banco. Para checar isso, basta abrir system/application/config/database.php e verificar se a configuração “pconnect” está setada para TRUE.
Nesse ponto alguns podem estar se perguntando: Opa, mas pera aí.. Conexão persistente não é aquela que é encerrada apenas explicitamente? Pois não sendo persistente, existe essa nota, direto da documentação do PHP:
Nota: A conexão com o servidor será fechada assim que a execução do script terminar, a menos que tenha sido fechada anteriormente usando-se explicitamente mysql_close().
Pois bem, as vezes essa nota não se faz verdadeira, e com certeza foi pensando nisso que desenvolveram o seguinte código no final do arquivo system/codeigniter/CodeIgniter.php:
if (class_exists('CI_DB') AND isset($CI->db)) { $CI->db->close(); }
“Garantindo” assim, que no final da execução de qualquer página, a conexão com o banco seja explicitamente encerrada.
Êpa!! agora complicou mais ainda né? Minha conexão não está persistente, e ainda assim o CI fecha a conexão ao término da execução do script. Como pode existirem conexões não encerradas?
Queria saber em que arquivo essas conexões não fechadas estavam sendo abertas. Então, modifiquei a função db_connect dentro de system/database/drivers/mysql/mysql_driver.php para o seguinte:
function db_connect() { if ($this->port != '') { $this->hostname .= ':'.$this->port; } $conexao = @mysql_connect($this->hostname, $this->username, $this->password, TRUE); $msg = "[".mysql_thread_id($conexao)."] CONEXÃO INICIADA NO ENDEREÇO: ".$_SERVER['PHP_SELF']." PARA {$this->hostname} "; $fp = fopen("./system/application/public/conexao.txt", "a"); fwrite($fp, $msg); fclose($fp); return $conexao; }
Com essa mosificação, comecei a gravar um log em TXT com todas as conexões abertas pelo CodeIgniter, identificando o ID de conexão com o banco, o arquivo em que essa conexão foi iniciada e para qual host a conexão apontava. Monitorando novamente o “Proccess List” e agora o log, comparei o ID das conexões que estavam com status “Sleep” a muito tempo e cheguei ao Controller onde todas as conexões não fechadas estavam sendo abertas.
Esse Controller não tinha nenhuma consulta demorada e muito menos um loop infinito. A única particularidade dele é que abria conexão a mais de um banco:
$DB2 = $this->load->database('banco2', TRUE);
O segundo parâmetro, TRUE, identifica que o objeto de conexão será retornado, ao invés de substituído no $this->db. É aí que está o problema:
O CI cria uma nova conexão, mas não a encerra, pois a conexão encerrada ao fim do script, é a carregada dentro do $this->db.
Se você leu a função mostrada a pouco, deve ter reparado num quarto parâmetro no mysql_connect:
$conexao = @mysql_connect($this->hostname, $this->username, $this->password, TRUE);
Segundo a documentação do PHP:
Se uma segunda chamada é feita a mysql_connect() com os mesmos argumentos, não é estabelecida uma nova conexão, mas ao invés, o identificador da conexão que já esta aberta é retornado. O parâmetro new_link modifica este funcionamento e faz mysql_connect() sempre abrir uma nova conexão, mesmo que mysql_connect() seja chamado antes com os mesmos parâmetros.
Em geral, classes de database iniciam e fecham a conexão dentro da função query(). Sendo assim, não é necessário deixar esse parâmetro como TRUE, pois nunca teremos mais de uma conexão simultânea, por mais que utilizemos mais de um database.
Resultado:
O PHP realmente não garante que todas as conexões sejam fechadas ao término do script;
O CodeIgniter fecha manualmente a conexão. Porém, ao gerar uma nova conexão e retorná-la como objeto, essa não será fechada ao término da execução;
Solução:
Ideal mesmo seria o CodeIgniter gravar em um array, na sua instância, o resource de todas as conexões abertas durante o script. Para então fechar uma por uma ao término da execução. Mas enquanto ele não faz isso:
Não retorne objetos de conexão: Encerre a conexão atual, destrua o objeto $this-db e retire o parâmetro “TRUE” do método load->database()).
Sendo assim, carregue um database, faça as queries necessárias e recarregue o database default, caso ainda vá executar alguma query nele.
Exemplo:
$this->db->close(); $this->db = ''; $this->load->database('banco2'); $x = $this->db->query("alguma query em outro banco"); $this->db->close(); $this->db = ''; $this->load->database('default'); $y = $this->db->query("alguma query no banco default");
.
Ahhh, duas observações:
Criei uma solicitação no Bug Tracker do CodeIgniter sugerindo tal mudança.
Quanto mais mexo no core do CodeIgniter, mais me apaixono por esse framework
Até a próxima!
3 Comments | Tags: CodeIgniter, Database, PHP