quarta-feira, 21 de novembro de 2012

Liberando o GIL do python para paralelizar seu código com threads

No Python o Global Interpreter Locker impede que duas threads executem ao mesmo tempo. Uma thread só é executada quando nenhuma outra estiver executando. A solução mais comum para aproveitar todos os cores de uma máquina em python é abandonar threads e utilizar vários processos. No entanto há uma maneira de aproveitar todos os cores com threads no python. Para isso vamos precisar criar uma extensão em C.

Primeiro vamos criar uma extensão que não libere o GIL para podermos comparar e conferir a melhoria de performance depois. O pivô dessa extensão é a seguinte função:

static int reduce_com_gil(int max, int (*f)(int x, int y)) {
   int retorno = 0;
   int i;
   for(i=0; i < max; i++){
       retorno = (*f)(retorno, i);
   }
   return retorno;
}

Essa função faz algo similar ao que um reduce faria, mas indo de 0 ao valor indicado em max. Para cada iteração a função enviada como segundo parâmetro é executada. A idéia é causar um grande processamento para que meu core fique travado. O uso dela está descrito no código abaixo:

static PyObject *antigil_calcular_com_gil(PyObject *self) {
   int valor = reduce_com_gil(100*1000, *antigil_calculos);
   char numero [5000];
   sprintf(numero, "%d", valor );
   return Py_BuildValue("s", numero);
}


Repare que eu passo para a função reduce_com_gil o ponteiro da função antigil_calculos. Essa outra função faz diversos cálculos a cada iteração do reduce. O nome antigil é o nome da extensão de exemplo. O código completo da extensão pode ser visto aqui.

Instalada a extensão, podemos testar a performance da lib com o seguinte código:

import antigil
antigil.calcular_com_gil()


E o seguinte comando:

$ time python teste.py
real 0m2.702s
user 0m2.692s


Mas o que a gente quer é saber como se comportam as threads. No caso o seguinte script abre 4 threads para aproveitar os 4 cores da minha máquina:

import antigil
from threading import Thread

threads = []
for i in xrange(4):
   t = Thread(target=antigil.calcular_com_gil)
   t.start()
   threads.append(t)

for t in threads:
   t.join()


No entando, acaba não aproveitando. Repare que ao executar 4 threads, o GIL age e impede que duas sejam executadas ao mesmo tempo. Dessa forma só um core da máquina é aproveitado. Isso pode ser observado pelo tempo total que é aproximadamente quatro vezes o tempo de execução de uma:

$ time python teste.py
real 0m10.910s
user 0m10.881s


Bom, vamos então à extensão não bloqueante. A chave do sucesso nesse caso são as macros Py_BEGIN_ALLOW_THREADS e Py_END_ALLOW_THREADS que respectivamente liberam o GIL e obtem o GIL de volta. Essas macros estão definidas em Python.h. O código da função reduce ficaria assim:

static int reduce_sem_gil(int max, int (*f)(int x, int y)) {
   int retorno = 0;
   Py_BEGIN_ALLOW_THREADS
   int i;
   for(i=0; i < max; i++){

       retorno = (*f)(retorno, i);
   }
   Py_END_ALLOW_THREADS
   return retorno;
}


Pronto, basta criar a função antigil_calcular_sem_gil similar à antigil_calcular_com_gil, utilizando a função reduce_sem_gil e então podemos repetir o teste. No caso parametrizei o script de teste para que você possa escolher qual função deseja executar:

import antigil
from threading import Thread
import sys

if 'com-gil' in sys.argv:
    calcular = antigil.calcular_com_gil
elif 'sem-gil' in sys.argv:
    calcular = antigil.calcular_sem_gil
else:
    print "Informar com-gil ou sem-gil"
    exit(0)

threads = []
for i in xrange(4):
   t = Thread(target=calcular)
   t.start()
   threads.append(t)

for t in threads:
   t.join()


O resultado é bem animador e mostra bem que o código rodou em paralelo:

$ time python teste.py sem-gil
real 0m3.414s
user 0m13.413s


Como o código utiliza apenas CPU, sem IO algum, ao aumentar para 8 threads, mesmo a versão que libera o GIL dobra de tempo pois só tenho disponível 4 cores na minha máquina. No entanto acredito que se fizer alguma operação de IO entre as macros Py_BEGIN_ALLOW_THREADS e Py_END_ALLOW_THREADS outras threads poderão ser executadas no caminho. Isso eu ainda preciso validar.

