Myhro Blog

Detecção de IP por trás de um proxy reverso com o Django

A pilha de serviços mais comum em um ambiente de produção de uma aplicação Django hoje consiste basicamente em:

  • Um servidor WSGI, como o Gunicorn ou o uWSGI, responsável por servir sua aplicação.
  • Um proxy reverso, como o nginx, servindo arquivos estáticos e passando todas as outras requisições para o servidor de aplicação.

Esta é uma solução bacana, eficiente, escalável e não há nada de errado com isso. Entretanto, precisamos tomar um certo cuidado ao realizar ações que envolvem a detecção de IP do cliente qual está acessando a aplicação. Quando não temos um proxy reverso na frente, basta verificar o conteúdo do request.META['REMOTE_ADDR'] para descobrirmos o IP qual originou a requisição. Do contrário, a situação muda de figura.

Quando o proxy reverso recebe uma requisição do cliente e a encaminha para o servidor de aplicação, o IP de origem da requisição passa a ser o do próprio proxy reverso. Chegamos em uma situação onde, por exemplo, para a aplicação todos as requisições parecem vir de um mesmo cliente, com mesmo IP. Para contornar esta situação, o padrão de facto é utilizar o cabeçalho X-Forwarded-For do protocolo HTTP, qual indica que se trata de uma requisição previamente encaminhada.

Procurando sobre como detectar o IP por trás de um proxy reverso com o Django no Google, você poderia se deparar com este trecho de código, qual executa uma simples verificação retorna o IP detectado. A fim de simplificar a explicação do problema, vamos utilizar uma versão reduzida do mesmo como exemplo, qual basicamente verifica se o cabeçalho está presente e extrai o primeiro IP do campo. Caso contrário, retorna apenas o REMOTE_ADDR:

1
2
3
4
5
6
def get_client_ip(request):
    ip = request.META.get('HTTP_X_FORWARDED_FOR')
    if ip:
        return ip.split(', ')[0]
    else:
        return request.META['REMOTE_ADDR']

Imediatamente, podemos levantar a seguinte pergunta: como assim o primeiro IP?!

Isto acontece pois caso já exista uma entrada X-Forwarded-For no cabeçalho, ao encaminhar a requisição o nginx irá concatenar o IP detectado do cliente ao final da string deste campo, resultando em dois IPs ao invés de um só. Portanto, o correto é pegar o último endereço extraído e não o primeiro, pois do contrário o cliente poderia facilmente forjar seu IP:

1
2
3
4
    (...)
    if ip:
        return ip.split(', ')[-1]
    (...)

Multiboot com o GRUB a partir de subvolumes Btrfs

Estava lendo o excelente (e gratuito) livro do Raphaël Hertzog e Roland Mas The Debian Administrator’s Handbook: Debian Wheezy from Discovery to Mastery, quando me deparei com o capítulo sobre Logical Volume Manager (LVM). LVM é um daqueles assuntos que sempre ouvia falar, mas deixava de fuçar por pura preguiça e comodismo, no estilo ”gosto dos meus discos particionados do jeito que estão”, sem compreender de fato os benefícios quais poderiam ser obtidos com um mínimo de esforço.

A ideia de ter uma ou mais partições físicas no disco (Physical Volume - PV) divididas em volumes lógicos (Logical Volumes - LV) me resolveria um problema qual venho tendo desde quando passei utilizar o disco encriptado com LUKS/cm-crypt: a impossibilidade de aumentar ou diminuir o tamanho das partições. Se eu estivesse utilizando LVM desde o começo, isto nunca teria me acontecido. Esta abordagem, entretanto, ainda possui uma limitação: embora volumes lógicos possam ser redimensionados, às vezes sem nem mesmo precisa desmontar o sistema de arquivos, estes ainda precisam ter um tamanho fixo definido.

