在Flask 应用程序中使用Celery的4种用例
Celery可以帮助你异步或者定期运行你想在大多数web项目中执行的常见事情的代码。
上周,我是可盈利的Python播客的一名嘉宾,在直播中,我们主要讨论了如何从零开始发展观众,以及作为一名新开发人员如何在6个月内创造潜在的收入。
但是我们还讨论了其他一些事情,其中之一是什么时候在Flask项目中或者任何由Python驱动的web应用程序中使用Celery可能会是一个好主意。由于这只是此播客的一个副主题,而我想在这个主题上做进一步的阐述,所以就有了这篇文章。
这里有几个你什么时候会想到去使用Celery的用例。就我个人而言,我发现我几乎在自己创建的每个Flask应用程序中都使用了它。
用例#1: 发送邮件
我想说,这是教科书上最典型的例子之一,它说明了为什么使用Celery或寻找一个允许异步执行任务的解决方案是一个好主意。
例如,假设有人访问了你的站点的联系页面,希望填写该页面并向你发送电子邮件。当他们点击发送电子邮件按钮后,一封电子邮件将被发送到你的收件箱。
在不使用Celery的情况下发送邮件
解释Celery为什么有用的最好方法是首先演示如果你不使用Celery,发送邮件是如何工作的。
一个典型的工作流程是:
- 用户访问/contact页面,并看到一个联系表单
- 用户填写联系表单
- 用户单击发送电子邮件按钮
- 用户的鼠标光标变成一个繁忙的图标,此时,他们的浏览器挂起
- 你的Flask应用程序处理该POST请求
- 你的Flask应用程序验证表单
- 你的Flask应用程序可能会编译一个电子邮件的模板
- 你的Flask应用程序接收该电子邮件并将其发送到你配置的电子邮件提供商
- 你的Flask应用程序将等待电子邮件提供商(gmail、sendgrid等)进行响应
- 你的Flask应用程序通过重定向到一个页面来向用户返回一个HTML响应
- 用户的浏览器呈现该新页面,繁忙的鼠标指针将消失
注意步骤4和11是如何用斜体显示的。这些都是非常重要的步骤,因为在步骤4和步骤11之间,用户坐在那里,看着一个忙碌的鼠标光标图标,而你的站点对该用户的加载速度似乎很慢。他们正在等待一个响应。
以上工作流程存在的问题:
这里真正的问题是,你无法控制第8步和第9步花费多长时间。他们可能需要500毫秒,2秒,20秒,甚至会在120秒后暂停。
这是因为你正在联系一个外部站点。在本例中,它们gmail的SMTP服务器或诸如sendgrid或mailgun之类的其它事务性电子邮件服务。你完全无法控制它们处理你的请求需要花费多长时间。
这个情形真正危险的地方是,现在想象一下,如果有10个访问者试图填写你的联系表单,而你运行的是流行的Python应用程序服务器gunicorn或uwsgi。
如果你没有为它们配置多个worker和/或线程,那么你的应用服务器将会陷入困境,它将无法处理所有这10个请求,直到每个请求按顺序完成。
通常情况下,如果你的请求完成得很快,比如在不到100ms的时间内完成,那么这不是一个问题; 如果你有两个进程在运行,那么这尤其就不是什么大问题了。你可以在短时间内快速处理数十个并发请求,但如果每个请求需要花费2或3秒,情况就不同了——这会改变一切。
它还会变得更糟,因为其它请求也将开始挂起。这些请求可能是另一个访问者试图访问你的主页或应用程序的任何其它页面。
基本上,你的应用服务器会因为等待而超载,你的请求响应时间就会越长,在你知道这一点之前,它对每个请求的响应也就越差,现在加载一个简单的主页需要8秒,而不是80毫秒。
Celery为什么是一个很棒的工具
从技术上讲,解决上述问题的秘诀是能够在后台执行步骤8和9。这就是为什么Celery经常被贴上一个“background worker(后端工人)”的标签。
我在这里说“技术上”,是因为你可以使用类似Python 3的async /await功能来解决这个问题,但这是一个非常不可靠的开箱即用的解决方案。
Celery将跟踪你在数据库后端(如Redis或RabbitMQ)中发送给它的工作。这将把状态排除在应用程序服务器的进程之外,这意味着即使你的应用程序服务器崩溃了,你的任务队列将仍然保留。
Celery还可以让你跟踪失败的任务。一个“任务”其实就是你让Celery做的一些工作,比如发送一封电子邮件。它可以是任何事情。
Celery还允许你为失败的任务设置重试策略。例如,如果那封邮件发送失败,你可以指示Celery去尝试5次,甚至做一些高级重试策略,如指数退避,这意味着在4秒后再试一次,然后在8、16、32和64秒后再试一次。你可以非常详细地配置所有这些。
Celery还可以让你对任务进行限流。例如,如果你想保护你的联系表单,使每个访问者每10秒不允许超过1封电子邮件,你可以很容易地设置这样的自定义规则。你可以根据IP地址甚至是你系统上每一个登录的用户来完成此操作。
我一直在尝试说明的是,Celery是一个非常强大的工具,它可以让你在几乎没有样板文件和很少配置的情况下完成生产准备工作。这就是为什么我非常喜欢使用它,而不是async / await或其它异步解决方案。
这也是我为什么在我的《使用Flask构建一个SAAS应用程序》课程中很早就引入了Celery。我们在这门课中做的第一件事是为一个联系表单发送电子邮件,我们一开始就使用了Celery,因为我很乐意提供现成的生产例子,而不是玩具例子。
一个很棒的地方是我们在那门课程中使用了Docker,所以在该项目中添加Celery和Redis一点也不是事。只需要几行YAML配置,我们就可以完成。你可以在我们在课程中构建的应用程序的开源版本中看到这些。
使用 Celery发送邮件
掌握了以上知识之后,我们来稍微调整一下工作流程:
- 用户访问/contact页面,并会看到一个联系表单
- 用户填写联系表单
- 用户单击发送电子邮件按钮
- 用户的鼠标光标变成一个繁忙的图标,现在他们的浏览器挂起
- 你的Flask应用程序处理POST请求
- 你的Flask应用程序验证表单
- 你的Flask应用程序调用一个你创建的Celery任务
- 你的Flask应用程序通过重定向到一个页面来向用户返回一个HTML响应
- 用户的浏览器呈现该新页面,繁忙的鼠标指针将消失
上述工作流与原始工作流的不同之处在于,步骤4到9几乎会立即完成执行。如果一切都在20毫秒内完成,我不会感到惊讶。这意味着你可以在1秒内处理50个这样的请求,而你的应用服务器上只有一个进程/线程。
这是一个巨大的改进,也是非常一致的。我们返回去去控制用户获得响应所需的时间,而不会让我们的应用服务器陷入停滞。只需添加更多的应用服务器进程(或从根本上来说是CPU内核),我们就可以轻松地扩展到每秒数百个并发请求。
我们不再需要在请求/响应周期内发送电子邮件,并等待来自你的电子邮件供应商的响应。我们可以在后台执行Celery任务,并使用一个重定向来立即进行响。
用户真的不需要知道邮件是否已经送达。他们可能只会看到一条简单的即时消息,上面写着感谢他们联系你,你很快就会回复他们。
但如果你确实想监控该任务,并在任务完成时得到通知,你也可以用Celery来做到。然而,在这种情况下,邮件是否在500毫秒或5秒后送达并不重要,因为从用户的角度来看,这是完全相同的。
顺便说一下,如果你想知道,新的步骤7的Celery任务将是原来工作流程的步骤7-9,它们是:
- 你的Celery任务可能会编译一个邮件的模板
- 你的Celery任务接收该电子邮件并将其发送给你配置的电子邮件提供商
- 你的Celery任务将进行等待直到你的电子邮件提供商 (gmail、sendgrid等)进行响应
因此,完成的是同样的事情。只不过Celery是在后台处理它。
用例 #2: 连接到第三方API
我们刚刚讨论了发送电子邮件,但实际上这适用于对一个你无法控制的外部服务进行任何第三方API调用。实际上是任何外部网络调用。
这也确实会在你的一个HTTP连接的典型的请求/响应周期中嵌入执行API调用。例如,当用户访问一个页面时,你想要连接一个第三方API,并且现在你想要对该用户进行响应。
对于上面的用例,我几乎总是会使用Celery,如果我需要在从该API获取数据后更新一个页面的UI,那么我会使用websockets或者好用的老式的长轮询。
这两种方法都允许你立即进行响应,然后在你获得数据后更新你的页面。
Websockets很好用,因为一旦你从Celery任务中的API获取了数据,你就可以将其广播给用户,但如果你已经设置好了长轮询,那么它也可以工作。很多人不喜欢长轮询,但在某些情况下,它可以让你在不需要引入使用websockets的复杂性的情况下处理的更好。
顺便说一下,在《使用Flask构建一个SAAS应用程序》课程中,我最近添加了一个免费的更新,其中涵盖了使用websockets的内容。你将看到如何无缝地将其集成到一个Celery任务中。
用例#3: 执行长时间运行的任务
另一个用例是做一些要花费很长时间的事情。这可能是生成一个需要2分钟来生成的报告或对一个视频进行转码。
这些也是你希望看到进度条的事情。
你能想象如果你不用Celery来做这些事情它会有多疯狂吗?想象一下,加载一个页面来生成一个报告,然后必须保持浏览器选项卡打开整整两分钟,否则报告将无法生成。
那会让人疯掉,但是Celery可以让这个任务在没有上述限制的情况下非常容易地完成。你可以使用与第二个用例完全相同的策略来根据需要更新你的UI。使用websockets也可以很容易地推动进度更新。
用例#4: 按计划运行任务
最后一个用例与上面列出的其它3个用例不同,但它是非常重要的一个。
想象一下,如果你想在每天午夜执行一项任务。在过去,你可能会使用定时任务,对吗?配置一个定时任务来运行该任务并不算太糟糕。
但是使用cron命令有几个问题。请记住,使用systemd定时器也存在同样的问题。
对于初学者,你可能必须将计划好的功能分割到它自己的文件中,以便你能够独立地调用它。
实际上,这并不是太糟,但是这是你想要做的事情,如果你必须处理加载该文件的配置设置或环境变量,这可能会变得很烦人(对于每个计划的任务,这是要额外处理的1件事)。
此外,在当今世界,我们正朝着将大多数东西放入容器的方向发展,每个容器只运行一个进程被认为是一个最佳实践。
换句话说,你不希望在同一个容器中同时运行cron守护进程和你的应用程序服务器。如果你这样做,那么你将与经过社区审核的最佳实践背道而驰。
因此,你可能认为只需在Docker主机上运行cron,并将你的cron任务更改为运行一个Docker命令,而不是直接调用你的Flask文件。这是完全可行的,也会奏效,但这种方法也有问题。
如果你已经扩展到3个web应用程序服务器呢?如果每个服务器都有自己的cron任务,那么你将每天运行该任务3次,而不是一次,潜在地要做三倍的工作量。这绝对不是一个预期的结果,如果你不小心的话,可能会引入竞争条件。
现在,我知道,你可以决定在这3台服务器中的1台上配置cron任务,但这是一条非常不确定的道路,因为现在你突然有了这3台服务器,但其中1台是不同的。如果你正在进行滚动重启,而分配给cron任务的那台服务器在任务应该运行时不可用,会发生什么情况?
它将很快成为一个配置噩梦(我知道,因为我以前尝试过)!
使用Celery可以解决上述问题。它有一个“beat”服务器的概念,你可以在其中运行配置任务,这些任务可以按照你想要的时间表运行。它甚至支持cron样式语法,所以你可以在每个月的第二个星期二的凌晨1点进行各种各样的任务计划。
它还可以与你的应用程序的配置集成的非常好。因为这只是另一个任务,所以你的应用程序的所有配置和环境变量都是可用的。这是一个很大的好处,你不需要在每个文件的基础上处理这些配置和环境变量。
另一个好处是,这个任务计划的状态存储在你的Celery后端,比如Redis,它只保存在一个位置。这意味着如果你有1或100个web应用程序服务器,你的任务将只会被执行一次。
在滚动重启示例中,3个应用服务器中是否有1个不可用并不重要。只要其中至少有1个是可用的,那么你的计划任务就能够运行。
这是一笔相当不错的交易。在我看来,它甚至比一个cron任务更容易设置。
诸如此类的小事情有助于降低一个SAAS应用程序的客户流失率。有成千上万你会想要使用计划任务的例子。也许你也可以查找6个月没有活动的用户帐户,然后发送一封提醒邮件或从数据库中删除它们。你懂的!
Celery 毫无疑问是我最喜爱的Python库之一。
你正在使用Celery库干什么呢?记得告诉我哦!
英文原文:https://nickjanetakis.com/blog/4-use-cases-for-when-to-use-celery-in-a-flask-application
译者:测试