Só é preciso ter muito cuidado pois código entre essas macros está em território perigoso. A alteração de váriaveis globais ou ponteiros que podem ser compartilhados entre outras threads podem causar erros inesperados a qualquer momento. Portanto, é importante utilizar somente váriaveis locais e dados copiados.

O código completo da extensão em C antigil pode ser visto aqui.


quinta-feira, 9 de agosto de 2012

Ganhe convites de graça para a SEMCOMP


Este ano o maior evento de tecnologia da Bahia, a Semana da Computação da UFBA, será fantástica. A lista de palestrantes já confirmados é um dos pontos que chama a atenção, com presença de Nívio Ziviani, Osvaldo Matos, Fábio Akita, Sérgio Cavalcante entre outros. Você pode ver a lista completa dos palestrantes aqui: http://infojr.com.br/semcomp/palestrantes.

Os dois primeiros lotes de entradas ao evento se esgotaram logo nos primeiros dias de lançamento, e o terceiro pode estar em vias de acabar. Como a Globo.com está patrocinando o evento, ela me deu cinco convites para que eu distribuisse por conta própria. Resolvi então fazer de uma maneira divertida.

Para garantir o seu convite grátis para a SEMCOMP, basta fazer três coisas:

  1. Curtir o Música.com.br (novo site de música da Globo.com) no Facebook.
  2. Criar uma playlist no Música.com.br.
  3. Enviar a url dela para timotta@gmail.com indicando em quais quesitos ela se encaixa, lembrando de colocar no assunto do e-mail: Playlist SEMCOMP.

O criador da melhor playlist em cada um dos seguintes quesitos abaixo leva um convite para o evento.

  • Melhor playlist para programar em par
  • Melhor playlist para resolver bugs
  • Melhor playlist para configurar servidor
  • Melhor playlist para ajeitar layout web
  • Melhor playlist para programar sozinho

Você pode criar quantas playlists quiser. O próprio time de desenvolvimento do Música.com.br escolherá, em um método científico de experimentação, ou seja programando usando as playlists enviadas. Os vencedores serão anunciados apartir de 10 de Setembro de 2012 na página do facebook da SEMCOMP.

Mas veja que são apenas os convites. Não está incluído aí passagem ou hospedagem. Ou seja, caso você more em outro estado ou cidade, você irá arcar com esses custos. Será oferecido apenas o convite.

Para servir de inspiração, segue algumas das playlists que eu criei:


Que a diversão comece!

domingo, 22 de julho de 2012

Divagações sobre GIL, threads e IO em python e ruby


Uma coisa que eu sempre me confundi sobre ruby e python é a questão do Global Interpreter Locker (GIL). Na verdade, a dúvida maior é se operações de IO realmente bloqueiam os processos, impedindo a execução de outras threads.

Recentemente li um pouco mais sobre a versão 1.9 do Ruby e como ela passou a utilizar threads do sistema operacional, ao contrário das chamadas Green Threads da versão 1.8. No entanto, o GIL do ruby continua impedindo que duas threads executem ao mesmo tempo. A menos que uma esteja parada executando alguma operação de IO não bloqueante.

No caso do Python, o pouco de informação que tenho me leva a crer que a linguagem utiliza Green Threads. Fica então a minha dúvida se mesmo assim é possivel que o processo execute uma outra thread enquanto aguarda um retorno de IO.

Para começar fiz um pequeno script python para simular uma query pesada do MySql sendo executada 4 vezes. Se o script demorar por volta de dois segundo significa que durante uma operação de IO, o python prosseguiu executando as outras threads:

import _mysql
from threading import Thread

def executa():
    db = _mysql.connect(host="localhost",

                        user="root",
                        passwd="",

                        db="teste")
    db.query("select sleep(2)")
    r=db.use_result()
    r.fetch_row()

threads = []
for i in range(4):          
    t = Thread(target=executa, args=())
    t.start()
    threads.append(t)

for t in threads:
    t.join()
 
O resultado da execução pode ser visto abaixo, mostrando que o IO não bloqueou o programa:

> time python teste.py
real 0m2.030s
user 0m0.016s
sys 0m0.012s

Executei o mesmo teste com ruby, com o script similar abaixo:

require 'mysql2'