Seria possível conviver com esta limitação não-crucial perfeitamente, desde que uma solução mais flexível não existisse: os subvolumes do Btrfs. É claro que eles não são um substituto perfeito, pois não podem ser tratados como um dispositivo de bloco e particionados em um sistema de arquivos diferente, como no caso dos volumes lógicos do LVM. Entretanto, como minha necessidade era a de apenas isolar volumes como se fossem sistemas de arquivos separados (mas ainda do mesmo tipo), a solução se encaixaria perfeitamente. E no caso do Btrfs, a utilização de quotas para limitar a utilização do subvolume é opcional.

A utilização de subvolumes no Btrfs é tão simples quanto manipular um diretório. Um processo um pouco mais complicado, mas que ainda não é difícil de ser feito, é dar boot em uma instalação de uma distribuição Linux localizada em um subvolume. O complicador, neste caso, é instruir ao GRUB como encontrar cada diretório /boot e impedir que uma instalação sobrescreva a configuração do GRUB da outra - ou pelo menos tentar, porque às vezes o comando grub-install é chamado inevitavelmente, como quando se atualiza entre diferentes versões do Debian (do stable pro testing, por exemplo).

Como em tantas outros casos na computação, há diversas formas de se realizar este processo. O que será descrito aqui é apenas o mais simples que encontrei até o momento. Durante minha pesquisa, vi instruírem a mudar o subvolume padrão ou editar manualmente o grub.cfg, coisas que você não precisa ou não deve fazer de forma alguma. Vale ressaltar também que o Btrfs é considerado experimental até o presente momento e que você não deve realizar nenhum dos passos descritos aqui sem o devido backup dos seus dados, a não ser que não se importe em perdê-los. Ao mesmo tempo, há pessoas o utilizando há anos sem nenhum incidente sério e se deliciando com as possibilidades trazidas.

Todo o processo a seguir foi realizado a partir de uma instalação padrão do Debian Wheezy (mais especificamente a versão 7.6) em uma única partição formatada como Btrfs.

Movendo a instalação padrão para um subvolume

Diferentemente do Ubuntu e seus derivados, que criam dois subvolumes (@ e @home) na instalação padrão em um sistema Btrfs, o Debian simplesmente instala o sistema na raíz da partição, sem criar nenhum subvolume. É preciso, então, criar um subvolume manualmente e mover a instalação para este. Como moveremos o próprio sistema, não é possível fazer isto iniciando a partir do mesmo. É preciso utilizar um outro sistema, a partir de um LiveCD ou pendrive USB preparado para isto - basta que este tenha suporte ao Btrfs.

Montando a raíz do sistema e verificando o resultado:

root@xubuntu:~# mount /dev/sda1 /mnt/
root@xubuntu:~# mount | grep mnt
/dev/sda1 on /mnt type btrfs (rw)

Verificando a não existência de subvolumes e criando dois novos:

root@xubuntu:~# btrfs subvol list /mnt/
root@xubuntu:~# btrfs subvol create /mnt/@stable
Create subvolume '/mnt/@stable'
root@xubuntu:~# btrfs subvol create /mnt/@home
Create subvolume '/mnt/@home'
root@xubuntu:~# btrfs subvol list /mnt/
ID 260 gen 3727 top level 5 path @stable
ID 261 gen 3728 top level 5 path @home

Movendo a instalação para o subvolume @stable:

root@xubuntu:~# cd /mnt/
root@xubuntu:/mnt# for i in $(ls -1 | egrep -v '(@|home)'); do mv $i @stable/; done

Movendo a /home para o seu subvolume. O diretório criado dentro do subvolume @stable é para que o subvolume @home possa ser montado lá:

root@xubuntu:/mnt# mv home/* @home/
root@xubuntu:/mnt# rmdir home/
root@xubuntu:/mnt# mkdir @stable/home

Verificando o resultado, é possível notar que, conforme dito, na utilização os subvolumes não diferem muito de simples diretórios:

