因为 Gunicorn 一开始有 8 个工人(在你的例子中),所以forks该应用程序 8 次进入 8 个进程。这8个进程是从Master流程,它监视每个人的状态并能够添加/删除工人。
每个进程都会获取 APScheduler 对象的副本,该对象最初是主进程的 APScheduler 的精确副本。这导致每个“第 n”个工作线程(进程)执行每个作业总共“n”次。
解决这个问题的方法是使用以下选项运行gunicorn:
env/bin/gunicorn module_containing_app:app -b 0.0.0.0:8080 --workers 3 --preload
The --preload
标志告诉 Gunicorn“在分叉工作进程之前加载应用程序”。通过这样做,每个工人都“给定应用程序的副本,已由 Master 实例化,而不是实例化应用程序本身”。这意味着以下代码仅在 Master 进程中执行一次:
rerun_monitor = Scheduler()
rerun_monitor.start()
rerun_monitor.add_interval_job(job_to_be_run,\
seconds=JOB_INTERVAL)
此外,我们还需要设置jobstore成为除:memory:这样,虽然每个工作进程都是自己独立的进程,无法与其他 7 个进程进行通信,但通过使用本地数据库(而不是内存),我们可以保证作业存储上 CRUD 操作的单点真实性。
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
rerun_monitor = Scheduler(
jobstores={'default': SQLAlchemyJobStore(url='sqlite:///jobs.sqlite')})
rerun_monitor.start()
rerun_monitor.add_interval_job(job_to_be_run,\
seconds=JOB_INTERVAL)
最后,我们想使用后台调度程序由于其实施start()
。当我们打电话时start()
在BackgroundScheduler中,在后台启动一个新线程,负责调度/执行作业。这很重要,因为请记住步骤(1)中,由于我们--preload
标志我们只执行start()
在 Master Gunicorn 进程中运行一次。根据定义,分叉进程不会继承其父进程的线程,所以每个工作人员都不会运行BackgroundScheduler线程。
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
rerun_monitor = BackgroundScheduler(
jobstores={'default': SQLAlchemyJobStore(url='sqlite:///jobs.sqlite')})
rerun_monitor.start()
rerun_monitor.add_interval_job(job_to_be_run,\
seconds=JOB_INTERVAL)
所有这一切的结果是,每个 Gunicorn 工作线程都有一个 APScheduler,它被欺骗进入“已启动”状态,但实际上并未运行,因为它丢弃了其父级的线程!每个实例还能够更新作业存储数据库,只是不执行任何作业!
查看Flask-APScheduler https://github.com/viniciuschiele/flask-apscheduler一种在 Web 服务器(如 Gunicorn)中运行 APScheduler 的快速方法,并为每个作业启用 CRUD 操作。