threads = []
4.times do |i|
    thread = Thread.new do
        my = Mysql2::Client.new(host: "127.0.0.1", 

                                username: "root", 
                                database: "teste")
        my.query("select sleep(2)").collect{|i|i}
    end
    thread.run
    threads << thread
end

threads.each do |thread|  
    thread.join
end

E o resultado também foi satisfatório:

> time ruby teste.rb
real 0m2.178s
user 0m0.076s
sys 0m0.008s

Ou seja, tanto python como ruby estão lidando bem com execução paralela. Mesmo se não utilizar todos os cores disponiveis, no mínimo o IO para o MySql não está bloqueando.

Com esse bom resultado, resolvi então subir um pouco mais de nível e verificar se colocando o Django na equação poderíamos aproveitar esse bom desempenho. Criei essa pequena view para simular uma query lenta, assim como nos scripts acima, e iniciei o Django (versão 1.3) com o gunicorn.

from django.db import connection
def debug(request):
    cursor = connection.cursor()
    cursor.execute("select sleep(2)")
    return HttpResponse('ok')

E o resultado, como pode ser visto abaixo, foi ruim:

> ab -n 4 -c 4 http://127.0.0.1:8000/debug/
Time taken for tests:   8.161 seconds

O mesmo teste executado para a dupla ruby on rails tem resultado parecido:

> ab -n 4 -c 4 http://localhost:3000/politicos/
Time taken for tests:   8.043 seconds

Conclusão:

Embora python e ruby permitam IO não bloqueante, os frameworks Django e Rails ainda são bloqueantes. Um dos motivos daqueles memes de "Rails não escala" e "Django não escala". Felizmente sempre há alternativas.

É possivel escalar via processos como explicado em Solucionando IO bloqueante do mysql para Rails, e utilizando vários workers do gunicorn para Django. Caso seja necessário uma escalabilidade maior ainda, o ideal então é partir para soluções como Event Machine do ruby, GEvent para Python, ou até mesmo NodeJs. Combinando essas soluções com múltiplos processos.

segunda-feira, 9 de julho de 2012

Estratégias de persitência do Redis

Nestes ultimos meses eu ministrei algumas palestras sobre o uso de redis na prática, usando como exemplo algumas funcionalidades do Musica.com.br. Uma das dúvidas mais constantes nesses eventos é a questão das opções de persistência deste banco. Confesso que não havia estudado o suficiente sobre o assunto. Eis que com o crescimento do site, surgiu a necessidade de uma estratégia melhor sobre a manutenção desses dados. Então aproveitei minhas horas em aeroportos para realizar alguns testes e responder algumas dúvidas.

Pra começar o Redis possui três formas de persitência de dados: não persistir, dump no disco e append only. O dump no disco, basicamente consiste em gravar uma cópia da memória em disco de tempos em tempos. O append only grava todos os comandos em um log, de forma que para recuperar em caso de restarte ele refaz todo o caminho percorrido.

Vamos então às perguntas que eu me fazia, e as respostas que obtive com meus testes:

1) É possível converter o rdb (dump) para um aof (appendonly)?

Sim para isso é preciso executar o comando BGREWRITEAOF estopar o redis server e estartar de novo com a nova configuração habilitada para appendonly.

2) O tempo para gravar e carregar o rdb (dump) é grande?

Para os padrões do Redis gravar é sim um tempo grande. Mas talvez não seja algo que possa atrapalhar. Depende muito da quantidade de dados que sua base terá. Fiz um teste adicionando três vezes 1 milhão de registros e comparando o tempo que demorava para gerar o dump em disco. Essa tabela pode servir de guia para saber quando é o momento de colocar o seu redis em uma máquina separada, e quando é hora de trocar de estratégia de gravação.

RegistrosMemóriaDiscoTempo para salvarGravação de Memória/segundo
1.000.00099.94M24M0.56s178.46 M/s
2.000.000191.50M42M0.93s205.37 M/s
3.000.000283.03M62M1.32s214.41 M/s

No entanto, o load do dump com 3 milhões de registros foi irrisório.

3) O tempo para carregar o aof (appendonly) é grande?

Sim, muito grande. Fiz um teste similar ao do dump, com 3 milhões de registros colocando 313.57M em memória e gerando um arquivo aof de 192M. Ao restartar o servidor ele demorou 4 segundos para carregar o arquivo. Um resultado que achei péssimo. No entanto, se pensarmos que restartar servidor é algo que não faremos com tanta constância, pode não ser algo tão ruim. Só deve-se ficar em mente que manutenções desse tipo em uma base grande devem ser em horários com pouco ou nenhum uso do seu servidor master.