root@xubuntu:/mnt# ls -lh
total 0
drwxr-xr-x 1 root root  14 Jul 16 15:49 @home
drwxr-xr-x 1 root root 180 Jul 16 15:49 @stable
root@xubuntu:/mnt# ls -lh @home/
total 0
drwxr-xr-x 1 1000 1000 54 Jul 16 14:22 usuario
root@xubuntu:/mnt# ls -lh @stable/
total 8.0K
drwxr-xr-x 1 root root 1.3K Jul 16 14:04 bin
drwxr-xr-x 1 root root  186 Jul 16 14:05 boot
drwxr-xr-x 1 root root  418 Jul 16 13:52 dev
drwxr-xr-x 1 root root 2.5K Jul 16 15:32 etc
drwxr-xr-x 1 root root    0 Jul 16 15:49 home
lrwxrwxrwx 1 root root   30 Jul 16 13:52 initrd.img -> /boot/initrd.img-3.2.0-4-amd64
drwxr-xr-x 1 root root  532 Jul 16 13:54 lib
drwxr-xr-x 1 root root   40 Jul 16 13:52 lib64
drwxr-xr-x 1 root root   22 Jul 16 13:51 media
drwxr-xr-x 1 root root    0 Jun 11 21:07 mnt
drwxr-xr-x 1 root root    0 Jul 16 13:51 opt
drwxr-xr-x 1 root root    0 Jun 11 21:07 proc
drwx------ 1 root root   74 Jul 16 15:25 root
drwxr-xr-x 1 root root    0 Jul 16 15:24 run
drwxr-xr-x 1 root root 2.4K Jul 16 15:24 sbin
drwxr-xr-x 1 root root    0 Jun 10  2012 selinux
drwxr-xr-x 1 root root    0 Jul 16 13:51 srv
drwxr-xr-x 1 root root    0 Jul 14  2013 sys
drwxrwxrwt 1 root root    0 Jul 16 15:24 tmp
drwxr-xr-x 1 root root   70 Jul 16 13:51 usr
drwxr-xr-x 1 root root   90 Jul 16 13:51 var
lrwxrwxrwx 1 root root   26 Jul 16 13:52 vmlinuz -> boot/vmlinuz-3.2.0-4-amd64

A diferença está em como eles se comportam e o que podemos fazer com eles. O que faremos agora é montar o subvolume @stable como se fosse uma partição, montando nele os diretórios /dev, /proc e /sys para que possamos rodar os comandos do GRUB quando fizermos chroot nele:

root@xubuntu:/mnt# cd
root@xubuntu:~# umount /mnt
root@xubuntu:~# mount -o subvol=@stable /dev/sda1 /mnt/
root@xubuntu:~# for i in /dev /proc /sys; do mount -o bind $i /mnt$i; done
root@xubuntu:~# chroot /mnt/
root@xubuntu:/# update-grub
Generating grub.cfg ...
Found linux image: /boot/vmlinuz-3.2.0-4-amd64
Found initrd image: /boot/initrd.img-3.2.0-4-amd64
done

Verificando o resultado da atualização do GRUB, o importante aqui são dois detalhes envolvendo o subvolume @stable: este deve aparecer como se fosse o diretório pai do /boot e também estar presente no parâmetro rootflags=subvol=@stable. Do contrário, o GRUB não encontraria o kernel a ser inicializado e o subvolume não seria corretamente montado como diretório raíz:

root@xubuntu:/# grep '@' /boot/grub/grub.cfg
if loadfont /@stable/usr/share/grub/unicode.pf2 ; then
  set locale_dir=($root)/@stable/boot/grub/locale
    linux   /@stable/boot/vmlinuz-3.2.0-4-amd64 root=UUID=6ce02f0b-4fe0-447e-a418-a1c17f029233 ro rootflags=subvol=@stable  quiet
    initrd  /@stable/boot/initrd.img-3.2.0-4-amd64
    linux   /@stable/boot/vmlinuz-3.2.0-4-amd64 root=UUID=6ce02f0b-4fe0-447e-a418-a1c17f029233 ro single rootflags=subvol=@stable
    initrd  /@stable/boot/initrd.img-3.2.0-4-amd64

Estando tudo como esperado, é necessário executar a instalação do GRUB novamente:

root@xubuntu:/# grub-install /dev/sda
Installation finished. No error reported.

