1、现状问题
为了满足用户动态添加定时任务,因此选择apscheduler模块,apscheduler支持cron指令形式的定时任务,可通过api接口实现动态添加定时任务。
问题:
django uwsig部署apscheduler定时任务重复执行问题,由于uwsgi启动多个worker,导致每个worker执行一次定时任务,各种google、baidu搜索结果都大差不差,自定义的定时任务可解决重复执行问题,但是动态添加定任务重复执行问题始终未能解决!
2、apscheduler部署过程
- uwsgi.ini文件配置:(只列出关键的配置)
#由于GIL的存在,uwsgi索性默认不支持多线程,不对GIL进行初始化。但如果希望程序中的线程发挥作用,需要加入
enable-threads=true
- -在django项目wsgi.py文件同级目录下:创建apscheduler_start.py文件
该文件中是启动apscheduler定时任务的逻辑:(具体apscheduler使用方法这里不做说明)
apscheduler_start.py:
import logging
from apscheduler.events import EVENT_JOB_EXECUTED, EVENT_JOB_ERROR
from apscheduler.schedulers.background import BackgroundScheduler
from django_apscheduler.jobstores import DjangoJobStore
logger = logging.getLogger('scheduler_log')
logger.setLevel(logging.DEBUG)
jobstores = {
'default': DjangoJobStore()
}
job_defaults = {
'coalesce': True,
'max_instances': 1,
'misfire_grace_time': 60,
'replace_existing': True
}
scheduler = BackgroundScheduler(jobstores=jobstores, job_defaults=job_defaults)
# pool_pre_ping为True,即每次从连接池中取连接的时候,都会验证一下与数据库是否连接正常,如果没有连接,那么该连接会被回收。
# engine_options={'pool_pre_ping': True, 'pool_recycle': 100}
scheduler.start()
def listener_event(event):
job_id = event.job_id
scheduled_run_time = event.scheduled_run_time.strftime("%Y-%m-%d %H:%M:%S")
if event.exception:
logger.error('作业ID:{} 在 {} 执行失败,错误原因:{}'.format(job_id, scheduled_run_time, event.exception.args[0]))
scheduler._logger = logger
# 当任务执行完或任务出错时,listener_event
scheduler.add_listener(listener_event, EVENT_JOB_EXECUTED | EVENT_JOB_ERROR)
# 使用main_loop阻塞进程,防止进程挂起
# import time
# while True:
# time.sleep(60)
# sig = uwsgi.signal_wait()
- 通过api请求动态添加定时任务的逻辑代码apscheduler_task.py:
# 引入apscheduler_start 中的 scheduler
from ops.apscheduler_start import scheduler
from django.views import View
from django.utils.decorators import method_decorator
class CrontabTask(View):
@method_decorator(csrf_exempt)
def dispatch(self, request, *args, **kwargs):
res = super().dispatch(request, *args, **kwargs)
return res
def get(self, request):
pass
def post(self, request):
content = json.loads(request.body.decode())
try:
# crontab_time 页面录入crontab指令,eg: */3 * * * * *
job_time = content['crontab_time']
crontab_time = job_time.split(' ')
crontab_obj = ['minute', 'hour', 'day', 'month', 'day_of_week', 'year']
crontab_dict = dict(zip(crontab_obj, crontab_time))
# func需要定时执行的函数
job = scheduler.add_job(func, trigger='cron', second='0', minute=crontab_dict['minute'],
hour=crontab_dict['hour'], day=crontab_dict['day'], month=crontab_dict['month'],
day_of_week=crontab_dict['day_of_week'],
year=crontab_dict.get('year', '*'), args=[_job_id], coalesce=True, max_instances=1, misfire_grace_time=300, replace_existing=True)
except Exception as e:
logger.error(e)
# 此处忽略return response代码
def delete(self, request):
pass
def put(self, request):
pass
此时通过api接口可成功添加定时任务, 但是会存在重复执行问题
3、解决重复执行问题
通过redis_lock锁实现
# 下载redis_lock
# pip install python-redis-lock
import redis_lock
# 使用锁的案列
# conn redis链接对象; lock-key:写入redis的key
lock = redis_lock.Lock(conn, "lock-key")
if lock.acquire(blocking=False):
print("Got the lock.")
# 此处为上述动态添加接口中:定时执行func的逻辑
...
# 最后需要手动释放掉锁
lock.release()
else:
print("Someone else has the lock.")
4、apscheduler 跟随 uwsgi重启自动重启
- 踩坑点:由于apscheduler需要手动触发重启,即在页面添加定时任务时,方可触发重启,导致定时任务执行的函数可能存在执行旧代码的情况!!! uwsgi重启时,apscheduler不会跟着重启!要保证uwsgi和apscheduler同步重启!
- 解决方案:
django项目中在apscheduler_start.py文件同级目录中__init__.py文件中添加:
import django
django.setup()
from ops.apscheduler_start import scheduler
__all__ = ('scheduler',)
此时重启uwsgi服务,apscheduler会自动重启。
**备注一下:**大部分是在wsgi.py文件中添加的,我尝试过,添加后定时任务会重启,但是django项目访问504,测试过多次,依然不行,不知道是不是方法不对。。。
5、较好的优化解决方案
通过类似celery的方式将django_apscheduler定时任务独立出来单独启动一套服务,设定单进程运行。这种方法解决了重复执行的问题,同时解放了django后台并发限制,并且可以处理即时的异步任务。(此处只有纯描述, 重要点:uwsgi中的mule模块!!!https://uwsgi-docs-zh.readthedocs.io/zh_CN/latest/Mules.html)