celery详解
celery了解
celery,实现了异步的分布式任务队列。中间是两个队列,左边是执行任务,将任务提交到中间的队列中,右边是worker,能自动发现队列中的任务。左边调用任务,右边worker就会拿到这个任务运行,并且是异步运行,运行的结果会放到结果队列中,如果需要,调用任务的程序能拿到这个结果。这就是分布式任务队列,它跟我们的项目是分开的,是独立运行的。
选择一个broker,就是选择一个消息队列,可以使用rabbimq,redis或者是其它。我们这里的任务队列和任务结果队列都用redis作为broker
当客户端发起任务到服务端,服务的django接收到任务之后,比如前端登录时发送短信,服务端调用程序给短信接口发送请求,发送短信给客户并接收响应信息,这种情况就可以用celery去做成异步的任务,我们也可以用协程去做。
Celery[6] 是一个简单,灵活,可靠的分布式系统,用于处理大量消息,同时为操作提供维护此类系统所需的工具,也可用于任务调度。Celery 的配置比较麻烦,如果你只是需要一个轻量级的调度工具,Celery 不会是一个好选择。
Celery 是一个强大的分布式任务队列,它可以让任务的执行完全脱离主程序,甚至可以被分配到其他主机上运行。我们通常使用它来实现异步任务(async task)和定时任务(crontab)。异步任务比如是发送邮件、或者文件上传,图像处理等等一些比较耗时的操作 ,定时任务是需要在特定时间执行的任务。
需要注意,celery 本身并不具备任务的存储功能,在调度任务的时候肯定是要把任务存起来的,因此在使用 celery 的时候还需要搭配一些具备存储、访问功能的工具,比如:消息队列、Redis 缓存、数据库等。官方推荐的是消息队列 RabbitMQ,有些时候使用 Redis 也是不错的选择。
它的架构组成如下图:
Celery 架构,它采用典型的生产者-消费者模式,主要由以下部分组成:
- Celery Beat,任务调度器,Beat 进程会读取配置文件的内容,周期性地将配置中到期需要执行的任务发送给任务队列。
- Producer:需要在队列中进行的任务,一般由用户、触发器或其他操作将任务入队,然后交由 workers 进行处理。调用了 Celery 提供的 API、函数或者装饰器而产生任务并交给任务队列处理的都是任务生产者。
- Broker,即消息中间件,在这指任务队列本身,Celery 扮演生产者和消费者的角色,brokers 就是生产者和消费者存放/获取产品的地方(队列)。
- Celery Worker,执行任务的消费者,从队列中取出任务并执行。通常会在多台服务器运行多个消费者来提高执行效率。
- Result Backend:任务处理完后保存状态信息和结果,以供查询。Celery 默认已支持 Redis、RabbitMQ、MongoDB、Django ORM、SQLAlchemy 等方式。
实际应用中,用户从 Web 前端发起一个请求,我们只需要将请求所要处理的任务丢入任务队列 broker 中,由空闲的 worker 去处理任务即可,处理的结果会暂存在后台数据库 backend 中。我们可以在一台机器或多台机器上同时起多个 worker 进程来实现分布式地并行处理任务。
安装
pip install -U celery
Celery 官网:Celery - Distributed Task Queue — Celery 5.2.7 documentation
Celery 官方文档英文版:Celery - Distributed Task Queue — Celery 5.3.0b1 documentation
Celery 官方文档中文版:Celery - 分布式任务队列 — Celery 3.1.7 文档
Celery是一个简单、灵活且可靠的,处理大量消息的分布式系统
应用¶
首先你需要一个 Celery 实例,称为 Celery 应用或直接简称应用。既然这个实例用于你想在 Celery 中做一切事——比如创建任务、管理职程——的入口点,它必须可以被其他模块导入。
在此教程中,你的一切都容纳在单一模块里,对于更大的项目,你会想创建 独立模块 。
让我们创建 tasks.py :
from celery import Celery
app = Celery('tasks', broker='amqp://guest@localhost//')
@app.task
def add(x, y):
return x + y
Celery 的第一个参数是当前模块的名称,这个参数是必须的,这样的话名称可以自动生成。第二个参数是中间人关键字参数,指定你所使用的消息中间人的 URL,此处使用了 RabbitMQ,也是默认的选项。更多可选的中间人见上面的 选择中间人 一节。例如,对于 RabbitMQ 你可以写 amqp://localhost ,而对于 Redis 你可以写 redis://localhost .
你定义了一个单一任务,称为 add ,返回两个数字的和。
根据情况我们创建如下的目录结构
我们在主程序中创建app,需要指定broker等参数。我们可以如下方式去配置。我们也可以写一个config文件,然后导入配置
简单使用示范
创建目录和配置一些信息
我们可以先创建一个celery的目录,下面创建主程序文件和配置文件,我们先配置一下borker地址
# 任务队列的链接地址(变量名必须叫这个) broker_url = 'redis://127.0.0.1:6379/14' # 结果队列的链接地址(变量名必须叫这个) result_backend = 'redis://127.0.0.1:6379/15'
我们在主程序中创建celery对象,然后根据配置文件导入配置
接下来就是写任务了。我们创建一个包,并创建一个文件,文件名必须是tasks
看下官网是如何写的,是在任务函数上面添加celery的装饰器,跟flask视图函数装饰器的使用差不多。
我们将创建的celery对象app导入,然后在tasks文件中写任务函数,并用app.task作为装饰器给任务函数使用。task后面不用加括号
我们如何去发现我们写的任务呢,我们在主程序中调用自动发现任务的方法,指定任务的路径。任务的路径就写到tasks文件的目录就可以,它就会自动找到这个目录下对应的tasks文件中app.task装饰的函数。
运行celery
运行celery,到celery的外层目录执行命令
celery -A mycelery.main worker --loglevel=info (或者直接写info也行) #-A是指定celery启动入口
-A 指定是mycelery下的main文件文件启动,使用worker来启动,
需要修改一下
任务要放到列表里面
我们可以看到。任务成功运行,看到配置信息,8个并发。上面仍然报错,是因为celery -A 的文件名字写错了,写的是mycelery ,而实际上是mcwcelery。
还可以看到任务函数名称,上面可以看到,celery对象名称,传输的任务队列,结果队列,并发熟练,任务事件等信息。
这个xiaomaguohe123不知道是哪里来的
我们再添加一个任务2,重新运行,可以看到任务2出来了,我们给任务装饰器函数里面传name参数。运行后发现的这个任务函数就不是点分路径的显示了,而是定义的这个name值显示的,也就是指定名称,它是默认以路径显示任务的。
我们可以在多个目录中创建tasks.py文件创建任务函数,来规划我们的不同类型的任务函数。
我们在celery目录下创建了新的任务目录和tasks文件,里面定义了任务函数。然后将新的任务函数的路径添加到自动发现任务方法的参数列表中,可以看到新建的任务目录下的任务。
调用celery任务
上面主程序里面能自动发现任务,那么如何调用这些任务呢。
因为我这个目录下的目录名字和某个模块可能是名字冲突了,结果导致我导入模块运行的时候总是出错
把名字改了之后就能正常导入执行了
我们要调用任务,创建一个mcw文件,导入任务函数,任务函数点delay方法,右击运行这个程序,成功运行
这里将celery启动起来
celery -A mcwcelery.main worker --loglevel=info
启动起来后,我们可以看到有celery ready好的日志。后面还有上面刚刚运行mcw.py时调用任务函数的情况,显示了是那个worker处理的,任务函数路径,任务id,结果是成功,运行结束时间是多少,返回的内容是什么。
查看celery任务情况
我们进入redis,查看一下任务队列和结果队列。我们查看任务的运行结果,可以通过任务id取,也可以查看任务的状态。
任务队列是编码之后的,我们能看懂的是结果队列。
执行任务时,导入任务函数,然后调用delay方法,运行任务后返回的显示的是任务id(不是字符串id),我们可以用变量接收获取到任务id,从而根据任务id来获取结果队列中的任务执行结果信息。
再来看下图片
取任务执行结果方法1
import time from mycelery.sms.tasks import send_sms from mycelery.mail.tasks import send_email ret = send_sms.delay() print(ret,type(ret)) print(ret.ready()) print(ret.id) # time.sleep(3) print(ret.ready()) print(ret.get(timeout=1),)
我们接收delay结果,然后可以id,get 取id和执行结果。
ready应该是判断任务是否执行结束
取任务执行结果方法2
import time
from mcwcelery.sms.tasks import send_sms
from mcwcelery.emailxx.tasks import send_email
from celery.result import AsyncResult
ret = send_sms.delay() #执行的任务如果需要参数,那么就直接在delay方法里面写:send_sms(mobile,sms_code),执行时:delay(mobile,sms_code)
async_task = AsyncResult(id=ret.id,app=send_sms)
print(async_task.successful())
result = async_task.get()
print(result)
print(async_task.status)
from celery import Celery app=Celery('mcw01') app.config_from_object('mcwcelery.config') app.autodiscover_tasks(['mcwcelery.sms','mcwcelery.emailxx'])
# 任务队列的链接地址(变量名必须叫这个) broker_url = 'redis://10.0.0.12:6379/14' # 结果队列的链接地址(变量名必须叫这个) result_backend = 'redis://10.0.0.12:6379/15'
from mcwcelery.main import app @app.task(name='mcw1send_duanxin') def send_sms(): return '发送短信' @app.task def send_sms2(): return '发送短信2'
从celery导入AsyncResult,通过调用任务函数返回,然后传参任务id和任务函数,创建一个AsyncResult对象,通过这个对象再获取任务的结果,比如是否运行成功,任务函数返回值信息,任务状态是什么等等。
定时任务和周期任务
一个任务的运行案例
Celery其实是一个专注于实时处理和调度任务的分布式任务队列,同时提供操作和维护分布式系统所需要的全部数据, 因此可以用它提供的接口快速实现并管理一个分布式的任务队列,它本身不是任务队列,它是封装了操作常见任务队列的各种操作, 可以使用它快速进行任务队列的使用与管理.在Python中的组成部分是 1.用户任务 app 2.管道 broker 用于存储任务 官方推荐的是 redis rabbitMQ / backend 用于存储任务执行结果的 3, 员工 worker 大致流程入下:
最左边的是用户, 用户发起1个请求给服务器, 要服务器执行10个任务,将这10个任务分给10个调度器,即开启10个线程进行任务处理,worker会一直监听调度器是否有任务, 一旦发现有新的任务, 就会立即执行新任务,一旦执行完就会返回给调度器, 即backend, backend会将请求发送给服务器, 服务器将结果返回给用户, 表现的结果就是,这10个任务同时完成,同时返回,,这就是Celery的整个工作流程, 其中的角色分别为,任务(app_work), 调度器(broker + backend), 将任务缓存的部分, 即将所有任务暂时存在的地方,相当于生产者, 消费者(worker 可以指定数量, 即在创建worker命令的时候可以指定数量), 在worker拿到任务后,人就控制不了了, 除非把worker杀死, 不然肯定会执行完.
也即 任务来了以后, 调度器(broker)去缓存任务, worker去执行任务, 完成后返回backend,接着返回,
还有就是关于定时任务和周期任务在linux上为什么不用自身所带着的去做,是因为linux周期定时任务是不可控的, 不好管理, 返回值保存也是个麻烦事, 而celery只要开启着调度器, 就可以随时把人物结果获取到,即使用celery控制起来是非常方便的.
接下来就是实例代码:
worker.py
from celery import Celery import time # 创建一个Celery实例, 就是用户的应用app 第一个参数是任务名称, 可以随意起 后面的就是配置的broker和backend diaoduqi= Celery("mytask", broker="redis://10.0.0.12:6379/", backend="redis://10.0.0.12:6379/") # 接下来是为应用创建任务 ab @diaoduqi.task def ab(a,b): time.sleep(15) return a+b
brokers.py
from xiaoma.worker import ab # 将任务交给Celery的Worker执行 res = ab.delay(2,4) #返回任务ID print(res.id)
backends.py
from celery.result import AsyncResult from xiaoma.worker import diaoduqi # 异步获取任务返回值 async_task = AsyncResult(id="617cabe9-2f42-47c7-9b02-d68ff76fe5fc",app=diaoduqi) # 判断异步任务是否执行成功 if async_task.successful(): #获取异步任务的返回值 result = async_task.get() print(result) else: print("任务还未执行完成")
首先是启动worker.py,创建celery对象和任务函数并执行命令运行,终端窗口命令运行
执行该命令后 处于就绪状态, 需要发布任务, 即brokers.py进行任务发布, 方法是使用delay的方式执行异步任务, 返回了一个任务id。
调用任务,将任务交给worker执行,获取任务id,右击运行程序
backends.py中取这个任务id, 去查询任务是否完成,判定条件即任务.successful 判断是否执行完, 上面就是celery异步执行任务的用法与解释
将之前运行brokers获取都的任务id,在backends文件中使用,然后获取任务的执行返回结果。右击运行backends.py
在命令执行情况中查看任务的运行情况
上面可以看出 ,5.0.5版本的celery,默认是启动8个worker, -c可以指定worker数量
定时任务的案例
接下来就是celery在项目中的应用
在实际项目中应用celery是有一定规则的, 即目录结构应该如下.
结构说明 首先是创建一个CeleryTask的包,接着是在里面创建一个celery.py,必须是这个文件 关于重名的问题, 找寻模块的顺序是先从当前目录中去寻找, 根本找不到,接着是从内置模块中去找, 根本就找不到写的这个celery这个文件,
celery.py
from celery import Celery DDQ = Celery("DDQ",broker="redis://10.0.0.12:6379/", backend="redis://10.0.0.12:6379/", include=["CeleryTask.TaskOne","CeleryTask.TaskTwo"])
TaskOne.py
import time from CeleryTask.celery import DDQ @DDQ.task def one1(a,b): # time.sleep(3) return a+b @DDQ.task def one2(): time.sleep(2) return "one2"
taskTwo.py
import time from CeleryTask.celery import DDQ @DDQ.task def two1(): time.sleep(2) return "two1" @DDQ.task def two2(): time.sleep(3) return "two2"
getR.py
from mcwcelery2.CeleryTask.TaskOne import one1 as one # one.delay(10,10) # two.delay(20,20) # 定时任务我们不在使用delay这个方法了,delay是立即交给task 去执行 # 现在我们使用apply_async定时执行 # 首先我们要先给task一个执行任务的时间 import datetime, time # 获取当前时间 此时间为东八区时间 ctime = time.time() # 将当前的东八区时间改为 UTC时间 注意这里一定是UTC时间,没有其他说法 utc_time = datetime.datetime.utcfromtimestamp(ctime) # 为当前时间增加 10 秒 add_time = datetime.timedelta(seconds=10) action_time = utc_time + add_time print(ctime,utc_time) print(add_time,action_time) # action_time 就是当前时间未来10秒之后的时间 # 现在我们使用apply_async定时执行 res = one.apply_async(args=(6, 9), eta=action_time) # res = one.apply_async(args=(6, 9), eta=action_time) print(res.id,res.status,time.time()) time.sleep(2) print(res.id,res.status,time.time()) time.sleep(9) print(res.id,res.status,time.time()) # 这样原本延迟5秒执行的One函数现在就要在10秒钟以后执行了
接着是在命令行cd到与CeleryTask同级目录下, 使用命令 celery worker -A CeleryTask -l INFO -P eventlet -c 50 这样 就开启了worker 接着去 发布任务, 在定时任务中不再使用delay这个方法了,
接着是在命令行cd到与CeleryTask同级目录下, 使用命令 celery worker -A CeleryTask -l INFO -P eventlet -c 50 这样 就开启了worker 接着去 发布任务, 在定时任务中不再使用delay这个方法了,delay是立即交给ttask去执行, 在这里使用 apply_async定时执行 指的是调度的时候去定时执行
需要设置的是UTC时间, 以及定时的时间(多长时间以后执行) 之后使用 celery worker -A CeleryTask -l INFO -P eventlet -c 50 命令开启worker, 之后运行 getR.py文件发布任务, 可以看到在定义的时间以后执行该任务
接着是在命令行cd到与CeleryTask同级目录下, 使用命令 celery worker -A CeleryTask -l INFO -P eventlet -c 50 这样 就开启了worker 接着去 发布任务, 在定时任务中不再使用delay这个方法了,delay是立即交给ttask去执行, 在这里使用 apply_async定时执行 指的是调度的时候去定时执行
执行没有的话,安装一下这个模块
这次执行命令,只需要指定celery对象所在的目录就可以了,不需要指定celery对象所在的模块名,因为celery对象的模块名就叫celery,因此命令运行时指定这个目录入口,自动就从celery模块下读取到celery对象。而任务函数是放到celery对象的include参数里面了。任务队列和结果队列都是作为celery对象的参数传进去的。在任务模块中,需要导入celery对象,然后用任务装饰器将函数转换为celery的任务函数。这样当执行终端命令运行celery,实例化celery对象之后,就能通过include找的任务函数,也能在命令执行输出中看到已发现的任务函数
因为获取结果程序放到CeleryTask下,出现导入错误,就把它放到同级目录下了。右击运行程序,可以看到调用了6次,就在redis中生成6个任务,任务结果的建命名由celery-任务-元数据-任务id组成。我们是测试,所以先将redis db清空,然后执行的任务,可以看到任务是失败的
delay是立即交给ttask去执行, 在这里使用 apply_async定时执行 指的是调度的时候去定时执行
需要设置的是UTC时间, 以及定时的时间(多长时间以后执行) 之后使用 celery worker -A CeleryTask -l INFO -P eventlet -c 50 命令开启worker, 之后运行 getR.py文件发布任务, 可以看到在定义的时间以后执行该任务
查看运行状态,任务运行失败
报错信息是键获取错误了。因为我们注册是CeleryTask.TaskOne.one1。我们getR.py导入使用时是 from mcwcelery2.CeleryTask.TaskOne import one1 as one
因此它找的键的名称是 mcwcelery2.CeleryTask.TaskOne.one1,而不是我们注册时写的CeleryTask.TaskOne.one1,报了找不到键的错误,导致任务运行失败
上面的解决方法如下:给任务函数添加name,那么就不会存在键错误的问题了,如下,任务成功执行。
定时任务使用方法如下:
给任务函数添加name,调用函数的程序中导入任务函数 ,
给当前时间添加指定的时间段大小,生成执行任务的时间,这是未来某个时刻
任务函数调用apply_async方法,发布执行任务,返回结果中可以取结果队列中的数据。
给任务函数传参,是在apply_async方法的args中传参;通过设置eta参数,设置执行任务的时间
任务显示在pending,大概是我们设置的10秒之后执行,也就是10秒后执行任务函数,成功后返回任务情况,这里是成功执行。这就是定时10秒后的时间运行任务。
celery -A CeleryTask worker -l INFO -P eventlet -c 50
周期任务
周期任务 指的是在指定时间去执行任务 需要导入的一个模块有 crontab
文件结构如下:
# 设置定时启动的频率,没分钟执行一次任务函数 'schedule': crontab(minute='*/1'), # 每周一至周五早上8点执行任务函数 #好像不生效下面这个.每天几点几分的一直没有成功 'schedule': crontab(minute=0, hour=8,day_of_week=[1, 2, 3, 4, 5]),
"schedule": 3, # 每3秒钟执行一次
#每个小时的13分钟执行一次
"schedule": crontab(minute='13', hour='*/1'),
结构同定时任务差不多,只不过需要变动一下文件内容 GetR文件已经不需要了,可以删除.
crontab配置参考如下
celery.py
from celery import Celery from celery.schedules import crontab DDQ = Celery("DDQ", broker="redis://10.0.0.12:6379", backend="redis://10.0.0.12:6379", include=["CeleryTask.TaskOne", "CeleryTask.TaskTwo"]) # 我要要对beat任务生产做一个配置,这个配置的意思就是每10秒执行一次Celery_task.task_one任务参数是(10,10) DDQ.conf.beat_schedule = { "each10s_task": { "task": "CeleryTask.TaskOne.one1", "schedule": 3, # 每3秒钟执行一次 "args": (10, 10) }, "each1m_task": { "task": "CeleryTask.TaskOne.one1", "schedule": crontab(minute='*/1') , # 每1分钟执行一次 也可以替换成 60 即 "schedule": 60。如果只是minute=1,这样配置是不生效的,'*/1'是生效的
"args": (10, 10) } }
TaskOne.py
import time from CeleryTask.celery import DDQ @DDQ.task def one1(a,b): # time.sleep(3) return a+b @DDQ.task def one2(): time.sleep(2) return "one2"
taskTwo.py
import time from CeleryTask.celery import DDQ @DDQ.task def two1(): time.sleep(2) return "two1" @DDQ.task def two2(): time.sleep(3) return "two2"
以上配置完成以后,这时候就不能直接创建worker了,因为要执行周期任务,需要首先有一个任务的生产方, 即 celery -A CeleryTask beat , 用来产生创建者, 接着是创建worker worker的创建命令还是原来的命令, 即 celery -A CeleryTask worker -l INFO -P eventlet -c 50 , 创建完worker之后, 每10秒就会由beat创建一个任务给 worker去执行.至此, celery创建异步任务, 周期任务,定时任务完毕, 伙伴们自己拿去测试吧.
一个窗口执行celery -A CeleryTask beat ,这里不需要gerR.py文件了。对实例化的celery对象进行配置了,调度任务起个名字,然后指定任务函数位置,指定调度执行周期,要给任务函数传参,就放到args里面, 启动定时任务调度器[celery beat] 。还产生了三个文件跟调度器相关的
然后我们再开一个窗口,执行命令celery -A CeleryTask worker -l INFO -P eventlet -c 50
我们这里设置每三秒执行一次,我们可以看到这个任务函数的确是每三秒执行一个任务,不过这里的第二个任务没有执行不知道怎么回事。
原来是我配置错了,每分钟执行一次得是下面那样配置
每个小时的13分钟
外部脚本调用django的时候需要用到下面配置
在flask中使用celery
Flask 与 Celery 整合是十分简单,不需要任何插件。一个 Flask 应用需要使用 Celery 的话只需要初始化 Celery 客户端像这样:
from flask import Flask
from celery import Celery
app = Flask(__name__)
app.config['CELERY_BROKER_URL'] = 'redis://localhost:6379/0'
app.config['CELERY_RESULT_BACKEND'] = 'redis://localhost:6379/0'
celery = Celery(app.name, broker=app.config['CELERY_BROKER_URL'])
celery.conf.update(app.config)
正如你所见,Celery 通过创建一个 Celery 类对象来初始化,传入应用名称以及消息代理的连接 URL,这个 URL 我把它放在 app.config 中的 CELERY_BROKER_URL 的键值。URL 告诉 Celery 代理服务在哪里运行。如果你运行的不是 Redis,或者代理服务运行在一个不同的机器上,相应地你需要改变 URL。
Celery 其它任何配置可以直接用 celery.conf.update() 通过 Flask 的配置直接传递。CELERY_RESULT_BACKEND 选项只有在你必须要 Celery 任务的存储状态和运行结果的时候才是必须的。展示的第一个示例是不需要这个功能的,但是第二个示例是需要的,因此最好从一开始就配置好。
任何你需要作为后台任务的函数需要用 celery.task 装饰器装饰。例如:
@celery.task
def my_background_task(arg1, arg2):
# some long running task here
return result
接着 Flask 应用能够请求这个后台任务的执行,像这样:
task = my_background_task.delay(10, 20)
delay() 方法是强大的 apply_async() 调用的快捷方式。这样相当于使用 apply_async():
task = my_background_task.apply_async(args=[10, 20])
当使用 apply_async(),你可以给 Celery 后台任务如何执行的更详细的说明。一个有用的选项就是要求任务在未来的某一时刻执行。例如,这个调用将安排任务运行在大约一分钟后:
task = my_background_task.apply_async(args=[10, 20], countdown=60)
delay() 和 apply_async() 的返回值是一个表示任务的对象,这个对象可以用于获取任务状态。我将会在本文的后面展示如何获取任务状态等信息,但现在让我们保持简单些,不用担心任务的执行结果。
更多可用的选项请参阅 Celery 文档 。
celery定时任务
学习链接:
在flask中使用celery http://www.pythondoc.com/flask-celery/first.html
flask中使用celery的四种场景
Celery可以帮助你异步或者定期运行你想在大多数web项目中执行的常见事情的代码。
快速跳转:用例#1:发送电子邮件|用例#2:连接到第三方API |用例#3:执行长时间运行的任务|用例#4:按计划运行任务
上周,我是可盈利的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库之一。