Continuando o post Django e Cache: Uma dupla de alta performance, vamos ver na prática como utilizar o framework de cache do Django.
Embora eu esteja utilizando o Memcached para escrever estes artigos, vale ressaltar que a abstração do Django lhe permite utilizar a ferramenta mais apropriada para você.
Podemos utilizar a camada de cache em diferentes pontos da aplicação. Por exemplo, podemos utilizá-la antes de uma consulta ao banco de dados, armazenar resultados de operações complexas, armazenar o parsing de um template, etc. Com o esquema de middlewares do Django, podemos ter essa camada aplicada diretamente ao fluxo de interpretação do framework, o que pode reduzir consideravelmente o uso de recursos de nossa hospedagem, sem mesmo termos alterado código das nossas apps.
Vamos ver a diferença, e casos de usos, dessas formas de utilização do cache.
Quando você quer ser “incisivo”, utilizar a API de forma “granular” é uma ótima opção.
Por exemplo, no Globoesporte.com nós fazemos algumas consultas a um banco de dados semântico para trazer informações de eventos, jogos e atletas. Como esta consulta é consideravelmente demorada, utilizamos a API de cache para melhorar os tempos de resposta. Exemplo:
from django.core.cache import cache
...
def jogos_por_edicao(edicao_slug):
jogos = cache.get('jogos_%s' % edicao_slug)
if not jogos:
jogos = pega_jogos_da_semantica(edicao_slug)
cache.set('jogos_%s' % edicao_slug, jogos)
...
Passamos ao cache.set
uma chave (que deve ser menor que 250
caracteres, e não utilizar caracteres especiais) e um valor. Ele também
aceita um terceiro parâmetro, que é o tempo de vida desta informação em
cache. Quando omitido, o tempo definido nas configurações do backend
é utilizado.
Para remover esta informação do cache, basta utilizarmos o método
cache.delete
:
cache.delete('jogos_%s' % edicao_slug)
Você tem a liberdade de fazer caching de qualquer região da sua aplicação. Mas é bom tomarmos cuidado para que o gerenciamento desses pontos não passem a ser um problema. O framework de cache pode ser aplicado em outras camadas da abstração, dispensando (em muitos casos) a necessidade desse tipo de controle em modelos e views.
Assim como é possível fazer caching de forma minuciosa com a API acima, é possível fazer um controle muito interessante de cache com os templates do Django.
A cada nova requisição, o Django carrega o arquivo de template do
disco, interpreta-o com o contexto, e retorna o seu resultado. Podemos
melhorar um pouquinho este fluxo, sem necessitar do Memcached, basta
adicionarmos o django.template.loaders.cached.Loader
ao
TEMPLATE_LOADERS
do settings.py
:
TEMPLATE_LOADERS = (
('django.template.loaders.cached.Loader', (
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
)),
)
Esse loader manterá o arquivo de template em memória, evitando com que o Django tenha que recorrer ao disco para obter o seu conteúdo. O “trade-off” é mais utilização da memória do seu servidor (que, dependendo do cenário, nem é um problema tão grande assim) e a necessidade de, quando houver atualizações em templates, efetuar o restart do serviço de WSGI que você utiliza.
Outro ponto a se observar é que as template tags que você utilizar deverão ser thread-safe.
O framework de cache permite “cachear” fragmentos de um template. Essa modalidade de cache é bem interessante quando utilizamos filtros ou tags que executam operações que aumentam consideravelmente o tempo de interpretação do template.
Na documentação há um exemplo bem interessante, onde é feito o cache de um sidebar inteiro:
{% load cache %}
{% cache 500 sidebar %}
<!-- conteúdo do sidebar -->
{% endcache %}
Passamos para a template tag cache
o tempo de vida do conteúdo
(500 segundos), e o identificador deste conteúdo (sidebar
). Se a
chave não existir, o Django interpretará as instruções dentro do bloco
e armazenará o seu resultado no Memcached para que, num próximo
acesso, esse resultado seja recuperado sem necessitar interpretar todo o
bloco novamente.
Uma das maneiras mais práticas de utilizarmos o cache em nossos websites e aplicações Web escritos em Django, é através do método chamado per-site cache.
Basicamente, o Django analisará requisições realizadas através dos
métodos GET
e HEAD
, e utilizará a sua URL como chave para a
verificação em cache. Caso ele encontre a ocorrência, retornará ao
usuário o resultado “cacheado”, senão, interpretará a view e ao final
armazenará o seu resultado.
Para que isso seja possível, é necessário a utilização dos middlewares
django.middleware.cache.UpdateCacheMiddleware
e
django.middleware.cache.FetchFromCacheMiddleware
:
MIDDLEWARE_CLASSES = (
'django.middleware.cache.UpdateCacheMiddleware',
...
'django.middleware.cache.FetchFromCacheMiddleware',
)
Além da configuração das seguintes constantes:
CACHE_MIDDLEWARE_ALIAS
: O identificador da conexão (padrão
default
)CACHE_MIDDLEWARE_SECONDS
: O tempo de vida (em segundos) das
páginas em cache (padrão 600 segundos
)CACHE_MIDDLEWARE_KEY_PREFIX
: Chave para prevenir problemas
quando o cache é um serviço compartilhado entre diferentes
instâncias Django. Pode-se, por exemplo, colocar o nome do site
como prefixo.Ao acessar as views, temos uma agradável surpresa:
$ curl -I http://localhost:8000/
HTTP/1.0 200 OK
Date: Wed, 04 Jul 2012 00:12:17 GMT
Server: WSGIServer/0.1 Python/2.7.2
Cache-Control: max-age=600
Vary: Cookie
Expires: Wed, 04 Jul 2012 00:22:17 GMT
Content-Type: text/html; charset=utf-8
Last-Modified: Wed, 04 Jul 2012 00:12:17 GMT
Ganhamos cabeçalhos HTTP com os valores correspondentes às nossas
configurações de cache! Por exemplo, Last-Modified
corresponde a
data de acesso, Expires
é a data de acesso acrescentando os 600
segundos de cache, e o Expires
corresponde ao tempo de
CACHE_MIDDLEWARE_SECONDS
.
Interessante não? Temos um controle de cache na “borda” da nossa aplicação… não precisamos interferir nas nossas views, modelos ou consultas.
É natural que certas views necessitem de um tempo de cache diferente de outras. Para tanto, podemos utilizar decorators que “sobrescrevem” as configurações utilizadas pelo per-site, permitindo assim um controle mais granular sobre o tempo de cache das views. Exemplo:
from django.views.generic.simple import direct_to_template
from django.views.decorators.cache import cache_page
urlpatterns = patterns('',
...
url(r'^outra-view/$', cache_page(60 * 2)(direct_to_template),
{'template': 'outra-view.html'}, name='outra-view'),
...
)
No exemplo acima estou usando a generic view direct_to_template
para ilustrar. Através do decorator cache_page
eu informo o tempo
de vida desta view em cache (2 minutos, 60 * 2 para ficar mais
legível).
E quando acessamos esta view, é possível reparar que inclusive os valores dos cabeçalhos HTTP são outros:
$ curl -I http://localhost:8000/outra-view/
HTTP/1.0 200 OK
Date: Wed, 04 Jul 2012 00:28:07 GMT
Server: WSGIServer/0.1 Python/2.7.2
Last-Modified: Wed, 04 Jul 2012 00:28:07 GMT
Expires: Wed, 04 Jul 2012 00:30:07 GMT
Content-Type: text/html; charset=utf-8
Vary: Cookie
Cache-Control: max-age=120
Ainda é possível passar como parâmetro para o cache_page
, o
cache
que você deseja utilizar (por padrão default
), e um
key_prefix
.
É normal que algumas rotas da sua aplicação não possam fazer utilização
desse tipo de cache. Por exemplo, views para usuários autenticados,
que necessitam transitar informações de sessão e cookies, ou até mesmo
views que precisam receber informações através de POST
.
Vamos imaginar que, dentro desse cenário, a sua app faça uma consulta “custosa” ao banco de dados. Logo, concluímos que adicionar um controle de cache a esta consulta seria extremamente interessante para a velocidade de resposta da view. A primeira opção é utilizar o framework de cache do Django de forma granular, através de sua API. A outra opção é fazer caching “dentro” do ORM.
É apoiado nessa última proposta que o Johnny Cache se baseia: “cachear” dados transitados através do ORM do Django, não interferindo no código das apps.
O Johnny Cache está no PyPi, então basta um pip install johnny-cache
para realizarmos a instalação. Para configurar,
precisamos adicionar algumas informações ao settings.py
:
MIDDLEWARE_CLASSES = (
'johnny.middleware.LocalStoreClearMiddleware',
'johnny.middleware.QueryCacheMiddleware',
...
)
CACHES = {
'default' : {
'BACKEND': 'johnny.backends.memcached.MemcachedCache',
'LOCATION': ['127.0.0.1:11211'],
'JOHNNY_CACHE' True,
)
}
Onde:
johnny.middleware.LocalStoreClearMiddleware:
O Johnny utiliza
este middleware para gerenciar o cache de uma maneira "thread-safe".
Ele basicamente limpa este objeto ao final de cada
requisição.johnny.middleware.QueryCacheMiddleware:
É o middleware
responsável pela “mágica” de caching no ORM.O backend johnny.backends.memcached.MemcachedCache
é basicamente
uma subclasse do backend built-in do Django para o Memcached,
com a adição do seguinte comportamento: Se o timeout for setado como 0
(zero), o cache fica “infinito”.
Com a opção JOHNNY_CACHE
como True
, estamos informando ao
Johnny que este é o pool de cache que ele deve usar para caching
das queries. É possível ter uma configuração diferenciada, como
encontrada no projeto Cifonauta.
Com a ajuda do django-debug-toolbar, podemos ver o número de queries diminuírem consideravelmente (fatalmente ocasionando um tempo de resposta menor). Quando um registro for adicionado, editado ou removido, o Johnny Cache remove as queries envolvendo determinada tabela do cache, permitindo assim uma nova “batida” no banco de dados (e um novo armazenamento dos resultados).
Como já mencionei, gosto muito dos ensinamentos e experiências compartilhadas pelo pessoal da 37Signals. Um deles é para nos preocuparmos com performance quando isto for realmente um problema.
Logo, (IMO) não construa uma mega infraestrutura para uma aplicação que atende 50 usuários por dia. Você está desperdiçando o seu tempo e linhas de código. Em contrapartida, qualquer esforço para melhorar a experiência do usuário, e para economizar recursos, é sempre bem-vinda.
Vale sempre ressaltar que problemas de performance podem estar relacionados a qualidade do código produzido, e não necessariamente com o consumo da aplicação. Então, se a demanda está baixa e mesmo assim você tem tempos de resposta absurdamente altos, talvez seja a hora de “refatorar” o seu código.