问题
开发中涉及到一些执行时间长的任务,造成用户等待时间过长影响体验,并发量大的话任务执行也不稳定。
方案
使用celery分布式任务调度框架,生产者将任务先缓存至消息中间件,消费者从中间件获取任务执行;
优势:
1. 异步提升性能,任务生成和任务执行逻辑分离,降低耦合性,增强用户体验
2. 数据缓冲,任务上报的速率由用户决定,服务端不可控,此模式可以缓存任务执行速率,达到流量削峰目的,避免引发系统崩溃
3. 易于扩展,在任务量大时,通过增加任务处理Worker来扩展,提高处理速率
劣势:系统复杂度增加(可以接受)
实施
工具
broker:rabbitmq
backend:redis
操作流程
生产者配置:
django web站点执行任务发布,settings.py配置如下:
from celery import Celery
from kombu import Exchange
# celery配置
APP = Celery()
REDIS_URI = "redis://:%s@%s:%s/%s" % (
CONFIG_INFO.get("REDIS_PASSWORD"),
CONFIG_INFO.get("REDIS_HOST"),
CONFIG_INFO.get("REDIS_PORT"),
CONFIG_INFO.get("REDIS_DB")
)
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': REDIS_URI,
'OPTIONS': {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
'MAX_ENTRIES': 1000,
'CULL_FREQUENCY': 3,
}
}
}
APP.conf.update(
task_serializer='msgpack', # 任务序列化和反序列化使用msgpack方案
# result_serializer='json', # 读取任务结果, 一般性能要求不高,所以使用可读性更好的JSON
timezone='Asia/Shanghai', # 指定时区,不指定默认为 'UTC'
broker_url='amqp://{}:{}@{}:{}'.format(CONFIG_INFO.get('MQ_NAME'), CONFIG_INFO.get('MQ_PASSWORD'),
CONFIG_INFO.get('MQ_URL'), CONFIG_INFO.get('MQ_PORT')), # broker地址
result_backend=REDIS_URI # 存储任务结果
)
# 定义交换机,和消费者保持一致
Exchange('classify', type='topic')
生产者发布任务:
def task_classify(args=None):
"""题目归类任务"""
task_id = '' # 任务ID
args = args or {}
result = settings.APP.send_task('tasks.classify.classify', (args,), exchange='classify',
routing_key="tasks.classify.classify", eta=None)
task_id = result.id
log.info(result)
return task_id
任务发布后返回任务id,供客户端获取任务执行结果时用
生产者获取任务结果:
def get_task(request):
"""获取指定ID的task"""
task_id = request.POST.get('task_id', '')
result = AsyncResult(task_id)
data = {
'task_id': result.id,
'status': result.status,
'result': result.result
}
return ajax_ok(data)
消费者配置:
消费者使用python脚本,通过supervisord部署管理,使用flower查看执行结果
任务执行模块:
关键配置:
# broker地址
broker_url = 'amqp://{}:{}@{}:{}'.format(CONFIG_INFO.get('MQ_NAME'),
CONFIG_INFO.get('MQ_PASSWORD'),
CONFIG_INFO.get('MQ_URL'),
CONFIG_INFO.get('MQ_PORT'))
# 存储任务结果地址
result_backend = f"redis://:{CONFIG_INFO.get('REDIS_PASSWORD')}@{CONFIG_INFO.get('REDIS_HOST')}:" \
f"{CONFIG_INFO.get('REDIS_PORT')}/{CONFIG_INFO.get('REDIS_DB')}"
# 创建交换机
default_exchange = Exchange('default', type='topic')
classify_exchange = Exchange('classify', type='topic') # 归类
# 指定队列
task_queues = (
Queue('default', exchange=default_exchange, routing_key='*.default.*', delivery_mode=1),
# 路由键包含“defaul”的消息都进default队列
Queue('classify', exchange=classify_exchange, routing_key='*.classify.*', delivery_mode=1),
# 路由键包含“classify”的消息都进classify队列
)
使用supervisord启用消费者和flower服务
[program:celery.worker.zykxcopy]
;celery directory
directory=/code
;运行目录下执行命令,启动worker
command=celery worker -A tasks.classify -Q classify -P gevent -c 8 -l info -n classify_worker
[program:flower]
directory=/code
command=celery flower -A tasks.flower --conf=/code/configs/flowerconfig.py --basic_auth=****:****