sexta-feira, 5 de novembro de 2010

Melhoria de performance projetos Django usando Gunicorn, Nginx e Memcached

Estou estudando formas para a melhoria de performance de aplicações na web. Atualmente eu trabalho com Python/Django no desenvolvimento de aplicações web. Pesquisando na internet sobre como melhorar o tempo de respostas das requisições ao máximo encontrei vários artigos explicando diversas formas de fazer isso. Muitos desses artigos me chamaram a atenção, pois faz exatamente o que eu imaginava. O próprio Nginx se comunica com o memcached e verifica se existe, aquela determinada página que o usuário está requisitando, no cache e apenas se não tiver, é que a requisição é repassada para o stack do python/django.

 

Depois de visualizar esse funcionamento eu resolvi implementar alguns testes em uma simples aplicação rodando localmente para verificar este funcionamento e os ganhos em performance que isso pode proporcionar. Fiz uma compilação das ideias apresentadas pelos artigos que eu li sobre o assunto.

Instalando o software necessário

Começei instalando o memcached e colocando ele para rodar com 512mb de mémoria.

rafaelcaricio@ubuntu:~$ sudo apt-get install memcached
rafaelcaricio@ubuntu:~$ memcached -d -m 512 -l 127.0.0.1 -p 11211
view raw gistfile1.sh hosted with ❤ by GitHub

Pronto, depois disso podemos verificar se o memcached está rodando corretamente.

rafaelcaricio@ubuntu:~$ ps ax | grep memcached
4185 ? Ssl 0:00 memcached -d -m 512 -l 127.0.0.1 -p 11211
view raw gistfile1.sh hosted with ❤ by GitHub
Agora o memcached está instalado e funcionando. Vamos instalar o nginx, é bem simples.

rafaelcaricio@ubuntu:~$ sudo apt-get install nginx
view raw gistfile1.sh hosted with ❤ by GitHub

E também temos que instalar o suporte ao memcached no Python e instalar também o Gunicorn para rodar nossa aplicação.

rafaelcaricio@ubuntu:~$ sudo easy_install python-memcached
rafaelcaricio@ubuntu:~$ sudo easy_install gunicorn
view raw gistfile1.sh hosted with ❤ by GitHub

Hora de verificar se o nginx está rodando.

rafaelcaricio@ubuntu:~$ sudo /etc/init.d/nginx stop
Stopping nginx: nginx.
rafaelcaricio@ubuntu:~$ sudo /etc/init.d/nginx start
Starting nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
configuration file /etc/nginx/nginx.conf test is successful
nginx.
view raw gistfile1.sh hosted with ❤ by GitHub

Podemos ver que está acessando a url http://localhost/ e vendo a mensagem "Wellcome to nginx!".

Configurando para a aplicação rodar usando o gunicorn

Agora vamos configurar o nginx para acessar nossa aplicação que vamos colocar rodando sobre o gunicorn. Primeiro vamos configurar o nginx para enviar repassar as requisições.

rafaelcaricio@ubuntu:~$ sudo vim /etc/nginx/sites-available/easyproject.conf
view raw gistfile1.sh hosted with ❤ by GitHub

No arquivo de configuração, eu coloquei:

 

upstream easy_gunicorn {
server 127.0.0.1:9000;
}
server {
listen 80;
server_name localhost 127.0.0.1;
client_max_body_size 10M;
access_log /var/log/nginx/easyproject.access.log;
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
if (!-f $request_filename) {
proxy_pass http://easy_gunicorn;
break;
}
}
}
view raw gistfile1.txt hosted with ❤ by GitHub

Temos que deletar/desabilitar as configurações padrão do nginx, pois nem vamos usar para este caso. Eu preferi deletar o arquivo.

 

rafaelcaricio@ubuntu:~$ sudo rm /etc/nginx/sites-enabled/default 

 

 

Agora criamos um link para as configurações no diretório de sites-enabled do gunicorn.

rafaelcaricio@ubuntu:~$ sudo ln /etc/nginx/sites-available/easyproject.conf /etc/nginx/sites-enabled/easyproject.conf 

E reiniciamos o nginx para pegar as novas configurações.

 

rafaelcaricio@ubuntu:~$ sudo /etc/init.d/nginx reload

Reloading nginx configuration: the configuration file /etc/nginx/nginx.conf syntax is ok

configuration file /etc/nginx/nginx.conf test is successful

nginx.

Pronto, agora se acessarmos http://localhost vamos ver um erro 502 do nginx, isso acontece porque nossa aplicação não está rodando ainda. Vamos colocar nossa aplicação django para rodar agora. Para isso, precisamos criar a configuração do gunicorn pra rodar o projeto. Assim eu crio o arquivo gunicorn.conf.py dentro do meu projeto django.

