APScheduler与celery定时任务方法介绍

引言

APScheduler是一个 Python 定时任务框架,它提供了基于日期、固定时间间隔以及 crontab 类型的任务,实现持久化任务、并以 daemon 方式运行应用。

Celery(芹菜)是一个异步任务队列/基于分布式消息传递的作业队列。它侧重于实时操作,但对调度支持也很好。Celery用于生产系统每天处理数以百万计的任务。Celery是用Python编写的,但该协议可以在任何语言实现。它也可以与其他语言通过webhooks实现。

APScheduler使用

APScheduler的全称是Advanced Python Scheduler。APScheduler 支持三种调度任务:固定时间间隔,固定时间点(日期),Linux 下的 Crontab 命令。同时,它还支持异步执行、后台执行调度任务。安装方式为:

pip install APScheduler

APScheduler 使用起来还算是比较简单。运行一个调度任务只需要以下三部曲。

  1. 新建一个 schedulers (调度器) 。
  2. 添加一个调度任务(job stores)。
  3. 运行调度任务。

那么第一步,我们可以根据它提供的,选取合适的调度器:

  • BlockingScheduler : 调度器在当前进程的主线程中运行,也就是会阻塞当前线程。
  • BackgroundScheduler : 调度器在后台线程中运行,不会阻塞当前线程。
  • AsyncIOScheduler : 结合 asyncio 模块(一个异步框架)一起使用。
  • GeventScheduler : 程序中使用 gevent(高性能的Python并发框架)作为IO模型,和 GeventExecutor 配合使用。
  • TornadoScheduler : 程序中使用 Tornado(一个web框架)的IO模型,用 ioloop.add_timeout 完成定时唤醒。
  • TwistedScheduler : 配合 TwistedExecutor,用 reactor.callLater 完成定时唤醒。
  • QtScheduler : 你的应用是一个 Qt 应用,需使用QTimer完成定时唤醒。

网上的案例大同小异,应该都是根据官网翻译而来,其中有几个定时任务还是能跑跑:


import datetime
import time
from apscheduler.schedulers.background import BackgroundScheduler
"""
每隔2s运行一遍timedTask,每隔5s,打印一次当前时间
"""
def timedTask():
    print(datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3])

if __name__ == '__main__':
    # 创建后台执行的 schedulers
    scheduler = BackgroundScheduler()
    # 添加调度任务
    # 调度方法为 timedTask,触发器选择 interval(间隔性),间隔时长为 2 秒
    scheduler.add_job(timedTask, 'interval', seconds=2)
    # 启动调度任务
    scheduler.start()

    while True:
        print(time.time())
        time.sleep(5)


"""
周一到周五每天早上6点半启动的程序
"""
from apscheduler.schedulers.blocking import BlockingScheduler
from datetime import datetime
# 输出时间
def job():
    print(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
# BlockingScheduler
scheduler = BlockingScheduler()
scheduler.add_job(job, 'cron', day_of_week='1-5', hour=6, minute=30)
scheduler.start()

具体的案例可以看:花10分钟让你彻底学会定时任务框架apscheduler

上述代码中add_job中添加的参数,是apscheduler的triggers(触发器),apscheduler从上面那张微信架构中可以看到程序运行顺序:
在这里插入图片描述
其中常见的触发器有三种,分别为date、interval和cron。cron的可选参数最多,date最少,而且也并不常用.

cron参数为:

"""
cron参数
"""
year (int|str)4-digit year
month (int|str) – month (1-12)
day (int|str) – day of the (1-31)
week (int|str) – ISO week (1-53)
day_of_week (int|str) – number or name of weekday (0-6 or mon,tue,wed,thu,fri,sat,sun)
hour (int|str) – hour (0-23)
minute (int|str) – minute (0-59)
second (int|str) – second (0-59)
start_date (datetime|str) – earliest possible date/time to trigger on (inclusive)
end_date (datetime|str) – latest possible date/time to trigger on (inclusive)
timezone (datetime.tzinfo|str) – time zone to use for the date/time calculations (defaults to scheduler timezone)

interval参数为:

weeks (int) – number of weeks to wait
days (int) – number of days to wait
hours (int) – number of hours to wait
minutes (int) – number of minutes to wait
seconds (int) – number of seconds to wait
start_date (datetime|str) – starting point for the interval calculation
end_date (datetime|str) – latest possible date/time to trigger on
timezone (datetime.tzinfo|str) – time zone to use for the date/time calculations

它们的使用场景为:

"""
interval
"""
import datetime
from apscheduler.schedulers.background import BackgroundScheduler

def job_func(text):
    print(datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3])

