因为 Gunicorn 从 8 个工作人员开始(在您的示例中),所以这会将应用程序 8 次分叉为 8 个进程。这 8 个进程是从主进程派生出来的,主进程监控每个进程的状态并能够添加/删除工作人员。
每个进程都会获得您的 APScheduler 对象的副本,该对象最初是您的主进程的 APScheduler 的精确副本。这导致每个“nth”工作人员(进程)执行每个作业总共“n”次。
解决此问题的方法是使用以下选项运行 gunicorn:
env/bin/gunicorn module_containing_app:app -b 0.0.0.0:8080 --workers 3 --preload
该--preload
标志告诉 Gunicorn “在分叉工作进程之前加载应用程序”。通过这样做,每个工作人员都“获得了应用程序的副本,已经由 Master 实例化,而不是实例化应用程序本身”。这意味着以下代码仅在 Master 进程中执行一次:
rerun_monitor = Scheduler()
rerun_monitor.start()
rerun_monitor.add_interval_job(job_to_be_run,\
seconds=JOB_INTERVAL)
此外,我们需要将jobstore设置为:memory:以外的任何内容。这样,虽然每个 worker 都是自己的独立进程,无法与其他 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)
最后,我们想使用BackgroundScheduler,因为它实现了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以获得在 Web 服务器(如 Gunicorn)中运行 APScheduler 的快速方法,并为每个作业启用 CRUD 操作。