Django中使用celery来异步处理和定时任务

Django Web项目中我们经常需要执行耗时的任务比如发送邮件、调用第三方接口、批量处理文件等等,将这些任务异步化放在后台运行可以有效缩短请求响应时间。另外服务器上经常会有定时任务的需求,比如清除缓存、备份数据库等工作。Celery是一个高效的异步任务队列/基于分布式消息传递的作业队列,可以轻松帮我们在Django项目中设置执行异步和周期性任务。

简介

Celery的工作原理安装项目依赖文件Celery配置测试Celery是否工作正常编写任务异步调用任务查看任务执行状态及结果设置定时和周期性任务配置文件添加任务Django Admin添加周期性任务通过Crontab设置定时任务启动任务调度器beatFlower监控任务执行状态Celery高级用法与注意事项给任务设置最大重试次数不同任务交由不同Queue处理忽略不想要的结果避免启动同步子任务Django的模型对象不应该作为参数传递使用on_commit函数处理事务小结

Celery的工作原理

Celery是一个高效的基于分布式消息传递的作业队列。它主要通过消息(messages)传递任务,通常使用一个叫Broker(中间人)来协调client(任务的发出者)和worker(任务的处理者)。clients发出消息到队列中,broker将队列中的信息派发给 Celery worker来处理。Celery本身不提供消息服务,它支持的消息服务(Broker)有RabbitMQ和Redis。小编一般推荐Redis,因为其在Django项目中还是首选的缓存后台。

整个工作流程如下所示:

安装项目依赖文件

  # pip安装必选
 Django==3.2
 celery==5.0.5
 redis==3.5.3
 
 # 可选,windows下运行celery 4以后版本,还需额外安装eventlet库
 eventlet 
 
 # 推荐安装, 需要设置定时或周期任务时安装,推荐安装
 django-celery-beat==2.2.0
 
 # 视情况需要,需要存储任务结果时安装,视情况需要
 django-celery-results==2.0.1
 
 # 视情况需要,需要监控celery运行任务状态时安装
 flower==0.9.7

Celery配置

在正式使用celery和django-celery-beat之前,你需要做基础的配置。假如你的Django项目文件夹布局如下所示,你首先需要在myproject/myproject目录下新增celery.py并修改__init__.py。

 - myproject/
   - manage.py
   - project/#项目文件,不是app文件
     - __init__.py # 修改这个文件
     - celery.py # 新增这个文件
     - asgi.py
     - settings.py
     - urls.py
     - wsgi.py


新建celery.py,添加如下代码:

 import os
 from celery import Celery
 
 # 设置环境变量
 os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
 
 # 实例化,需要改成自己的项目名称
 app = Celery('myproject')
 
 # namespace='CELERY'作用是允许你在Django配置文件中对Celery进行配置
 # 但所有Celery配置项必须以CELERY开头,防止冲突
 app.config_from_object('django.conf:settings', namespace='CELERY')
 
 # 自动从Django的已注册app中发现任务
 app.autodiscover_tasks()
 
 # 一个测试任务
 @app.task(bind=True)
 def debug_task(self):
     print(f'Request: {self.request!r}')


修改__init__.py,如下所示:

from .celery import app as celery_app
__all__ = ('celery_app',)


接下来修改Django项目的settings.py,添加Celery有关配置选项,如下所示:

 # 最重要的配置,设置消息broker,格式为:db://user:password@host:port/dbname
 # 如果redis安装在本机,使用localhost
 # 如果docker部署的redis,使用redis://redis:6379
 CELERY_BROKER_URL = "redis://127.0.0.1:6379/0"

# 使用rabbit数据库
CELERY_BROKER_URL = "amqp://用户名:密码@需要连接的服务器ip本地localhost:5672//"
 
 # celery时区设置,建议与Django settings中TIME_ZONE同样时区,防止时差
 # Django设置时区需同时设置USE_TZ=True和TIME_ZONE = 'Asia/Shanghai'
 CELERY_TIMEZONE = TIME_ZONE

其它Celery常用配置选项包括:

 # 为django_celery_results存储Celery任务执行结果设置后台
 # 格式为:db+scheme://user:password@host:port/dbname
 # 支持数据库django-db和缓存django-cache存储任务状态及结果
 CELERY_RESULT_BACKEND = "django-db"
 # celery内容等消息的格式设置,默认json
 CELERY_ACCEPT_CONTENT = ['application/json', ]
 CELERY_TASK_SERIALIZER = 'json'
 CELERY_RESULT_SERIALIZER = 'json'
 
 # 为任务设置超时时间,单位秒。超时即中止,执行下个任务。
 CELERY_TASK_TIME_LIMIT = 5
 
 # 为存储结果设置过期日期,默认1天过期。如果beat开启,Celery每天会自动清除。
 # 设为0,存储结果永不过期
 CELERY_RESULT_EXPIRES = xx
 
 # 任务限流
 CELERY_TASK_ANNOTATIONS = {'tasks.add': {'rate_limit': '10/s'}}
 
 # Worker并发数量,一般默认CPU核数,可以不设置
 CELERY_WORKER_CONCURRENCY = 2
 
 # 每个worker执行了多少任务就会死掉,默认是无限的
 CELERY_WORKER_MAX_TASKS_PER_CHILD = 200