4) É possivel compactar o aof (appendonly) com BGREWRITEAOF?

Um dos problemas do aof é que se um registro for incluido e removido, as duas instruções serão gravadas, de forma que não há uma razão clara entre memória do servidor e tamanho do arquivo em disco gerado. Minha dúvida era se eu poderia limpar o aof reescrevendo somente aquilo que estava na memória. E sim, isso é possível com o comando BGREWRITEAOF.

5) Os comandos ficam mais lentos com aof (appendonly) ligado?

Sim. Fiz um teste com concorrência. Três processos inserindo 1 milão de itens em listas diferentes. Com rdb (dump) habilitado a média de tempo foi de 0.33s. Com aof (appendonly) habilitado e rdb (dump) desabilitado a média de tempo foi de 0.43s. Essa proporção de 30% mais demorado para aof foi constante nos três testes seguintes que fiz como tira-teima.

6) Se o redis slave ficar down por um tempo ele recebe os dados do master?

Sim, fiz uma série de testes e percebi que o redis slave ignora os arquivos salvos por ele mesmo. Sempre que se inicia ele recebe todos os dados novamente do master, seja ele configurado como rdb ou como aof. Portanto, se estiver utilizando master slave, uma boa é configurar o seu slave para não gravar nada.

7) Gravar o rdb (dump) afeta a performance na resposta a outros comandos?

Sim, fiz um teste gravando três vezes uma memória de 2GB enquanto estava inserindo 1 milhão de novos registros. A insersão deles demorou 25% mais que inserir a mesma quantidade de registros sem salvar nenhuma vez o rdb. Só para se ter uma idéia, o tempo de gravação deste dump foi de 9 segundos e o tempo de load foi de 4 segundos.

Conclusão:

- Manter seus dados com rdb deixa o redis mais rápido, mas pode exigir muito de IO nos intervalos de persistência. Se a memória estiver muito grande, vale a pena colocar o servidor master em uma máquina dedicada para que não atrapalhe outros serviços.

- Manter seus dados com aof deixa o redis mais lento, mas garante que nenhuma transação será perdida. É preciso ficar atento para o arquivo gerado e periodicamente compactar ele com BGREWRITEAOF. Também tomar cuidado nos restarts que podem demorar.

quarta-feira, 2 de maio de 2012

Bate-papo sobre o Redis no 3o Dev in Santos

Neste sábado dia 5 de Maio participarei do maior encontro de desenvolvedores da baixada santista, falando um pouco sobre o Redis, e como o utilizamos pra tornar o desenvolvimento do Musica.com.br mais fácil, estável e seguro.

O programa completo do Dev in Santos com todas as apresentações você pode ver aqui: http://www.mktvirtual.com.br/mailing/2012/dev-in-santos/. Repare que teremos ótimas apresentações sobre uma grande variedade de assuntos como NodeJS, Python, IOS, Unity3D e é claro, Redis.

Se você estiver pela baixada santista, ou até mesmo em São Paulo neste fim de semana, será uma bela oportunidade para bater um papo sobre desenvolvimento e outras nerdices. Não deixe de se inscrever enquanto ainda há vagas.

Vejo vocês lá.

segunda-feira, 23 de abril de 2012

Palestras no Flisol de Salvador

Este sábado, dia 28 de Abril estarei presente no Flisol, Festival Latino-Americano de Instalação de Software livre, conversando um pouco sobre como nginx e algumas técnicas de otimização para web. Serão duas palestras que envolvem o mesmo tema:

Na primeira debaterei com o público sobre algumas ferramentas utilizadas no Musica.com.br para tornar o portal mais dinâmico e rápido, sem sobrecarregar o servidor. Bibliotecas como Head.js, backbone.js e localStorage serão abordadas.

A segunda falaremos sobre como escalar seu site com Nginx. Mostrarei alguns problemas que tivemos e como solucionamos eles em diversos produtos da Globo.com.

A programação completa do Flisol Salvador você confere aqui: http://softwarelivre.org/flisol-ssa/programacao.

Portanto, se estiver por salvador neste feriadão, não deixe de participar deste evento e puxar um papo comigo.