django[十] - 高级应用

 

celery异步任务

 

Django中的异步请求

Django Web中从一个http请求发起,到获得响应返回html页面的流程大致如下:http请求发起 -- http handling(request解析) -- url mapping(url正则匹配找到对应的View) -- 在View中进行逻辑的处理、数据计算(包括调用Model类进行数据库的增删改查)--将数据推送到template,返回对应的template/response

 

 

同步请求:所有逻辑处理、数据计算任务在View中处理完毕后返回response。在View处理任务时用户处于等待状态,直到页面返回结果。

异步请求:View中先返回response,再在后台处理任务。用户无需等待,可以继续浏览网站。当任务处理完成时,我们可以再告知用户。

 

关于Celery

Celery是基于Python开发的一个分布式任务队列框架,支持使用任务队列的方式在分布的机器/进程/线程上执行任务调度

它采用典型的生产生-消费者模式,主要由三部分组成:broker(消息队列)、workers(消费者:处理任务)、backend(存储结果)。实际应用中,用户从Web前端发起一个请求,我们只需要将请求所要处理的任务丢入任务队列broker中,由空闲的worker去处理任务即可,处理的结果会暂存在后台数据库backend中。我们可以在一台机器或多台机器上同时起多个worker进程来实现分布式地并行处理任务。

基本操作 

创建一个任务

tests.py

from celery import Celery,platforms,task,shared_task


# 创建Celery实例
app = Celery('tasks', broker="amqp://127.0.0.1")
platforms.C_FORCE_ROOT = True  

# 创建任务
@app.task
def add(x,y):
    print "%s + %s = %s" % (x,y,x+y)
    return x+y


如果使用redis,可以把broker改为 broker='redis://127.0.0.1:6379/0'
默认是rabbitmq的

  

启动worker

启动Celery Worker来监听并执行任务

$ celery -A tests worker --loglevel=info 
$ celery -A tests worker --l debug  # 或者可以这么起 

-A参数后面是Celery实例,实例的名字可以省略,写全是tests.app  

 

调度任务

python

>>> import tests
>>> tests.add.delay(1,5)
<AsyncResult: d737e63a-366b-4064-83f4-166db847a6e0>
>>> x=tests.add.delay(1,5)
>>> x.task_id
'7e7f040d-12e5-4384-a48c-0fcc01571147'
>>> x.task_name
'tests.add'

  

上面只是一个发送任务的调用,结果是拿不到的.没有定义backend,就是保存任务结果的位置

 

获取返回结果

# set coding:utf-8
from celery import Celery,platforms,task,shared_task

app = Celery('tasks', broker="amqp://127.0.0.1",backend="rpc://127.0.0.1")
    # 新版本rpc将初步替代amqp,用的还是RabbitMQ
    # backend='amqp://127.0.0.1',  # 如果是旧版本,没有rpc,那只能用amqp
platforms.C_FORCE_ROOT = True  


@app.task
def add(x,y):
    print "%s + %s = %s" % (x,y,x+y)
    return x+y

然后重启worker,就可以顺利获取任务结果了:

>>> import tests
>>> tests.add(1,6)
1 + 6 = 7
7

其他操作

t.get() 进入阻塞,没有异步效果了

t.ready() 方法可以返回任务是否执行完成,等到返回True了再去get,就马上能拿到结果:
>>>t.ready()
False
>>>t.ready()
False
>>>t.ready()
True
>>>t.get()
7

t.get(timeout=10) 可以设置超时时间(10s)
>>> t.get(timeout=10)
Traceback (most recent call last):
  File "<pyshell#17>", line 1, in <module>
    t.get(timeout=1)
  File "G:\Steed\Documents\PycharmProjects\venv\Celery\lib\site-packages\celery\result.py", line 169, in get
    no_ack=no_ack,
  File "G:\Steed\Documents\PycharmProjects\venv\Celery\lib\site-packages\celery\backends\base.py", line 238, in wait_for
    raise TimeoutError('The operation timed out.')
celery.exceptions.TimeoutError: The operation timed out.

