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。