Celery 是一个异步任务队列。你可以使用它在你的应用上下文之外执行任务。总的想法就是你的应用程序可能需要执行任何消耗资源的任务都可以交给任务队列,让你的应用程序自由和快速地响应客户端请求。使用 Celery 运行后台任务并不像在线程中这样做那么简单。但是好处多多,Celery 具有分布式架构,使你的应用易于扩展。一个 Celery 安装有三个核心组件:
Celery 客户端: 用于发布后台作业。当与 Flask 一起工作的时候,客户端与 Flask 应用一起运行。
Celery workers: 这些是运行后台作业的进程。Celery 支持本地和远程的 workers,因此你就可以在 Flask 服务器上启动一个单独的 worker,随后随着你的应用需求的增加而新增更多的 workers。
Broker消息代理: 客户端通过消息队列和 workers 进行通信,Celery 支持多种方式来实现这些队列。最常用的代理就是 RabbitMQ
和 Redis
。
二、简单应用
通过一个简单例子,了解Flask的定时任务,项目名称:celery_demo,目录结构如下:
|---app
| |---__init__.py
| |---celery_manage
| | | ---- __init__.py
| | | ---- views.py
|---config.py
|---celery_worker.py
pip install flask-celery-helper
另:Flask官网上的flask-celery包可能因为长期未维护的原因,在应用中与python的celery包存在兼容问题,所以选择了这个依赖包。
#!/usr/bin/python
# -*- coding: utf-8 -*-
import os
from datetime import timedelta
basedir = os.path.abspath(os.path.dirname(__file__))
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard to guess string'
# 定时任务配置
CELERY_BROKER_URL = 'redis://localhost:6379',
CELERY_RESULT_BACKEND = 'redis://localhost:6379'
# CELERY_TIMEZONE = 'Asia/Shanghai'
CELERYBEAT_SCHEDULE = {
# 定义任务名称:import_data
# 执行规则:每10秒运行一次
'import_data': {
'task': 'import_data',
'schedule': timedelta(seconds=10)
},
}
@staticmethod
def init_app(app):
pass
class DevelopmentConfig(Config):
DEBUG = True
SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or \
'sqlite:///' + os.path.join(basedir, 'data-dev.sqlite')
class TestingConfig(Config):
TESTING = True
SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or \
'sqlite:///' + os.path.join(basedir, 'data-test.sqlite')
class ProductionConfig(Config):
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
'sqlite:///' + os.path.join(basedir, 'data.sqlite')
config = {
'development': DevelopmentConfig,
'testing': TestingConfig,
'production': ProductionConfig,
'default': DevelopmentConfig
}
from flask import Blueprint
celery_manage = Blueprint('celery_manage', __name__)
from . import views
#!/usr/bin/python
# -*- coding: utf-8 -*-
from flask import Flask
from config import config
from flask_celery import Celery
# 创建Celery实例
celery = Celery()
def create_app(config_name):
app = Flask(__name__)
app.config.from_object(config[config_name])
config[config_name].init_app(app)
celery.init_app(app)
#注册celery管理蓝本
from .celery_manage import celery_manage as celery_manage_blueprint
app.register_blueprint(celery_manage_blueprint)
return app
#!/usr/bin/python
# -*- coding: utf-8 -*-
from app import celery
from celery.utils.log import get_task_logger
logger = get_task_logger(__name__)
# 定时导入
@celery.task(name="import_data")
def import_data():
print "定时任务:每10秒执行一次"
# 记录日志
logger.info(u"导入成功")
注:任务名称为配置文件config.py中定义的名称,即“import_data”, 是task所定义的名字
#!/usr/bin/python
# -*- coding: utf-8 -*-
from app import create_app, celery
import os
application = create_app(os.getenv('FLASK_CONFIG') or 'default')
application.app_context().push()
if __name__ == '__main__':
application.run()
celery worker -l INFO -c 100 -A celery_worker.celery --beat
说明:
参数-l INFO可以详细输出任务信息。
celery默认会开启4个线程来处理任务,参数-c可以开启更多任务线程。
参数–beat:执行定时任务时, Celery会通过celerybeat进程来完成。Celerybeat会保持运行, 一旦到了某一定时任务需要执行时, Celerybeat便将其加入到queue中,适用于周期性任务。
Celery除了可以异步执行任务之外,还可以定时执行任务。在实例代码的基础上写个测试方法:
#coding:utf-8
from celery.task.schedules import crontab
from celery.decorators import periodic_task
@periodic_task(run_every=crontab())
def some_task():
print('periodic task test!!!!!')
time.sleep(5)
print('success')
return True
该代码是每分钟执行一次some_task
方法。
启动celery是使用worker。但worker不能启动定时任务。启动方式如下:
1)先确保中间人broker是否打开,我使用redis
作为中间人。
2)进入django项目的根目录执行如下代码启动celery的worker:
celery -A myproject worker -l info
其中,myproject是我的django项目名称。
当出现celery@xxx ready
说明redis启动成功。
有两个任务,其中一个myapp.tasks.sendmail
添加的异步处理任务。myapp.tasks.some_task
是本次的定时任务。
3)同样在django项目的根目录下再打开一个命令行界面,执行如下代码:
celery -A myproject beat -l info
这个beat是检查定时任务,并启动定时任务丢给worker执行。
可以稍微等待几分钟,多执行几次任务。如下图:
上面的窗口是beat,下面的窗口是worker。可看到print的信息在worker中输出。
我们甚至不用打开Django项目的服务器,就可以执行定时任务。
若你觉得1分钟等待时间太长。可以设置为每10秒执行一次定时任务。将上面的periodic_task
修改如下:
@periodic_task(run_every=10)
修改代码,需要重启Celery的worker和beat。
这个run_every
参数是设置定时任务的时间间隔或执行的时间。该参数设置有3种方式。
1)直接设置秒数
例如刚刚所说的10秒间隔,run_every=10,每10秒执行一次任务。1分钟即是60秒;1小时即是3600秒。
2)通过datetime
设置时间间隔
有时直接设置秒数不方便,需要通过计算得到具体秒数。
例如,1小时15分钟40秒 = 16060 + 15*60 + 40。这种情况可读性也不高。
可以采用datetime设置,代码如下:
1 from celery.decorators import periodic_task
2 import datetime
3
4 @periodic_task(run_every=datetime.timedelta(hours=1, minutes=15, seconds=40))
5 def some_task():
6 print('periodic task test!!!!!')
代码可读性明显提升,而且设置方便。
但这种不能满足定时定点的时间设置。假如我想固定每天12点15分的时候,执行一次任务。datetime和直接设置秒数的方式都无法实现。这时得使用第3种方式。
3)celery的crontab表达式
crontab
是比较完善,且稍微有点复杂(相对前面两种方式而言)的方式。可以实现我们各种设置时间的需求。
例如,前面给出的代码:
#coding:utf-8
from celery.task.schedules import crontab
from celery.decorators import periodic_task
@periodic_task(run_every=crontab())
def some_task():
print('periodic task test!!!!!')
表示每分钟0秒时刻执行一次(后面不提这个0秒,大家都知道就行了,省点口水)。
其中,crontab
()实例化的时候没设置任何参数,都是使用默认值。crontab一共有7个参数,常用有5个参数分别为:
minute:分钟,范围0-59;
hour:小时,范围0-23;
day_of_week:星期几,范围0-6。以星期天为开始,即0为星期天。这个星期几还可以使用英文缩写表示,例如“sun”表示星期天;
day_of_month:每月第几号,范围1-31;
month_of_year:月份,范围1-12。
a、默认参数
这些参数可以设置表达式,表达稍微复杂的设置。默认值都是"*"星号,代表任意时刻。即crontab()相当与:
crontab(minute='*', hour='*', day_of_week='*', day_of_month='*', month_of_year='*')
含义是每天、每小时、每分钟执行一次任务。这说法太反人类语言习惯,简单说就是每1分钟执行一次任务。
b、具体某个值
上面提到这些参数的取值范围。我们可以直接设置某个值。例如:
crontab(minute=15)
即每小时的15分时刻执行一次任务。直接指定某个时刻。以此类推可以设置每天0点0分时刻执行任务的设置如下:
crontab(minute=0, hour=0)
当然,也可以设置多个值。例如0分和30分执行一次任务:
crontab(minute='0,30')
这里使用字符串,用逗号隔开数值。这里的逗号是表示多个表达式or逻辑关系。
c、设置范围
设置范围也是设置多个值,例如指定9点到12点每个小时的每分钟执行任务。
crontab(minute='*', hour='9-12')
这里*号是默认值,可以省略如下:
crontab(hour='9-12')
上面提到逗号是or逻辑关系。拓展一下,指定9点到12点和20点中每分钟执行任务:
crontab(hour='9-12,20')
crontab的表达式越来越复杂了。celery还提供了一个类得到表达式解析结果,代码如下:
1 from celery.task.schedules import crontab_parser
2 r = crontab_parser(23, 0).parse('9-12,20')
3 print(r)
其中,crontab_parse
是一个解析类。第1个参数是范围的最大值;第2个参数是范围的最小值。通过parse输入表达式,可得到表达式的解析结果:
set([9, 10, 11, 12, 20])
下面很多地方我们都可以通过该方法验证解析结果。
d、设置间隔步长
假如我要设置1、3、5、7、9、11月份每天每分钟执行任务,按照上面的做法可以设置如下:
crontab(day_of_month='1,3,5,7,9,11')
观察数据可以发现,都是间隔2的步长。需要设置的数字比较少,若数字比较多显得很麻烦。例如我想每间隔2分钟就执行一次任务,要写30个数字想想就觉得很麻烦。crontab表达式还提供了间隔的处理,例如:
crontab(minute='*/2')
crontab(minute='0-59/2') #效果等同上面
这个/号不是除以的意思。相当与range的第3个参数,例如:
range(0, 59+1, 2)
差不多crontab表达式就这些,多举几个例子:
#每2个小时中每分钟执行1次任务
crontab(hour='*/2')
#每3个小时的0分时刻执行1次任务
#即[0,3,6,9,12,15,18,21]点0分
crontab(minute=0, hour='*/3')
#每3个小时或8点到12点的0分时刻执行1次任务
#即[0,3,6,9,12,15,18,21]+[8,9,10,11,12]点0分
crontab(minute=0, hour='*/3,8-12')
#每个季度的第1个月中,每天每分钟执行1次任务
#月份范围是1-12,每3个月为[1,4,7,10]
crontab(month_of_year='*/3')
#每月偶数天数的0点0分时刻执行1次任务
crontab(minute=0, hour=0, day_of_month='2-31/2')
#每年5月11号的0点0分时刻执行1次任务
crontab(0, 0, day_of_month='11', month_of_year='5')