É preciso editar também o /etc/fstab, informando o ponto de montagem dos subvolumes. O resultado deve ser parecido com este:

1
2
3
4
5
6
7
8
9
# /etc/fstab: static file system information.
#
(...)
#
# <file system> <mount point>   <type>  <options>       <dump>  <pass>
# / was on /dev/sda1 during installation
UUID=6ce02f0b-4fe0-447e-a418-a1c17f029233    /        btrfs    defaults,subvol=@stable    0    0
UUID=6ce02f0b-4fe0-447e-a418-a1c17f029233    /home    btrfs    defaults,subvol=@home      0    0
(...)

Obs.: Talvez a coluna <pass> esteja definida com outro valor maior que zero. Isto acontece pois em sistemas de arquivos tradicionais, o sistema precisa rodar fsck caso o sistema não tenha sido desmontado corretamente e seu log precise ser verificado antes de ser montado novamente. Isto não é necessário no caso do Btrfs, por isso definimos o valor como 0.

Em seguida, basta sair do chroot e reiniciar o sistema normalmente:

root@xubuntu:/# exit
exit
root@xubuntu:~# reboot
root@xubuntu:~#
Broadcast message from xubuntu@xubuntu
    (/dev/pts/8) at 16:18 ...

The system is going down for reboot NOW!

Ao logar no sistema, podemos verificar que os subvolumes foram montados corretamente:

root@stable:~# df -h
Filesystem                                              Size  Used Avail Use% Mounted on
rootfs                                                  8.0G  541M  6.8G   8% /
udev                                                     10M     0   10M   0% /dev
tmpfs                                                    50M  216K   50M   1% /run
/dev/disk/by-uuid/6ce02f0b-4fe0-447e-a418-a1c17f029233  8.0G  541M  6.8G   8% /
tmpfs                                                   5.0M     0  5.0M   0% /run/lock
tmpfs                                                   100M     0  100M   0% /run/shm
/dev/sda1                                               8.0G  541M  6.8G   8% /home
root@stable:~# ls -lh /home/
total 0
drwxr-xr-x 1 usuario usuario 54 Jul 16 11:22 usuario

Clonando o sistema em outro subvolume

Até o presente momento, apenas movemos a instalação do Debian para um subvolume, criando outro para a /home e configuramos o sistema para dar boot na nova configuração. Isto não é muito diferente do que a instalação do Ubuntu faria por padrão. Mas é agora que as coisas começam a ficar interessantes: vamos clonar a instalação atual, realizando um snapshot do subvolume que possa ser atualizado para o Debian Jessie - sem influenciar em nada na instalação atual.

Como estamos utilizando um subvolume como raíz, é necessário montar a partição sem especificar um dos subvolumes criados, para que possamos criar outros:

root@stable:~# mount /dev/sda1 /mnt/
root@stable:~# ls -lh /mnt/
total 0
drwxr-xr-x 1 root root  14 Jul 16 12:49 @home
drwxr-xr-x 1 root root 180 Jul 16 12:49 @stable
root@stable:~# btrfs subvol list /mnt/
ID 260 top level 5 path @stable
ID 261 top level 5 path @home
root@stable:~# btrfs subvol snapshot /mnt/@stable /mnt/@testing
Create a snapshot of '/mnt/@stable' in '/mnt/@testing'
root@stable:~# btrfs subvol list /mnt/
ID 260 top level 5 path @stable
ID 261 top level 5 path @home
ID 262 top level 5 path @testing

É importante frisar: este processo de criação do snapshot é instantâneo e praticamente não consome espaço em disco. Devido a natureza Copy-on-write (COW) do Btrfs, este snapshot é apenas uma referência ao conteúdo atual do subvolume qual o snapshot foi realizado - os dados serão gravados a medida que o subvolume do snapshot for modificado.

Realizaremos novamente o processo de chroot, desta vez no clone do subvolume, repetindo as alterações realizadas no /etc/fstab e rodando o update-grub, com cuidado para não executar o grub-install pois não é isto o que queremos, pois do contrário não conseguiríamos mais inicializar no sistema atual.

