如何使用Celery来安排任务

432 阅读13分钟

有多种方法可以在你的Django应用中安排任务,但使用Celery有一些优势。它得到了支持,扩展性好,而且与Django配合得很好。鉴于它的广泛使用,也有很多资源可以用来学习更多关于它的知识,而且一旦学会了,这些知识很可能在其他项目中也会有用。

顺便说一下,这篇博客是我在2014年写的关于同一主题的博客的更新版本,但它适用于Celery 3.0.x。

Celery 5.0.x版本

本文档适用于Celery 5.0.x。Celery的早期或后期版本可能会有不同的表现。另外,如果Celery的较新版本重新组织了文档,指向Celery文档的链接可能会停止工作,这种情况确实会发生。

关于Celery的介绍

Celery的目的是让你根据时间表来运行代码。为什么这可能是有用的呢?下面是几个常见的案例。

**案例1:**假设一个用户提出了一个网络请求,他正在等待请求的完成,以便在他们的浏览器中加载一个新的页面。 根据他们的请求,你有一些代码需要运行一段时间(比这个人可能想等待一个网页的时间要长),但是你并不真的需要在响应网络请求之前运行这些代码。你可以使用Celery,让你的长期运行的代码稍后被调用,然后继续运行并立即响应网页的请求。

如果你需要访问一个远程服务器来处理请求,这种情况很常见。 你的应用程序无法控制远程服务器需要多长时间来响应,或者远程服务器可能已经停机。

**情况2:**另一种常见的情况是想定期运行代码。例如,也许每隔一小时你就想查一下最新的天气报告并存储数据。你可以写一个任务来做这项工作,然后要求Celery每小时运行一次。该任务运行并将数据放入数据库,然后你的Web应用程序就可以访问最新的天气报告了。

一些 Celery 术语。

一个任务只是一个Python函数。 你可以把调度任务看作是对该函数的延时调用。例如,你可以让Celery在五分钟后调用你的函数task1,参数为(1, 3, 3)。或者你可以让你的函数batchjob在每天晚上的午夜被调用。

当一个任务准备好被运行时,Celery会把它放在一个队列里,这是一个准备好被运行的任务的列表。你可以有很多队列,但为了简单起见,我们在这里假设只有一个队列。

把一个任务放在队列中只是把它添加到一个待办事项列表中,为了使任务被执行,一些其他的进程,称为工作者,必须观察队列中的任务。当它看到队列中的任务时,它将拉出第一个任务并执行它,然后回去等待更多的任务。 你可以有许多工作者,可能在许多不同的服务器上,但我们现在假设只有一个工作者。

我们稍后会讨论更多关于队列、工作者和另一个我们还没有提到的重要过程,但现在已经足够了。

棘手的部分

为了使所有这些工作顺利进行,Django和Celery进程必须在大部分配置上达成一致,而且Celery进程必须运行足够多的Django的设置,以便我们的任务能够访问数据库等等。这有点复杂,因为Django和Celery的启动代码完全不同。

(旁白。在Celery的几个主要版本之前,这要简单得多,那时我们可以把Celery应用程序作为Django管理命令来运行。啊,那些日子......)

下面是一些关键点。

  • DJANGO_SETTINGS_MODULE_必须_在启动Celery进程之前在环境中设置。它在环境中的存在会触发Celery的内部魔法,在正确的时间运行Django设置。
  • Celery的 "应用程序 "必须在Django和Celery的启动过程中被创建和配置。
  • 所有的Celery任务都需要在Django和Celery的启动过程中被导入。

在本地安装Celery

我们将在Celery中使用Redis。所以你需要安装Redis。 我将指出Redis的安装文档,而不是在这里重现它。然后用一条命令安装Celery和它所需要的所有依赖,以使用Redis,在我们的虚拟环境中运行。

.

为Celery配置Django

我们只需要将Celery配置为与runningerver一起使用。 对于Celerybroker,即让Django和Celery工作者进行通信,我们将使用Redis。Redis很好,因为它既容易设置,又适用于许多生产环境。

