celery异步任务队列入门

参考:
Celery入门
任务调度delay&apply_async
celery 简要概述
Celery 中文手册
Celery动态添加定时任务
全网最细之Celery 4.x动态添加定时任务(这个还可以)
win10 Celery异步任务报错: Task handler raised error: ValueError('not enough values to unpack (expected 3, got 0)
celery最佳实践
Flower_0.9.1-Celery 监控工具
Celery使用指南
celery redis版本踩过的坑
celery配置 - Celery官方文档
使用Celery调度任务(下)
将CELERY_ALWAYS_EAGER设置为True会引发错误
celery同步执行任务(在项目中debug任务)
CELERY 常用配置介绍
Celery 信号

前言

在开发工作中,我们很多时候需要对一些任务做异步处理,或者要执行某些定时任务。在蓝鲸python开发框架中,采用了celery这个异步任务队列框架,去调度和执行我们开发的任务。今天来了解一下celery的几个概念、处理流程原理和使用配置。

概念

Celery是一个异步任务的调度工具,是Distributed Task Queue,分布式任务队列,分布式决定了可以有多个worker的存在,队列表示其是异步操作,即存在一个产生任务提出需求的工头,和一群等着被分配工作的码农。

celery可以执行和调度任务,但本身不提供队列服务,所以我们还需要一个消息队列中间件。

来看看celery的几个核心概念:

  • task:就是任务,celery调度和执行的对象,有异步任务和周期任务
  • broker:中间人,接收生产者发来的消息即task,将任务存入队列,由worker去消费。一般会配置使用redis和rabbitmq作为celery的消息中间件。
  • worker:执行任务的单元,它实时监控消息队列,如果有任务就获取任务并执行它。也就是我们任务队列的消费者,worker可以有多个。一个worker只能同时执行一个任务,多个worker可以并行执行,如果worker都满了那剩下的任务就会进入队列排队。
  • beat:任务调度器,beat进程会读取配置文件的内容,周期性的将配置中到期需要执行的任务发送给任务队列。django-celery-beat插件本质上是对数据库表变化检查,一旦有数据库表改变,调度器重新读取任务进行调度
  • backend:任务执行结果存储,保存任务执行的结果和状态,可以是数据库,或者缓存。

image.png

使用

来看看在蓝鲸python3开发框架中怎么使用celery
先找到PROJECT_ROOT/config/default.py

# CELERY 开关,使用时请改为 True,修改项目目录下的 Procfile 文件,添加以下两行命令:
# worker: python manage.py celery worker -l info
# beat: python manage.py celery beat -l info
# 不使用时,请修改为 False,并删除项目目录下的 Procfile 文件中 celery 配置
IS_USE_CELERY = True
# CELERY 并发数,默认为 2,可以通过环境变量或者 Procfile 设置
CELERYD_CONCURRENCY = os.getenv("BK_CELERYD_CONCURRENCY", 8) # noqa
# CELERY 配置,申明任务的文件路径,即包含有 @task 装饰器的函数文件
CELERY_IMPORTS = ("portrait.celery_tasks", "data_sync.celery_tasks", "common.celery_tasks")
"""
以下为框架代码 请勿修改
"""
# celery settings
if IS_USE_CELERY:
INSTALLED_APPS = locals().get("INSTALLED_APPS", [])
import djcelery
INSTALLED_APPS += ("djcelery",)
djcelery.setup_loader()
CELERY_ENABLE_UTC = False
# django_celery_beat调度器
CELERYBEAT_SCHEDULER = "djcelery.schedulers.DatabaseScheduler"

把IS_USE_CELERY打开,设置并发的worker数量,以及从哪些模块中导入celery任务。
这里CELERY_IMPORTS如果不配置,默认导入每个app下面的tasks模块

然后到我们定义的celery模块去写我们的任务,比如common.celery_tasks

@task
def a():
print("aaa")
@periodic_task(run_every=crontab(minute="*/1"))
def b():
print("bbb")

加上了@task、@periodic_task这样装饰器的函数,就是可以被celery调度执行的任务,我们可以使用它的delay()和apply_async()来调度执行任务。@periodic_task装饰的任务还会被beat监听并周期性加入到worker。

delay()与apply_async()

delay与apply_async都是用来做任务调度,但如果查看delay的源码会发现最终还是调用了apply_async,简单来说delay就是apply_async的快捷方式,而apply_async则有很多参数来控制任务调度。

apply_async支持常用参数:

  • eta:指定任务执行时间,类型为datetime时间类型;
  • countdown:倒计时,单位秒,浮点类型;
  • expires:任务过期时间,如果任务在超过过期时间还未开始执行则回收任务,浮点类型(单位秒)获取datetime类型;
  • retry:任务执行失败时候是否尝试,布尔类型;
  • serializer:序列化方案,支持pickle、json、yaml、msgpack;
  • priority:任务优先级,有0~9优先级可设置,int类型;
  • retry_policy:任务重试机制,其中包含几个重试参数,类型是dict如下:
    • max_retries:最大重试次数
    • interval_start:重试等待时间
    • interval_step:每次重试叠加时长,假设第一重试等待1s,第二次等待1+n秒
    • interval_max:最大等待时间

自定义任务类

我们可以自定义celery的Task和PeriodicTask任务类,重写回调方法加入自己的处理逻辑。

from celery.task.base import PeriodicTask, Task
from blueapps.utils.logger import logger_celery
class CustomTask(Task):
"""自定义Celery任务类"""
abstract = True
pass
class CustomPeriodicTask(PeriodicTask):
"""自定义Celery周期任务类"""
abstract = True
def on_success(self, retval, task_id, args, kwargs):
"""异步任务执行成功时,会执行这个回调方法"""
return super().on_success(retval, task_id, args, kwargs)
def on_failure(self, exc, task_id, args, kwargs, einfo):
"""异步任务执行失败时,会执行这个回调方法"""
logger_celery.exception("task执行失败,name: {}[{}],msg: {}".format(self.name, task_id, exc))
return super().on_failure(exc, task_id, args, kwargs, einfo)
def retry(
self, args=None, kwargs=None, exc=None, throw=True, eta=None, countdown=None, max_retries=None, **options
):
"""异步任务尝试重试时,会执行这个回调方法"""
return super().retry(args, kwargs, exc, throw, eta, countdown, max_retries, **options)
def after_return(self, status, retval, task_id, args, kwargs, einfo):
"""异步任务执行成功,并且return了一些内容,会执行这个回调方法"""
return super().after_return(status, retval, task_id, args, kwargs, einfo)
def update_state(self, task_id=None, state=None, meta=None):
"""可以手动调用这个方法来更新任务状态"""
return super(CustomPeriodicTask, self).update_state(task_id, state, meta)
def send_error_email(self, context, exc, **kwargs):
"""异步任务执行失败时,并且配置了send_error_emails=True时,会执行这个回调方法"""
return super().send_error_email(context, exc, **kwargs)

绑定任务

我们可以给任务绑定自身实例,拿到任务相关的信息

@task(bind=True)
def b(self):
time.sleep(4)
print("bbb", self.request)

这里self就是任务实例b,self.request包含了task、id、args、kwargs、retries、eta、expires等很多属性。

我们可以拿到task_id去获取任务执行状态。

@task(bind=True)
def b(self):
time.sleep(4)
task_id = self.request.id
print("bbb", task_id)
result = self.AsyncResult(task_id)
status = result.status

当然了没有开启结果存储会报错AttributeError("'DisabledBackend' object has no attribute '_get_task_meta_for'",)

AsyncResult对象里面存的就是一个异步的结果,当任务完成时result.ready()为true,然后用result.get()也可以取结果。

>>> b.delay()
<AsyncResult: ae3d31f5-8efe-4d2b-af1c-46a1be83d445>
>>> b.AsyncResult("ae3d31f5-8efe-4d2b-af1c-46a1be83d445")
<AsyncResult: ae3d31f5-8efe-4d2b-af1c-46a1be83d445>
>>> result = b.AsyncResult("ae3d31f5-8efe-4d2b-af1c-46a1be83d445")
>>> result
<AsyncResult: ae3d31f5-8efe-4d2b-af1c-46a1be83d445>
>>> result.ready()
True
>>> result.status
'SUCCESS'
>>> result._get_task_meta()
{'status': 'SUCCESS', 'result': None, 'traceback': None, 'children': []}

celery的工作流

Task.delay() --> apply_async --> send_task --> amqp.create_task_message --> amqp.send_task_message --> result=AsyncResult(task_id)返回result

delay之后调用实际上是apply_async之后 调用的send_task之后开始创建任务,发送任务,然后生成一个异步对象,把这个结果返回。

日志打印

在celery任务中,我们使用print()打印的信息级别是warning,建议使用logging明确日志级别打印。打印的日志正常是在worker中,beat只与周期任务的调度有关。

celery命令

在win10下需要指定协程类型eventlet,否则可能会报错
-P/--pool是POOL的配置,默认是prefork(并发),solo是串行,并发可以有prefork、gevent、eventlet,
-l日志级别,-f日志文件路径,在linux上可以用-B参数同步启动celery beat

python manage.py celery worker -l INFO -P eventlet
python manage.py celery beat -l INFO

动态添加定时任务

PeriodicTask
此模型定义要运行的单个周期性任务。

  1. 必须为任务指定一种Schedule,即clocked, interval, crontab, solar四个字段必须填写一个,且只能填写一个
  2. name字段给任务命名,它是unique的
  3. task字段指定运行的Celery任务,如"proj.tasks.test_task"
  4. one_off:默认值为False,如果one_off=True,任务被运行一次后enabled字段将被置为False,即任务只会运行一次
  5. args:传递给任务的参数,是一个json字符串,如 ["arg1", "arg2"]
  6. expires:过期时间,过期的任务将不再会被驱动触发

使用ClockedSchedule:在特定的时间执行任务

def test_clock():
clock = ClockedSchedule.objects.create(clocked_time=datetime.now() + timedelta(seconds=10))
PeriodicTask.objects.create(
name="%s" % str(datetime.now()),
task="project_celery.celery_app.test_task",
clocked=clock,
# 如果使用ClockedSchedule,则one_off必须为True
one_off=True
)

使用IntervalSchedule:以特定间隔运行的Schedule
用IntervalSchedule能够实现与ClockedSchedule同样的功能:计算目标时间与当前时间的时间差,令此时间差作为IntervalSchedule的周期,并且将任务的one_off参数置为True

def time_diff(target_time):
diff = target_time - datetime.now()
return int(diff.total_seconds())
def test_interval():
seconds = time_diff(datetime.strptime("2020-3-19 15:39:00", "%Y-%m-%d %H:%M:%S"))
schedule = IntervalSchedule.objects.create(every=seconds, period=IntervalSchedule.SECONDS)
PeriodicTask.objects.create(
name="%s" % str(datetime.now()),
task="project_celery.celery_app.test_task",
interval=schedule,
one_off=True
)

使用CrontabSchedule
使用CrontabSchedule一定要注意将时区设置为当前地区时区

model参数与crontab表达式的对应关系:

minite, hour, day_of_week, day_of_month, month_of_year

全部默认为"*"

def test_crontab():
# 表示 * * * * * ,即每隔一分钟触发一次
schedule = CrontabSchedule.objects.create(timezone='Asia/Shanghai')
PeriodicTask.objects.create(
name="%s" % str(datetime.now()),
task="project_celery.celery_app.test_task",
crontab=schedule,
one_off=True
)

flower可视化工具

pip install flower==0.9.1
python manage.py flower

访问Celery Flower

celery配置

# 时区
CELERY_TIMEZONE = "Asia/Shanghai"
# 消息中间件
BROKER_URL = "redis://:password@localhost:6379/0"
# 使用redis存储任务执行结果,默认不使用
# 遇到redis.exceptions.ResponseError时降低版本pip install --upgrade redis==2.10.6
CELERY_RESULT_BACKEND = "redis://:123456@localhost:6379/1"
# 任务序列化和反序列化为json
CELERY_TASK_SERIALIZER = "json"
# 存储结果序列化为json
CELERY_RESULT_SERIALIZER = "json"
# CELERY 并发数,worker数量
CELERYD_CONCURRENCY = 2
# 禁用时区设置
CELERY_ENABLE_UTC = True
# 限制任务的执行频率
# 下面这个就是限制tasks模块下的add函数,每秒钟只能执行10次
CELERY_ANNOTATIONS = {'tasks.add':{'rate_limit':'10/s'}}
# 或者限制所有的任务的刷新频率
CELERY_ANNOTATIONS = {'*':{'rate_limit':'10/s'}}
# 也可以设置如果任务执行失败后调用的函数
def my_on_failure(self,exc,task_id,args,kwargs,einfo):
print('task failed')
CELERY_ANNOTATIONS = {'*':{'on_failure':my_on_failure}}
# 并发的worker数量,也是命令行-c指定的数目
# 事实上并不是worker数量越多越好,保证任务不堆积,加上一些新增任务的预留就可以了
CELERYD_CONCURRENCY = 4
# celery worker每次去mq取任务的数量,默认值就是4
CELERYD_PREFETCH_MULTIPLIER = 4
# 每个worker执行了多少次任务后就会死掉,建议数量大一些
CELERYD_MAX_TASKS_PER_CHILD = 200
# celery任务执行结果的超时时间
CELERY_TASK_RESULT_EXPIRES = 1200
# 单个任务的运行时间限制,超过会被杀死
CELERYD_TASK_TIME_LIMIT = 60
CELERY_DISABLE_RATE_LIMITS = True
# 任务本地阻塞执行
CELERY_ALWAYS_EAGER = False

CELERY_ALWAYS_EAGER

开启后所有任务都会本地阻塞执行,apply_async()和delay()都只会在执行完返回结果,与普通方法一样执行而没有异步效果。

不会送到队列和worker,一切由beat调度执行,是串行的。

因为不经过worker和中间件,所以只在beat打印日志,并且对于异常中断不会打印任何信息,worker和中间件看不到任何记录。

好处是你可以在不启动worker或消息中间件的情况下执行你的任务或者debug你的任务。

CELERY_ALWAYS_EAGER = False

以下代码在CELERY_ALWAYS_EAGER开关下分别放到celery中执行,就可以看到明显的串行和并行区别。

@periodic_task(run_every=crontab(minute="*/1"), base=CustomPeriodicTask)
def a():
time.sleep(30)
print("aaa")
raise Exception("hahah")
@periodic_task(run_every=crontab(minute="*/1"), base=CustomPeriodicTask)
def b(self):
time.sleep(4)
print("bbb")
posted @   云白Li  阅读(1143)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 记一次.NET内存居高不下排查解决与启示
点击右上角即可分享
微信分享提示