sexta-feira, 5 de novembro de 2010

Deployment de Django usando NGINX, Memcached e um pouco de código em C

Depois do post anterior eu começei a pensar em como eu poderia melhorar ainda mais a performance da nossa aplicação Django. Então tive a ideia de fazer cache das páginas estáticas e servi-las direto do memcached para o usuário. Isso funcionou muito bem, mas existe um problema quando essa mesma ideia é aplicada a uma aplicação onde os usuários realizam login e precisam acessar páginas personalizadas com coisas do tipo "Olá {{ nome_do_usuario }}, bem-vindo!". E pensei bastante sobre isso e deixei pra resolver este problema depois. Resolvi atacar inicialmente os visitantes externos, que não são usuários do sistema consequentemente que não estão autenticados no sistema. Neste grupo de visitantes se encontram também os engines de pesquisa como Google, Yahoo!, Bing e etc. E também os navegantes de primeira viagem que chegaram no site. Assim, quanto melhor a experiência que estes visitantes tiverem no primeiro acesso ao site maior as chances deles voltares ou se cadastrarem para usar o sistema. A velocidade de carregamento das páginas do site é um dos criterios de qualidade que estes visitantes analisam para decidirem se voltam ou não. Inclusive o Google recentemente anunciou que vai usar/usa o tempo de carregamento como critério no seu algoritmo de PageRank, ou seja, mais um motivo para motivar a desbravar esta ideia.

Para deixar o acesso dos visitantes não autenticados mais rápido não precisamos pensar muito, se você leu o post anterior e fez um teste simples deve ter percebido os ganhos em velocidade que foram extremamente absurdos. Assim, precisamos enviar esses visitantes não autenticados para o memcached diretamente sem que a requisição passe pela stack do Python / Django. Dessa forma desafogamos um pouco (ou muito) nossa aplicação pois ela só vai processar requisições que são realmente interessantes e que precisam de um processamento mais dinâmico. Se pararmos pra pensar, percebemos que os visitantes não precisam acessar as páginas com dados atualizando em tempo real (depende muito da aplicação, mas de um modo geral isso é verdade). Por exemplo, se nossa aplicação tem um forúm os visitantes do site não precisam ver o forúm se atualizando a cada acesso ou a cada segundo que ele visita o site. Já os usuários autenticados, precisam e devem ver o forúm atualizado o mais rápido possível para que eles possam interagir com os outros usuários do site. E essa ideia é aplicada a todas as páginas externas do site pois nem os engines de pesquisa ficam acessando o site a cada milisegundo pra ver o que mudou. Resumindo, os visitantes do site não precisam ver as coisas atualizadas eles provávelmente estão entrando no site pela primeira vez e tudo pra eles é novidade.

Agora que decidimos que precisamos fazer cache de tudo que é acessado por usuários não autenticados ficou fácil, não é? Não, como vamos saber quando o usuário está autenticado ou não? No código Python / Django podemos descobrir isso fácil mas como descobrir isso antes da requisição chegar ao Python? Precisamos validar se o usuário está autenticado ou não o mais rápido possível e enviar a resposta pra ele. O ideal é descobrir isso diretamente no NGINX, mas não tem uma forma de fazer isso. Inicialmente eu pensei em validar a existência do cookie do Django nas requisições e se ele existisse o usuário estaria autenticado. Porém isso não funciona pois o Django cria um cookie de sessão mesmo se o usuário não for autenticado. Sendo assim, o que fazer? Essa foi minha dúvida e decidi estudar o código do Django nessa parte de autenticação e gerenciamento de sessão para saber o que eu preciso fazer pra descobrir se o usuário está autenticado ou não. Depois de fazer isso, resumindo, cheguei a conclusão que não dava pra deserializar o objeto pickler que o Django salva no BD pra descobrir se o usuário está autenticado ou não. Então, analisando o comportamento do cookie do Django eu percebi que dava pra fazer uma validação simples, que não dá 100% de acerto sobre a questão do usuário estar autenticado ou não, mas já é uma boa (do meu ponto de vista). Eu descobri que posso validar o tamanho dos dados da sessão, não dá pra deserializar o objeto da sessão, mas dá pra validar o tamanho da string salva no BD. E é assim que eu faço a validação pra saber se o usuário está autenticado. Se os dados da sessão forem maior que um limite mínimo, quer dizer que o usuário está autenticado. No meu caso isso funciona pra mim porquê eu não salvo nada na sessão quando o usuário não está autenticado (é importante essa informação). Se você salva algo na sessão do usuário em páginas públicas da sua aplicação esse esquema, do jeito que eu faço agora, pode não funcionar pra você.