t.get()设置只获取错误结果,不触发异常
>>> t.get(propagate=False)
AttributeError("'int' object has no attribute 'upper'",)
>>>
traceback 里面存着错误信息
>>> t.traceback
'Traceback (most recent call last):\n  File "g:\\steed\\documents\\pycharmprojects\\venv\\celery\\lib\\site-packages\\celery\\app\\trace.py", line 240, in trace_task\n    R = retval = fun(*args, **kwargs)\n  File "g:\\steed\\documents\\pycharmprojects\\venv\\celery\\lib\\site-packages\\celery\\app\\trace.py", line 438, in __protected_call__\n    return self.run(*args, **kwargs)\n  File "G:\\Steed\\Documents\\PycharmProjects\\Celery\\task1.py", line 25, in upper\n    return v.upper()\nAttributeError: \'int\' object has no attribute \'upper\'\

  

Django中Celery的实现

在实际使用过程中,发现在Celery在Django里的实现与其在一般.py文件中的实现还是有很大差别,Django有其特定的使用Celery的方式。这里着重介绍Celery在Django中的实现方法,简单介绍与其在一般.py文件中实现方式的差别。 

  1. 建立消息队列

  首先,我们必须拥有一个broker消息队列用于发送和接收消息。Celery官网给出了多个broker的备选方案:RabbitMQ、Redis、Database(不推荐)以及其他的消息中间件。在官网的强力推荐下,我们就使用RabbitMQ作为我们的消息中间人。

参考 http://www.cnblogs.com/richardzgt/articles/7771821.html

       2. 安装django-celery

  pip install  celery django-celery  

  #注:安装django-celery会自动安装依赖版本的celery

celery==3.1.26.post2
celery-with-redis==3.0
django-celery==3.2.2
django-celery-beat==1.4.0

  4+以上的celery还是有很大区别的,混用环境让我浪费了很多时间,最后还是使用了3的最新版本

  说明一下,3.1.26也可以使用定时任务,并不像网上所说只能4+版本使用定时任务.

  以下配置都是以3.1版本为例 [refer]

  目录结构

project
 -settings.py
 -manage.py
 -app1
  -views.py
  -models.py
 -app2
  -views.py
  -models.py

======

project
 -settings.py
 -manage.py
 -app1
  -__init__py ==>修改文件
  -celery.py  ==>新增文件
  -tasks.py   ==>新增文件
  -views.py
  -models.py
 -app2
  -views.py
  -models.py

  参考: https://ruddra.com/posts/perodic-tasks-by-celery-3-1-example/

 

  3. 配置settings.py

  首先,在Django工程的settings.py文件中加入如下配置代码:

import djcelery
djcelery.setup_loader()
BROKER_URL= 'amqp://guest@localhost//' 
CELERY_RESULT_BACKEND = 'amqp://guest@localhost//'
CELERY_IMPORTS = ('vsvms.tasks', )  # 在3.1中是需要的

  

  然后,在INSTALLED_APPS中加入djcelery:

INSTALLED_APPS = (
    ……   
    'vms',
    'djcelery'
    ……   
)   

  

  4. 增加celery.py文件在我们的app1目录下

# -*- coding: utf-8 -*-
# @Author: richardzgt​
# @Date:   2019-01-10 16:49:40
# @Last Modified by:   richardzgt​
# @Last Modified time: 2019-01-12 17:42:10
# 不适用! 
# 用了3.1的celery配合3.2.2的djcelery
# 而4+的celery怎么也安装不上,所以就没有crontab定时执行的功能了
# 

from __future__ import absolute_import, unicode_literals # 注意绝对导入
import os
from celery import Celery
from django.conf import settings

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'webResty2.settings')

app = Celery('vsvms')
# Using a string here means the worker doesn't have to serialize
# the configuration object to child processes.
# - namespace='CELERY' means all celery-related configuration keys
#   should have a `CELERY_` prefix.
app.config_from_object('django.conf:settings')
# Load task modules from all registered Django app configs.
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)

app.conf.update(
      CELERY_RESULT_BACKEND='djcelery.backends.database:DatabaseBackend',
      )
@app.task(bind=True)
def debug_task(self):
  print('Request: {0!r}'.format(self.request))

 

5.修改此项目的__init__.py文件

from __future__ import absolute_import # 注意此处的绝对导入,少了会出现no module "django" 之类的错误
from celery import app as celery_app

 