完整配置选项见:

Configuration and defaults — Celery 5.2.0 documentation

注意:

  • 在Django中正式编写和执行自己的异步任务前,一定要先测试redis和celery是否安装好并配置成功。
  • 一个无限期阻塞的任务会使得工作单元无法再做其他事情,建议给任务设置超时时间。

测试Celery是否工作正常

首先你要启动redis服务。windows进入redis所在目录(比如C:\redis),使用redis-server.exe启动redis。Mac下redis-server启动。

启动redis服务后,你要先进入项目所在文件夹运行python manage.py runserver命令启动Django服务器(无需创建任何app),然后再打开一个终端terminal窗口输入celery命令,启动worker。

# MAC下测试,启动Celery,这里myproject是项目名称
 Celery -A myproject worker -l info
 
 # Windows下测试,启动Celery
 Celery -A myproject worker -l info -P eventlet
 
 # 如果Windows下Celery不工作,输入如下命令
 Celery -A myproject worker -l info --pool=solo

如果你能看到[tasks]下所列异步任务清单如debug_task,以及最后一句celery@xxxx ready, 说明你的redis和celery都配置好了,可以开始正式工作了。

 -------------- celery@DESKTOP-H3IHAKQ v4.4.2 (cliffs)
 --- ***** -----
 -- ******* ---- Windows-10-10.0.18362-SP0 2020-04-24 22:02:38
 
 - *** --- * ---
 - ** ---------- [config]
 - ** ---------- .> app:         myproject:0x456d1f0
 - ** ---------- .> transport:   redis://127.0.0.1:6379/0
 - ** ---------- .> results:     redis://localhost:6379/0
 - *** --- * --- .> concurrency: 4 (eventlet)
   -- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)
   --- ***** -----
    -------------- [queues]
                 .> celery           exchange=celery(direct) key=celery
 
 
 [tasks]
   . myproject.celery.debug_task
 
 [2020-04-24 22:02:38,484: INFO/MainProcess] Connected to redis://127.0.0.1:6379/0
 [2020-04-24 22:02:38,500: INFO/MainProcess] mingle: searching for neighbors
 [2020-04-24 22:02:39,544: INFO/MainProcess] mingle: all alone
 [2020-04-24 22:02:39,572: INFO/MainProcess] pidbox: Connected to redis://127.0.0.1:6379/0.
 [2020-04-24 22:02:39,578: WARNING/MainProcess] c:\users\missenka\pycharmprojects\django-static-html-generator\venv\lib\site-packages\celery\fixups\django.py:203: UserWarning: Using sett
 ings.DEBUG leads to a memory
             leak, never use this setting in production environments!
   leak, never use this setting in production environments!''')
 [2020-04-24 22:02:39,579: INFO/MainProcess] celery@DESKTOP-H3IHAKQ ready.

 就是看最后一句话,只要是显示ready就说明环境配置好了

编写任务

Celery配置完成后,我们就可以编写任务了。Django项目中所有需要Celery执行的异步或周期性任务都放在tasks.py文件里,该文件可以位于project目录下,也可以位于各个app的目录下。专属于某个Celery实例化项目的task可以使用@app.task装饰器定义,各个app目录下可以复用的task建议使用@shared_task定义。

两个示例如下所示:

# myproject/tasks.py
 # 专属于myproject项目的任务
 app = Celery('myproject')
 @ app.task
 def test():
     pass
 
 # app/tasks.py, 可以复用的task
 from celery import shared_task
 import time
 
 @shared_task
 def add(x, y):
     time.sleep(2)
     return x + y

上面我们定义一个名为add的任务,它接收两个参数,并返回计算结果。为了模拟耗时任务,我们中途让其sleep 2秒。现在已经定义了一个耗时任务,我们希望在Django的视图或其它地方中以异步方式调用执行它,应该怎么做呢? 下面我们将给出答案。

注意:

  • 使用celery定义任务时,避免在一个任务中调用另一个异步任务,容易造成阻塞。
  • 当我们使用@app.task装饰器定义我们的异步任务时,那么这个任务依赖于根据项目名myproject生成的Celery实例。然而我们在进行Django开发时为了保证每个app的可重用性,我们经常会在每个app文件夹下编写异步任务,这些任务并不依赖于具体的Django项目名。使用@shared_task装饰器能让我们避免对某个项目名对应Celery实例的依赖,使app的可移植性更强。

异步调用任务

Celery提供了2种以异步方式调用任务的方法,delay和apply_async方法,如下所示:

 # 方法一:delay方法
 task_name.delay(args1, args2, kwargs=value_1, kwargs2=value_2)
 
 # 方法二:apply_async方法,与delay类似,但支持更多参数
 task.apply_async(args=[arg1, arg2], kwargs={key:value, key:value})


我们接下来看一个具体的例子。我们编写了一个Django视图函数,使用delay方法调用add任务。

 # app/views.py
 from .tasks import add
 
 def test_celery(request):
     add.delay(3, 5)
     return HttpResponse("Celery works")
 
 # app/urls.py
 urlpatterns = [
     re_path(r'^test/$', views.test_celery, name="test_celery")
 ]


当你通过浏览器访问/test/链接时,你根本感受不到2s的延迟,页面可以秒开,同时你会发现终端的输出如下所示,显示任务执行成功。

我们现在再次使用apply_async方法调用add任务,不过还要打印初任务的id (task.id)和状态status。Celery会为每个加入到队列的任务分配一个独一无二的uuid, 你可以通过task.status获取状态和task.result获取结果。注意:apply_async传递参数的方式与delay方法不同。

 # app/views.py
 from .tasks import add
 
 def test_celery(request):
     result = add.apply_async(args=[3, 5])
     return HttpResponse(result.task_id + ' : ' + result.status)


Django返回响应结果如下所示。这是在预期之内的,因为耗时任务还未执行完毕,Django就已经返回了响应。

那么问题来了,这个异步任务执行了,返回了个计算结果(8),那么我们系统性地了解任务状态并获取这个执行结果呢? 答案是django-celery-results。

查看任务执行状态及结果

通过pip安装django-celery-results后,需要将其加入到INSTALLED_APPS并使用migrate命令迁移创建数据表。以下几项配置选项是与这个库相关的。

#settings.py
 # 支持数据库django-db和缓存django-cache存储任务状态及结果
 # 建议选django-db
 CELERY_RESULT_BACKEND = "django-db"
 # celery内容等消息的格式设置,默认json
 CELERY_ACCEPT_CONTENT = ['application/json', ]
 CELERY_TASK_SERIALIZER = 'json'
 CELERY_RESULT_SERIALIZER = 'json'


安装配置完成后,进入Django admin后台,你就可以详细看到每个任务的id、名称及状态。

点击单个任务id,你可以看到有关这个任务的更多信息,比如传递的参数和返回结果,如下所示:

设置定时和周期性任务

借助于装django-celery-beat后, 你可以将任一Celery任务设置为定时任务或周期性任务。使用它你只需要通过pip安装它,并加入INSTALLED_APPS里去。

#settings.py
INSTALLED_APPS = [
    ...
    'django_celery_results',
    'django_celery_beat'

]

django-celery-beat提供了两种添加定时或周期性任务的方式,一是在celery.py中添加,二是通过Django admin后台添加。

配置文件添加任务

同一任务可以设置成不同的调用周期,给它们不同的任务名就好了。

# celery.py文件里面新增下面的代码

from datetime import timedelta

# 新增的定时方法案列
# sum-task 名字任意取
app.conf.update(
    CELERYBEAT_SCHEDULE={
        'sum-task': {
            'task': 'app名字.tasks.add',
            'schedule':  timedelta(seconds=5),
            'args': (5, 6)
        },

        'sum-task1': {
            'task': 'app名字.tasks.add',
            'schedule':  timedelta(seconds=3),
            'args': (3,8)
        },
    

Django Admin添加周期性任务

如果每次添加或修改周期性任务都要修改配置文件非常不方便,一个更好的方式是使用任务调度器。先在settings.py中将任务调度器设为DatabaseScheduler

CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler'

然后就可以进入Periodic Task表添加和修改周期性任务了。

​​​​​​​ 

启动任务调度器beat

多开几个终端,一个用来启动任务调度器beat,另一个启动celery worker,你的任务就可以在后台执行啦。

这几个,第一个用于启动Django,第二个用于启动worker,第三个是启动beat定时任务,第四个其实是启动flower(这个后面解释)

python mananger.py runserver


Celery -A myproject  worker -l info


Celery -A myproject  beat -l info

Flower监控任务执行状态

除了django_celery_results, 你可以使用flower监控后台任务执行状态。它提供了一个可视化的界面,在测试环境中非常有用。

 pip install flower


安装好后,你有如下两种方式启动服务器。启动服务器后,打开http://localhost:5555即可查看监控情况。

 # 从terminal终端启动, proj为项目名
 $ flower -A proj --port=5555  
 # 从celery启动
 $ celery flower -A proj --address=127.0.0.1 --port=5555

 

Celery高级用法与注意事项

给任务设置最大重试次数

定义任务时可以通过max_retries设置最大重试次数,并调用self.retry方法调用。因为要调用self这个参数,定义任务时必须设置bind=True。

 @shared_task(bind=True, max_retries=3)
 def send_batch_notifications(self):
    try:
        something_raising()
        raise Exception('Can\'t send email.')
    except Exception as exc:
        self.retry(exc=exc, countdown=5)
    send_mail(
        subject='Batch email notifications',
        message='Test email',
        from_email='no-reply@example.com',
        recipient_list=['john@example.com']
    )


不同任务交由不同Queue处理

不同的任务所需要的资源和时间不一样的。为了防止一些非常占用资源或耗时的任务阻塞任务队列导致一些简单任务也无法执行,可以将不同任务交由不同的Queue处理。下例定义了两个Queue队列,default执行普通任务,heavy_tasks执行重型任务。

 CELERY_TASK_DEFAULT_QUEUE = 'default'
 CELERY_TASK_DEFAULT_ROUTING_KEY = 'default'
 CELERY_QUEUES = (
    Queue('default', Exchange('default'), routing_key='default'),
    Queue('heavy_tasks', Exchange('heavy_tasks'), routing_key='heavy_tasks'),
 )
 CELERY_TASK_ROUTES = {
    'myapp.tasks.heave_tasks': 'heavy_tasks'
 }


忽略不想要的结果

如果你不在意任务的返回结果,可以设置 ignore_result 选项,因为存储结果耗费时间和资源。你还可以可以通过 task_ignore_result 设置全局忽略任务结果。

 @app.task(ignore_result=True)
 def my_task():
     something()


避免启动同步子任务

让一个任务等待另外一个任务的返回结果是很低效的,并且如果工作单元池被耗尽的话这将会导致死锁。

 # 话例子
 @app.task
 def update_page_info(url):
     page = fetch_page.delay(url).get()
     info = parse_page.delay(url, page).get()
     store_page_info.delay(url, info)
  
 @app.task
 def fetch_page(url):
     return myhttplib.get(url)
  
 @app.task
 def parse_page(url, page):
     return myparser.parse_document(page)
  
 @app.task
 def store_page_info(url, info):
     return PageInfo.objects.create(url, info)
# 好例子

 def update_page_info(url):
     chain = fetch_page.s(url) | parse_page.s() | store_page_info.s(url)
     chain()
  
 @app.task()
 def fetch_page(url):
     return myhttplib.get(url)
 
 @app.task()
 def parse_page(page):
     return myparser.parse_document(page)
 
 @app.task(ignore_result=True)
 def store_page_info(info, url):
     PageInfo.objects.create(url=url, info=info)


在好例子里,我们将不同的任务签名链接起来创建一个任务链,三个子任务按顺序执行。

使用on_commit函数处理事务

我们再看另外一个celery中处理事务的例子。这是在数据库中创建一个文章对象的 Django 视图,此时传递主键给任务。它使用 commit_on_success 装饰器,当视图返回时该事务会被提交,当视图抛出异常时会进行回滚。

from django.db import transaction
  
 @transaction.commit_on_success
 def create_article(request):
     article = Article.objects.create()
     expand_abbreviations.delay(article.pk)


如果在事务提交之前任务已经开始执行会产生一个竞态条件;数据库对象还不存在。解决方案是使用 on_commit 回调函数来在所有事务提交成功后启动任务。

 from django.db.transaction import on_commit
  
 def create_article(request):
     article = Article.objects.create()
     on_commit(lambda: expand_abbreviations.delay(article.pk))

结尾:

python mananger.py runserver


Celery -A myproject  worker -l info


Celery -A myproject  beat -l info
 

        刚开始用时,迷惑的地点有这几处:

  • 以前django只用Python manager.py runserver既可以启动,刚用celery没明白Celery -A myproject  worker -l info这是怎么整的,最后明白了,runserver只是启动了django服务,没有启动worker服务,worker服务于django服务时相互独立的,所以需要同时启动django服务和worker服务,可以开多个终端窗口
  • 随后用celery定时服务器,刚开始也是没明白为啥要启动beat服务,随后发现它跟worker服务一样,都是独立的,我刚开始以为beat是集成到worker中,只用一个命令即可,随后发现它也是独立的。
  • celery在window环境下使用很不友好,没有Linux中使用方便,特别是自动化部署,所以如果只是用于定时服务的话,建议还是用APSchedule,这个可以集成到django中,不用单独的命令单独集成。就是需要每次重启后都访问下网站启动下这个django服务。
  • 1
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值