root@stable:~# umount /mnt
root@stable:~# mount -o subvol=@testing /dev/sda1 /mnt/
root@stable:~# for i in /dev /proc /sys; do mount -o bind $i /mnt$i; done
root@stable:~# chroot /mnt/
root@stable:/# sed -i s/@stable/@testing/ /etc/fstab
root@stable:/# update-grub
Generating grub.cfg ...
Found linux image: /boot/vmlinuz-3.2.0-4-amd64
Found initrd image: /boot/initrd.img-3.2.0-4-amd64
done

Vamos aproveitar também para alterar o hostname, de forma a identificar mais facilmente se inicializamos o sistema correto logo mais:

root@stable:/# sed -i s/stable/testing/ /etc/hostname
root@stable:/# sed -i s/stable/testing/ /etc/hosts

Saindo do chroot, é necessário alterar as configurações do GRUB do primeiro sistema, pois o que faremos é um chainload entre as instalações do GRUB. Este recurso normalmente é utilizado para que o GRUB passe o processo de instalação para o outro sistema operacional não suportado, como o Windows, mas aqui faremos com que exista uma opção no menu de inicialização que simplesmente carregue a configuração do GRUB armazenada em outro subvolume.

root@stable:/# exit
root@stable:~# vi /etc/grub.d/40_custom

Adicionaremos uma entrada muito simples, consistindo em apenas quatro linhas. Seu arquivo 40_custom deve ficar parecido com isto:

1
2
3
4
5
6
7
8
9
#!/bin/sh
exec tail -n +3 $0
# This file provides an easy way to add custom menu entries.  Simply type the
# menu entries you want to add after this comment.  Be careful not to change
# the 'exec tail' line above.
menuentry 'Debian testing' {
    set root='(hd0,msdos1)'
    configfile /@testing/boot/grub/grub.cfg
}

A linha set root=... pode variar conforme sua configuração, de acordo com a nomenclatura utilizada pelo GRUB. Neste caso, hd0 é o primeiro disco (sda) e msdos1 é a primeira partição (sda1). Em seguida, basta rodar o update-grub e reiniciar a máquina para ter acesso a opção de inicialização no novo subvolume. É normal que neste ponto não apareça nada sobre a opção que foi adicionada ao 40_custom, pois o update-grub exibe apenas os kerneis quais detectou automaticamente:

root@stable:~# update-grub
Generating grub.cfg ...
Found linux image: /boot/vmlinuz-3.2.0-4-amd64
Found initrd image: /boot/initrd.img-3.2.0-4-amd64
done
root@stable:~# reboot

Broadcast message from root@stable (pts/0) (Wed Jul 16 14:08:21 2014):

The system is going down for reboot NOW!

Podemos ver a opção do chainload “Debian testing”:

E escolhendo-a, somos apresentados a tela do GRUB do volume @testing, qual só possui seus próprios kerneis, acompanhada da opção de voltar pra tela anterior com a tecla ESC:

Após a inicialização, podemos conferir a montagem correta dos subvolumes, com o @home compartilhado entre ambas as instalações stable e testing:

root@testing:~# df -h
Filesystem                                              Size  Used Avail Use% Mounted on
rootfs                                                  8.0G  542M  6.8G   8% /
udev                                                     10M     0   10M   0% /dev
tmpfs                                                    50M  216K   50M   1% /run
/dev/disk/by-uuid/6ce02f0b-4fe0-447e-a418-a1c17f029233  8.0G  542M  6.8G   8% /
tmpfs                                                   5.0M     0  5.0M   0% /run/lock
tmpfs                                                   100M     0  100M   0% /run/shm
/dev/sda1                                               8.0G  542M  6.8G   8% /home
root@testing:~# ls -lh /home/
total 0
drwxr-xr-x 1 usuario usuario 54 Jul 16 11:22 usuario