6. 在要使用该任务队列的app根目录下(比如vms),建立tasks.py,

在tasks.py中我们就可以编码实现我们需要执行的任务逻辑,在开始处import task,然后在要执行的任务方法开头用上装饰器@task。需要注意的是,与一般的.py中实现celery不同,tasks.py必须建在各app的根目录下,且不能随意命名。

比如:

from __future__ import absolute_import
from celery import task,shared_task
from celery.decorators import periodic_task
from datetime import timedelta

@shared_task   # 加了这个装饰器,其他的项目也能共享这个task
def test_add(x,y):
    return x + y

@shared_task
def test(x,y):
    return x + y

@shared_task
def dynamic_task(): 
    print "dynamic_task runing.....",time.ctime()

@periodic_task(run_every=timedelta(seconds=30), name="calcu_weigth", ignore_result=True)  # 虽然django admin中可以配置interval,但是当加了run_every后,就直接以这个为主
def probeLoadGenSnap():
    with open("/tmp/test",'a') as f:
        f.write("testforme=====%s===\n" % time.ctime())
    print time.localtime()
    return True

   

  7. django views

  在需要执行该任务的View中,通过VMHandler.delay的方式来创建任务,并送入消息队列【不要写逻辑和不能序列化的东西,要主要的逻辑都写在task.py里面】。比如:

class VMsCreate(View):
    def get(self, request):
        try:
            vm =VMHandler.delay()
            return vm
        except Exceptions as e:
            pass

  8. 启动worker的命令

  

#先启动服务器
python manage.py runserver

#再启动worker 
python manage.py celery worker -c 4 --loglevel=info

# 同时启动worker和beat(周期性任务),并监测新task注册
python manage.py celery worker --loglevel=info --beat --autoreload -c 2


# 4+版本中已经摒弃上面的启动方法,采用celery自己的进程
celery -A app1 worker -B -l info

  

  9.定时周期任务

     django admin中配置crontab

 

  添加一个每分钟执行一次的crontab

 

 添加周期任务,并把task和crontab关联

 

 

 


 

在admin中定义定时任务

 

10 关于动态任务:

1 shared_task 装饰器一定要加,单加task可能报错: 动态新增的task function的decorator使用@tasks会报"NotRegistered: u'apps.tasks.funcName'"的错误,用@shared_task才可以正确被worker执行

2 修改apps/tasks.py文件,往其中增减task function,会实时反映在admin中Task(registered)的下拉列表上,验证了django的autodiscover_tasks功能的确实有效的。但是在不重启celery worker的情况下,添加该task function的crontab任务,celery worker无法调用该task function,从而报错

3 使用 --autoreload 自动reload celery的task列表

不过,用来实现apps/tasks.py里面增减函数、自动被worker检测,是足够了的.在原生的celery中,--autoreload要搭配CELERY_IMPORT或include设定来用(autoreload这两项设定中的module文件),不过在django中,INSTALLED_APPS设定可以用来替代CELERY_IMPORT,所以django中可以使celery worker autoreload INSTALLED_APPS设定目录下的tasks.py文件。最终,django的autodiscover_tasks用来使django admin页面动态检测apps/tasks.py里task function的变化,celery的autoreload用来使celery worker动态检测apps/tasks.py里task function的变化,从而实现动态增减task function并正确执行的功能。注意,动态新增的task function的decorator使用@tasks会报"NotRegistered: u'apps.tasks.funcName'"的错误,用@shared_task才可以正确被worker执行
这时候就可以动态去添加任务了(前台功能)

import datetime
import json
from djcelery import models as celery_models
from django.utils import timezone

def create_task(name, task, task_args, crontab_time):
    '''
    创建任务
    name       # 任务名字
    task       # 执行的任务 "myapp.tasks.add"
    task_args  # 任务参数  {"x":1, "Y":1}
    crontab_time # 定时任务时间 格式:
	    {
	      'month_of_year': 9  # 月份
	      'day_of_month': 5   # 日期
	      'hour': 01         # 小时
	      'minute':05  # 分钟
	    }
    
    '''
    # task任务, created是否定时创建
    task, created = celery_models.PeriodicTask.objects.get_or_create(
        name=name,
        task=task)
   # 获取 crontab
    crontab = celery_models.CrontabSchedule.objects.filter(
        **crontab_time).first()
    if crontab is None:
    		# 如果没有就创建,有的话就继续复用之前的crontab
        crontab = celery_models.CrontabSchedule.objects.create(
            **crontab_time)
    task.crontab = crontab # 设置crontab
    task.enabled = True # 开启task
    task.kwargs = json.dumps(task_args) # 传入task参数
    expiration = timezone.now() + datetime.timedelta(day=1)
    task.expires = expiration # 设置任务过期时间为现在时间的一天以后
    task.save()
    return True