#!/usr/bin/python
import sys
import os
from django.conf import settings
try:
import settings # Assumed to be in the same directory.
sys.path.insert(0, os.path.join(settings.PROJECT_ROOT, "apps"))
except ImportError:
sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
sys.exit(1)
bind = '127.0.0.1:9000'
workers = 3
worker_connections = 2048
worker_class = 'egg:gunicorn#sync'
logfile = '/tmp/gunicorn_easyproject.log'

Essas são as configurações básicas que fazem meu projeto executar corretamente. Agora podemos mandar executar o gunicorn com as configurações escolhidas.

rafaelcaricio@ubuntu:~/development/easyproject$ gunicorn_django -c gunicorn.conf.py settings.py

Pronto! Agora já podemos acessar nossa aplicação através do nginx que repassa as requisições para o gunicorn. Isso já deixa a nossa aplicação bem rápida. Porém esse é o básico que podemos fazer pra deixar tudo rodando. A minha ideia aqui é ir um pouco mais além e deixar as coisas funcionando mais rápido ainda. E para isso vou fazer um esquema de cache na aplicação django criando um middleware para salvar os resultados das requisições no memcached. E deixar o nginx perguntar ao memcached sobre a existência do resultado da requisição antes de repassar a requisição para o django.

Para isso acontecer eu modifiquei as configurações no nginx.

 

upstream easy_gunicorn {
server 127.0.0.1:9000;
}
server {
listen 80;
server_name localhost 127.0.0.1;
client_max_body_size 10m;
access_log /var/log/nginx/easyproject.access.log;
location / {
default_type text/html;
if ($request_method = POST) {
proxy_pass http://easy_gunicorn;
break;
}
# monta a chave de consulta ao memcached.
set $memcached_key "easyproject:$uri";
memcached_pass localhost:11211;
proxy_intercept_errors on;
# se nao existir no cache a requisicao e repassada para o django.
error_page 404 502 501 = /django$uri;
}
location /django {
proxy_set_header x-forwarded-for $proxy_add_x_forwarded_for;
proxy_set_header host $http_host;
proxy_redirect off;
proxy_pass http://easy_gunicorn;
}
}
view raw gistfile1.txt hosted with ❤ by GitHub

 

E também criei um novo middleware que foi adicionado ao meu projeto no django. E adicionei algumas opções ao meu settings.py. 

 

#!/usr/bin/python
# -*- coding: UTF-8 -*-
import re
from django.core.cache import cache
from django.conf import settings
class NginxCacheMiddleware:
def process_response(self, request, response):
cacheIt = True
if request.method != "GET":
cacheIt = False
for exp in settings.CACHE_IGNORE_REGEXPS:
if re.match(exp, request.get_full_path()):
cacheIt = False
if cacheIt:
key = "%s:%s" % (settings.CACHE_KEY_PREFIX, request.get_full_path())
cache.set(key, response.content)
return response
view raw middleware.py hosted with ❤ by GitHub

 

No settings.py eu adicionei as seguintes configurações:

 

# ...
CACHE_BACKEND = 'memcached://127.0.0.1:11211/'
# criado por mim
CACHE_KEY_PREFIX = 'easyproject'
CACHE_IGNORE_REGEXPS = (
r'/admin.*',
)
# minha configuração de middlewares ficou assim:
MIDDLEWARE_CLASSES = (
'django.middleware.cache.UpdateCacheMiddleware',
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.cache.FetchFromCacheMiddleware',
'easyform.nginx_cache_middleware.NginxCacheMiddleware',
)
# ...
view raw settings.py hosted with ❤ by GitHub

 

Assim, todas as páginas que o django serve que sejam GET serão adicionadas ao memcached e o nginx vai pegar de lá o seu conteúdo. Assim as respostas ficarão bem mais rápidas.

Conclusão

Esta configuração é incrivelmente mais rápida, pois as requisições não vão direto para o django. O django só vai processar requisições POST, telas de erro e no caso de uma nova página ser acessada pela primeira vez. A ideia agora é pensar mais a frente, em como invalidar esse cache para que as informações mostradas aos usuários estejam sempre atualizadas. Essa tarefa não é muito complexa, apenas será uma coisa a mais que vai ter que ser feita quando houver modificações no banco de dados. Porém merece uma atenção e cuidado maior, para não ter grande impacto no processo de desenvolvimento. Vou analisar várias implementações e técnicas de invalidação de cache. Assim, no próximo post eu falarei mais sobre isso e demostrarei qual foi a solução que eu encontrei para isso.

 

Referências

http://kovyrin.net/2007/08/05/using-nginx-ssi-and-memcache-to-make-your-web-applications-faster/

http://amix.dk/blog/post/19414

http://jimmyg.org/blog/2009/ssi-memcached-nginx.html

http://www.willmcgugan.com/blog/tech/2009/3/1/fast-caching-with-django-and-nginx/

http://tabbedthinking.posterous.com/nginx-memcached-uwsgi-django

http://soyrex.com/articles/django-nginx-memcached.html