定时任务服务设计




需求

目前我们平台有多个需要定期执行的任务,举例说明:

  1. 平台定期邮件报表(已经开发的功能)
  2. 弹性伸缩服务的定时伸缩功能(即将开发的功能)
  3. 云主机定期备份(已经开发的功能)
  4. 回收站云主机到期删除(目前永远不删除的逻辑不太合适,放入回收站时可设置一个过期删除时间)

未来可能还会有其他定时任务增加,如云硬盘回收站定期清空功能。

这些任务的执行规律都比较类似,都是由用户设定类似Linux crontab的执行周期(比如每周一早上2点整执行),或者每隔多少时间执行一次(比如每1800s执行一次),然后我们后台检查是否到执行时间,进而执行预设操作。

现状

当前的定时任务执行的代码实现比较多,每个功能都单独实现一种执行方法显然不合适,比较难维护,扩展也比较复杂,高可用等设计也没考虑到。

举例说明,云主机定期备份功能是用crontab功能实现的,只能单节点部署(同时在两个节点部署的话,就可能备份两次)。

另外平台定期报表功能,是新开发的服务,已经考虑到一部分异常、高可用实现,但缺乏通用性,只能给这一个功能使用,如果弹性伸缩服务需要增加定时任务,还需要再单独实现一个类似的后台服务,需求很类似,copy代码再修改实现一个服务显然是比较浪费的,因此可以考虑实现一个通用的定时任务框架,来统一的执行所有定时任务。

方案

python中有比较成熟的定时任务调度库,比如APScheduler(https://apscheduler.readthedocs.io/en/latest/userguide.htmlhttps://nummy.github.io/2016/02/19/aps/https://lz5z.com/Python定时任务执行方式),使用这个库可以轻松实现各种定时任务。

因为我们可以考虑基于APScheduler来实现一个通用的定时任务服务,给所有需要执行定时任务的功能使用。

任务管理

我们可以在用户设置定时任务时,将任务保存到我们自建的数据库中,并区分任务类型,供web查询展示使用,以及定时任务服务分类执行使用。

Job表结构设计如下(可根据实际需要添加索引):

字段名称
类型
用途
ID char Job的唯一标识符,32位UUID表示,索引字段
resource text Job关联的资源,最多65535个字符,json格式存储,如果是平台报表,则可留空,如果是弹性伸缩定时任务,则关联到集群UUID、伸缩规则UUID,如果云主机回收站清理或者定期备份,则关联到云主机UUID
name char Job的名称,用于web展示,255位字符,可考虑支持中文
type char Job的类型,255位字符,用于保存Job的使用方或者说用途,目前考虑支持平台邮件报表(platform_report)、弹性伸缩定时规则(auto_scaling)、云主机定期备份(auto_backup)、清空云主机回收站(empty_vm_recycle_bin)
trigger char Job的触发器类型,255位字符,参考APScheduler中trigger类型,共3种可选:date、interval、cron,分别表示一次性任务、固定时间间隔任务、cron任务
owner char Job所属的租户,用于加入租户内执行各种操作,32位字符
policy char Job执行周期策略,255位字符,根据trigger_type类型不同,这里格式也不同,date为某一时间点,interval为固定时间段如1800,cron则参考crontab语法
executor char Job执行节点的hostname,255为字符
last_exec_time datetime Job上次执行时间,需注意时区问题,建议统一为UTC时间
created_at datetime Job创建时间,需注意时区问题,建议统一为UTC时间
updated_at datetime Job更新时间,需注意时区问题,建议统一为UTC时间
deleted_at datetime Job删除时间,需注意时区问题,建议统一为UTC时间
deleted int Job是否已删除,0表示未删除,其他数字表示已删除,索引字段

任务执行

使用APScheduler来管理所有Job的调度执行任务,设置一个主Job定期扫描DB的Job表,与定时任务服务中正在执行的Job列表比较,检查是否有任务新增或者修改,如有则把新Job加入调度队列(add_job,并将resource_id作为参数传给job function),或者修改已调度Job(modify_job)。

需要为每种Job类型编写对应的job function,用于作为add_job方法的func参数,在任务被触发时具体执行该类任务,如云主机回收站清空任务,则可以编写一个类似如下代码的method:

清空云主机回收站示例任务代码
1
2
3
4
5
6
def empty_vm_recycle_bin(*args, **kwargs):
    # 首先从args、kwargs中获取resource_id,也即云主机uuid
    vm_uuid = kwargs.get('resource_id')
    if vm_uuid is None:
        raise Exception()  # 注意需要raise APScheduler指定的异常
    # 根据配置文件中的keystone管理员账号获取token,然后组装url并发送http请求(建议使用python requests库)给nova,执行删除云主机操作

高可用及负载均衡

为了避免单点故障导致所有定时任务都无法执行,需要考虑多节点部署定时任务服务,为此需要考虑任务的分发和加锁,以及节点异常后其他节点的接管流程。任务分发还需要考虑每个节点执行的总任务数量,防止所有任务都集中在某一个节点,而其他节点没有任务可做的情况发生。

高可用场景需要增加一个executor表,字段设计如下(可根据需要添加索引):

字段
类型
用途
host char Job执行节点hostname,255位字符
last_heartbeat_time datetime Job执行节点上次心跳时间

定时任务服务需要定期上报其心跳到DB的executor表(可用一个独立的job实现,每10s上报一次),用于其他节点检测该节点的服务是否异常,如果心跳超时(超时时间可配置到配置文件中)则表示异常,异常后其他节点需要接管该节点负责的Job(修改Job表的executor字段),该节点恢复上线后可以继续接受新Job。判断一个节点异常的条件可设置为(需全部同时满足):

  1. 节点心跳超时(根据executor表中last_heartbeat_time判断,心跳超时时间可设置到配置文件,比如5分钟)
  2. 节点任务执行时间超时(根据Job表中last_exec_time判断,每种任务可设置不同的超时时间,如平台报表任务执行时间较长可设置为15分钟,云主机删除任务执行时间较短,可设置为5分钟,超时时间保存在配置文件中)

负载均衡问题:可以在每个节点主Job定期扫描DB的Job表时检查Job总量,并与所有存活的节点数量进行比较,保证当前节点执行的Job数量不超过math.ceil(总Job数/总存活节点数),并保证没有Job被遗漏而永远执行不到。

其他事项

  1. 本文档只是描述一种初步考虑的定时任务框架解决方案,还需要实际的编写原型代码进行可行性验证,并进行方案细化。
  2. 以配置文件中的管理员用户权限去操作租户资源时(如为租户创建云主机),需要先把管理员用户加入到租户内(member role),之后用租户+管理员用户获取token,再去执行操作。