任务是构建 Celery 应用程序的组成模块。
任务是从任何可调用创建的类,它有两种角色,一种角色定义了调用任务时发生的事情(发送消息),另外一种角色为职程(Worker)收到任务消息时该发生的事件。
每一个任务类都有一个唯一的名称,并且在消息中引用该名称,便于职程(Worker)找到对应的执行函数。 在职称(Worker)确认消息之前,不会从任务队列中删除该任务消息。职称(Worker)可以提前订阅许多消息,如果职称(Worker)被 kill
,或断电的情况,所有的消息会被传递给其它的职称(Worker)。
理想的情况下,任务函数是幂等的:在使用相同的参数多次调用函数,函数不会出现其它的情况。由于职程(Worker)是无法检测任务是否是幂等的,因此默认行为是在执行之前提前确认消息,便于从未在此执行已经启动的任务调用。
如果您的任务是幂等的,可以设置 acks_late
选项,职程(Worker)执行任务返回后确认消息。 注意:如果执行任务的子进程终止(通过调用sys.exit()的任务或通过信号),即使启用了acks_late,职程(Worker)也会确认消息。这种行为的目的是:
我们不希望重新运行迫使内核向进程发送SIGSEGV(分割错误)或类似信号的任务。
我们假设系统管理员故意终止任务,不希望任务自动重启。
分配过多的内存任务触发 OOM killer,同样的情况可能再次发生。
重新分配任务失败的任务可能导致高频率的循环。
启动 task_reject_on_worker_lost
选项可以重新传递导任务。
一直阻塞的任务可能会导致其他的职程(Worker)无法执行其他的任务。 如果您的任务操作了 I/O ,建议设置超时,例如使用 requests
发送请求时设置超时时间:
connect_timeout, read_timeout = 5.0, 30.0
response = requests.get(URL, timeout=(connect_timeout, read_timeout))
限制时间可以确保所有任务及时返回,但限制时间可能会强制终止任务,所以只使用它们来检测尚未使用手动超时的情况。
默认的prefork池针对长时间的任务支持不是很好,如果任务运行时间有数分钟/小时,建议启用 Celery 的 -Ofair 命令参数。
如果职程(Worker)被挂起,在提交问题之前最好先确认清楚运行的任务,很有可能是由于网络的原因造成。
可以使用 task()
装饰器轻松的创建任何可被调用的任务:
from .models import User
@app.task
def create_user(username, password):
User.objects.create(username=username, password=password)
也可以通过 task()
装饰器针对任务设置 选项参数
:
@app.task(serializer='json')
def create_user(username, password):
User.objects.create(username=username, password=password)
多个装饰器
如果有多个装饰器进行组合使用时,必须确保 task()
装饰器被放置在首位:
@app.task
@decorator2
@decorator1
def add(x, y):
return x + y
任务装饰器在 Celery 应用程序实例中使用,如果不清楚是什么,建议阅读 Celery 初次使用。 如果你是一个库的作者,你可能使用想要使用 shared_task()
装饰器:
from celery import shared_task
@shared_task
def add(x, y):
return x + y
被绑定的任务意味着任务的第一个参数总是任务实例(self),就像Python绑定方法一样:
logger = get_task_logger(__name__)
@task(bind=True)
def add(self, x, y):
logger.info(self.request.id)
对于重试(使用 app.Task.retry()
)、访问当前任务的信息以及添加到自定义任务基类的任何附加功能,都需要绑带任务。
装饰器的 base
参数可以指定继承的任务基类:
import celery
class MyTask(celery.Task):
def on_failure(self, exc, task_id, args, kwargs, einfo):
print('{0!r} failed: {1!r}'.format(task_id, exc))
@task(base=MyTask)
def add(x, y):
raise KeyError()
每一个任务都必须有一个唯一任务名称。 如果没有指定任务名称,装饰器会根据当前任务所在的模块以及任务函数的名称进行生成一个: 设置名称案例:
>>> @app.task(name='sum-of-two-numbers')
>>> def add(x, y):
... return x + y
>>> add.name
'sum-of-two-numbers'
最好的做法是将模块名称作为任务名称空间,如果有一个任务与另外一个模块中的任务名称相同,这样就不会发生的冲突。
>>> @app.task(name='tasks.add')
>>> def add(x, y):
... return x + y
可也通过调用任务的 .name
属性来进行获取任务名称:
>>> add.name
'tasks.add'
在案例中我们指定的任务名称为 task.add
,如果该任务函数在 task.py
文件中时自动生成任务与我们指定的任务名称相同:
tasks.py:
# tasks.py
@app.task
def add(x, y):
return x + y
>>> from tasks import add
>>> add.name
'tasks.add
相对导入和自动命名兼容不是很好,如果导入方式为相对导入吗,建议明确设置名称。
Python 2 可以通过在每个模块中的顶部添加:
from __future__ import absolute_import
使用之后会强制使用绝对导入,使用相对名称的任务不会出现问题。 绝对导入时 Python 3 默认的导入方式,如果开发使用的是 Python 3 就不需要写入以上代码。
例如,如果客户端导入模块 myapp.tasks
为 .tasks
,职程(Worker)需要将模块导入为 myapp.tasks
,生成的名称不会匹配,可能会出现 NotRegistered 错误信息。
在使用 Django 和 project.myapp-style
在 INSTALLED_APPS
命名时也会出现这种情况:
INSTALLED_APPS = ['project.myapp']
如果以project.myapp的名称安装应用程序,则任务模块将作为project.myapp.tasks导入,因此必须确保始终使用相同的名称导入任务:
>>> from project.myapp.tasks import mytask # << GOOD
>>> from myapp.tasks import mytask # << BAD!!!
在第二个案例中由于导入的模块不同,职程(Worker)和客户端会导致任务的命名不同:
>>> from project.myapp.tasks import mytask
>>> mytask.name
'project.myapp.tasks.mytask'
>>> from myapp.tasks import mytask
>>> mytask.name
'myapp.tasks.mytask'
所以,必须在导入模块的方式一致 同样不应该使用 old-style 进行导入:
from module import foo # BAD!
from proj.module import foo # GOOD!
可以使用 new-style 进行导入:
from .module import foo # GOOD!
在不重构的代码的情况下,可以直接指定名称不依赖自动命名来进行使用:
@task(name='proj.tasks.add')
def add(x, y):
return x + y
4.0版中的新功能。 在某些情况默认的自动命名很合适。在许多不同的模块中有很多任务:
project/
/__init__.py
/celery.py
/moduleA/
/__init__.py
/tasks.py
/moduleB/
/__init__.py
/tasks.py
如果使用自动命名,每一个人都会生成一个名词,例如 moduleA.tasks.taskA
、moduleA.tasks.taskB
、moduleB.tasks.test
等。可以通过重写 app.gen_task_name()
进行修改默认的所有任务名称中的 tasks
。基于刚刚的例子,稍微修改(celery.py):
from celery import Celery
class MyCelery(Celery):
def gen_task_name(self, name, module):
if module.endswith('.tasks'):
module = module[:-6]
return super(MyCelery, self).gen_task_name(name, module)
app = MyCelery('main')
这样所有的任务名称都类似 moduleA.taskA
、moduleA.taskB
、moduleB.test
。
需要确保 app.gen_task_name()
是一个函数,同样的输出和输出都必须相同。
app.Task.request
包含与当前执行任务相关的信息和状态。
该请求定义了以下属性:
属性名称 | 说明 |
id | 执行任务的唯一ID |
group | 任务组的唯一ID(该任务是组成员的情况下) |
chord | 此任务所属的和弦的惟一id(如果该任务是标题的一部分) |
correlation_id | 用于重复数据删除的自定义ID |
args | 选项参数 |
kwargs | 关键字参数 |
origin | 发送任务的主机名 |
retries | 任务重试次数,默认是从 0 开始的 |
is_eager | 如果任务是由客户端执行,并非职程(Worker)执行,设置 True |
eta | 任务预计时间(如果已经设置的情况下),时间为 UTC 格式(取决于 enable_utc 设置) |
expires | 任务执行到期时间(如果已经设置的情况下),默认为 UTC 格式(取决于 enable_utc 设置) |
hostname | 执行任务的职程(Worker)实例的节点名 |
delivery_info | 添加附加传递消息,主要用于包含交付任务的交换和路由键的映射, |
reply-to | 回复的发送的队列名称(例如,与 RPC 结果后端一起使用) |
called_directly | 如果职程(Worker)未执行任务,则此标志设置为true |
timelimit | 当前(软、硬)时间限制的元组(如果有的话) |
callbacks | 如果此任务成功返回,将调用的签名列表 |
errback | 如果此任务失败,将调用的签名列表 |
utc | 设置为 true ,启用 UTC |
3.1 版本的新功能 | |
headers | 与任务消息一起发送的消息头的映射(可以为 None) |
reply_to | 回复的地址(队列名称) |
correlation_id | 一般与任务的ID相同,通常用于AMQP中跟踪回复的内容 |
4.0 版本的新功能 | |
root_id | 此任务所属工作流中的第一个任务的唯一ID(如果有) |
parent_id | 调用此任务的任务的惟一id(如果有) |
chain | 反转形成链的任务列表(如果有)。列表中最后一个任务是当前任务执行成功之后的下一个任务。如果使用任务协议的第一个版本,则链任务将位于 request.callbacks 中 |
案例
访问上下文访问信息的一个任务案例:
@app.task(bind=True)
def dump_context(self, x, y):
print('Executing task id {0.id}, args: {0.args!r} kwargs: {0.kwargs!r}'.format(
self.request))
bind 参数表示该函数绑是一个绑定方法,可以通过访问任务类型实例中的属性和方法。
职程(Worker)会自动记录日志信息,也可以手动配置日志记录信息。
Celery 中有一个 celery.task
,可以通过继承的方式获取日志的一部分任务名称以及唯一ID。
最好的做法是在模块顶部为所有任务创建一个共有的日志记录器:
from celery.utils.log import get_task_logger
logger = get_task_logger(__name__)
@app.task
def add(x, y):
logger.info('Adding {0} + {1}'.format(x, y))
return x + y
Celery 使用的是 Python 标准的日志库。 也可以使用 print() ,所有标准 out/-err 都会被重定向到写入到日志系统中。
如果在任务或者任务模块中的某个位置创建了日志记录器实例,职程(Worker)不会更新重定向。
如果需要将 sys.stdout
和 sys.stderr
重定向到自定义日志记录器中,必须手动启用该功能,例如:
import sys
logger = get_task_logger(__name__)
@app.task(bind=True)
def add(self, x, y):
old_outs = sys.stdout, sys.stderr
rlevel = self.app.conf.worker_redirect_stdouts_level
try:
self.app.log.redirect_stdouts_to_logger(logger, rlevel)
print('Adding {0} + {1}'.format(x, y))
return x + y
finally:
sys.stdout, sys.stderr = old_outs
如果需要将 Celery 日志记录器不打印日志,应该检查记录器值是否正确传播。在此案例中,启用 celery.app.trace
,以便发出 "succeeded in" 日志信息:
import celery
import logging
@celery.signals.after_setup_logger.connect
def on_after_setup_logger(**kwargs):
logger = logging.getLogger('celery')
logger.propagate = True
logger = logging.getLogger('celery.app.trace')
logger.propagate = True
4.0 新版本功能
Celery 会校验调用任务时传递的参数信息,就像 Python 调用普通函数时一样:
>>> @app.task
... def add(x, y):
... return x + y
# Calling the task with two arguments works:
>>> add.delay(8, 8)
<AsyncResult: f59d71ca-1549-43e0-be41-4e8821a83c0c>
# Calling the task with only one argument fails:
>>> add.delay(8)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "celery/app/task.py", line 376, in delay
return self.apply_async(args, kwargs)
File "celery/app/task.py", line 485, in apply_async
check_arguments(*(args or ()), **(kwargs or {}))
TypeError: add() takes exactly 2 arguments (1 given)
可以通过设置任务的 typing
参数设置为 False
来关闭参数校验:
>>> @app.task(typing=False)
... def add(x, y):
... return x + y
# Works locally, but the worker receiving the task will raise an error.
>>> add.delay(8)
<AsyncResult: f59d71ca-1549-43e0-be41-4e8821a83c0c>
4.0 新版本功能
使用 task_protocol 2 或更高版本的时(自 4.0 开始为默认值),可以重写位置参数和关键字参数在日志中的表现方式,并且可以使用 argsrepr
和 kwargsrepr
调用参数监控事件:
>>> add.apply_async((2, 3), argsrepr='(<secret-x>, <secret-y>)')
>>> charge.s(account, card='1234 5678 1234 5678').set(
... kwargsrepr=repr({'card': '**** **** **** 5678'})
... ).delay()
警告
任何能够从中间人(Broker)中读取任务消息或以其他方式拦截任务消息的人都可以访问敏感信息。 如果消息中包含敏感信息,应该针对该敏感信息进行加密,在本案例中使用性用卡号对实际的号码进行加密,并且进行安全存储,然后在任务中进行检索解密。
当任务出现错误时,可以通过 app.Task.retry()
重新执行。
当调用 retry
时,会发送与原始任务相同的ID发送一条消息,将该消息发送到原始任务的对列中。
当任务被重试时,也会被记录为一个任务状态,便于通过 result 实例来跟踪任务。
这是一个 retry
的例子:
@app.task(bind=True)
def send_twitter_status(self, oauth, tweet):
try:
twitter = Twitter(oauth)
twitter.update_status(tweet)
except (Twitter.FailWhaleError, Twitter.LoginError) as exc:
raise self.retry(exc=exc)
app.Task.retry()
调用时会引发异常,在重试之后将无法访问任何代码。重试异常,并不是作为错误处理的,表示职程(Worker)需要重试任务,便于在启用结果后端时存储正确的任务状态。
重试异常是正常的,除非将 throw
设置为 False
可以避免该情况发生。
任务装饰器的 bind
参数将允许访问 self
(任务类型实例)。
exc
参数主要用传递日志和存储任务结果时的使用的异常信息。exception
和 traceback
都将在任务状态中可用(如果启用了结果后端)。
任务如果有一个 max_retries
值,超出了重试的最大次数,则会重新引发当前的异常信息,但如果:
exc
参数没有设置
该情况会引发 MaxRetriesExceededError
异常
没有异常
如果没有初始异常来重新引发exc参数,可以使用:
self.retry(exc=Twitter.LoginError())
设置 exc
参数值
当任务需要重试时,可以等待一段时间来进行重试,默认延迟可以通过 default_retry_delay
来进行设置的。默认时间为 3 分钟,延迟时间的单位是秒(类型可以为 int 或 float)。
也可以通过 retry()
中的 countdown
参数来覆盖默认值。
@app.task(bind=True, default_retry_delay=30 * 60) # retry in 30 minutes.
def add(self, x, y):
try:
something_raising()
except Exception as exc:
# overrides the default delay to retry after 1 minute
raise self.retry(exc=exc, countdown=60)
4.0 版本的新功能
有时,您只想在引发特定异常时重试任务。 可也通过 Celery 中 task()
装饰器中的 autoretry_for
参数进行自动重试任务:
from twitter.exceptions import FailWhaleError
@app.task(autoretry_for=(FailWhaleError,))
def refresh_timeline(user):
return twitter.refresh_timeline(user)
可以通过 task()
中的 retry_kwargs
参数来指定 retry()
内部调用参数:
@app.task(autoretry_for=(FailWhaleError,),
retry_kwargs={'max_retries': 5})
def refresh_timeline(user):
return twitter.refresh_timeline(user)
这是手动处理异常的替代方法,上面的示例与在 try
... except
语句中包含的代码块一样:
@app.task
def refresh_timeline(user):
try:
twitter.refresh_timeline(user)
except FailWhaleError as exc:
raise div.retry(exc=exc, max_retries=5)
如果你想自动重试任何错误,只需使用:
@app.task(autoretry_for=(Exception,))
def x():
...
4.2 版本新功能
如果任务依赖于另外一个服务,例如向 API 发起请求,可以使用 exponential backoff
,避免请求淹没服务。 Celery 的自动重试可以通过 retry_backoff
参数进行兼容:
from requests.exceptions import RequestException
@app.task(autoretry_for=(RequestException,), retry_backoff=True)
def x():
...
默认情况下,exponential backoff
会随机引入 jitter,最大延迟限制在10分钟,可以通过下面的选项进行自定义配置。
Task.autoretry_for
异常类的列表或元组,如果任务在执行的过程中引发异常,任务将自动重试。默认情况下不会自动重试任何异常。
Task.retry_kwargs
字典类型,自定义配置自动重试参数。注意,如果使用下面的 exponential backoff
选项是,countdown
任务选项将由 Celery 的自动重试系统决定,字典中包含 countdown
会被忽略。
Task.retry_backoff
一个布尔值或一个数字。如果将此选项设置为True,则自动重试将按照 exponential backoff
规则延迟。第一次重试延迟 1 秒,第二次重试延迟 2 秒,第三次延迟 4 秒,第四次延迟 8 秒,以此类推。(如果启用了 retry_jitter
会修改延迟值)。如果该选项设置为数字,则作为延迟因子,例如,该选项设置为 3,那么第一次重试将延迟 3 秒,第二次将延迟 6 秒,第三次延迟 12 秒,第四次延迟 24秒,以此类推。默认情况下,该选项设置为 False,自动重试不会延迟。
Task.retry_backoff_max
一个数字,如果启动了 retry_backoff
,该选项在任务自动重试之间设置以秒为单位的最大延迟。默认情况,该选项默认值为 600,即 10分钟。
Task.retry_jitter
一个布尔值,Jitter 用于随机性引入指数回退延迟,防止队列中所有任务同时执行,如果该选项设置为 True,则将 retry_backoff
计算的延迟作为最大值,实际的延迟值为一个介于 0 和最大值之间的一个随机数。默认情况下,该选项为 True。
任务装饰器有很多配置选项,例如可以使用 rate_limit
选项配置任务的频率。
传递给任务装饰器的任何关键字参数都为任务结果类的属性,这是一个内置的属性列表。
Task.name
任务注册的名称。 可以手动设置任务名称,也可以通过模块名和类名进行自动生成。
Task.request
如果该任务正处于执行状态,该信息包含该任务的请求信息。使用多线程本地存储。
Task.max_retries
当前任务调用 self
或使用 autoretry_for
参数时才会启用。如果重试的次数超过最大限制,会引发 MaxRetriesExceededError
异常。
Note:
在异常时不会自动重试,所以必须手动调用 retry()
。
默认值重试次数为3次,如果设置为 None
会关闭重试限制,直到任务执行成功为止。
Task.throws
预期内的异常,如果在元组中含有该异常类,将不会被视为异常。
列表中的错误将错误信息记录到结果后端中,但职程(Worker)不会将该事件记录为错误信息,也没有回溯。
例如:
@task(throws=(KeyError, HttpNotFound)):
def get_foo():
something()
错误类型:
预期错误(在 Task.throws)
记录 INFO
信息,但回溯排除。
非预期错误
记录 ERROR
信息,可回溯。
Task.default_retry_delay
默认重试任务休眠事件(以秒为单位)。可以为 int
,也可以为 float
类型,默认情况为 3 分钟。
Task.rate_limit
限制指定任务类型的速率(限制在指定时间内运行的任务数量)。当速率限制生效时,任务仍然会完成,但是可能需要一些时间才能开始。如果限制速率为 None
,表示速率限制无效。速率可以为 int
也可以为 float
类型,则被表示为“每秒任务数”。速率限制也可以在数值后面添加 "/s"、"/m" 或 "/h",以秒、分钟或以小时为单位。任务将在指定的时间内平均分配。例如:"`00/m" (每分钟100个任务)。则强制会在同一个职程(Worker)实例上启动俩个任务之间至少 600ms 的延迟。默认值通过 task_default_rate_limit
进行设定:如果未指定,表示默认情况禁用任务的速率限制。
注意,该速率限制为每一个职程(Worker)实例的限制,并非全局速率限制。配置全局速率限制(例如,API每秒最多请求的次数),必须制定队列。
Task.time_limit
该任务的硬时间限制(以秒为单位),如果没有设置职程(Worker)时,使用默认值。
Task.soft_time_limit
该任务的软时间限制(以秒为单位),如果没有设置职程(Worker)时,使用默认值。
Task.ignore_result
不存储任务状态信息,如果配置该选项 AsyncResult
将失效,无法进行检测任务情况以及返回内容。
Task.store_errors_even_if_ignored
如果设置为 True
,即使任务被忽略,也会存储错误信息。
Task.serializer
标识需要使用默认序列化的字符串。默认为 task_serializer
,也可以为 pickle
、json
、yaml
或者通过 kombu.serialization.registry
注册的自定义序列化方法。
Task.compression
标识需要使用默认压缩方法的字符串。默认为 task_compression
,可以设置为 gzip
、bzip2
或通过 kombu.compression
注册的自定义压缩方案。
Task.backend
结果后端的实例,用于任务结果后端,默认为 app.backend
,可以通过 result_backend
进行配置。
Task.acks_late
如果设置为 True
,任务执行后(而不是执行前,默认为执行前)才会确认该任务的消息。注意:如果职程(Worker)执行过程中崩溃,任务可能会执行多次。可以通过 task_acks_late
参数来进行全局配置。
Task.track_started
如果设置为 True
,当职程(Worker)执行任务时,任务状态为 stared
。默认为 False
,因为在正常情况下是不需要该颗粒度级别的。任务要么挂起、完成要么等待重试。如果有长时间运行的任务吗,并且需要报告任当任务状态时,stared
比较有用。任务执行的职程(Worker)和进程 id 可以通过状态的元数据中进行查看(例如:result.info['pid'])。可以通过 task_track_started
进行全局配置。