Application
Celery 库在使用之前必须初始化,一个celery实例被称为一个应用(或者缩写 app)。
Celery 应用是线程安全的,所以多个不同配置、不同组件、不同任务的 应用可以在一个进程空间里共存。
下面创建一个 celery 应用:>>>from celery import Celery >>>app = Celery() >>>app
最后一行显示的是 celery 应用的文本表示: 包含应用类的名称(Celery),当前主模块的名称(main),以及应用对象的内存地址(0x100469fd0)。
Main Name
上述文本表示中只有一部分是重要的,那就是主模块名称。下面分析下它为何重要。
当你发送一个消息给 Celery,消息中不会包含任何源码,而只有你想要执行的任务的名称。这就好像因特网上的域名映射原理一般:每个执行单元维护着一个任务名称到实际任务函数的映射,这个映射被称为任务注册表。
当你定义一个任务,这个任务就会被添加到本地注册表:>>>@app.task ...def add(x, y): ... return x + y >>>add >>>add.name __main__.add >>>app.tasks['__main__.add'] 1
2
3
4
5
6
7
8
9
10
11
12
__main__
2. 应用在python交互终端创建。
app.worker_main()
tasks.py:from celery import Celery app = Celery() @app.task def add(x, y): return x + y if __name__ == '__main__': app.worker_main()1
2
3
4
5
6
7
8
__main__tasks>>>from tasks import add >>>add.name tasks.add1
2
3
你可以给主模块声明另外一个名称:>>> app = Celery('tasks') >>> app.main 'tasks' >>> @app.task ... def add(x, y): ... return x + y >>> add.name tasks.add1
2
3
4
5
6
7
8
9
10
具体可以查看名称这节。
配置
你可以设置一些选项来改变 Celery 的工作方式。这些选项可以直接在 app 实例上进行设置,或者也可以使用一个指定的配置模块。
app.conf>>>app.conf.timezone 'Europe/London'1
2
你可以直接设置配置值:>>>app.conf.enable_utc = True1
update>>> app.conf.update( ... enable_utc=True, ... timezone='Europe/London', ...)1
2
3
4
3. 默认配置(celery.app.defaults)
app.add_defaults()
另外: 所有可用配置的完整列表及其默认值请参照 Configuration reference。
config_from_object
app.config_from_object() 方法从一个配置对象加载配置。
它可以是一个配置模块,或者任意包含配置属性的对象。
config_from_object
app.config_from_object()from celery import Celery app = Celery() app.config_from_object('celeryconfig')1
2
3
4
celeryconfig.py:enable_utc = True timezone = 'Europe/London'1
2
import celeryconfig
你还可以传递一个已经加载的模块对象,但是不作为常规建议。
建议使用模块名的方式加载,因为这种情况下当prefork池使用时,配置模块不必序列化。如果遇到配置问题或者序列化错误,可以尝试使用模块名的方式加载配置。import celeryconfig from celery import Celery app = Celery() app.config_from_object(celeryconfig)1
2
3
4
5
6
示例3:使用配置类/对象from celery import Celery app = Celery() class Config: enable_utc = True timezone = 'Europe/London' app.config_from_object(Config) # or using the fully qualified name of the object: # app.config_from_object('module:Config')1
2
3
4
5
6
7
8
9
10
11
config_from_envvar
app.config_from_envvar()
例如,从环境变量 CELERY_CONFIG_MODULE 所声明的模块加载配置:import os from celery import Celery #: Set default configuration module name os.environ.setdefault('CELERY_CONFIG_MODULE', 'celeryconfig') app = Celery() app.config_from_envvar('CELERY_CONFIG_MODULE')1
2
3
4
5
6
7
8
你可以通过环境变量声明要使用的模块:$ CELERY_CONFIG_MODULE="celeryconfig.prod" celery worker -l info1
敏感配置
如果你想打印配置信息,作为调试信息或者类似,你也许不想暴露密码和API秘钥这类信息。
humanize()>>>app.conf.humanize(with_defaults=False, censored=True)1
请注意Celery不会移除所有的敏感信息,因为它只是仅仅使用一个正则表达式来匹配配置项键名。如果你添加包含敏感信息的定制化配置,你应该使用 celery 能识别为敏感信息的键名。
API,TOKEN,KEY,SECRET,PASS,SIGNATURE,DATABASE
延迟加载
set_as_current
app.on_init()
app.task()
repr())任务才会被创建:>>> @app.task >>> def add(x, y): ... return x + y >>> type(add) >>> add.__evaluated__() False >>> add # >>> add.__evaluated__() True1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
app.finalize()app.tasks
任务默认是被共享的,但是如果任务装饰器的共享参数被设置为禁用时任务会为被绑定的应用所私有。对所有未求值的任务求值
任务绑定到了应用实例,所以可以读取配置的默认值。
celery 并不是一开始有应用实例这个概念,最早只有一个模块级别的API,为了向后兼容老的API,这个模块级别API会保留直到celery 5.0发布。
celery 会创建一个特殊的应用实例 - 默认应用实例,如果没有自定义的应用实例被初始化,这个默认应用实例将会被启用。
例如,老的任务基类使用了许多兼容特性,其中一些与新的特性不兼容,比如任务方法。from celery.task import Task # << OLD Task base class. from celery import Task # << NEW base class.1
2
3
即使你使用老的模块级别的API,也推荐使用新的基类。
打破链式操作
虽然可以依赖于当前设置的应用实例,但是将应用实例作为参数传递给所有需要它的对象仍然是最佳操作实践。
称这种操作为“应用实例链”的原因是因为它依赖所传递的应用实例创建了一个链。
下面这个例子被认为是差的实践:from celery import current_app class Scheduler(object): def run(self): app = current_app1
2
3
4
5
6
它应用将 app 作为一个参数传递:class Scheduler(object): def __init__(self, app): self.app = app1
2
3
4
celery.app_or_default()from celery.app import app_or_default class Scheduler(object): def __init__(self, app=None): self.app = app_or_default(app)1
2
3
4
5
CELERY_TRACE_APP$ CELERY_TRACE_APP=1 celery worker -l info1
例如,最开始可以使用任何一个可调用对象作为一个任务:def hello(to): return 'hello {0}'.format(to) >>>from celery.execute import apply_async >>>apply_async(hello, ('world!',))1
2
3
4
5
6
可以创建一个任务类,设置特定属性,或者覆盖其他行为from celery.task import Task from celery.registry import tasks class Hello(Task): queue = 'hipri' def run(self, to): return 'hello {0}'.format(to) tasks.register(Hello) >>>Hello.delay('world!')1
2
3
4
5
6
7
8
9
10
11
picklefrom celery.task import task @task(queue='hipri') def hello(to): return 'hello {0}'.format(to)1
2
3
4
5
抽象任务
Task
你可以使用装饰器的 base 参数给任务声明一个不同的基类:@app.task(base=OtherTask): def add(x, y): return x + y1
2
3
创建一个自定义的任务类,你应该继承这个中性类:celery.Taskfrom celery import Task class DebugTask(Task): def __call__(self, *args, **kwargs): print('TASK STARTING: {0.name}[{0.request.id}]'.format(self)) return super(DebugTask, self).__call__(*args, **kwargs)1
2
3
4
5
6
7
__call__
这个中性类比较特殊,因为它不会绑定到任意特殊应用实例。一旦任务绑定到一个应用实例,它将读取应用的配置信息来设置默认值等等。
app.task()@app.task(base=DebugTask) def add(x, y): return x + y1
2
3
app.Task>>>from celery import Celery, Task >>>app = Celery() >>>class MyBaseTask(Task): ... queue = 'hipri' >>>app.Task = MyBaseTask >>>app.Task >>>@app.task ...def add(x, y): ... return x + y >>>add >>>add.__class__.mro() [>, , , ]
Task
任务是构建 celery 应用的基础块。
任务是可以在任何除可调用对象外的地方创建的一个类。它扮演着双重角色,它定义了一个任务被调用时会发生什么(发送一个消息),以及一个工作单元获取到消息之后将会做什么。
每个任务都有不同的名称,发给 celery 的任务消息中会引用这个名称,工作单元就是根据这个名称找到正确的执行函数。
任务消息只有在被工作单元确认后才会从队列中删除。工作单元会预先保存许多任务消息,如果工作单元被杀死-由于断电或者其他原因-任务消息将会重新传递给其他工作单元。
理想的任务函数应该是具有幂等性的:这意味着即使一个任务函数以同样的参数被调用多次也不会导致不可预料的效果。因为工作单元无法探测任务是否是幂等的,所以默认的行为是在即将执行之前预先确认任务消息,这使得已经开始的任务不会再被执行。
acks_late
acks_late
SIGSEGV
4. 一直失败的任务再重新递送消息时会导致高频的消息循环影响到整个系统。
task_reject_on_worker_lost
一个无限期阻塞的任务会使得工作单元无法再做其他事情。
requestsconnect_timeout, read_timeout = 5.0, 30.0 response = requests.get(URL, timeout=(connect_timeout, read_timeout))1
2
时间限制对确保所有任务在规定的时间内返回很方便,但是一个超市事件将会强制终止进程,所以应该只有在没有手动设置超时时间的地方使用。
prefork-ofairPrefork pool prefetch settings
如果你的工作单元被挂起了,请先看看它运行的是什么任务,而不是先提交问题,因为大部分情况下挂起是由于一个或多个任务阻塞在网络操作上。
- Example
基础
task()from .models import User @app.task def create_user(username, password): User.objects.create(username=username, password=password)1
2
3
4
5
任务上可以设置很多选项,这些选项作为参数传递给装饰器:@app.task(serializer='json') def create_user(username, password): User.objects.create(username=username, password=password)1
2
3
task@app.task @decorator2 @decorator1 def add(x, y): return x + y1
2
3
4
5
app
任务装饰器可以从 Celery 应用实例上获取,如果不理解,请先看 First Steps with Celery。
Djangoshared_task()from celery import shared_task @shared_task def add(x, y): return x + y1
2
3
4
5
绑定任务
一个绑定任务意味着任务函数的第一个参数总是任务实例本身(self),就像 Python 绑定方法类似:logger = get_task_logger(__name__) @task(bind=True) def add(self, x, y): logger.info(self.request.id)1
2
3
4
5
app.Task.retry()
任务继承
baseimport 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()1
2
3
4
5
6
7
8
9
10
任务名称
每个任务必须有不同的名称。
如果没有显示提供名称,任务装饰器将会自动产生一个,产生的名称会基于这些信息: 1)任务定义所在的模块, 2)任务函数的名称
显示设置任务名称的例子:>>>@app.task(name='sum-of-two-numbers') >>>def add(x, y): ... return x + y >>>add.name 'sum-of-two-numbers'1
2
3
4
5
6
最佳实践是使用模块名称作为命名空间,这样的话如果有一个同名任务函数定义在其他模块也不会产生冲突。>>>@app.task(name='tasks.add') >>>def add(x, y): ... return x + y1
2
3
.name>>>add.name 'tasks.add'1
2
tasks.py@app.task def add(x, y): return x + y1
2
3>>>from tasks import add >>>add.name 'tasks.add'1
2
3
自动命名与相对导入
对于 python2,开发者的最佳实践是在每个模块前添加下面这句代码:from __future__ import absolute_import1
在 python3 中,默认就是绝对导入的,所以不需要再额外添加其他代码。
相对导入和任务名称自动生成混合使用时会有些问题,所以如果你使用相对导入,你应该显示设置任务名称。
myapp.tasks.tasksmyapp.tasksNotRegistered
project.myapp-INSTALLED_APPS = ['project.myapp']1
project.myappproject.myapp.tasks, 所以你必须确保总是使用相同的名称导入任务:>>>from project.myapp.tasks import mytask # << GOOD >>>from myapp.tasks import mytask # << BAD!!!1
2
3
第二个例子里任务的名称会不一样,因为工作单元与客户端在不同的名称空间下导入模块:>>>from project.myapp.tasks import mytask >>>mytask.name 'project.myapp.tasks.mytask' >>>from myapp.tasks import mytask >>>mytask.name 'myapp.tasks.mytask'1
2
3
4
5
6
7
基于这一点,你必须在导入模块时保持一致,这也是 python 的最佳实践。
同样的,你不应该使用老式的相对导入:from module import foo # BAD! from proj.module import foo # GOOD!1
2
3
新式的相对导入能够被正常使用:from .module import foo # GOOD!1
如果你使用 celery 的项目里已经重度使用了这些模式,而且你没时间再去重构现有代码,那么你可以考虑现实声明任务名称而不是依赖于自动名称生成。@task(name='proj.tasks.add') def add(x, y): return x + y1
2
3
改变自动名称生成形式
4.0 版本新特性。
在有些情况,默认的自动名称生成规则并不合适。例如你在多个不同模块定义了多个任务:project/ /__init__.py /celery.py /moduleA/ /__init__.py /tasks.py /moduleB/ /__init__.py /tasks.py1
2
3
4
5
6
7
8
9
moduleA.tasks.taskA, moduleA.tasks.taskB, moduleB.tasks.testapp.gen_task_name()celery.pyfrom 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)1
2
3
4
5
6
7
8
9
moduleA.taskA, moduleA.taskB, moduleB.test。
app.gen_task_name()
任务请求
app.Task.request
enable_utc
enable_utc
app.Task.retry()
True
utc: 如果调用者使能了 UTC(enable_utc),这个属性为True
None
correlation_id: 通常与任务id相同,一般在amqp中用来跟踪回复是发送到哪里
chain: 组成一个任务链的预留任务的列表(如果存在)。列表中最后一项将是当前任务的下一个任务
下面是一个访问任务上下文信息的任务函数示例@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))1
2
3
4
绑定参数说明这个函数是一个”绑定方法”,所以可以访问任务实例的属性和方法。
日志
任务工作单元会自动给你设置日志环境,当然你也可以手动配置日志。
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 + y1
2
3
4
5
6
7
8
celery 使用 python 标准日志库,可以在 python 官方文档中找到。
print()worker_redirect_stdouts
如果你在任务函数或者模块中创建一个日志句柄,任务工作单元不会更新这个重定向行为。
sys.stdoutsys.stderrimport 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_outs1
2
3
4
5
6
7
8
9
10
11
12
13
14
参数检查
4.0版本新特性
当你调用任务函数时,Celery 会验证传递的参数,就像调用一个普通函数时 Python 所做的检查。>>>@app.task ...def add(x, y): ... return x + y # Calling the task with two arguments works: >>>add.delay(8, 8) # Calling the task with only one argument fails: >>>add.delay(8) Traceback (most recent call last): File "", line 1, in 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)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typingFalse>>> @app.task(typing=False) ... def add(x, y): ... return x + y # Works locally, but the worker reciving the task will raise an error. >>> add.delay(8) 1
2
3
4
5
6
7
隐藏参数中的敏感信息
4.0版本新特性。
task_protocal 2argsreprkwargsrepr>>> add.apply_async((2, 3), argsrepr='(, )') >>> charge.s(account, card='1234 5678 1234 5678').set( ... kwargsrepr=repr({'card': '**** **** **** 5678'}) ... ).delay()1
2
3
4
5
对于可以从任务中间件中读取任务消息或者可以截取到消息的人来说,敏感信息仍然是可以访问的。
基于这个原因,如果你的消息中含有敏感信息,你应该加密信息,或者如上示例中带有信用卡号之类的信息可以将其加密存储到一个安全的存储,然后任务中从存储中获取并解密。
重试
app.Task.retry()
retry
一个任务被重试将记录为一个任务状态,因此你可以使用结果实例跟踪任务的进度(查看状态这一节)。
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)1
2
3
4
5
6
7
app.Task.retry()retryRetry
retrythrowFalse,这个异常将总是会抛出。
exc
max_retries
这种情况下,MaxRetriesExceededError
如果没有原始异常被重新抛出,excself.retry(exc=Twitter.LoginError())1
exc
使用自定义重试延迟
default_retry_delay
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)1
2
3
4
5
6
7
对已知异常的自动尝试
4.0 版本新特性。
有时候,你只想在特定异常抛出时重试任务。
autoretry_forfrom twitter.exceptions import FailWhaleError @app.task(autoretry_for=(FailWhaleError,)) def refresh_timeline(user): return twitter.refresh_timeline(user)1
2
3
4
5
Task.retryretry_kwargs@app.task(autoretry_for=(FailWhaleError,), retry_kwargs={'max_retries': 5}) def refresh_timeline(user): return twitter.refresh_timeline(user)1
2
3
4
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)1
2
3
4
5
6
如果你想在发生任意错误时重试,可以这样:@app.task(autoretry_for=(Exception,)) def x(): ...1
2
3
4.1 版本新特性。
retry_backofffrom requests.exceptions import RequestException @app.task(autoretry_for=(RequestException,), retry_backoff=True) def x(): ...1
2
3
4
5
jitter
Task.autoretry_for
一个异常类的列表或者元组。如果在任务执行期间任何一个其中异常抛出,任务将会被自动重试。默认情况下,没有异常会被重试。
Task.retry_kwargs
countdowncountdown
Task.retry_backoff
retry_jitterFalse,此时重试不会有延迟。
Task.retry_backoff_max
retry_backoff
Task.retry_jitter
布尔值。jitterTrueretry_backoffTrue。
选项列表
rate_limit
传递给任务装饰器的关键字参数将会设置为结果任务类的一个属性,下面是内建属性的列表。
Task.name
任务注册的名称
你可以手动设置任务名称,或者任务名称将依据模块名与类名自动生成。
另见任务名称这一节。
Task.request
如果任务将被执行,这个属性会包含当前请求的信息。会使用Thread local 存储。
另见任务请求这一节。
Task.max_retries
self.retryautoretry_for