Houve um certo trabalho em que participei, onde foi necessário instalar um WordPress-MU de alta disponibilidade, que utilizava além de dois servidores Web, dois servidores de banco de dados. Neste cenário, os arquivos do servidor Web estavam sempre replicados nas duas máquinas, e os bancos de dados também tinham seus dados sempre nas duas máquinas. Com os dados sempre replicados em várias máquinas, tivemos que customizar o WordPress para entender isso.
Mas antes de customizar, pensamos em algumas várias soluções que podiam ser adotadas. Uma delas seria não mexer em nada na aplicação (leia-se código do WordPress) e tentar fazer essa distribuição de forma transparente (RW Splitting).
Este artigo está antigo e obsoleto, não é recomendado seguir ele para as versões mais atuais do WordPress! Ao invés de ler esta solução, leia o outro artigo aqui no site: Balanceando bancos de dados do WordPress com o HyperDB.
Uma das soluções que fiquei testando por um bom tempo foi o MySQL Proxy, uma espécie de gateway de conexões MySQL onde todas as consultas SQL são enviadas para o servidor MySQL Proxy e este servidor ia repassando as consultas SQL para os diferentes bancos de dados que eu quisesse, podendo ser até de forma balanceada. O interessante também desta ferramenta é que como é um intermediário (Proxy), o servidor podia re-escrever as consultas de diversas formas com vários filtros e uma linguagem própria de re-escrita. Bem poderoso. Tão poderoso que acabamos por não utilizar, pois nos muitos testes que fiz, pareceu mais problema utilizá-lo para aquilo que queríamos do que outros métodos. E também houve o fato de que haviam alguns bugs reportados por aí afora, me fazendo achar que o programa ainda não estava totalmente pronto para usar em produção (posso estar muito enganado quanto à isto).
Chamei o camarada José Roberto (aka jragomes) que estava cuidando da parte de desenvolvimento para conversar sobre o assunto e definimos que uma alteração no core do WordPress MU seria a melhor solução. Mas por que?
- O WordPress, como todos sabem, é muito bem feito. Eles conseguem fabricar os códigos de uma forma totalmente extensível e de fácil customização. Isso foi um ponto muito positivo na hora de seguir este caminho;
- Pesquisando em diversos grandes hosts de blogs por aí (incluindo o próprio WordPress.com que utiliza o WordPress MU), percebemos que esse tipo de customização é muito comum, principalmente para os acessos a bancos de dados.
- A customização necessária, no caso, era modificar a chamada de conexões no arquivo “wp-includes/wp-db.php“, que é responsável por toda conexão feita ao banco de dados.
Com isso, resolvemos fazer essas alterações. As alterações a seguir correspondem ao WordPress mais atual no momento que escrevo este artigo, ou seja, versão WordPress MU 2.8.6. Provavelmente em outra versões, isso funcionará (com ou sem pequenas alterações no código da customização), mas não posso dizer ao certo pois não testei.
Mas antes de apresentar o código…
Como vai funcionar
Como citei anteriormente, toda vez que o WordPress tenha qualquer coisa acessado, ele carrega o wp-db.php, que é o arquivo responsável pelas conexões com o banco de dados. Todas as informações e configurações da ferramenta estão todas no banco de dados, então é realmente sempre que ele lê esse arquivo e conecta.
Existe uma constante não muito bem documentada no WordPress MU que é a WP_USE_MULTIPLE_DB. Essa constante é criada originalmente para hosts com um grande volume de blogs, onde é necessário separar os blogs em bancos de dados diferentes. Em outras palavras, ao invés de existirem, por exemplo, 10.000 blogs (cada um com umas 8 tabelas) em um único banco de dados, pode-se separar isso e não ficar apenas um banco com 80.000 tabelas, hehehe.
No nosso caso, não queríamos separar os bancos de dados e sim fazer com que o WordPress MU balanceasse entre os dois bancos de dados. Na nossa replicação de dados, utilizamos o conceito de replicação Master-Slave, onde as alterações são gravadas no banco de dados Master e automaticamente replicadas para todos os Slaves. Como todos os dados estão replicados, é possível ler de qualquer um: o Master ou todos os outros Slaves.
Sendo assim, queríamos:
- Servidor Master: Escrita de dados, leitura de dados.
- Servidor Slave 1: Leitura de dados
- Servidor Slave 2: Leitura de dados
Ou seja, grava-se em um e lê-se de três. O objetivo aqui é não sobrecarregar os bancos de dados com a quantidade excessiva de acessos, sendo que a grande maioria dos acessos são sempre de leitura.
Agora voltando ao WordPress MU…
Modificamos então o wp-db.php, adicionando mais uma funcionalidade. Na hora que o WordPress for se conectar ao banco de dados, ele vai ler o wp-config.php (configuração da ferramenta) e ver qual os bancos de dados de escrita e quais os de leitura, então:
- Toda vez que houver uma operação de escrita (por exemplo: INSERT, UPDATE, DELETE, CREATE, ALTER, REPLACE), ele encaminha a consulta para o servidor Master.
- Toda vez que houver uma operação de leitura (por exemplo: SELECT), ele encaminha aleatoriamente para a quantidade de servidores de leitura configurados.
Para fazer isso, acontecem duas coisas com esta modificação:
- Para cada acesso a estas funções do WordPress, duas conexões são abertas: uma com o servidor de escrita (Mestre) e outra com qualquer um dos servidores de leitura. Fizemos assim pois, além de necessitar de menos modificações ao núcleo do WordPress, constatamos que não importa que tipo de acesso, o WordPress MU sempre faz operações de escrita, mesmo que muito pequenas.
- Para cada consulta, é feito uma busca na string (com preg_match) afim de conhecer a natureza da consulta (se é apenas leitura ou se é de escrita).
Então o único overhead adicional que temos nesta implementação é uma conexão a mais e uma busca de expressão regular a mais por consulta. Não consideramos que este overhead fosse muito preocupante em relação as vantagens que tivemos ao implementar isso. O desempenho não denegriu de forma alguma.
Implementando
Chega de bla bla bla e vamos ao que interessa!!!
A primeira coisa que se deve fazer é aplicar o patch. Se você tem Windows, me desculpe, mas eu não sei como fazer isso, mas no Linux, basta utilizar o comando patch.
Primeiro copie o código abaixo e o coloque no diretório do wordpress com o nome patch-wpdb-rw-splitting.patch. Se preferir, salve o arquivo com este link. Este código foi inteiramente escrito pelo camarada José Roberto (aka jragomes), então nessa parte eu só fiquei olhando! :)
--- wp-includes/wp-db.php.orig 2009-11-28 18:11:46.000000000 -0300 +++ wp-includes/wp-db.php 2009-11-28 18:39:51.000000000 -0300 @@ -686,36 +686,62 @@ global $db_list, $global_db_list; if( is_array( $db_list ) == false ) return true; - - if( $this->blogs != '' && preg_match("/(" . $this->blogs . "|" . $this->users . "|" . $this->usermeta . "|" . $this->site . "|" . $this->sitemeta . "|" . $this->sitecategories . ")/i",$query) ) { - $action = 'global'; - $details = $global_db_list[ mt_rand( 0, count( $global_db_list ) -1 ) ]; - $this->db_global = $details; - } elseif ( preg_match("/^\\s*(alter table|create|insert|delete|update|replace) /i",$query) ) { - $action = 'write'; - $details = $db_list[ 'write' ][ mt_rand( 0, count( $db_list[ 'write' ] ) -1 ) ]; - $this->db_write = $details; - } else { - $action = ''; + + + /** + * Open One connection for reading + */ + + $action = 'read'; + $dbhname = "dbh$action"; + if(!is_resource($this->$dbhname)){ $details = $db_list[ 'read' ][ mt_rand( 0, count( $db_list[ 'read' ] ) -1 ) ]; $this->db_read = $details; + $this->$dbhname = @mysql_connect( $details[ 'db_host' ], $details[ 'db_user' ], $details[ 'db_password' ] ); + + if (!$this->$dbhname ) { + $this->bail(" + <h1>Error establishing a database connection for Reading</h1> + <p>This either means that the username and password information in your <code>wp-config.php</code> file is incorrect or we can't contact the database server at <code>$dbhost</code>. This could mean your host's database server is down.</p> + <ul> + <li>Are you sure you have the correct username and password?</li> + <li>Are you sure that you have typed the correct hostname?</li> + <li>Are you sure that the database server is running?</li> + </ul> + <p>If you're unsure what these terms mean you should probably contact your host. If you still need help you can always visit the <a href='http://wordpress.org/support/'>WordPress Support Forums</a>.</p> + "); + } + @mysql_query( "SET NAMES '{$this->charset}'", $this->$dbhname);//set NAMES + $this->select( $details[ 'db_name' ], $this->$dbhname ); } - $dbhname = "dbh" . $action; - $this->$dbhname = @mysql_connect( $details[ 'db_host' ], $details[ 'db_user' ], $details[ 'db_password' ] ); - if (!$this->$dbhname ) { - $this->bail(" -<h1>Error establishing a database connection</h1> -<p>This either means that the username and password information in your <code>wp-config.php</code> file is incorrect or we can't contact the database server at <code>$dbhost</code>. This could mean your host's database server is down.</p> -<ul> - <li>Are you sure you have the correct username and password?</li> - <li>Are you sure that you have typed the correct hostname?</li> - <li>Are you sure that the database server is running?</li> -</ul> -<p>If you're unsure what these terms mean you should probably contact your host. If you still need help you can always visit the <a href='http://wordpress.org/support/'>WordPress Support Forums</a>.</p> -"); + /** + * Open connection for writing + */ + + $action = 'write'; + $dbhname = "dbh$action"; + if(!is_resource($this->$dbhname)){ + $details = $db_list[ 'write' ][ mt_rand( 0, count( $db_list[ 'write' ] ) -1 ) ]; + $this->db_write = $details; + + $this->$dbhname = @mysql_connect( $details[ 'db_host' ], $details[ 'db_user' ], $details[ 'db_password' ] ); + + if (!$this->$dbhname ) { + $this->bail(" + <h1>Error establishing a database connection for Writing</h1> + <p>This either means that the username and password information in your <code>wp-config.php</code> file is incorrect or we can't contact the database server at <code>$dbhost</code>. This could mean your host's database server is down.</p> + <ul> + <li>Are you sure you have the correct username and password?</li> + <li>Are you sure that you have typed the correct hostname?</li> + <li>Are you sure that the database server is running?</li> + </ul> + <p>If you're unsure what these terms mean you should probably contact your host. If you still need help you can always visit the <a href='http://wordpress.org/support/'>WordPress Support Forums</a>.</p> + "); + } + @mysql_query( "SET NAMES '{$this->charset}'", $this->$dbhname);//set NAMES + $this->select( $details[ 'db_name' ], $this->$dbhname ); } - $this->select( $details[ 'db_name' ], $this->$dbhname ); } /** @@ -750,32 +776,29 @@ // Perform the query via std mysql_query function.. if ( defined('SAVEQUERIES') && SAVEQUERIES ) $this->timer_start(); - + // use $this->dbh for read ops, and $this->dbhwrite for write ops // use $this->dbhglobal for gloal table ops unset( $dbh ); if( defined( "WP_USE_MULTIPLE_DB" ) && CONSTANT( "WP_USE_MULTIPLE_DB" ) == true ) { - if( $this->blogs != '' && preg_match("/(" . $this->blogs . "|" . $this->users . "|" . $this->usermeta . "|" . $this->site . "|" . $this->sitemeta . "|" . $this->sitecategories . ")/i",$query) ) { - if( false == isset( $this->dbhglobal ) ) { - $this->db_connect( $query ); - } - $dbh =& $this->dbhglobal; - $this->last_db_used = "global"; - } elseif ( preg_match("/^\\s*(alter table|create|insert|delete|update|replace) /i",$query) ) { + + if ( preg_match("/^\\s*(alter|create|drop|insert|delete|update|replace|rename|truncate) /i",$query) ) { if( false == isset( $this->dbhwrite ) ) { $this->db_connect( $query ); } $dbh =& $this->dbhwrite; $this->last_db_used = "write"; } else { - $dbh =& $this->dbh; + if( false == isset( $this->dbhread ) ) { + $this->db_connect( $query ); + } + $dbh =& $this->dbhread; $this->last_db_used = "read"; } } else { $dbh =& $this->dbh; $this->last_db_used = "other/read"; } - $this->result = @mysql_query($query, $dbh); ++$this->num_queries;
Com o arquivo salvo, basta aplicar o patch no código:
cd <diretorio-do-wordpress> patch -p0 < patch-wpdb-rw-splitting.patch
Feito isso, e sem ter gerado erros, é hora de configurar. Abra o arquivo wp-config.php localizado na raiz da sua instalação WordPress e adicione, logo antes da linha:
/* That's all, stop editing! Happy blogging. */
Colocando, por exemplo:
$db_list = array( 'write' => array( array('db_host'=>'servidor-escrita.example.com', 'db_user'=>'usuario_rw', 'db_password'=>'supersegredo', 'db_name'=>'wordpressmu') ), 'read' => array( array('db_host'=>'servidor-escrita.example.com', 'db_user'=>'usuario_ro', 'db_password'=>'supersegredo_ro', 'db_name'=>'wordpressmu'), array('db_host'=>'servidor-leitura1.example.com', 'db_user'=>'usuario_ro', 'db_password'=>'supersegredo_ro', 'db_name'=>'wordpressmu') array('db_host'=>'servidor-leitura2.example.com', 'db_user'=>'usuario_ro', 'db_password'=>'supersegredo_ro', 'db_name'=>'wordpressmu') ) ); $global_db_list = array( array('db_host'=>'servidor-escrita.example.com', 'db_user'=>'usuario_rw', 'db_password'=>'supersegredo', 'db_name'=>'wordpressmu') ); define('WP_USE_MULTIPLE_DB', 1);
Pronto, a partir deste momento, o WordPress já estará funcionando de forma balanceada. No exemplo acima, podemos ver que a variável $db_list é responsável pela lista dos bancos de dados que iremos acessar. Dentro desta array, temos duas seções: write (escrita) e read (leitura). Para adicionar mais servidores no balanceamento da leitura, basta acrescentar mais itens na seção read, como coloquei no exemplo acima. Ele vai escalando automaticamente.
Note também que eu estou usando dois usuários de banco de dados: usuario_ro e usuario_rw. Isso é puramente opcional, coloquei mais por segurança, e é algo que é feito diretamente no banco de dados MySQL.
Atenção: não se esqueça de configurar as outras opções do arquivo wp-config.php normalmente, essas opções acima são adicionais para esta customização.
Atenção 2: Como toda modificação de núcleo, tome cuidado ao atualizar o wordpress para ele não sobrescrever automaticamente este arquivo (wp-db.php). Então, nas atualizações, fique ligado no que se faz e se preciso, utilize o patch novamente (ou com pequenas modificações para se adaptar à nova versão).
Bom proveito!