**警告。**在使用Redis之前,除了在你的本地系统上玩耍之外,请阅读关于Redis的安全问题,并计划保护你的Redis服务器。它_不是_被设计成开箱即用的安全产品。

在你的Django设置文件中,添加。

注意: _broker_是最重要的一个配置值,因为它告诉Django和Celery如何进行通信。如果它们在这个设置中没有相同的值,就不会有任务运行。

创建Celery应用程序

我们需要一个小的Python文件,以我们想要的方式初始化Celery,无论是在Django还是Celery进程中运行。

在我们项目的顶层创建一个文件celery.py是很诱人的,但这正是我们不能使用的名字,因为Celery拥有celery包的命名空间。相反,我将在我现有的一个包中创建一个celery.py。在这些例子中,这将是myapp/celery.py。

下面是你需要添加的代码。

你把Celery()对象分配给什么变量并不重要,只要它在模块的顶层,Celery就会找到它。

config_from_object很重要,这样我们就可以把CELERY_*设置放在我们的Django设置中,让Celery使用这些值。我们想要配置Celery的任何东西,只要找到合适的配置设置,把它改成大写字母,把CELERY_放在前面,然后在Django设置中设置。

例如,有一个Celery设置的时区,如果我们想设置它,我们可以在Django设置中这样写。

在Django设置的后期,在你所有的Django应用被注册和模型被加载之后,导入这个文件是非常关键的。我建议在任何Django应用程序的ready()方法中导入它。

如果你使用的是3.2之前的Django,为了确保这个文件被使用,请将其添加到myapp/__init__.py中。

你会在下面看到,我们将告诉Celery的进程使用一个命令行选项来加载这个。

编写一个任务

如前所述,一个任务可以只是一个Python函数。 然而,Celery确实需要知道它。在Django中使用Celery时,这很容易。 只要在应用程序中添加一个tasks.py文件,将你的任务放在该文件中,并使用@shared_task()来装饰它们。 下面是一个微不足道的tasks.py。

将一个函数标记为任务并不妨碍正常调用它。你仍然可以调用它:z = add(1, 2),它将和以前一样工作。把它标记为任务只是给了你额外的调用方法。

当它被导入时,Celery会将这个方法注册为我们应用程序的任务。 或者调用app.autodiscover_tasks()会加载你所有/tasks.py文件中的任务。

所有的任务都_必须_在Django和Celery启动时被导入,这样Celery才会知道它们。如果我们把它们放在/tasks.py文件中并调用app.autodiscover_tasks(),这就可以了。或者我们可以把任务放在我们的模型文件中,或者从那里导入,或者从应用程序的准备方法中导入。

排列任务

让我们从上面提到的那个简单案例开始。我们想尽快运行我们的任务。我们只是不想让它耽误我们当前的线程。我们可以通过在任务的名称中添加.delay来实现。

Celery将把任务添加到它的队列中("worker,请调用myapp.tasks.add(2, 2)")并立即返回。一旦一个空闲的工作者在队列的头部看到它,该工作者将从队列中移除它,然后执行它,类似这样。

.

关于导入名称的警告

重要的是,你的任务总是被导入并使用相同的包名。 例如,取决于你的 Python 路径是如何设置的,它可能被称为myproject.myapp.tasks.add或myapp.tasks.add。 或者从myapp.views,你可以把它导入为.tasks.add。但是Celery没有办法知道这些都是同一个任务。

测试它

如前所述,必须运行一个单独的进程,即工作者,以实际执行你的Celery任务。 下面是我们如何为我们的开发需求启动一个工作者。

首先,打开一个新的 shell 或窗口。在那个shell中,设置相同的Django开发环境--激活你的虚拟环境,或者在你的Python路径中添加东西,不管你做什么,这样你就可以使用runnserver来运行你的项目。

另外,即使你不这样做,你也必须在你的环境中设置DJANGO_SETTINGS_MODULE,否则Celery将无法识别它正在与Django一起运行。

现在你可以在那个 shell 中启动一个工作者

工作者将在该窗口中运行,并在那里发送输出。

-A命令行的 "选项 "并不是真正的可选项。Celery将导入该模块,并在那里寻找我们的Celery应用程序对象。

