Celery - 最佳实践

原文作者:Deni Bertovic
原文日期:2014-06-18
原文链接:Celery - Best Practices

前言

翻译纯属个人喜爱和收藏,分享以让更多的人阅读受益。原文虽然5-6年前写作,但至今任然适用。为了更符合中文阅读习惯,我把部分段落进行了分解或合并。后续我将在原文基础之上,基于实践适当添加更多内容。

序言

如果你在从事Django某些方面的工作,那么你可能有些后台长时运行任务处理的需求。可能你已经使用了某种任务队列,而Celery是Python(以及Django)界(还有更多其他的)处理这种事务当前最流行的项目。

在从事几个用Celery作为任务队列的项目中,我收集了几个最佳实践并决定记录下来。不过,这不仅仅是我认为正确使用的方式,还有些Celery生态提供但并未充分利用的特性。

一、不使用数据库作为AMQP Broker

让我解释下使用数据库作为队列后端为什么不对(除了Celery官网文档支出的局限之外)。数据库并非为了像消息队列软件如RabbitMQ所设计那样去处理事务,某个时刻它可能在并非那么多流量或者用户的生成环境中崩溃。我猜大家使用数据库最常用的理由是,既然网页程序以及有一个了,那为什么不复用呢,配置简单而且不用担心另一个组件(比如RabbitMQ)。

例如一个实际的场景:假设你有4个处理放置于数据库的后台任务的worker,这就意味着有4个进程频繁地从数据库拉取新任务,先不说每个进程可以有多并发进程。在某个时刻你发现任务处理出现延迟,任务新增量超过处理完成量。自然而然的你会增加worker数量,但是数据库因为多worker的请求而变慢,磁盘IO到达极限,网页程序从而受到影响而变慢,因为woker基本上就是在DDOS攻击数据库。

如果你有个恰当的AMQP比如RabbitMQ这种情况就不会发生。一个,队列存储在内存不涉及磁盘;二个,worker不需依托于请求数据库因为队列有方式推送新任务给worker,即便AMQP因为其他原因而过载,至少它也不会拖慢面向用户的前端程序。

所以我说即便是开发环境也不要用数据库作为broker,更何况有Docker及非常多开箱即用的容器镜像可以运行RabbitMQ。

二、使用多个队列(不仅仅默认的队列)

Celery配置相当简单,自带一个默认队列放置所有的任务,除非你区分开。常见情况就像这样:

@app.task()
def my_taskA(a, b, c):
    print("doing something here...")

@app.task()
def my_taskB(x, y):
    print("doing something here...")

如此两个任务都在同一个队列(如果没有在celeryconfig.py文件中单独指定)。我一定能听见这样的声音因为一个装饰器就能跑好后台任务。我的顾虑是taskA和taskB可能在做完全不同的事,并且可能其中一个甚至重要得多,那么为什么把它们都放一个篮子里呢?即便你只有一个worker同时处理这两个任务,假设不重要的任务taskB在某个时刻数量总到以至于taskA都获取不到资源怎么办?此刻增加worker数量可能能解决问题,但是worker还是得处理两个任务,taskB数量太大同样会导致taskA获取不到应有的资源。这就引申到下一点。

三、使用特定worker

解决上述问题的方式就是把taskA单独放到一个队列,taskB放另一个队列,并且分配x个worker来处理Q1,其他的worker都给任务密集的Q2。如此你依然可以保证taskB一直有足够多的worker来处理,同时有几个指定的worker来处理taskA,不至于让它等太久。

所以,手工定义好你的队列:

CELERY_QUEUES = (
    Queue('default', Exchange('default'), routing_key='default'),
    Queue('for_task_A', Exchange('for_task_A'), routing_key='for_task_A'),
    Queue('for_task_B', Exchange('for_task_B'), routing_key='for_task_B'),
)

还有决定什么任务分配到哪里的路由

CELERY_ROUTES = {
    'my_taskA': {'queue': 'for_task_A', 'routing_key': 'for_task_A'},
    'my_taskB': {'queue': 'for_task_B', 'routing_key': 'for_task_B'},
}

他们将允许你为不同任务运行worker:

celery worker -E -l INFO -n workerA -Q for_task_A
celery worker -E -l INFO -n workerB -Q for_task_B

四、使用Celery的错误处理机制

我看到业界大多数任务几乎都没有错误处理意识,如果任务失败那就失败。在一些使用场景可能没问题,但是我看到的大多数任务都会调用三方API因为网络错误或者其他"资源不可用"而出错。处理这样的问题最简单方式就是重跑任务,因为也许三方API只是有服务器/网络错误但可以马上恢复,为什么不这么做呢?

@app.task(bind=True, default_retry_delay=300, max_retries=5)
def my_task_A():
    try:
        print("doing stuff here...")
    except SomeNetworkException as e:
        print("maybe do some clenup here....")
        self.retry(e)

而我更倾向于为每个任务定义默认重试等待时间,以及最终放弃的最大重试次数(分别对应参数default_retry_delaymax_retries)。我认为这是错误处理的最基本的方式但却几乎没看到使用。当前Celery还提供了更多的错误处理方法,大家可以参考官方文档。

五、使用Flower

Flower项目是个监控Celery任务和worker非常好的工具,它基于网页可以查看任务进度、细节和worker状态,带起更多worker等。可以点击前面链接查看它的全部功能清单。

六、除非真的需要才保存状态结果

任务状态是任务成功或者失败的退出信息,对后续的某种统计有用。关键是退出状态并非任务执行内容的结果,这些结果作为某种边际效应更多的是更新数据库(比如更好友列表)。

我见过的大部分项目都不在乎任务完成后状态的持续跟踪,但大部分要么用默认的sqllite来保存这些东西,更甚者还用了常规的数据库(PostgreSQL或者其他的)。

为什么毫无理由的去消耗网页程序的数据库呢?使用celeryconfig.py文件中的CELERY_IGNORE_RESULT = True来规避存储任务状态结果吧。

七、不要传递数据库ORM给任务

在本地的一个Python小聚会上分享之后,一些朋友建议添加这条到清单中。什么意思呢,就是不要传递数据库对象(比如用户模型)给后台任务,因为序列化对象可能包含旧数据。你需要做的就是给任务传递用户ID然后让任务去数据库获取新的用户对象数据。

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: Age of Ai 设计师:meimeiellie 返回首页