A partir deste momento, você pode utilizar o sistema normalmente. O fato dele estar em um subvolume, tendo sido inicializado a partir de um encadeamento de configurações do GRUB não influenciará em praticamente nada no seu fucionamento, com exceção de que você deve tomar cuidado para que o comando grub-install não sobrescreva a configuração atual. Se isto acontecer, será possível inicializar apenas este subvolume @testing, sendo necessário realizar novamente o processo de restauração do GRUB, montando o subvolume @stable, os diretórios especiais e executando o grub-install dentro do chroot.

Você poderia, também, utilizar uma configuração do 40_custom nesta instalação que possibilitasse o encadeamento de outro GRUB, mas o processo pode ficar confuso, já que a ordem do menu seria alterada dependendo de qual instalação sobrescreveu o GRUB por último. Mesmo assim, esta pode ser uma opção melhor do que simplesmente perder o acesso às outras instalações devido ao acontecimento de algum imprevisto.

Referências:

  1. Make maintaining GRUB easier for multi-booters, a “How To.”
  2. More BTRFS fun: Multibooting to subvolumes on the same partition.

Simplificando a utilização do Cron

O Cron é o agendador de tarefas padrão de facto do mundo Unix. No Linux, a versão mais difundida é o Vixie Cron, desenvolvido originalmente por Paul Vixie em 1987 (é incrível perceber como os utilitários do Unix são atemporais). Além de poder simplesmente adicionar scripts para serem executados nas pastas /etc/cron.{hourly,daily,weekly,monthly}, é possível utilizar um arquivo crontab, para obter maior flexibilidade no agendamento de tarefas. Esta abordagem, entretanto, apresenta duas complicações principais:

  • A sintaxe dos horários dos agendamentos, embora clara, pode ser um pouco críptica para casos mais simples.
  • O ambiente de execução das tarefas é diferente de um shell habitual, sendo muito mais restrito, tornando bastante comum o caso de scripts que funcionam perfeitamente quando testados na linha de comandos, mas que falham quando adicionados ao cron.

O primeiro caso possui um facilitador não muito conhecido, qual só descobri quando me deparei com este artigo no Linux Journal no mês passado: é possível utilizar atalhos, tornando a sintaxe muito mais legível:

Atalho Significado
@reboot Executar uma vez, na inicialização.
@hourly Executar uma vez por hora, equivalente a "0 * * * *".
@daily Executar uma vez por dia, equivalente a "0 0 * * *".
@midnight (O mesmo que @daily)
@weekly Executar uma vez por semana, equivalente a, "0 0 * * 0".
@monthly Executar uma vez por mês, equivalente a "0 0 1 * *".
@yearly Executar uma vez por ano, equivalente a "0 0 1 1 *".
@annually (O mesmo que @yearly)

Esta lista pode ser encontrada na man page do crontab, seção 5.

No segundo caso, o sintoma mais comum é o fato dos comandos simplesmente não funcionarem: uma chamada ao wget gera uma mensagem de ”command not found”. Um paleativo é simplesmente substituir os comandos pelo caminho completo do executável, como /usr/bin/wget. Mas no caso de scripts mais complexos, isso não apenas fica impraticável, como pode se tornar impossível caso este chame um outro script externo (como no caso de um script em shell chamando outro escrito em Python). A solução definitiva é definir a variável $PATH no próprio crontab, antes das linhas onde os comandos são agendados, como:

# Edit this file to introduce tasks to be run by cron.
#
# Each task to run has to be defined through a single line
# indicating with different fields when the task will be run
# and what command to run for the task
#
(...)
#
# For more information see the manual pages of crontab(5) and cron(8)
#
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
# m h  dom mon dow   command
(...)

E deste momento em diante, todos os scripts executados a partir do cron passarão a estar cientes dos caminhos dos executáveis do sistema. Caso necessário, é possível também definir a variável $SHELL, para que os scripts sejam executados em outro shell que não o padrão /bin/sh.

Filas assíncronas no Django com o Redis Queue