scheduler = BackgroundScheduler()
# 每隔两分钟执行一次 job_func 方法
scheduler .add_job(job_func, 'interval', minutes=2)
# 在 2017-12-13 14:00:01 ~ 2017-12-13 14:00:10 之间, 每隔两分钟执行一次 job_func 方法
scheduler .add_job(job_func, 'interval', minutes=2, start_date='2017-12-13 14:00:01' , end_date='2017-12-13 14:00:10')

scheduler.start()

"""
cron
"""
import datetime
from apscheduler.schedulers.background import BackgroundScheduler

def job_func(text):
    print("当前时间:", datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3])

scheduler = BackgroundScheduler()
# 在每年 1-3、7-9 月份中的每个星期一、二中的 00:00, 01:00, 02:00 和 03:00 执行 job_func 任务
scheduler .add_job(job_func, 'cron', month='1-3,7-9',day='0, tue', hour='0-3')

scheduler.start()

而以上内容都只是针对执行程序但并没有考虑将执行结果保存,如果定时任务崩溃或者需要记录定时任务的日志,当然可以使用logging模块写成access.log,但apscheduler也是支持mongodb作为载体记录信息:

from pymongo import MongoClient
from apscheduler.schedulers.blocking import BlockingScheduler
from apscheduler.jobstores.mongodb import MongoDBJobStore
from apscheduler.jobstores.memory import MemoryJobStore
from apscheduler.executors.pool import ThreadPoolExecutor, ProcessPoolExecutor


def my_job():
    print 'hello world'
host = '127.0.0.1'
port = 27017
client = MongoClient(host, port)

jobstores = {
    'mongo': MongoDBJobStore(collection='job', database='test', client=client),
    'default': MemoryJobStore()
}
executors = {
    'default': ThreadPoolExecutor(10),
    'processpool': ProcessPoolExecutor(3)
}
job_defaults = {
    'coalesce': False,
    'max_instances': 3
}
scheduler = BlockingScheduler(jobstores=jobstores, executors=executors, job_defaults=job_defaults)
scheduler.add_job(my_job, 'interval', seconds=5)

try:
    scheduler.start()
except SystemExit:
    client.close() 

另外,如果定时任务有异常原因退出,还能加上发邮件的功能,这样在24小时内能以最短时间检查到错误并完成修改,具体的邮件模板可以看python利用STMP发送gmail、QQ邮件错误及笔记总结

def send_verify_mail(to_email, code):
    sender = settings.EMAIL_HOST_USER

    receiver = [to_email]
    subject = 'Mooncake account registration get code'
    username = settings.EMAIL_HOST_USER
    password = settings.EMAIL_HOST_PASSWORD
    print(password, to_email)

    msg = MIMEText('Mooncake verify code %s' % code,
                   _charset='utf-8')  #中文需参数‘utf-8',单字节字符不需要
    print("The code", code)
    print(username, password, to_email)
    msg['Subject'] = Header(subject, 'utf-8')
    msg['From'] = settings.EMAIL_FROM
    msg['To'] = to_email
    smtp = smtplib.SMTP_SSL()
    smtp.connect(settings.EMAIL_HOST)
    smtp.login(username, password)
    smtp.sendmail(sender, receiver, msg.as_string())
    smtp.quit()

def dingshirenwu():
    test()  

if __name__ == '__main__':
    scheduler = BlockingScheduler()
    # scheduler.add_job(Timing, 'interval', seconds=3)     # 每3秒执行一次
    scheduler.add_job(Timing, 'cron', minute="*/2", id='Timing')  # 每两分钟执行一次
    try:
        scheduler.start()
    except (KeyboardInterrupt, SystemExit):
        trace_callback()   # 失败发送回调消息
        SendEmail().send_mail(receive=ORDER_RECEIVER, mail_data=mail_data)