def disable_task(name):
    '''
    关闭任务
    '''
    try:
        task = celery_models.PeriodicTask.objects.get(name=name)
        task.enabled = False # 设置关闭
        task.save()
        return True
    except celery_models.PeriodicTask.DoesNotExist:
        return True

  <未测试>

 

补充

  遇到不能以root启动的情况:

  可以在启动脚本或者/etc/profile中加入

export C_FORCE_ROOT=1

  

  Django下要查看其他celery的命令,包括参数配置、启动多worker进程的方式都可以通过python manage.py celery --help来查看:

        

 

       另外,Celery提供了一个工具flower,将各个任务的执行情况、各个worker的健康状态进行监控并以可视化的方式展现,如下图所示:

 

 Django下实现的方式如下: 

  1. 安装flower:

pip install flower  # 不行的话就换一个源

  2. 启动flower(默认会启动一个webserver,端口为5555):

$ python manage.py celery flower
[I 190110 11:39:40 command:139] Visit me at http://localhost:5555
[I 190110 11:39:40 command:144] Broker: amqp://guest:**@localhost:5672//
[I 190110 11:39:40 command:147] Registered tasks: 

  

  3. 进入http://localhost:5555即可查看。

 

信号

概念

django自带一套信号机制来帮助我们在框架的不同位置之间传递信息。也就是说,当某一事件发生时,信号系统可以允许一个或多个发送者(senders)将通知或信号(signals)发送给一组接受者(receivers)。

 注意之前我认为models的修改应该也能被信号记录到,其实不然。对信号来说,最多的应用是解耦多个应用

信号系统包含以下三要素:

  • 发送者-信号的发出方

  • 信号-信号本身

  • 接收者-信号的接受者

Django内置了一整套信号,下面是一些比较常用的:

  • django.db.models.signals.pre_save & django.db.models.signals.post_save

在ORM模型的save()方法调用之前或之后发送信号

  • django.db.models.signals.pre_delete & django.db.models.signals.post_delete

在ORM模型或查询集的delete()方法调用之前或之后发送信号。

  • django.db.models.signals.m2m_changed

当多对多字段被修改时发送信号。

  • django.core.signals.request_started & django.core.signals.request_finished

当接收和关闭HTTP请求时发送信号。

Model signals
    pre_init                    # django的modal执行其构造方法前,自动触发
    post_init                   # django的modal执行其构造方法后,自动触发
    pre_save                    # django的modal对象保存前,自动触发
    post_save                   # django的modal对象保存后,自动触发
    pre_delete                  # django的modal对象删除前,自动触发
    post_delete                 # django的modal对象删除后,自动触发
    m2m_changed                 # django的modal中使用m2m字段操作第三张表(add,remove,clear)前后,自动触发
    class_prepared              # 程序启动时,检测已注册的app中modal类,对于每一个类,自动触发
Management signals
    pre_migrate                 # 执行migrate命令前,自动触发
    post_migrate                # 执行migrate命令后,自动触发
Request/response signals
    request_started             # 请求到来前,自动触发
    request_finished            # 请求结束后,自动触发
    got_request_exception       # 请求异常后,自动触发
Test signals
    setting_changed             # 使用test测试修改配置文件时,自动触发
    template_rendered           # 使用test测试渲染模板时,自动触发
Database Wrappers
    connection_created          # 创建数据库连接时,自动触发

  

监听信号

要接收信号,请使用Signal.connect()方法注册一个接收器。

Signal.connect(receiver, sender=None, weak=True, dispatch_uid=None)[source]

  

