APScheduler不完全踩坑指南.md

2021/11/28 PythonWeb

# 踩坑指南

这是一篇排错指南,源自博主近期用到APScheduler较多,特此总结了一些常见的问题,希望对大家有帮助。

# 问题1: Can't Connect to MySQL的问题

如果用的是SQLAlchemy作为job的存储介质,那如果参数没配置好的话,就会导致这个问题。

  • 问题描述

    在涉及到job数据读取/更新的时候,我们会调用SQLAlchemy的Session去完成相关操作。但偶尔会弹出Can't Connect to MySQL或者Lose Connection的错误

  • 问题分析

    MySQL对每个数据库连接是有个有效期的,默认应该是8小时,但这个是可以配置的。而SQLAlchemy的连接都是在连接池里面,而且有一个默认的回收时间。

    假如SQLAlchemy开辟的连接回收时间是2小时,但是MySQL配置的连接失效时间是1小时,那么就会出现一个问题:

    SQLAlchemy认为连接仍然有效,但MySQL认为没效了,所以连接到了MySQL那边就不认了。就会出现以上错误了。

  • 解决方法

    在jobstore处配置pool_recycle参数,这个参数要确保小于数据库的连接超时时间。

    比如我配置25分钟,那么数据库超时时间就算只有30分钟,那我也能正常工作。

# 连接回收时间为1500秒
job_store = {
        'default': SQLAlchemyJobStore(url="数据库jdbc连接地址", engine_options={"pool_recycle": 1500})
}
1
2
3
4

# 问题2 job莫名其妙被删除的问题

不得不说,这个设定很变态,问题表象是: 你的服务跑着跑着,你的定时任务全不见了。这个非常非常蛋疼。

  • 问题描述

    我这边的情况是,当job无法从数据库中反序列化到内存的时候,APScheduler会自动删除这样的job。

  • 问题分析

    注意看具体的日志,经过我的排查,发现APScheduler用的序列化方式是pickle,与JSON相比,是个咱们不太熟悉但又听过的序列化类库。

    但这个类库有个很严重的问题,比如我的用的python3.7,里面有个pickle.DEFAULT_PROTOCOL参数,在3.7的时候是3,而到Python3.9以后,这个值变成了4.

    APScheduler里面这个参数默认用的pickle.DEFAULT_PROTOCOL参数。

    想象一下,你本地是Python3.9, 但服务器是3.7,那你添加job的时候可能是用的DEFAULT_PROTOCOL=4,但是服务器反序列化job的时候,拿到的就是3了,导致无法反序列化,最终删除job。

  • 解决方法

    指定序列化类型,最好知道你即将部署的机器的python版本,目前可以指定为2或者3。

job_store = {
        'default': SQLAlchemyJobStore(url="数据库连接地址", pickle_protocol=3, engine_options={"pool_recycle": 1500})
1
2

# 问题3 用gunicorn或者uvicorn重复执行的问题

可以看一下我上一篇文章,用socket/分布式锁都可以解决。

https://mp.weixin.qq.com/s?__biz=MzAxMjc1OTkxMA==&mid=2247486807&idx=1&sn=cf8d30893f14faccc07f6fa425ea4aea&chksm=9badab5eacda2248d8ce6147e25a62f4c5e68ccc07690b20a8f630b7558fc1ff81b4c43e7593&token=1020134831&lang=zh_CN#rd (opens new window)

# 问题4 分布式部署的问题

如上所说,不管是分布式部署,还是多worker模式。APScheduler的支持都不是很友好。

因为它旨在给单个Python项目完成定时任务方案,而没有考虑集群等分布式问题。在单个python服务里面,他可以运行良好。我们来看一个例子:

  • 服务器A 部署了web服务

  • 服务器B 部署了web服务

    2个服务一模一样,之所以部署2台机器,是为了负载均衡考虑。那么我此时调用新增job的方法,请求打到了服务器A,那会是什么过程呢:

  1. 服务器A接收数据,与本地的func联动并加载job到内存
  2. 讲加载后的内存持久化到数据库

但你要说服务器B的jobstore有这个任务吗?那当然是没有的,因为它没有任何集群的概念。不存在服务器A接受命令,送达到服务器B的机制。

这最直观地导致,你在gunicorn/uvicorn开启N个worker,并调用add接口添加job,不会感知到job重复执行的问题。

而一旦你重启了服务,8个worker都从数据库加载了job到内存中,那你会很快遇到重复执行的任务

本文仅限于我对APScheduler的浅薄理解而写,如有不周之处还望指正。