但上述除了调度MongoDB使用的是多线程,其余都是单线程,在web方面,我曾经用多线程测试的时候,发现了重复调用问题,当时有记录这一问题,依照:解决多进程中APScheduler重复运行的问题

我记得当时是有写一个物联网的功能是定时去监控数据库,根据某些表的字段比对得到更新后的值,再根据这些最新的值向当前表插入一条新记录,但看见了有重复写入的情况导致程序出错,于是我对数据库操作加了锁并进行了一些限制,解决了这类问题,另外看上篇博文的根据socket锁住重复执行感觉有点意思,这里mark一下:


import sys, socket
 
try:
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(("127.0.0.1", 47200))
except socket.error:
    print "!!!scheduler already started, DO NOTHING"
else:
    from apscheduler.schedulers.background import BackgroundScheduler
    scheduler = BackgroundScheduler()
    scheduler.start()
    print "scheduler started"

celery使用

作为一个在web面试中必问的一个知识点,很久之前我也只是写过发送短信验证码或者邮件等常用操作,上面代码的邮件模板就是当时写celery的,但
它的功能远不止这些,它内部的机制是非常复杂的,有着非常完善的一套业务线:
在这里插入图片描述
celery的一些简单介绍就直接跳过了,直接来看例子,例子引用自Celery的第四个demo–启动定时任务

新建一个文件夹名为fourth_celerydemo,文件夹下新建三个文件加上初始的__init__.py:

celery.py

# 拒绝隐式引入,因为celery.py名字和celery的报名冲突
from __future__ import absolute_import
from celery import Celery

# app是Celery的实例,third_celerydemo.tasks这个模块
app = Celery("fourth_celerydemo", include=["third_celerydemo.tasks"])
#使用config_from_object来加载存放在celeryconfig.py里面的关于celery的配置
app.config_from_object("fourth_celerydemo.celeryconfig")

if __name__ = "__main__":
    app.start()

celeryconfig.py

# coding = utf-8

from kombu import Queue
from datetime import timedelta

# 指定消息中间件,又称为消息代理
BROKER_URL = 'redis://localhost:6379/1'
# 任务处理完保存状态信息和结果,以供查询
CELERY_RESULT_BACKEND = 'redis/localhost:6379/0'
# 客户端和消费者之间传输数据需要序列化和反序列化
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
CELERY_TASK_RESULT_EXPIRES = 60 * 60 * 24
CELERY_ACCEPT_CONTENT = ["json"]

CELERY_QUEUES = ( # 定义任务队列
	Queue('default', routing_key='task.#'),  # 路由键以"task."开头的消息都进default队列
    Queue('web_tasks', routing_key='web.#'), # 路由键以"web."开头的消息都进web_tasks队列
)

CELERY_DEFAULT_EXCHANGE = 'tasks'  # 默认的交换机的名字为tasks
CELERY_DEFAULT_EXCHANGE_TYPE = 'topic' # 默认的交换类型是topic
CELERY_DEFAULT_ROUTING_KEY = 'task.default'  # 默认的路由键是task.default, 这个路由键符合上面的default队列

CELERY_ROUTES = {
    'fourth_celerydemo.tasks.add': { # tasks.add的消息会进入web_tasks队列
        'queue': 'web_tasks',
        'routing_key':'web.add',
    }
}

CELERYBEAT_SCHEDULE = {
    'add': {
        'task': 'fourth_celerydemo.tasks.add',
        'schedule':timedelta(second=2),
        'args':(1,25)
    }
}

tasks.py

from __future__ import absolute_import
from fourth_celerydemo import app

@app.task
def add(x,y):
	return x + y

然后我们就能用命令进行启动,但这里需要注意的是在Windows下,celery有很多的bug,具体bug如下:

pip install celery

然后我们就可以运行celery的启动命令:

# 后台启动 celery worker进程 
celery multi start work_1 -A appcelery  
# work_1 为woker的名称,可以用来进行对该进程进行管理

# 多进程相关
celery multi stop WOERNAME      # 停止worker进程,有的时候这样无法停止进程,就需要加上-A 项目名,才可以删掉
celery multi restart WORKNAME        # 重启worker进程