receiver :当前信号连接的回调函数,也就是处理信号的函数。 
sender :指定从哪个发送方接收信号。 
weak : 是否弱引用
dispatch_uid :信号接收器的唯一标识符,以防信号多次发送。

  

下面以如何接收每次HTTP请求结束后发送的信号为例,连接到Django内置的现成的request_finished信号。

1 编写信号

接收器其实就是一个Python函数或者方法

def my_callback(sender, **kwargs):
    print("Request finished!")

请注意,所有的接收器都必须接收一个sender参数和一个**kwargs通配符参数。  

 

2. 连接接收器

有两种方法可以连接接收器,一种是下面的手动方式:

from django.core.signals import request_finished

request_finished.connect(my_callback)

可以写到__init__.py ,在django里面可以写到apps.py,就可以自动加载

from .signals import pre_save_callback
from .models import UserInfo

class UserConfig(AppConfig):
    name = 'UserConfig'
    def ready(self):
        pre_save.connect(pre_save_callback, sender=UserInfo, dispatch_uid="Pre-Save User Info")

  

 

  

另一种是使用receiver()装饰器:

from django.core.signals import request_finished
from django.dispatch import receiver

@receiver(request_finished)
def my_callback(sender, **kwargs):
    print("Request finished!")

  

3. 接收特定发送者的信号

一个信号接收器,通常不需要接收所有的信号,只需要接收特定发送者发来的信号,所以需要在sender参数中,指定发送方。下面的例子,只接收MyModel模型的实例保存前的信号。

@receiver(pre_save)  
def pre_handler(sender, **kwargs):
    """
    :param sender:
    :param kwargs:
    :return:
    """
    created = kwargs.get('created','')
    instance = kwargs.get('instance', '')
    print("pre_save_handler")
    if created:
        u=sender.objects.get(id=instance.id)
        print(u.username)
    else:
        print("创建")


@receiver(post_save, sender=UserInfo)
def post_handler(sender, **kwargs):
    print("post_save_handler")
    # True 创建
    created = kwargs.get('created','')
    instance = kwargs.get('instance','')
    if created:
        print("我是创建拉")
    else:
        # 更新是无法被触发的,所以不用信号去获取改变
        print(kwargs)
        print(instance.username)
        print("我是更新咯")

    

  • 定义了两个handler分别对pre_save 和 post_save做操作
  • 当不写sender时,就是对所有models操作都生效
  • 如果要对多个model做hook,只需要写多个装饰器并指定到对应的models

 

 

4. 防止重复信号

为了防止重复信号,可以设置dispatch_uid参数来标识你的接收器,标识符通常是一个字符串,如下所示:

request_finished.connect(my_callback, dispatch_uid="my_unique_identifier")
request_finished.connect(my_callback_2, dispatch_uid="my_unique_identifier")  # 只有第一次被绑定了

   

最后的结果是,对于每个唯一的dispatch_uid值,你的接收器都只绑定到信号一次my_callback。

 

自定义信号

除了Django为我们提供的内置信号(比如前面列举的那些),很多时候,我们需要自己定义信号。

类原型:class Signal(providing_args=list)[source]

所有的信号都是django.dispatch.Signal的实例。providing_args参数是一个列表,由信号将提供给监听者的参数的名称组成。可以在任何时候修改providing_args参数列表。

在sg.py中定义一个新信号(需要提前在__init__.py中注册哦):

import django.dispatch

pizza_done = django.dispatch.Signal(providing_args=["toppings", "size"])


def callback(sender, **kwargs):
    print("callback")
    print(sender, kwargs)


pizza_done.connect(callback)

  

  

在view中调用这个信号

from signalApp.sg import pizza_done


def custom_sg(request):
    pizza_done.send(sender='seven', toppings=123, size=456)
    return HttpResponse("自定义信号")

  

当调用custom_sg时,会触发这个信号,一般应用在触发自定义事件,比如在jumpserver中定义个ldap的开关

 

断开信号

Signal.disconnect(receiver=None, sender=None, dispatch_uid=None)[source]

  Signal.disconnect()用来断开信号的接收器。和Signal.connect()中的参数相同。如果接收器成功断开,返回True,否则返回False。

 

posted @ 2018-04-26 12:14  richardzgt  阅读(610)  评论(0编辑  收藏  举报