分布式任务调度系统设计
一、思路
任务调度器、任务执行器、任务
任务调度器不关心业务逻辑,只关心任务的触发策略、失败策略、路由策略、阻塞处理策略
任务执行器只需要监听任务触发接口,按要求执行任务,成功或失败时异步通知任务调度器
任务的基本属性需要包含任务ID、触发策略、失败策略、路由策略、阻塞处理策略、创建时间、创建用户、任务参数、任务当前抢占调度器、任务状态(调度中、执行中、执行完成)
HA集群
为了避免单点故障,任务调度系统通常需要通过集群实现系统高可用,同时通过扩展提高系统的任务负载量上限。
鉴于任务调度系统的特殊性,“调度”和“执行”两个模块需要均支持集群部署,由于职责不同,因此各自集群侧重点也有有所不同。
- “调度器”集群,目标为避免调度模块单点故障。同时,集群节点需要通过锁或命名服务保证单个任务的单次触发,只在其中一个节点上生效,以防止任务的重复触发。
- 调度器为何采用集群而不是主从?
- 主从模式只能提供HA的特性,不能提高提高系统的任务负载量上限。
- 主从需要实现选举算法,保证CP或者AP
- 集群只需要将状态都迁移到全局的存储器中(例如DB),任务可以采用抢占机制(全局锁),抢占后在任务后标识任务状态即可。
- 调度器为何采用集群而不是主从?
- “执行器”集群:目标为避免任务模块单点故障。进一步可以通过自定义路由策略实现Failover等高级功能,从而在执行器某台机器节点故障时自动转移不会影响到任务的正常触发执行。
节点故障处理
节点:
任务调度器S1、S2、S3
任务执行器E1、E2、E3
任务T1、T2、T3
情景1:
在S1上手动触发一个任务T1,T1任务信息入库(任务状态调度中),根据路由策略选择E3执行任务T1【a】(返回画面任务手动触发成功,任务状态执行中),任务T1进入E3的任务队列【b】,E3开始执行任务【c】,任务执行完成【d】,结果同步通知S2【e】(返回通知成功后才可以提交任务的事务),S2根据任务信息修改数据库T1任务信息记录【f】(任务状态执行完毕,画面定时刷新查询任务状态)
假设在【a】处时,T1发生宕机,需要保证T1的任务信息不会入库,开启DB事务,保证任务成功调度给执行器后再提交事务;画面会显示错误通知,提示调度器宕机,需要重新刷新画面,Nginx将自动选择新的S2或S3节点继续提供服务
假设在【b、c、d、e】处时,E3发生宕机,此时调度器S1、S2、S3发现E3下线(执行器下线Hook检查),在DB中查询分配给E3的任务(执行中的任务),重新抢占该任务,假设S2抢占到该任务,根据任务T1的策略调度任务,选择E1执行任务T1,T1任务执行完毕任务后异步随机通知调度器S1,S1根据任务信息修改数据库T1任务信息记录
假设在【e】处时,S2发生宕机,E3发现通知失败,将会随机重新选择可用的调度器节点再次发起通知,如果超过重试次数仍然无法通知成功(说明E3与调度器间出现了网络问题,调度器会认为E3下线),则发生异常,保证任务的事务回滚,主动通知调度器集群执行器E3故障下线,调度器集群将调度器列表中的E3排除,E3故障排除后重启E3即可重新注册到调度器中
假设在【f】处时,S2发生宕机,E3发现通知的连接异常断开,将会随机重新选择可用的调度器节点再次发起通知,如果超过重试次数仍然无法通知成功(说明E3与调度器间出现了网络问题,调度器会认为E3下线),则发生异常,保证任务的事务回滚,主动通知调度器集群执行器E3故障下线,调度器集群将调度器列表中的E3排除,E3故障排除后重启E3即可重新注册到调度器中
假设在【f】处时,E3发生宕机,S2发现响应发生异常,此时需要将事务回滚,既不能将任务状态修改为执行完毕,然后E3宕机无法向集群中心发送心跳,触发调度器的下线检查,任务将会重新被调度器抢占调度执行。
触发策略
任务的触发方式可以分为定时触发和手动触发,定时触发可以是cron表达式形式,也可以是自定义触发策略类形式。
触发策略可以是delay、FixedDelay、FixedRate、cron表达式、手动触发、以及自定义触发实现
失败策略
这里的失败策略指的是业务发生失败的处理策略,而不是因为节点故障导致任务没有执行完成导致的失败
任务失败是一种很常见的情况,当任务失败时有两点非常重要,一个是快速发现问题,另一个是及时解决问题。
任务业务逻辑千差万别,如索引同步、pv统计、订单超时处理等等。任务失败可能会导致非常严重的后果,比如索引同步任务失败可能导致搜索不匹配,pv统计失败可能导致打点报表的生成,订单超时处理任务的失败可能导致商品库存的大量无效占用等等。
针对上述情况,通常有几种处理方案:
- 失败告警(快速发现问题):任务失败时,主动向任务负责人发送告警通知,如邮件、短信等方式。这是一种常用的处理方案,原理和实现都比较简单。负责人接收的告警邮件时,通过人工的方式进行故障处理,如手动触发一次任务执行。
- 失败重试(快速解决问题):任务失败时,调度中心主动尝试触发一次重试任务。优点在于不需要人为接入,重试在一定程度上可以大大提高任务的成功率。但是,失败重试需要注意限制重试次数,否则将会导致”失败-重试-失败”的死循环,造成资源浪费。
路由策略
由于任务执行器存在多个实例,调度器如何选择任务执行器同样是个问题。
为每个任务配置不同的路由策略(为配置则使用默认路由策略)
常见的策略如下:
- 随机策略
- 轮训策略
- 任务IDHash策略
- 背压策略(需要调度器与执行器通讯报告执行器未执行任务数量)
阻塞处理策略
任务阻塞经常发生在耗时任务场景中。举个例子,假如一个索引同步任务每10min运行一次,由于依赖的服务出现long service导致一次任务运行了15min,那么任务下次触发时将会遭遇阻塞。
在调度比较密集,而执行器来不及处理的情况下,任务阻塞策略可以指导执行器快速处理阻塞的触发请求。
常见的阻塞策略有以下几种:
- 单机串行(默认):调度请求进入执行器后,调度请求进入FIFO队列并以串行方式运行;
- 丢弃后续调度:调度请求进入执行器后,发现执行器存在运行的调度任务,本次请求将会被丢弃并标记为失败;
- 覆盖之前调度:调度请求进入执行器后,发现执行器存在运行的调度任务,将会终止运行中的调度任务并清空队列,然后运行本地调度任务;