1. 调度对象封装
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys,os,logging
import asyncio
import datetime
from pytz import timezone
from api_monitor.utils import scheduler_config
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.events import EVENT_ALL




def default_func():
    pass

class APIScheduler():

    def __init__(self,executors=scheduler_config.executors,
                 jobstores=scheduler_config.jobstores,
                 job_defaults=scheduler_config.job_defaults):

        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)

        self._sched = AsyncIOScheduler(executors=executors,
                                      jobstores=jobstores,
                                      job_defaults=job_defaults,
                                       timezone=timezone('Asia/Shanghai'))
        self.default_id = 'default'
        #设置scheduler默认为运行状态
        self._sched.state = 1
        # self._sched.add_listener(self.listener,EVENT_ALL)
        self._sched._logger = scheduler_config.logger()

        #解决job从redis获取后没有scheduler属性问题
        for jobstore in jobstores:
            jobstores[jobstore]._scheduler = self._sched

    def listener(self,event):
        if event.exception:
            print('CHUCUO')
        else:
            print('正常。。。')


    def add_job(self,**kwargs):
        """
        添加任务到数据库中
        :param kwargs:
         func – callable (or a textual reference to one) to run at the given time
         trigger (str|apscheduler.triggers.base.BaseTrigger) – trigger that determines when func is called
         args (list|tuple) – list of positional arguments to call func with
         kwargs (dict) – dict of keyword arguments to call func with
         id (str|unicode) – explicit identifier for the job (for modifying it later)
         name (str|unicode) – textual description of the job
         misfire_grace_time (int) – seconds after the designated runtime that the job is still allowed to be run
         coalesce (bool) – run once instead of many times if the scheduler determines that the job should be run more than once in succession
         max_instances (int) – maximum number of concurrently running instances allowed for this job
         next_run_time (datetime) – when to first run the job, regardless of the trigger (pass None to add the job as paused)
         jobstore (str|unicode) – alias of the job store to store the job in
         executor (str|unicode) – alias of the executor to run the job with
         replace_existing (bool) – True to replace an existing job with the same id (but retain the number of runs from the existing one)`
        :return:
        """
        #state=1时才会调用_real_add_job方法添加数据到数据库

        job = self._sched.add_job(**kwargs)


    def _add_default_job(self,job_id='default',jobstore='default'):

        self.add_job(
            func=default_func, trigger='interval', seconds=1,
            jobstore=jobstore,next_run_time=datetime.datetime.now(),
            id=job_id, replace_existing=True, misfire_grace_time=3, coalesce=True,
            max_instances=1
        )


    def remove_job(self,job_id,jobstore='default'):
        """
        根据job_id删除任务
        :param job_id:
        :param jobstore:
        :return:
        """
        self._sched.remove_job(job_id=job_id,jobstore=jobstore)

    def remove_all_jobs(self,jobstore='default',default_jobid='default'):
        """
        从jobstore内删除所有除job id为default的任务
        :param jobstore:
        :param default_jobid:
        :return:
        """
        valid_job_ids = (job.id for job in self._sched.get_jobs(jobstore=jobstore)
                         if job.id != default_jobid)
        try:
            for job_id in valid_job_ids:
                self.remove_job(job_id,jobstore)
        except AttributeError as e:
            print(e.with_traceback())

    def pause_job(self,job_id,jobstore='default'):
        """
        根据job_id暂停任务
        :param job_id:
        :param jobstore:
        :return:
        """
        self._sched.pause_job(job_id=job_id,jobstore=jobstore)

    def resume_job(self,job_id,jobstore='default'):
        """
        根据job_id唤醒任务
        :param job_id:
        :param jobstore:
        :return:
        """
        self._sched.resume_job(job_id=job_id,jobstore=jobstore)

    def star(self,add_default_job=True,jobstore='default'):
        """
        开始处理任务
        :return:
        """
        try:
            if add_default_job:
                self._add_default_job(jobstore=jobstore)
            self._sched.state = 0
            self._sched.start()
            loop = asyncio.get_event_loop()
            loop.run_forever()
        finally:
            self.shutdown()

    def shutdown(self,wait=True):
        self._sched.shutdown(wait=True)
        
 2. 调度任务存储配置
 #!/usr/bin/env python
# -*- coding: utf-8 -*-

import logging
from apscheduler.executors.pool import ThreadPoolExecutor,ProcessPoolExecutor
from apscheduler.jobstores.redis import RedisJobStore

executors = {
        'default':ThreadPoolExecutor(200),
        'processpool':ProcessPoolExecutor(5)
    }

jobstores = {
    'default':RedisJobStore(db=0,
                            jobs_key='apscheduler.jobs',
                            run_times_key='apscheduler.run_times',
                            host='127.0.0.1',
                            port='6379',
                            password='',
    ),
}