Resolvido esses problemas, eu tentei resolver o problema de acessar o banco de dados para pegar os dados que estão armazenados na sessão do usuário e validar se ele está autenticado antes da requisição ir para o Django. Essa validação tinha que ser feita no próprio NGINX ou em alguma coisa bastante rápida pra conseguir obter essa resposta. Então procurei várias alternativas. Primeiro eu pensei em usar node.js, mas cai no problema que eu ainda não conheço a tecnologia direito e então isso poderia ser um problema. Tentei procurar outra coisa, um modulo para o NGINX, talvez. Até achei um módulo que dá pra usar código em Lua no NGINX, mas eu também tinha que aprender Lua e não me pareceu necessário nesse momento também. Tentei achar uma outra solução e foi ai que achei um post em um blog mostrando um exemplo de consulta ao memcached usando código em C. Entao me ocorreu que eu poderia modificar esse código para atender as minhas necessidades. E foi isso que eu fiz. Código em C é o mais rápido que eu poderia conseguir (tá, eu sei... mas eu não sei Assembly tão bem assim, ainda).

Para explicar a ideia que eu tive, eu criei alguns diagramas de sequencia pra ficar mais fácil de entender. Abaixo eu mostro a primeira situação. Nesse caso eu mostro o que acontece quando o usuário acessa a aplicação e não está autenticado. Eu chamo o meu código em C de UserRouter, ele valida se o usuário está autenticado e se não estiver ele consulta no memcached a página que o usuário está tentando acessar e envia de volta para o NGINX no caso da página existir no memcached. Caso a página não exista ele envia a requisição para o Django normalmente.

arquitetura

No segundo cenário eu quero mostrar o que acontece quando um usuário autenticado acessa a página ou quando não existe uma página pública ainda em cache.

sem dados

A parte que coloca a página no memcached é opcional, pois se for uma página interna da aplicação ou uma página que apenas usuários autenticados acessam, ele nunca vai pra o memcached.
Então, para suportar esse funcionamento eu reusei o middleware do post anterior.

Adicionei algumas configurações ao settings.py:




E o código em C que faz toda essa mágica acontecer.





PS.: Bem, eu só vou avisando logo que esse código em C tem uma falha de SQL Injection que eu ainda vou corrigir (e atualizo aqui pra vocês).
Pra compilar esse código vocês teem que instalar algumas bibliotecas, mas essa parte é fácil se você usa o ubuntu.

rafaelcaricio@ubuntu:~/development$ sudo apt-get install libmysqlclient-dev libevent-dev libmemcached-dev

E pra compilar o código. A linha de comando é:

rafaelcaricio@ubuntu:~/development$ gcc -o v_auth_and_cache -levent -I/usr/include/mysql  -DBIG_JOINS=1  -fno-strict-aliasing   -DUNIV_LINUX -DUNIV_LINUX verify_auth_and_cache.c -Wl,-Bsymbolic-functions -rdynamic -L/usr/lib/mysql -lmysqlclient -lmemcached

Depois é só executar o servidor UserRouter colocando na porta 8000. Usando essa linha de comando:

rafaelcaricio@ubuntu:~/development$ ./v_auth_and_cache localhost 8000

E fazer algumas alterações na configuração no NGINX. Assim a requisição sempre vai primeiro pra o UserRouter e depois, se for o caso, passa para o Django.


No final eu realizei bastante testes e verifiquei que tudo isso realmente funciona e deixa o acesso a páginas externas extremamente rápido. Certo que eu ainda vou testar muito isso e melhorar esse funcionamento para poder usar no AtéPassar. Mas tenho certeza que isso vai melhorar bastante a velocidade de indexação pelos engines de busca e melhorar também a experiência dos usuários que estão acessando o site pela primeira vez. Eles vão ver tudo funcionando incrivelmente rápido. Espero ter ajudado ou aberto a mente de alguns de vocês para novas possibilidades. Em breve estarei escrevendo mais um post sobre outra ideia ou sobre alguma outra coisa que eu já fiz nas minhas horas vagas. Abraços e até logo.

Nenhum comentário:

Postar um comentário