Django Utilidades :: Marinho Brandaohttp://marinhobrandao.com/pt-brTue, 06 Jan 2009 07:12:14 -0000Load balancing e Cache com MySQL Proxyhttp://marinhobrandao.com/blog/p/load-balancing-e-cache-com-mysql-proxy/<div class="document"> <p>Que tal aprender uma nova linguagem enquanto faz uma boa tarefa para melhorar a escalabilidade do banco de dados?</p> <p>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: <strong>Load Balancing</strong> e <strong>Cache de resultados</strong>.</p> <p><strong>Load Balancing</strong></p> <p>Eu conheço 3 formas de fazer load balancing com MySQL:</p> <blockquote> <ul class="simple"> <li>MySQL Cluster <a class="footnote-reference" href="#id21" id="id1" name="id1">[1]</a></li> <li>MySQL Proxy <a class="footnote-reference" href="#id22" id="id2" name="id2">[2]</a></li> <li>MySQL Master/Slave <a class="footnote-reference" href="#id23" id="id3" name="id3">[3]</a></li> </ul> </blockquote> <p>Mas na verdade não conhecia em profundidade nenhum deles, apenas uma idéia superficial.</p> <p>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 <a class="footnote-reference" href="#id24" id="id4" name="id4">[4]</a>. Mas nem o meu backend improvisado, nem o do <strong>Ivan Sagalaev</strong> me seduziram: muito limitado e cheio de falhas de design.</p> <p>Pois bem, o MySQL Cluster também me desanimou depois que eu li diversos depoimentos contrários <a class="footnote-reference" href="#id25" id="id5" name="id5">[5]</a>.</p> <p>Sobrou o MySQL Proxy.</p> <p><strong>MySQL Proxy</strong></p> <p>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.</p> <img alt="http://marinho.webdoisonline.com/blog/p/diagrama_mysql_proxypng_172/?img=1" src="http://marinho.webdoisonline.com/blog/p/diagrama_mysql_proxypng_172/?img=1" /> <p>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 <strong>(1105, '#07000(proxy) all backends are down')</strong>, portanto, antes de ir migrando seu servidor, faça muitos testes e verifique mais detalhes.</p> <p>O script, feito em Lua, ficou assim</p> <pre><code>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</code></pre><p>Salve o arquivo como <strong>load_balancing.lua</strong> e execute da seguinte forma:</p> <pre><code>mysql-proxy \ --proxy-lua-script=load_balancing.lua \ --proxy-backend-addresses=127.0.0.1:3306 \ --proxy-backend-addresses=vs2:3306</code></pre><p>O parâmetro <strong>--proxy-backend-addresses</strong> pode ser repetido quantas vezes quiser, um para cada réplica do MySQL, quanto mais réplicas, mais poderoso é o seu &quot;cluster&quot;.</p> <p>Os hosts &quot;127.0.0.1&quot; e &quot;vs2&quot; são respectivamente, minha máquina e uma máquina virtual rodando no <strong>vmware-server</strong>, portanto, use os IPs ou hostnames conforme a sua realidade.</p> <p>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 <a class="footnote-reference" href="#id26" id="id6" name="id6">[6]</a></p> <p>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 (<strong>local num = tonumber(os.date(&quot;%S&quot;)) % 2</strong>) seria feito com 10 (<strong>local num = tonumber(os.date(&quot;%S&quot;)) % 10</strong>) e o comando de chamada do mysql-proxy teria 10 vezes o parâmetro <strong>--proxy-backend-addresses</strong>. Simples né?</p> <p><strong>Cache em memória</strong></p> <p>Bom, eu já havia feito o mesmo tipo de coisa hackeando a QuerySet e criando o método .cache() <a class="footnote-reference" href="#id27" id="id7" name="id7">[7]</a>, 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ê.</p> <p>Este segundo script funciona da seguinte forma: quando uma consulta do tipo SELECT é feita ao banco de dados, seu resultado é armazenado em um servidor <strong>Memcached</strong>, 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.</p> <p>Bacana né?</p> <p>Então lá vai</p> <pre><code>-- 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</code></pre><p>Lá no início, as linhas que definem <strong>local conn</strong> e <strong>local patterns_expire_time</strong> devem ser ajustadas à sua realidade (endereço do servidor e patterns). Para saber como definir expressões regulares em Lua veja em <a class="footnote-reference" href="#id28" id="id8" name="id8">[8]</a> (é um pouquinho diferente do convencional).</p> <p>Este script depende de dois pacotes externos à linguagem: <strong>json.lua</strong> <a class="footnote-reference" href="#id29" id="id9" name="id9">[9]</a> e <strong>Memcached.lua</strong> <a class="footnote-reference" href="#id30" id="id10" name="id10">[10]</a>, que por sua vez depende do <strong>LuaSocket</strong> <a class="footnote-reference" href="#id31" id="id11" name="id11">[11]</a>.</p> <p>No Ubuntu, quando se instala o pacote <strong>mysql-proxy</strong> 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.</p> <p>A solução foi instalar a versão 5.1 em paralelo (o LuaSocket possui um pacote no Ubuntu chamado <strong>liblua5.1-socket2</strong>) e eliminar a pasta antiga de bibliotecas da versão 5.0 (<strong>/usr/share/lua/50/</strong>), criando um symlink da versão 5.1 (<strong>/usr/share/lua/5.1/</strong>) com o mesmo nome.</p> <p>Ainda foi necessário definir a seguinte variável de ambiente</p> <pre><code>export LUA_INIT=@/usr/share/lua/50/compat-5.1.lua</code></pre><p>Por fim, para dar vida ao script, basta executar</p> <pre><code>mysql-proxy \ --proxy-lua-script=cached_queries.lua \ --proxy-backend-addresses=127.0.0.1:3306</code></pre><img alt="http://marinho.webdoisonline.com/blog/p/tela_mysql_proxypng/?img=1" src="http://marinho.webdoisonline.com/blog/p/tela_mysql_proxypng/?img=1" /> <p>Ao executar este comando, será aberta a porta <strong>4040</strong> que deve ser setada na setting <strong>DATABASE_PORT</strong>. Caso queira saber como sobrepor a porta 3306, veja em <a class="footnote-reference" href="#id39" id="id12" name="id12">[19]</a>.</p> <p>Uma observação importante: o MySQL possui um bug <a class="footnote-reference" href="#id32" id="id13" name="id13">[12]</a> (ou sei lá o que é) que obriga conexões para &quot;localhost&quot; 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 <strong>DATABASE_HOST</strong> para <strong>&quot;127.0.0.1&quot;</strong> caso esteja usando <strong>&quot;localhost&quot;</strong>.</p> <p>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 <strong>ainda</strong>, mas <strong>recomendo expressamente que faça esse ajuste eu espere que eu o faça antes de colocar em um servidor de produção</strong>.</p> <p>Para mais detalhes sobre MySQL Proxy, veja em <a class="footnote-reference" href="#id33" id="id14" name="id14">[13]</a>, <a class="footnote-reference" href="#id34" id="id15" name="id15">[14]</a>, <a class="footnote-reference" href="#id35" id="id16" name="id16">[15]</a> e <a class="footnote-reference" href="#id36" id="id17" name="id17">[16]</a></p> <p>Para mais detalhes sobre Lua, veja em <a class="footnote-reference" href="#id37" id="id18" name="id18">[17]</a> e <a class="footnote-reference" href="#id38" id="id19" name="id19">[18]</a>.</p> <p>É isso aí... artigo feito no fim da noite tem quer ser rápido assim. Dúvidas, é só falar :)</p> <p>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.</p> <p>PS2: agradeço ao <strong>Javier Guerra</strong> e <strong>David Given</strong> pelos esclarecimentos sobre a arquitetura da Lua e ao <strong>Giuseppe Maxia</strong> pelo bom tutorial <a class="footnote-reference" href="#id39" id="id20" name="id20">[19]</a> que desenvolvou sobre MySQL Proxy</p> <p>PS3: essa foi a primeira vez na vida que escrevi algo em Lua, por favor, sinta-se à vontade para apontar eventuais falhas</p> <p><strong>Atualização:</strong> removendo a variável de ambiente LUA_INIT, o mysql-proxy rodou normalmente sobre a versão 5.1.</p> <p><strong>Links relacionados</strong></p> <table class="docutils footnote" frame="void" id="id21" rules="none"> <colgroup><col class="label" /><col /></colgroup> <tbody valign="top"> <tr><td class="label"><a class="fn-backref" href="#id1" name="id21">[1]</a></td><td><a class="reference" href="http://dev.mysql.com/downloads/cluster/index.html">http://dev.mysql.com/downloads/cluster/index.html</a></td></tr> </tbody> </table> <table class="docutils footnote" frame="void" id="id22" rules="none"> <colgroup><col class="label" /><col /></colgroup> <tbody valign="top"> <tr><td class="label"><a class="fn-backref" href="#id2" name="id22">[2]</a></td><td><a class="reference" href="http://forge.mysql.com/wiki/MySQL_Proxy">http://forge.mysql.com/wiki/MySQL_Proxy</a></td></tr> </tbody> </table> <table class="docutils footnote" frame="void" id="id23" rules="none"> <colgroup><col class="label" /><col /></colgroup> <tbody valign="top"> <tr><td class="label"><a class="fn-backref" href="#id3" name="id23">[3]</a></td><td><a class="reference" href="http://dev.mysql.com/doc/refman/5.1/en/connector-j-reference-replication-connection.html">http://dev.mysql.com/doc/refman/5.1/en/connector-j-reference-replication-connection.html</a></td></tr> </tbody> </table> <table class="docutils footnote" frame="void" id="id24" rules="none"> <colgroup><col class="label" /><col /></colgroup> <tbody valign="top"> <tr><td class="label"><a class="fn-backref" href="#id4" name="id24">[4]</a></td><td><a class="reference" href="http://softwaremaniacs.org/soft/mysql_cluster/en/">http://softwaremaniacs.org/soft/mysql_cluster/en/</a></td></tr> </tbody> </table> <table class="docutils footnote" frame="void" id="id25" rules="none"> <colgroup><col class="label" /><col /></colgroup> <tbody valign="top"> <tr><td class="label"><a class="fn-backref" href="#id5" name="id25">[5]</a></td><td><a class="reference" href="http://blog.globoi.com/producao/2008/04/16/brasileiros-na-mysql-conference/">http://blog.globoi.com/producao/2008/04/16/brasileiros-na-mysql-conference/</a></td></tr> </tbody> </table> <table class="docutils footnote" frame="void" id="id26" rules="none"> <colgroup><col class="label" /><col /></colgroup> <tbody valign="top"> <tr><td class="label"><a class="fn-backref" href="#id6" name="id26">[6]</a></td><td><a class="reference" href="http://www.howtoforge.org/mysql_master_master_replication">http://www.howtoforge.org/mysql_master_master_replication</a></td></tr> </tbody> </table> <table class="docutils footnote" frame="void" id="id27" rules="none"> <colgroup><col class="label" /><col /></colgroup> <tbody valign="top"> <tr><td class="label"><a class="fn-backref" href="#id7" name="id27">[7]</a></td><td><a class="reference" href="http://marinho.webdoisonline.com/blog/p/metodo-cache-para-queryset_158/">http://marinho.webdoisonline.com/blog/p/metodo-cache-para-queryset_158/</a></td></tr> </tbody> </table> <table class="docutils footnote" frame="void" id="id28" rules="none"> <colgroup><col class="label" /><col /></colgroup> <tbody valign="top"> <tr><td class="label"><a class="fn-backref" href="#id8" name="id28">[8]</a></td><td><a class="reference" href="http://lua-users.org/wiki/PatternsTutorial">http://lua-users.org/wiki/PatternsTutorial</a></td></tr> </tbody> </table> <table class="docutils footnote" frame="void" id="id29" rules="none"> <colgroup><col class="label" /><col /></colgroup> <tbody valign="top"> <tr><td class="label"><a class="fn-backref" href="#id9" name="id29">[9]</a></td><td><a class="reference" href="http://www.chipmunkav.com/downloads/Json.lua">http://www.chipmunkav.com/downloads/Json.lua</a></td></tr> </tbody> </table> <table class="docutils footnote" frame="void" id="id30" rules="none"> <colgroup><col class="label" /><col /></colgroup> <tbody valign="top"> <tr><td class="label"><a class="fn-backref" href="#id10" name="id30">[10]</a></td><td><a class="reference" href="http://luamemcached.luaforge.net/">http://luamemcached.luaforge.net/</a></td></tr> </tbody> </table> <table class="docutils footnote" frame="void" id="id31" rules="none"> <colgroup><col class="label" /><col /></colgroup> <tbody valign="top"> <tr><td class="label"><a class="fn-backref" href="#id11" name="id31">[11]</a></td><td><a class="reference" href="http://www.tecgraf.puc-rio.br/~diego/professional/luasocket/">http://www.tecgraf.puc-rio.br/~diego/professional/luasocket/</a></td></tr> </tbody> </table> <table class="docutils footnote" frame="void" id="id32" rules="none"> <colgroup><col class="label" /><col /></colgroup> <tbody valign="top"> <tr><td class="label"><a class="fn-backref" href="#id13" name="id32">[12]</a></td><td><a class="reference" href="https://bugs.launchpad.net/ubuntu/+source/mysql-dfsg-5.0/+bug/241802">https://bugs.launchpad.net/ubuntu/+source/mysql-dfsg-5.0/+bug/241802</a></td></tr> </tbody> </table> <table class="docutils footnote" frame="void" id="id33" rules="none"> <colgroup><col class="label" /><col /></colgroup> <tbody valign="top"> <tr><td class="label"><a class="fn-backref" href="#id14" name="id33">[13]</a></td><td><a class="reference" href="http://del.icio.us/marinho/mysql+escalabilidade">http://del.icio.us/marinho/mysql+escalabilidade</a></td></tr> </tbody> </table> <table class="docutils footnote" frame="void" id="id34" rules="none"> <colgroup><col class="label" /><col /></colgroup> <tbody valign="top"> <tr><td class="label"><a class="fn-backref" href="#id15" name="id34">[14]</a></td><td><a class="reference" href="http://dev.mysql.com/doc/refman/5.1/en/mysql-proxy.html">http://dev.mysql.com/doc/refman/5.1/en/mysql-proxy.html</a></td></tr> </tbody> </table> <table class="docutils footnote" frame="void" id="id35" rules="none"> <colgroup><col class="label" /><col /></colgroup> <tbody valign="top"> <tr><td class="label"><a class="fn-backref" href="#id16" name="id35">[15]</a></td><td><a class="reference" href="http://dev.mysql.com/doc/refman/5.1/en/mysql-proxy-scripting.html">http://dev.mysql.com/doc/refman/5.1/en/mysql-proxy-scripting.html</a></td></tr> </tbody> </table> <table class="docutils footnote" frame="void" id="id36" rules="none"> <colgroup><col class="label" /><col /></colgroup> <tbody valign="top"> <tr><td class="label"><a class="fn-backref" href="#id17" name="id36">[16]</a></td><td><a class="reference" href="http://classdump.org/articles/2008/02/14/mysql-proxy-enhancements">http://classdump.org/articles/2008/02/14/mysql-proxy-enhancements</a></td></tr> </tbody> </table> <table class="docutils footnote" frame="void" id="id37" rules="none"> <colgroup><col class="label" /><col /></colgroup> <tbody valign="top"> <tr><td class="label"><a class="fn-backref" href="#id18" name="id37">[17]</a></td><td><a class="reference" href="http://www.lua.org/manual/5.1/pt/">http://www.lua.org/manual/5.1/pt/</a></td></tr> </tbody> </table> <table class="docutils footnote" frame="void" id="id38" rules="none"> <colgroup><col class="label" /><col /></colgroup> <tbody valign="top"> <tr><td class="label"><a class="fn-backref" href="#id19" name="id38">[18]</a></td><td><a class="reference" href="http://lua-users.org/wiki/TutorialDirectory">http://lua-users.org/wiki/TutorialDirectory</a></td></tr> </tbody> </table> <table class="docutils footnote" frame="void" id="id39" rules="none"> <colgroup><col class="label" /><col /></colgroup> <tbody valign="top"> <tr><td class="label"><a name="id39">[19]</a></td><td><em>(<a class="fn-backref" href="#id12">1</a>, <a class="fn-backref" href="#id20">2</a>)</em> <a class="reference" href="http://dev.mysql.com/tech-resources/articles/proxy-gettingstarted.html">http://dev.mysql.com/tech-resources/articles/proxy-gettingstarted.html</a></td></tr> </tbody> </table> </div>