Tenho duas grandes paixões nesta vida: mulheres bonitas e cheirosas e softwares que resolvem um problema de forma magistral, embora excepcionalmente simples. Como exemplares da primeira opção andam escassos em meu cotidiano, falarei hoje sobre o Django-RQ. Este, como o próprio nome sugere, facilita a integração do Django com o RQ (Redis Queue), e foi recentemente apresentado a mim por Herberth Amaral (indivíduo que mesmo trabalhando 20h por dia ainda tem tempo para descobrir coisas novas).

Os porquês de se usar filas de processamento assíncrono/paralelo são largamente conhecidos e o Fabio Akita fez uma análise excelente, como sempre, neste artigo em seu blog. Resumidamente, no caso de aplicações web, você realizaria o processamento em background de qualquer ação qual a requisição do usuário não dependa para ser completada, como redimensionar uma imagem após seu upload ou enviar um e-mail de confirmação após um cadastro.

No ecossistema Python, a ferramenta padrão de facto utilizada para isto é o Celery, qual fiz uso por bastante tempo antes de conhecer o Django-RQ. O Celery é muito bom e funciona forma satisfatória, mas é um pouco lerdo para subir/matar seus workers e mais complicado do que deveria, até mesmo para começar a utilizá-lo pela primeira vez. Uma vantagem dele é que a comunicação aplicação -> fila -> workers pode ser feita utilizando o banco de dados do próprio Django, sem dependências externas. No caso do Django-RQ, precisamos do Redis, o banco de dados NoSQL mais popular do mundo, algo que não chega a ser necessariamente uma complicação.

A instalação e utilização do Django-RQ está descrita de forma bem objetiva em seu README. O que você precisa fazer, basicamente, é adicionar as configurações de acesso ao Redis no seu settings.py e, a cada vez que fosse chamar uma função cuja execução você espera que seja demorada, por motivos de CPU ou I/O, enfileiraria sua chamada. Os workers quais estiverem rodando ou forem criados posteriormente passarão a consumir as chamadas armazenadas na fila, processando-as de forma assíncrona.

O detalhe que me deixou verdadeiramente satisfeito e ao mesmo tempo completamente impressionado é muito simples, porém extremamente útil: se a execução de algum dos jobs (nome dado a cada um dos itens da fila) falhar, estes serão armazenados em uma fila separada, podendo ser re-enfileirados com apenas um botão na interface de administração do Django. Desta forma, você pode corrigir o problema qual causou o erro para que estes possam ser devidamente processados em uma próxima oportunidade, sem que os dados sejam descartados pela ocorrência da falha.

Cuide bem do seu requirements.txt

Uma das coisas que considero mais úteis, e ao mesmo tempo incrivelmente simples, em ambientes de desenvolvimento modernos é o gerenciamento de dependências externas. Se for preciso, por exemplo, utilizar uma biblioteca de acesso a API de um serviço externo, como Twitter ou Facebook, você não precisa commitar o código da mesma junto com seu repositório. Nestes casos, basta apenas declará-la no formato do seu gerenciador de dependências, e esta poderá ser facilmente instalável e mantida em qualquer outro ambiente além da sua própria máquina de desenvolvimento, como o computador de outro programador ou o servidor de produção.

Cada linguagem possui uma ou mais ferramentas específicas para isto. No caso do Python, o utilitário recomendado para esta tarefa é o pip, que substituiu o por muito tempo utilizado easy_install (por sua vez parte do Setuptools). Seu funcionamento é muito simples, bastando utilizar o comando pip install [pacote], de preferência em um virtualenv, para ter o pacote desejado instalado.

Um recurso muito bacana é a possibilidade de se especificar uma lista de dependências em um arquivo-texto qualquer, normalmente chamado requirements.txt, qual pode ser utilizada a partir do comando pip install -r <arquivo.txt>. O problema é que, justamente por se tratar de um arquivo muito simples, as pessoas muitas vezes não tem o devido zelo com a manutenção do mesmo - o que é algo que tenho visto, infelizmente, se tornar cada vez mais frequente mesmo entre programadores mais experientes. Por este motivo, compilei esta sucinta lista de boas práticas e cuidados a serem tomados com seus arquivos de dependências externas.