job_defaults = {
    'coalesce': True,
    'max_instances': 3
}

def logger(log_file='/tmp/scheduler.log'):
    logging.basicConfig(level=logging.INFO,
                        format='%(asctime)s %(levelname)s %(message)s',
                        datefmt='%Y-%m-%d %H:%M:%S',
                        filename=log_file,
                        filemode='a')
    return logging
    
3.任务函数
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import pycurl
from urllib import parse
from io import StringIO,BytesIO
import json
import os,sys
import pymysql
import time



db_host = "xxxxxx"
db_user = "xxxx"
db_passwd = "xxxxxx"
db_database = "xxxxxx"
conn = pymysql.connect(db_host,db_user,db_passwd,db_database)
cursor = conn.cursor()

def curl(url,params,method,header=[],save_data=True,time_out=6,**kwargs):

    api_info = {
        'api_monitor_id': kwargs['api_id'],
        'api_monitor_node_id': kwargs['node_id'],
    }

    buffer = BytesIO()
    c = pycurl.Curl()
    c.setopt(pycurl.HTTPHEADER, header)
    c.setopt(pycurl.WRITEDATA, buffer)
    # 请求超时时间
    c.setopt(pycurl.TIMEOUT, time_out)
    if method.lower() == 'get':
        #url = f'{url}?{parse.urlencode(params)}'
        url = url + '?' + parse.urlencode(params)
    else:
        c.setopt(pycurl.POSTFIELDS, json.dumps(params))
    c.setopt(pycurl.URL,url)
    try:
        c.perform()
        api_info['err_message'] = 0
    except pycurl.error as e:
        api_info['err_message'] = str(e)

    finally:
        api_info.update(
            {
                'http_code': c.getinfo(pycurl.HTTP_CODE),
                'totle_response_time': c.getinfo(pycurl.TOTAL_TIME),
                'dns_time': c.getinfo(pycurl.NAMELOOKUP_TIME),
                'connect_time': c.getinfo(pycurl.CONNECT_TIME),
                'redriect_time': c.getinfo(pycurl.REDIRECT_TIME),
                'ssl_time': c.getinfo(pycurl.APPCONNECT_TIME),
                'size_download': c.getinfo(pycurl.SIZE_DOWNLOAD),
                'speed_down': c.getinfo(pycurl.SPEED_DOWNLOAD),
                'content': buffer.getvalue().decode('utf-8'),

            }
        )
        if save_data:
            current_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
            sql = """insert into api_monitor_apimonitorhistory (http_code, totle_response_time, dns_time, connect_time, redriect_time,ssl_time,size_download, speed_down, content, err_message, api_monitor_id,api_monitor_node_id, create_time)values({http_code},{totle_response_time},{dns_time},{connect_time},{redriect_time},{ssl_time},{size_download},{speed_down},'{content}',{err_message},{api_monitor_id},{api_monitor_node_id},'{current_time}')""".format(http_code=api_info.get('http_code'),totle_response_time=api_info.get('totle_response_time'),dns_time=api_info.get('dns_time'),connect_time=api_info.get('connect_time'),
                                                   redriect_time=api_info.get('redriect_time'),
                                                   ssl_time=api_info.get('ssl_time'),
                                                   size_download=api_info.get('size_download'),
                                                   speed_down=api_info.get('speed_down'),
                                                   content=api_info.get('content'),
                                                   err_message=api_info.get('err_message'),
                                                   api_monitor_id=api_info.get('api_monitor_id'),
                                                   api_monitor_node_id=api_info.get('api_monitor_node_id'),
                                                    current_time=current_time)
            print(sql)
            try:
                cursor.execute(sql)
                   conn.commit()
            except Exception as e:
                print(e)
                conn.rollback()

            #ApiMonitorHistory.objects.create(**api_info)
        else:
            pass
        c.close()

 4.执行调度任务
     server端添加任务到redis
     job_obj = scheduler.APIScheduler()  # 调用job对象
     for node_id in api_monitor_node_id_list:
        api_conf = {
            "url":monitor_url,
            "params": params_data,
            "method": request_method,
            "header": http_header,
            "api_id": str(api_obj_id),
            "node_id": node_id
        }

     job_obj.add_job(id=str(node_id), func=tasks.curl, kwargs=api_conf,
                    trigger='interval',
                    seconds=5,next_run_time=datetime.datetime.now(),
                    replace_existing=True, misfire_grace_time=3, coalesce=True,
                    max_instances=2,
                    jobstore='default') # 加入任务到redis, 必须设置scheduler state=1才能加入redis
                    
     agent端监听任务
     from api_monitor.utils import scheduler

    job_obj = scheduler.APIScheduler()
    job_obj.star() # 启动监听
    #job_obj.remove_job(job_id="default")  # 根据任务id删除任务
    
    
参考链接: https://zhuanlan.zhihu.com/p/44185271