顺便说一下,我们可以在这里更具体一些,比如-Amyapp.celery:app 来告诉 Celery 我们希望它使用的应用程序在模块中的app顶级变量中。 但除非你在模块中有多个Celery应用程序,否则你不必这样做,而对于大多数Django项目来说,没有理由这样做。

运行你的任务

回到你的第一个窗口,启动一个Django shell并运行你的任务。

你应该在工作者窗口中看到输出,表明工作者已经运行了任务。

.

如果事情似乎没有进展,请回去确保Django应用程序的设置、myapp/__init__.py和myapp/apps.py有上述建议的修改。

一个例子

前面我们提到使用Celery来避免延迟响应网络请求。下面是一个使用该技术的简化Django视图。

.

疑难解答

要想让Celery任务正常工作,可能会让人感到沮丧,因为多个部分都必须存在,并且相互之间要有正确的沟通。许多常见的提示仍然适用。

  • 首先让最简单的配置工作。
  • 使用python调试器和打印语句来查看正在发生的事情。
  • 调高日志级别(例如:Worker上的--logo leveldebug)以获得更多的洞察力。

还有一些是Celery特有的工具。

急切的调度

在你的Django设置中,你可以添加。

并且Celery会绕过整个调度机制,直接调用你的代码。

换句话说,在CELERY_ALWAYS_EAGER = True的情况下,这两个语句的运行是一样的。

在引入Celery调度的复杂性之前,你可以用它来让你的核心逻辑工作。

检查结果

任何时候你安排一个任务,Celery都会返回一个AsyncResult对象。你可以保存该对象,然后在以后使用它来查看任务是否被执行,是否成功,以及结果是什么。

.

周期性调度

另一个常见的情况是在一个定期的时间表上运行一个任务。 Celery使用另一个进程celery beat来实现这一点。Celery beat持续运行,每当计划任务运行的时间到了,celery beat就排队等待执行。

由于显而易见的原因,应该只有一个celery beat进程在运行(不像workers,你可以根据你的需要运行很多)。

启动celery beat类似于启动一个worker。启动另一个窗口,设置好你的Django环境,然后。

**注意:**如果你运行celery beat的地方,它不会有一个跨调用的持久性文件系统,比如在一个容器里,那么忽略下面的说明,看我的另一篇博文。如何在容器中使用Celery Beat安排任务

要安排 "myapp.tasks "包中的 "add "任务每30秒运行一次,参数为(16,16),请在你的Django设置中添加这个。

为了安全起见,过期选项告诉Celery,如果它不能在15秒内运行这个任务,就直接取消它。我们知道,无论如何我们都会每隔30秒再排一个任务。

提示和技巧

**不要向任务传递模型对象。**由于任务不会立即运行,当任务运行并查看传递给它的模型对象时,数据库中的相应记录可能已经改变。如果任务随后对模型对象做了一些事情并保存了它,那么数据库中的这些变化就会被旧的数据覆盖。

几乎总是更安全的做法是保存对象,传递记录的键,并在任务中再次查找对象。

.

**在其他任务中安排任务。**在执行一个任务的同时安排另一个任务是完全正确的。 这是一个很好的方法,可以确保在第一个任务完成一些必要的工作之前,第二个任务不会运行。

**不要在另一个任务中等待一个任务。**如果一个任务等待另一个任务,第一个任务的工作者就会被阻断,在等待结束之前不能再做任何工作。这很可能会导致僵局,迟早的事。

如果你在任务A中,想安排任务B,并在任务B完成后,再做一些工作,最好是创建一个任务C来做这些工作,并在任务B完成后,让任务B安排任务C。

接下来的步骤

一旦你理解了基础知识,《Celery用户指南》中的部分内容就可以阅读了。我推荐你从以下章节开始阅读;其他章节要么与Django用户无关,要么更高级。

在生产中使用Celery

关于这个话题的信息,请查看我的另一篇博文,Celery在生产中的应用。

这就是了!我们希望你喜欢学习在Celery中进行调度的新方法。我们期待着听到你的评论!