Nunca gere o requirements.txt a partir do comando pip freeze sem usar o grep

Embora a própria documentação oficial recomende esta prática, por favor não faça isto. O motivo para evitar isso é muito simples: pacotes Python tem informações sobre suas próprias dependências, o que torna desnecessário especificá-las individualmente. Veja o seguinte exemplo, na instalação do Fabric:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(projetoenv)[myhro@wheezy:~/projeto]$ pip install fabric
Downloading/unpacking fabric
[...]
Successfully installed fabric paramiko pycrypto ecdsa
Cleaning up...
(projetoenv)[myhro@wheezy:~/projeto]$ pip freeze > requirements.txt
(projetoenv)[myhro@wheezy:~/projeto]$ cat requirements.txt
Fabric==1.8.3
argparse==1.2.1
distribute==0.6.24
ecdsa==0.11
paramiko==1.12.4
pycrypto==2.6.1
wsgiref==0.1.2

Veja que, além das próprias dependências do Fabric, foram adicionados até mesmo os pacotes instalados durante a criação do virtualenv. Em contrapartida, o requirements.txt ficaria muito mais simples e legível se criado com o auxílio do comando grep:

1
2
3
(projetoenv)[myhro@wheezy:~/projeto]$ pip freeze | grep -i fabric > requirements.txt
(projetoenv)[myhro@wheezy:~/projeto]$ cat requirements.txt
Fabric==1.8.3

A partir daí, novas dependências seriam concatenadas ao final do arquivo com a utilização do redirecionador >>:

1
2
3
4
5
6
7
8
9
(projetoenv)[myhro@wheezy:~/projeto]$ pip install django
Downloading/unpacking django
[...]
Successfully installed django
Cleaning up...
(projetoenv)[myhro@wheezy:~/projeto]$ pip freeze | grep -i django >> requirements.txt
(projetoenv)[myhro@wheezy:~/projeto]$ cat requirements.txt
Fabric==1.8.3
Django==1.6.5

Mantê-lo ordenado em ordem alfabética produz um resultado melhor ainda. A busca por uma forma de automatizar esta etapa fica como exercício para o leitor.

Especifique as versões das bibliotecas

Se você edita o arquivo requirements.txt na mão, pode se sentir tentado a adicionar uma entrada apenas com o nome da dependência, da mesma forma qual a instalou na linha de comandos. Embora isto possa parecer interessante, já que você sempre teria acesso à última versão da mesma, isto na verdade pode lhe gerar problemas no futuro. Hoje, o comando pip install django instalou o Django 1.6.5, mas e daqui a algum tempo, quando a versão 1.7 for lançada? Se você estiver fazendo uso de classes/funções/módulos quais foram removidos, parte de sua aplicação estará quebrada apenas por inconsistência entre as versões.

Remova dependências obsoletas

Esta dica é bem óbvia, mas é algo que muitas vezes passa batido: a lista de dependências externas também deve ser atualizada de acordo com as mudanças realizadas na sua base de código, não apenas para adicionar novas entradas, mas também para remover bibliotecas quais não serão mais utilizadas. Embora a economia de espaço em disco não seja muito grande, visto que a maioria das bibliotecas Python são pequenas, a confusão causada por uma dependência qual deveria ter sido removida da lista há meses pode ser.

Modularize o requirements.txt para utilização em diferentes ambientes, quando necessário

Pra que instalar o IPython no servidor de produção? Ou pra que instalar o Gunicorn na sua máquina, se localmente você só usa o servidor de desenvolvimento do Django? Como cada linha do arquivo com a lista de dependências é executada em um comando pip install, você pode simplesmente adicionar uma entrada -r <arquivo.txt>, para instalar as dependências presentes em outra lista.

1
2
3
(projetoenv)[myhro@wheezy:~/projeto]$ cat development.txt
-r requirements.txt
ipython==2.1.0

Desta forma, o comando pip install -r development.txt instalaria todas as dependências contidas no arquivo requirements.txt e também o IPython.