em Celery, Django, Português, programação, Python

Tarefas demoradas de forma assíncrona com Django e Celery

Estou escrevendo esse post empolgado por uma lightning talk que fiz no GruPy-SP (16/07). Assim aproveito pra explicar um pouco mais o que estou dizendo nos slides.

O problema de tarefas demoradas do lado do servidor

Sempre que o cliente faz uma requisição web (request), o servidor faz um processamento, normalmente pra quem usa o django, ele lê a requisição, trata os dados recebidos, salva ou recupera registros do banco de dados (através dos models), faz algum processamento do que será exibido para o usuário, renderiza isso em um template e manda uma resposta (response) para o cliente.

Dependendo da tarefa que você executa no servidor a resposta pode demorar muito e isso leva à problemas de TimeOut, a experiência do usuário fica comprometida (quanto mais o servidor demorar pra responder maior a chance do usuário sair do seu site) e também pode acontecer da lentidão ser causada por outros, como por exemplo o uso de uma API externa.

Existem diversas tarefas que podem demorar pra ser executadas. Se você tiver que criar um relatório pesado, acionado por um client web. Se de repente você precisar enviar diferentes emails para uma lista ou por exemplo editar um vídeo depois que o usuário faz o upload na sua página.

Caso real

Esse é um problema que me deparei um dia na geração de um relatório. O relatório levava cerca de 20 minutos para ser gerado, o cliente recebia um erro de timeout e obviamente não dá pra esperar 20 minutos pra gerar alguma coisa. Pra driblar isso e deixar a tarefa em background, alguém resolveu usar um comando do sistema operacional. (No linux se você colocar & no final de um comando esse comando roda em background).

django-view-os-system

Note que em seguida o django envia mostra uma mensagem ao usuário de que ele será avisado por e-mail ao final da geração do relatório.
Alguma coisa não parece certa aqui.

Me arrependo imediatamente dessa decisão

Celery – A solução para esses problemas!

O Celery é um gerenciador de tarefas assíncronas. Com ele você pode executar uma fila de tarefas (que ele recebe por meio de mensagens), pode agendar tarefas direto no seu projeto sem precisar do cron e ele ainda tem integração fácil com a maioria dos frameworks python mais utilizados (django, flask, tornado, etc.)

Como o Celery funciona

Essa imagem kibada da internet dá uma visão geral de como fica o fluxo da sua aplicação.

estrutura-celery

  • O User (ou Client ou Producer) é a sua aplicação Django.
  • O AMPQ Broker é um Message Broker. Um programa responsável por manter a fila de mensagens que serão trocadas entre o seu programa e o Celery, geralmente é o RabbitMQ ou o Redis
  • Os workers (ou consumers) que vão executar as tarefas que você quer que sejam assíncronas.
  • O Result Store, que é onde os workers podem ou não salvar os resultados das tarefas que foram executadas.

O Client pode passar uma tarefa, uma lista de tarefas, tarefas periódicas ou o retry de alguma tarefa pra fila do Message Broker. O Message Broker distribui essas tarefas entre os workers e o resultado dessas tarefas pode ser escrito em um Result Store (Memcahed, RDBMS, MongoDB, etc…) que mais tarde pode ser lido pelo Client novamente.

Qual Broker utilizar?

O recomendado pelo Celery é o RabbitMQ.

Instalando e configurando o RabbitMQ

Existem vários exemplos de como utilizar o Redis, eu vou mostrar como usar o RabbitMQ.

Você só precisa:

  1. Instalar o RabbitMQ (Baixar o pacote no site ou adicionar o repo deles no sources.list e instalar com APT-GET)
    sudo apt-get install rabbitmq-server
  2. Criar um usuário, um virtual host e dar permissões para esse usuário no virtualhost:
    
    sudo rabbitmqctl add_user myuser mypassword
    sudo rabbitmqctl add_vhost myvhost
    sudo rabbitmqctl set_permissions -p myvhost myuser ".*" ".*" ".*"
    

Instalando e configurando o Celery

pip install celery

No seu settings.py:

#Celery Config
BROKER_URL = 'amqp://guest:guest@localhost:5672//'
CELERY_ACCEPT_CONTENT = ['json']
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'

Na pasta do projeto (a mesma pasta do settings.py) crie um arquivo chamado celery.py:


from __future__ import absolute_import

import os
from celery import Celery
from django.conf import settings

# set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'nome_do_proj.settings')

app = Celery('nome_do_proj')

# Using a string here means the worker will not have to
# pickle the object when using Windows.
app.config_from_object('django.conf:settings')
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)

Esse autodiscover_tasks permite que seu projeto ache todas as tarefas assíncronas de cada app instalada nele. Nessa mesma pasta do settings.py, você vai modificar o arquivo de pacote __init__.py:


from __future__ import absolute_import

# This will make sure the app is always imported when
# Django starts so that shared_task will use this app.
from .celery import app as celery_app

Criando as tarefas assíncronas do seu app

Na pasta do seu app crie um arquivo chamado tasks.py:

from __future__ import absolute_import
from celery import shared_task
from relatorios import gerar_relatorio_excel


@shared_task
def gerar_relatorio(data_inicial, data_final, email):
    gerar_relatorio_excel(
        data_inicial = data_inicial,
        data_final = data_final,
        email = email
    )
    return "Relatorio Excel gerado"

Agora é só fazer importar essa função que foi criada usando o método delay que foi adicionado à sua função pelo decorator shared_task.

tabelao assincrono

Rodando o Celery

Agora você precisa rodar o celery. Ele vai rodar os seus workers pra executar suas tarefas de forma assíncrona. Por padrão ele inicia um worker para cada núcleo de processador, mas você pode determinar quantos com -c Numero_de_workers

celery --app=nome_projeto worker --loglevel=INFO

E então você verá essa tela:

celery-rodando

Você pode então testar sua função assíncrona com o shell do django mesmo:

chama funcao

E o que aparece na tela que está mostrando o celery:

aparece no celery

Colocando o Celery em Produção

Para usar o Celery em produção eu recomendo que você use o Supervisor

Para instalar o supervisor:

sudo apt-get install supervisor

Depois crie um arquivo de configuração para o celery e coloque um link simbólico para esse arquivo no /etc/supervisor/conf.d/

[program:celery]
command=/home/deploy/.virtualenvs/meu_virtual_env/bin/celery --app=nome_proj worker --loglevel=INFO
directory=/home/deploy/webapps/projeto_django
user=nobody
autostart=true
autorestart=true
redirect_stderr=true

Para o supervisor ler novamente os arquivos que estão em sua pasta de configuração:

sudo supervisorctl reread
sudo supervisorctl update

Agora é só iniciar o celery:

sudo supervisorctl start celery

Bom, acho que é só isso. Esses são os slides da minha apresentação:

Continuação

Celery com múltiplas filas, retry e tarefas agendadas

Fontes

E aqui vão as fontes de pesquisa (ou algumas) que eu utilizei nesse processo:

Bônus:

Como o Feed do Instagram usa o Celery e o RabbitMQ

Escreva um comentário

Comentário

Webmentions

  • Executing time-consuming tasks asynchronously with Django and Celery - Fernando Alves maio 29, 2017

    […] Originally posted in Portuguese […]

  • Celery com múltiplas filas, retry e tarefas agendadas - Fernando Alves maio 29, 2017

    […] ano passado eu escrevi um post sobre Tarefas demoradas de forma assíncrona com Django e Celery, que serve para a maioria dos casos de uso do celery. O problema é que você pode querer e fazer […]