Load balancing e Cache com MySQL Proxy
Que tal aprender uma nova linguagem enquanto faz uma boa tarefa para melhorar a escalabilidade do banco de dados?
Bom, foi isso que eu fiz hoje durante quase todo o dia. Descobri no início do dia que Lua é a linguagem de script do MySQL Proxy e mergulhei pra fazer duas coisas bastante interessantes: Load Balancing e Cache de resultados.
Load Balancing
Eu conheço 3 formas de fazer load balancing com MySQL:
Mas na verdade não conhecia em profundidade nenhum deles, apenas uma idéia superficial.
No final de semana testei o Master/Slave. Parece ser o mais poderoso dentre os três métodos, mas usá-lo no Django significa mexer em boa parte do código do ORM. Não é a minha intenção. Cheguei a construir um backend pra isso, em cima do backend do MySQL, e até descobri que havia outro semelhante [4]. Mas nem o meu backend improvisado, nem o do Ivan Sagalaev me seduziram: muito limitado e cheio de falhas de design.
Pois bem, o MySQL Cluster também me desanimou depois que eu li diversos depoimentos contrários [5].
Sobrou o MySQL Proxy.
MySQL Proxy
Este é um software um tanto recente da MySQL, que basicamente faz a ponte entre o(s) servidor(es) de MySQL e a sua aplicação. É vantajoso pois é independente de linguagem ou framework: você desenha as regras e o que vem depois delas pouco importa, funciona da mesma forma.
Não quero explicar como instala e usa este software, portanto vou me limitar ao script que montei para fazer load balancing. Não foi devidamente testado e em algumas situações foi exibido o erro (1105, '#07000(proxy) all backends are down'), portanto, antes de ir migrando seu servidor, faça muitos testes e verifique mais detalhes.
O script, feito em Lua, ficou assim
function connect_server()
local num = tonumber(os.date("%S")) % 2
proxy.connection.backend_ndx = num + 1
print("Using " .. proxy.backends[proxy.connection.backend_ndx].address)
endSalve o arquivo como load_balancing.lua e execute da seguinte forma:
mysql-proxy \
--proxy-lua-script=load_balancing.lua \
--proxy-backend-addresses=127.0.0.1:3306 \
--proxy-backend-addresses=vs2:3306O parâmetro --proxy-backend-addresses pode ser repetido quantas vezes quiser, um para cada réplica do MySQL, quanto mais réplicas, mais poderoso é o seu "cluster".
Os hosts "127.0.0.1" e "vs2" são respectivamente, minha máquina e uma máquina virtual rodando no vmware-server, portanto, use os IPs ou hostnames conforme a sua realidade.
Ele não suporta master/slave (não encontrei nada na documentação que orientasse como fazer nesse caso), portanto foi necessário configurar os hosts como master/master, que você pode ver como fazer em [6]
O funcionamento é assim: nos segundos pares as conexões são repassadas ao servidor 1, e nas ímpares as conexões são repassadas para o servidor 2. Se houvessem 10 servidores, o módulo de 2 (local num = tonumber(os.date("%S")) % 2) seria feito com 10 (local num = tonumber(os.date("%S")) % 10) e o comando de chamada do mysql-proxy teria 10 vezes o parâmetro --proxy-backend-addresses. Simples né?
Cache em memória
Bom, eu já havia feito o mesmo tipo de coisa hackeando a QuerySet e criando o método .cache() [7], que me ajudou muito. Esta solução que eu criei hoje pode ser considerado inferior à anterior por oferecer menos granularidade e funcionar somente com MySQL, mas caso você não queira modificar o código original do Django, esta pode ser uma boa alternativa para você.
Este segundo script funciona da seguinte forma: quando uma consulta do tipo SELECT é feita ao banco de dados, seu resultado é armazenado em um servidor Memcached, com tempo de expiração definido por pattern (cada pattern, ou seja, cada modelo de SELECT pode possuir um tempo de expiração diferente) e se uma cosulta idêntica for requisitada dentro do tempo de expiração, o servidor de cache será consultado, ao invés do banco de dados.
Bacana né?
Então lá vai
-- Packages required
require("Memcached") -- http://luamemcached.luaforge.net/
require("json") -- http://www.chipmunkav.com/downloads/Json.lua
-- Connect to memcached server
local conn = Memcached.Connect('localhost', 11211)
-- Default expire time for cache items
local default_expire_time = 30
-- Prefix for cache keys
local key_prefix = 'mysql-proxy-'
-- Patterns to define expiration time for different types of queries.
-- More details in: http://lua-users.org/wiki/PatternsTutorial
local patterns_expire_time = {
{'^%s*select .+from .*auth_user', 150},
{'^%s*select', default_expire_time},
}
-- Converts a string to valid key
function encode_key(str)
return key_prefix .. string.gsub(str, ' ', '-')
end
-- Converts a resultset to JSON. This can't be done by
-- Json library directly because it's a userdata datatype
-- instance
function resultset_to_str(resultset)
local rfields = resultset.fields
local rrows = resultset.rows
local fields = {}
local rows = {}
local pos = 1
-- Rows
for row in rrows do
rows[pos] = row
pos = pos + 1
end
-- Fields
pos = 1
for i = 0, #rfields do
if rfields[i] then
fields[pos] = {
type = rfields[i].type,
name = rfields[i].name,
}
end
pos = pos + 1
end
return Json.Encode({
fields = fields,
rows = rows,
})
end
-- Callback called before request database server
function read_query(packet)
if string.byte(packet) == proxy.COM_QUERY then
local sql = string.sub(packet, 2)
if string.match(string.lower(sql), '^%s*select') then
-- Transform to valid key string
local key = encode_key(sql)
-- Gets from cache
local rset = conn:get(key)
-- If not found in cache, requests from database server
if rset == nil then
proxy.queries:append(1, packet)
return proxy.PROXY_SEND_QUERY
end
-- Print out
print('from cache', key)
-- Json -> table
rset = Json.Decode(rset)
-- Todo: check for error returns
proxy.response.type = proxy.MYSQLD_PACKET_OK
proxy.response.resultset = rset
return proxy.PROXY_SEND_RESULT
end
end
end
-- Callback called after request to database server
function read_query_result(inj)
local res = resultset_to_str(inj.resultset)
local sql = string.lower(string.sub(inj.query, 2))
local key = encode_key(sql)
local expire_time = default_expire_time
-- Looks at patterns for respective expire time
for i = 0, #patterns_expire_time do
if patterns_expire_time[i] and string.match(sql, patterns_expire_time[i][1]) then
expire_time = patterns_expire_time[i][2]
break
end
end
-- Saves to cache
conn:set(key, res, expire_time)
endLá no início, as linhas que definem local conn e local patterns_expire_time devem ser ajustadas à sua realidade (endereço do servidor e patterns). Para saber como definir expressões regulares em Lua veja em [8] (é um pouquinho diferente do convencional).
Este script depende de dois pacotes externos à linguagem: json.lua [9] e Memcached.lua [10], que por sua vez depende do LuaSocket [11].
No Ubuntu, quando se instala o pacote mysql-proxy a lib da Lua 5.0 é instalada também, mas acontece que os pacotes que eu citei acima - especialmente o LuaSocket - não funcionam corretamente com a versão 5.0.
A solução foi instalar a versão 5.1 em paralelo (o LuaSocket possui um pacote no Ubuntu chamado liblua5.1-socket2) e eliminar a pasta antiga de bibliotecas da versão 5.0 (/usr/share/lua/50/), criando um symlink da versão 5.1 (/usr/share/lua/5.1/) com o mesmo nome.
Ainda foi necessário definir a seguinte variável de ambiente
export LUA_INIT=@/usr/share/lua/50/compat-5.1.luaPor fim, para dar vida ao script, basta executar
mysql-proxy \
--proxy-lua-script=cached_queries.lua \
--proxy-backend-addresses=127.0.0.1:3306Ao executar este comando, será aberta a porta 4040 que deve ser setada na setting DATABASE_PORT. Caso queira saber como sobrepor a porta 3306, veja em [19].
Uma observação importante: o MySQL possui um bug [12] (ou sei lá o que é) que obriga conexões para "localhost" serem via porta 3306, isso vale tanto para o client quanto para o pacote MySQLdb do Python. Você faz uma conexão para a porta 1365465321 ou qualquer outra e ele aponta para 3306. Portanto, ao usar a porta 4040, mude a setting DATABASE_HOST para "127.0.0.1" caso esteja usando "localhost".
No script acima há ainda uma séria limitação quanto ao tamanho da SELECT. Caso ela tenha mais que 245 caracteres, você terá um erro de tamanho da chave no cache. A solução é converter a expressão SELECT com MD5, SHA ou outor algorítimo que crie uma string única em cima de uma SELECT complexa. Eu não consegui fazer isso ainda, mas recomendo expressamente que faça esse ajuste eu espere que eu o faça antes de colocar em um servidor de produção.
Para mais detalhes sobre MySQL Proxy, veja em [13], [14], [15] e [16]
Para mais detalhes sobre Lua, veja em [17] e [18].
É isso aí... artigo feito no fim da noite tem quer ser rápido assim. Dúvidas, é só falar :)
PS: apesar de serem muito úteis para quem usa Django, todas essas configurações são compatíveis com qualquer linguagem ou sistema operacional, e com versões igual ou acima de 5.1 do MySQL.
PS2: agradeço ao Javier Guerra e David Given pelos esclarecimentos sobre a arquitetura da Lua e ao Giuseppe Maxia pelo bom tutorial [19] que desenvolvou sobre MySQL Proxy
PS3: essa foi a primeira vez na vida que escrevi algo em Lua, por favor, sinta-se à vontade para apontar eventuais falhas
Atualização: removendo a variável de ambiente LUA_INIT, o mysql-proxy rodou normalmente sobre a versão 5.1.
Links relacionados
| [1] | http://dev.mysql.com/downloads/cluster/index.html |
| [2] | http://forge.mysql.com/wiki/MySQL_Proxy |
| [3] | http://dev.mysql.com/doc/refman/5.1/en/connector-j-reference-replication-connection.html |
| [4] | http://softwaremaniacs.org/soft/mysql_cluster/en/ |
| [5] | http://blog.globoi.com/producao/2008/04/16/brasileiros-na-mysql-conference/ |
| [6] | http://www.howtoforge.org/mysql_master_master_replication |
| [7] | http://marinho.webdoisonline.com/blog/p/metodo-cache-para-queryset_158/ |
| [8] | http://lua-users.org/wiki/PatternsTutorial |
| [9] | http://www.chipmunkav.com/downloads/Json.lua |
| [10] | http://luamemcached.luaforge.net/ |
| [11] | http://www.tecgraf.puc-rio.br/~diego/professional/luasocket/ |
| [12] | https://bugs.launchpad.net/ubuntu/+source/mysql-dfsg-5.0/+bug/241802 |
| [13] | http://del.icio.us/marinho/mysql+escalabilidade |
| [14] | http://dev.mysql.com/doc/refman/5.1/en/mysql-proxy.html |
| [15] | http://dev.mysql.com/doc/refman/5.1/en/mysql-proxy-scripting.html |
| [16] | http://classdump.org/articles/2008/02/14/mysql-proxy-enhancements |
| [17] | http://www.lua.org/manual/5.1/pt/ |
| [18] | http://lua-users.org/wiki/TutorialDirectory |
| [19] | (1, 2) http://dev.mysql.com/tech-resources/articles/proxy-gettingstarted.html |
Marinho Brandão