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 |
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 |
rafaelcaricio@ubuntu:~$ sudo apt-get install nginx |
rafaelcaricio@ubuntu:~$ sudo easy_install python-memcached | |
rafaelcaricio@ubuntu:~$ sudo easy_install gunicorn |
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. |
rafaelcaricio@ubuntu:~$ sudo vim /etc/nginx/sites-available/easyproject.conf |
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; | |
} | |
} | |
} |
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.
#!/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' |
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; | |
} | |
} |
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 |
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', | |
) | |
# ... |
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