前言
celery-分布式任务框架是在rabbbitmq-分布式消息队列之上二次开发的产物。
rabbitmq侧重于处理消息的吞吐,而celery侧重于任务的处理执行。
celery可以在多个服务器上执行任务,但它不能进行消息通讯,需要借助rabbitmq(默认消息处理中间件)或redis来实现任务的分发。
同RabbitMQ一样均为生产者消费者设计模式:①生产者 = => 生成任务,消息②消息队列= =>缓存任务,消息③消费者= =>执行任务,消息
架构
celery有几个概念:
- Producer:调用了Celery提供的API、函数或者装饰器而产生任务并交给任务队列处理的都是任务生产者,如上所示即《clien+celery+异步任务》
- Celery Beat:任务调度器,Beat进程会读取配置文件的内容,周期性地将配置中到期需要执行的任务发送给任务队列,如上《client+celery+定时任务》,区别于Produce的是,Produce执行一次创建一次任务。而Celery Beat执行一次,创建了以按时规则为标准的N次任务
- Celery Worker:执行任务的消费者,通常会在多台服务器运行多个消费者来提高执行效率,如上《Celery Worker 1 2 3 . N》
- Result Backend:任务处理完后保存状态信息和结果,以供查询。Celery默认已支持Redis、RabbitMQ、MongoDB、Django ORM、SQLAlchemy等方式,如上《结果存储》
一、关于celery
1、简单:熟悉celery的工作流程之后,配置使用简单
2、高可用:当任务执行失败或执行任务的过程中发生连接中断,celery会自动尝试重新执行任务
3、快速:一个单进程的celery每分钟可处理上百万个任务
4、灵活:celery各个组件几乎都可以被扩展及自定制
5、精准:既可以查看任务的执行情况,又可以对任务编辑(添加|删除|更新)
- 场景
- 发送短信/邮件
- 音视频处理
- 定时任务
二、使用须知
1、安装celerypip install celery
(注意:我安装的是5.0以上的版本,命令跟5.0以下出入有点大)
2、安装RabbitMQ
三、代码
3.1、文件树结构
3.2、实例文件instance
from celery import Celery
# 创建celery实例
# Celery("celery_demo", broker="redis://127.0.0.1:6379", backend="redis://127.0.0.1:6379/0")
app = Celery("celery_demo") # celery_demo是Celery实例的名字
# 加载配置文件,绝对路径
app.config_from_object("config")
# 任务注册
"""绝对路径
方式一:导包
import task
方式二:调接口,切记指向包名,函数autodiscover_tasks会自动找包内的task任务文件(任务文件task在package包内,很明显这里没有包,不能使用)
app.autodiscover_tasks(["xxx.task", ])
方式三:Celery对象生成时配置,参数include
app = Celery('celery_demo', include=["task"])
方式四:配置文件config引入,本项目使用
"""
3.3、配置文件config
from datetime import timedelta
from celery.schedules import crontab
# 消息代理使用RabbitMQ。与Celery实例化时broker参数意义相同
broker_url = "amqp://guest:guest@localhost:5672/"
# 结果存储使用redis(默认数据库-零)。与Celery实例化时backend参数意义相同
result_backend = 'redis://127.0.0.1:6379/0'
# Celery打印Terl时LOG输出样式
worker_log_format = "[%(asctime)s] [%(levelname)s] %(message)s"
# Celery指定时区,默认UTC
timezone = "Asia/Shanghai"
# 任务注册到Celery,采用绝对路径。与Celery实例化时
imports = ("task", )
# 定时任务配置,开启进程程序(命令见下文),任务会被按时读取并发送到指定队列
beat_schedule = {
"add-erery-30-seconds": {
"task": "task.add", # 也可以用别名,绝对路径
"schedule": timedelta(seconds=30), # 每 30 秒执行一次
# "schedule": crontab(hour=9, minute=50), # 每天早上 9 点 50 分执行一次
"args": (3, 4)
}
}
#
3.4、任务文件task
import time
import subprocess
from instance import app
# @app.task(queue="#") # 配置queue之后调用producer可将消息发送至队列(如果RabbitMQ没有该队列名即新建并加入,同时配置的还有exchange与routing_key,值与队列名相同)
# @app.task(name="#") # 给任务起个别名(默认名为函数名)
@app.task
def add(x, y):
print("receive parameter")
print("start sleep")
time.sleep(5) # 模拟耗时行为
# subprocess.Popen("mkdir aa", shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
print("end sleep")
# 保存x+y结果到数据库redis中,因为已配置结果存储backend
return x + y
3.5、生产者文件(向队列放异步任务)
from task import add
# 异步任务,不需要等待
add.delay(1, 2) # delay封装了apply_async方法
print("已加入队列")
3.6、生产者程序(向队列放定时任务)
# 执行后会生成任务调度文件celerybeat-schedule,默认保存到当前位置(可通过"-s path/celerybeat-schedule"指明调度文件的存放位置)
celery --app instance beat
3.7、消费者Worker
celery worker的工作模式有进程process(常用)、gevent、协程evenlet
3.7.1、进程池
celery -A instance worker -l info --pool=solo -Q celery --concurrency=4
注:
- A 指向app所在,可用- -app instance代替
- l 指明celery的日志等级,可用- -loglevel代替
- Q 指明需要监听的队列名(默认celery),此项目可不填。常用于监听队列名非celery
- pool 指明并发模型,prefork (默认,multiprocessing),eventlet,gevent,threads,solo(window时使用,否则队列任务消费不了)
- concurrency 指定并发级别,prefork 模型下就是子进程数量,默认等于 CPU 核心数,根据需要决定是否需要配置
- worker 指明消费者的意思
四、图解
4.1、消费图解
4.2、生产图解
4.3、结果图解
五、路由配置,不常用(config配置文件末尾追加)
由上面代码测试验证之后,了解到,生产者发送消息,会传递到队列celery(没有即新建),于此同时它的交换机exchange名与路由routing_key名均为celery(没有即新建),具体队列信息也可以在消费者Terl运行后看到。
路由配置也就是手动配置队列名、交换机名、路由名以及它们之间的关系、任务task与它们的关系。
需要明确的是:当使用Redis作为Broker时,Exchange的名字必须和Queue的名字一样。任务队列task_queue与任务路由task_routes是一一对应关系(即当task_queue的routing_key不存在时,消息入队列失败;当队列是一个新的queue时,消息将发送到此新队列,而且其属性exchange&routing_key也是新的)
# 默认选项修改
task_default_exchange = 'tasks' # 将原默认交换机名字celery改为tasks
task_default_exchange_type = 'topic' # 将原默认的交换机类型direct改为topic
task_default_routing_key = 'task.default' # 将原默认的路由键celery改为task.default
# 定义任务队列,消费者程序启动时会监听的队列(没有即创建)
task_queues = (
Queue('default', routing_key='task.#'), # 路由键以“task.”开头的消息都进default队列
Queue('web_tasks', routing_key='web.#'), # 路由键以“web.”开头的消息都进web_tasks队列
)
# 定义任务路由配置,即任务进入哪个队列
task_routes = {
'task.add': { # task.add的消息会进入web_tasks队列,绝对路径
'queue': 'web_tasks',
'routing_key': 'web.add',
}
}
描述:
当我生产任务后,生成队列web_tasks(?因为被用到了),当然交换机、路由等属性配置肯定也有了。当我起消费者Worker监听程序后,生成default队列。
六、调试看任务状态
from celery.result import AsyncResult
from instance import app
from task import add
# 向队列添加任务,以此来实现查看任务状态
taskObj = AsyncResult(id=add.delay(2, 3).id, app=app)
# 获取状态码
print(taskObj.status)
# 获取是否消费
print('success', taskObj.successful())
# 获取返回结果,如果任务一直没有消费,一直阻塞等待。。。
print(taskObj.get())
七、django嵌入Celery
celery任务执行需要django代码支撑
7.1、引入django配置(多形式,只讲一种)
import os
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', app(项目名字).settings')
django.setup()
7.2、配置7.1(必须,否则ERROR)
配置 | 线上 |
---|---|
生产任务时 | 引入django配置 |
消费任务时 | 引入django配置 |