# 查看进程数
celery status -A celery_task       # 查看该项目运行的进程数   celery_task同级目录下

执行完毕后会在当前目录下产生一个二进制文件,celerybeat-schedule 。
该文件用于存放上次执行结果:
  1、如果存在celerybeat-schedule文件,那么读取后根据上一次执行的时间,继续执行。
  2、如果不存在celerybeat-schedule文件,那么会立即执行一次。
  3、如果存在celerybeat-schedule文件,读取后,发现间隔时间已过,那么会立即执行。

在这里插入图片描述

如果是Linux上,上面的代码运行时没有问题的,但在Windows上,就会有些问题。

Celery ValueError: not enough values to unpack (expected 3, got 0)

在windows下,还需要下载eventlet并加上这个参数才能运行任务:

pip install eventlet==0.26.0

# 使用eventlet再次启动
celery -A celery_tasks.tasks worker -l info -P eventlet 

这里eventlet一定需要下载0.26.0,因为默认是0.27.0,如果安装默认的话,会报错为:module ‘os‘ has no attribute ‘register_at_fork‘

那么到现在就能在win下启动:

celery beat -A fourth_celerydemo
celery -A fourth_celerydemo worker -Q web_tasks -l info -P eventlet

启动后,beat端就能看到很多的cid浮现,如果做了delay或者asysico,更能看到当前状态:

from fourth_celerydemo.celery import app
from celery.result import AsyncResult
# id = ''  # 失败任务
id = 'bbe46489-5bb4-4ddf-ade9-ef8c8e667202'  # 成功任务
if __name__ == '__main__':
    asyncs = AsyncResult(id=id, app=app)
    if asyncs.successful():
        result = asyncs.get()
        print(result)
    elif asyncs.failed():
        print('任务失败')
    elif asyncs.status == 'PENDING':
        print('任务等待中被执行')
    elif asyncs.status == 'RETRY':
        print('任务异常后正在重试')
    elif asyncs.status == 'STARTED':
        print('任务已经开始被执行')

在这里插入图片描述

但这样查询任务效率太慢,celery有提供可视化界面进行查看,为flower:

pip install flower

# 开启
flower -A fourth_celerydemo --port=5555

一个对 Celery 集群进行实时监控和提供 web 管理界面的工具,当然也是基于python实现的:https://github.com/mher/flower,用flower的前提是flower所在服务器能和任务队列所在服务器连通。
最牛逼的是起了flower后,你可以选择通过API调用方式来对任务和worker进行管理,而不用flower自带的web界面,等于是你可以自己开发个web界面来调flower的接口。

在这里插入图片描述
这里看起来时区好像乱了,所以要在启动任务的前面加上时区:

# 时区
app.conf.timezone = 'Asia/Shanghai'
app.conf.enable_utc = False

from datetime import timedelta
from celery.schedules import crontab
app.conf.beat_schedule = {
    'update_banner_list_task': {
        'task': 'fourth_celerydemo.tasks.add',  # task:任务源
        'schedule': timedelta(seconds=10),  # schedule:添加任务的时间配置
        # 'schedule': crontab(hour=8, day_of_week=1),  # 每周一早八点
        'args': (),  # args:执行任务所需参数
    }
}

然后在任务进行时还能看到图形:
在这里插入图片描述

而同样,因为选择的redis做的broker和backend,所以在redismanager中也能看到记录的执行情况:
在这里插入图片描述

关于celery和apscheduler的一些区别

celery和apscheduler一样,都支持定时任务,定时任务的特点是触发的方式是按照特定频率/特定时间,而调度方法支持阻塞/非阻塞/异步,但celery要比
celery复杂太多,并且大很多,如果说只是单进程的定时任务,那么完全不需要用celery,杀鸡焉用牛刀,我看了一些资料,看到的celery内容标题,起步就是百万,虽然我并不知道能不能达到,但这也侧面说明了celery的高并发以及可扩展性,另外也可以在我下面推荐的博文中看到一篇集群向的celery使用方法,所以之后还会再开坑来专门写celery的笔记,并好好研究研究。


参考与推荐:

[1]. celery 实例进阶

[2]. flower官方文档

[3]. 任务队列和消息队列,rpc的区别?

[4]. APScheduler定时框架

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

submarineas

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值