来个芹菜,Celery的使用
1、写在前面
最近的flask项目遇到一些需要并发的情况,开始图省事,直接搞上了多线程,但是多线程的并发量很小,而且延迟任务也不好搞,不利于扩展。
所以学习一下Celery的使用
2、基础概念
Celery是一个强大的分布式任务队列,他可以让任务的执行完全脱离主程序,甚至可以被分配到其他主机上运行。通常用来实现异步任务和定时任务。异步任务比如发送邮件,文件上传图像处理等;定时任务就是需要在特定时间执行的任务。
- 任务队列
任务队列是一种跨线程,跨机器工作的一种机制,任务队列中包含称作任务的工作单元。有专门的工作进程持续不断的监视任务队列,并从中获取新的任务并处理。 - 任务模块
包含异步任务和定时任务。异步任务通常在业务逻辑中被触发并发往任务队列;定时任务由Celery Beat进程周期性地将任务发往任务队列。 - 消息中间件Broker
Broker,就是任务调度队列,接收任务生产者发来的消息(任务),将任务存入到队列。Celery本身不提供队列服务,官方推荐使用RabbitMQ和Redis等。 - 任务执行单元Worker
Worker是执行任务的处理单元,它实时监控消息队列,获取队列中调度的任务,并执行它。 - 任务结果存储Backend
- Backend用于存储任务的执行结果,以供查询。同消息中间件一样,也可使用RabbitMQ和Redis,MySql等。
3、demo代码
3.1 异步任务
1 # -*- coding: utf-8 -*- 2 3 """ 4 Celery主类 5 启动文件名必须为celery.py!!! 6 """ 7 8 from __future__ import absolute_import # 为兼容Python版本,绝对引入 9 from celery import Celery, platforms 10 11 app = Celery( 12 main='celery_task', # celery启动包名称 13 broker='redis://127.0.0.1:6379/1', 14 backend='redis://127.0.0.1:6379/2', 15 include=['celery_task.tasks', ] # celery所有任务 16 ) 17 18 if __name__ == '__main__': 19 app.start()
1 # -*- coding: utf-8 -*- 2 3 """ 4 定义任务 5 """ 6 7 from __future__ import absolute_import 8 from celery import Celery, group 9 from .celery import app 10 from time import sleep 11 12 13 @app.task 14 def add(x, y): 15 sleep(5) 16 return x + y 17 18 19 @app.task 20 def substract(x, y): 21 sleep(5) 22 return x - y
1 # -*- coding: utf-8 -*- 2 3 from celery_task.tasks import add,substract 4 5 # 立即告知celery去执行test_celery任务,并传入一个参数 6 result1 = add.delay(5,5) 7 print(result1.id) 8 result2 = substract.delay(5,5) 9 print(result2.id)
1 # -*- coding: utf-8 -*- 2 3 from celery.result import AsyncResult 4 from celery_task.celery import app 5 6 ''' 7 获取任务结果,但要想办法获取到task_id 8 ''' 9 10 async = AsyncResult(id='8d04fc47-0f72-49a1-a58a-b74e28eb9a41', app=app) 11 12 if async.successful(): 13 result = async.get() 14 print(result) 15 # result.forget() # 将结果删除,执行完成,结果不会自动删除 16 # async.revoke(terminate=True) # 无论现在是什么时候,都要终止 17 # async.revoke(terminate=False) # 如果任务还没有开始执行呢,那么就可以终止。 18 elif async.failed(): 19 print('执行失败') 20 elif async.status == 'PENDING': 21 print('任务等待中被执行') 22 elif async.status == 'RETRY': 23 print('任务异常后正在重试') 24 elif async.status == 'STARTED': 25 print('任务已经开始被执行')
执行步骤
- 在celery_test目录下cmd执行celery worker -A celery_task -l info -P eventlet,开启worker,异步等待任务到来
- 执行send_tasks.py,将发布任务(任务写入borker)
- 等待一段时间后,可以在celery命令行看到执行结果
- 执行get_result.py获取执行结果
3.2 定时任务
1 # -*- coding: utf-8 -*- 2 3 # 拒绝隐式引入,如果celery.py和celery模块名字一样,避免冲突,需要加上这条语句 4 # 该代码中,名字是不一样的,最好也要不一样 5 from __future__ import absolute_import 6 from celery import Celery 7 8 app = Celery('celery_tasks') 9 app.config_from_object('celery_test2.config')
1 # -*- coding: utf-8 -*- 2 3 from __future__ import absolute_import 4 from celery.schedules import crontab 5 from datetime import timedelta 6 7 # 使用redis存储任务队列 8 broker_url = 'redis://127.0.0.1:6379/7' 9 # 使用redis存储结果 10 result_backend = 'redis://127.0.0.1:6379/8' 11 12 task_serializer = 'json' 13 result_serializer = 'json' 14 accept_content = ['json'] 15 # 时区设置 16 timezone = 'Asia/Shanghai' 17 # celery默认开启自己的日志 18 # False表示不关闭 19 worker_hijack_root_logger = False 20 # 存储结果过期时间,过期后自动删除 21 # 单位为秒 22 result_expires = 60 * 60 * 24 23 24 # 导入任务所在文件 25 imports = [ 26 'celery_test2.celery_task2.task1', 27 'celery_test2.celery_task2.task2' 28 ] 29 30 # 需要执行任务的配置 31 beat_schedule = { 32 'task1': { 33 # 具体需要执行的函数 34 # 该函数必须要使用@app.task装饰 35 'task': 'celery_test2.celery_task2.task1.add', 36 # 定时时间 37 # 每分钟执行一次,不能为小数 38 'schedule': crontab(minute='*/1'), 39 # 或者这么写,每小时执行一次 40 # "schedule": crontab(minute=0, hour="*/1") 41 # 执行的函数需要的参数 42 'args': (5,5) 43 }, 44 'task2': { 45 'task': 'celery_test2.celery_task2.task2.substract', 46 # 设置定时的时间,10秒一次 47 'schedule': timedelta(seconds=10), 48 'args': (5,5) 49 } 50 }
1 # -*- coding: utf-8 -*- 2 from .. import app 3 from time import sleep 4 5 @app.task 6 def add(x, y): 7 sleep(5) 8 print('add') 9 return x + y
1 # -*- coding: utf-8 -*- 2 from .. import app 3 from time import sleep 4 5 @app.task 6 def substract(x, y): 7 sleep(5) 8 print(substract) 9 return x - y
执行步骤
- 在celery_test2的同级目录下cmd执行celery -A celery_test2 worker -l info -P eventlet,开启worker,异步等待任务到来
- 在celery_test2的同级目录下cmd执行celery -A celery_test2 beat,发送定时任务,定时像borker推送任务
- 可在backend中查看执行结果,task_id
4、应用代码
4.1 Flask结合Celery,实现进度下载进度条
1 #coding=utf-8 2 from flask import Flask,url_for,jsonify,render_template 3 from celery import Celery 4 import random,time 5 6 app = Flask(__name__) 7 8 app.config['CELERY_BROKER_URL'] = 'redis://127.0.0.1:6379/1' 9 app.config['CELERY_RESULT_BACKEND'] = 'redis://127.0.0.1:6379/2' 10 11 #celery初始化 12 celery_obj = Celery(app.name, 13 broker=app.config['CELERY_BROKER_URL'], 14 backend=app.config['CELERY_RESULT_BACKEND'] ) 15 16 @app.route('/') 17 def hello_world(): 18 return 'Hello World!' 19 20 @app.route('/nanobar') 21 def nanobar(): 22 return render_template('nanobar.html') 23 24 @app.route('/longtask', methods=['POST']) 25 def longtask(): 26 #发布任务,类似于long_task.delay() 27 task1 = long_task.apply_async() 28 # 返回 202,与Location头,构建/status/task_id路由 29 return jsonify({}), 202, {'Location': url_for('taskstatus',task_id=task1.id)} 30 31 #通过task_id获取任务结果 32 @app.route('/status/<task_id>') 33 def taskstatus(task_id): 34 task = long_task.AsyncResult(task_id) 35 if task.state == 'PENDING': 36 response = { 37 'state': task.state, 38 'current': 0, 39 'total': 1, 40 'status': 'Pending...' 41 } 42 elif task.state != 'FAILURE': 43 response = { 44 'state': task.state, 45 'current': task.info.get('current', 0), 46 'total': task.info.get('total', 1), 47 'status': task.info.get('status', '') 48 } 49 if 'result' in task.info: 50 response['result'] = task.info['result'] 51 else: 52 # something went wrong in the background job 53 response = { 54 'state': task.state, 55 'current': 1, 56 'total': 1, 57 'status': str(task.info), # this is the exception raised 58 } 59 return jsonify(response) 60 61 62 # 通过 celery_obj.task 装饰器装饰耗时任务对应的函数 63 # bind为True,会传入self给被装饰的方法 64 #Celery有很多内建状态比如 STARTED , SUCCESS 等等,当然Celery也允许程序员自定义状态。 65 # 本例子中使用的是自定义状态, PROGRESS 。与 PROGRESS 一起的还有 metadata 。 66 # metadata 是一个字典,包含当前进度,任务大小,以及消息。 67 #当循环跳出时,返回字典,字典中包含任务的执行结果。 68 @celery_obj.task(bind=True) 69 def long_task(self): 70 """随机列表""" 71 list1 = ['列表1-1', '列表1-2', '列表1-3', '列表1-4', '列表1-5'] 72 list2 = ['列表2-1', '列表2-2', '列表2-3', '列表2-4', '列表2-5'] 73 list3 = ['列表3-1', '列表3-2', '列表3-3', '列表3-4', '列表3-5'] 74 message = '' 75 #模拟一个随机耗时的任务 76 total = random.randint(10, 50) 77 for i in range(total): 78 if not message or random.random() < 0.25: 79 message = '{0} {1} {2}...'.format(random.choice(list1), 80 random.choice(list2), 81 random.choice(list3)) 82 #每次将状态更新 83 self.update_state(state='PROGRESS', 84 meta={'current': i, 'total': total,'status': message}) 85 time.sleep(1) 86 #跳出循环,代表任务结束 87 return {'current': 100, 'total': 100, 'status': '任务完成!','result': 666} 88 89 90 91 if __name__ == '__main__': 92 app.run(debug=True)
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>Title</title> 6 <script src="//cdnjs.cloudflare.com/ajax/libs/nanobar/0.2.1/nanobar.min.js"></script> 7 <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script> 8 </head> 9 <body> 10 <h2>进度条测试</h2> 11 <button id="start-bg-job" onclick="start_long_task()">点击开始</button><br><br> 12 <div id="progress"></div> 13 </body> 14 15 <script> 16 //点击后添加进度条,并开始异步任务 17 function start_long_task() { 18 // 添加进度条 19 div = $('<div class="progress"><div></div><div>0%</div><div>...</div><div> </div></div><hr>'); 20 $('#progress').append(div); 21 22 // 进度条 23 var nanobar = new Nanobar({ 24 bg: '#44f', 25 target: div[0].childNodes[0] 26 }); 27 28 // 向后端/longtask发送请求,发布任务,获得task_id,开始更新进度条 29 $.ajax({ 30 type: 'POST', 31 url: '/longtask', 32 success: function(data, status, request) { 33 status_url = request.getResponseHeader('Location'); 34 update_progress(status_url, nanobar, div[0]); 35 }, 36 error: function() { 37 alert('Unexpected error'); 38 } 39 }); 40 }; 41 42 function update_progress(status_url, nanobar, status_div) { 43 // 请求后端 44 $.getJSON(status_url, function(data) { 45 // 更新进度条 46 percent = parseInt(data['current'] * 100 / data['total']); 47 nanobar.go(percent); 48 $(status_div.childNodes[1]).text(percent + '%'); 49 $(status_div.childNodes[2]).text(data['status']); 50 //任务完成,添加结果 51 if (data['state'] != 'PENDING' && data['state'] != 'PROGRESS') { 52 if ('result' in data) { 53 // show result 54 $(status_div.childNodes[3]).text('Result: ' + data['result']); 55 } 56 else { 57 // 没有result代表任务异常 58 $(status_div.childNodes[3]).text('Result: ' + data['state']); 59 } 60 } 61 else { 62 // 任务未完成,每1秒递归执行函数 63 setTimeout(function() { 64 update_progress(status_url, nanobar, status_div); 65 }, 1000); 66 } 67 }); 68 }; 69 </script> 70 </html>
执行步骤
- 在flask_tesk的同级目录下cmd执行celery -A app.celery_obj worker -l info -P eventlet,开启worker,异步等待任务到来
- python app.py run开启web应用
- 前端点击按钮,访问/longtask,发布任务,获取task_id,执行update_progress函数
- update_progress访问/status/task_id,获取任务结果,没有结束则每一秒递归运行
- 任务结束后,展示任务结果