Django UtilidadesMarinho Brandão

Jul/08 15

Load balancing e Cache com MySQL Proxy

banco de dados django lua
Publicado há 1 mês, 2 semanas por Marinho Brandao

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:

  • MySQL Cluster [1]
  • MySQL Proxy [2]
  • MySQL Master/Slave [3]

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.

http://marinho.webdoisonline.com/blog/p/diagrama_mysql_proxypng_172/?img=1

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)
end

Salve 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:3306

O 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)
end

Lá 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.lua

Por fim, para dar vida ao script, basta executar

mysql-proxy \
    --proxy-lua-script=cached_queries.lua \
    --proxy-backend-addresses=127.0.0.1:3306
http://marinho.webdoisonline.com/blog/p/tela_mysql_proxypng/?img=1

Ao 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

Links sociais


PyConBrasil 2008

Gabeira para prefeito do Rio

Comentários


Escreva o seu


.net adoradores ajax android apple banco de dados blogosfera brasil django emprego família gadgets google inovação java linux lua microsoft musica opensocial opinião publicidade python rails religiao screencast seguranca software-livre tdd web windows yadsel

Artigos recentes