/go-crontab

GO实现类似quartz的超轻量分布式crontab(已弃坑)

Primary LanguageGo

go-crontab

GO实现类似quartz的超轻量分布式crontab

特点

  • 仅支持crontab表达式
  • 仅依赖mysql
  • 调度与执行不隔离

设计

  • 基于mysql的节点注册与发现
    进程启动生成一次性唯一会话ID,插入到MYSQL会话表。
    
    心跳定时更新MYSQL会话表,与MYSQL失联超过阀值自杀(防脑裂的常见手段)。
  
  • 基于mysql的任务计划
    用一张任务表维护所有crontab,主要包括信息:
    
    任务名称、cron表达式、下次执行时间戳、任务启动时间、上一轮任务结束时间、任务启动唯一标识(会话ID+进程级计数)、执行的命令、任务状态、是否暂停、是否删除、任务归属会话。
    
    其中任务状态枚举:空闲、预备、执行任务按顺序在3个状态之间轮转。

  • 基于mysql的分布式锁
    因为集群部署多个节点共同参与调度,所以在访问共享数据时需要分布式锁保护。
    
    用一张锁表维护所有的分布式锁,基于innodb行锁实现。
    
    包括2把锁:
    1)任务表的锁:同一时刻只有一个节点可以进行任务管理。
    2)会话表的锁:同一时刻只有一个节点可以进行会话管理。

    根据quartz开源项目的原理,任务调度系统瓶颈通常不在于调度本身,悲观锁是可行的。
    
    因为mysql事务提交成功/回滚后,需要做一些原子的业务逻辑,所以在mysql事务锁外层应用一个进程级的mutex锁。
    
  • 调度线程
     (获取进程任务锁)

     (启动事务、获取任务锁)

    (注:select的limit需要根据内存prepare+executing的任务数量调控)
     周期性扫描任务表、查询出符合条件的任务(状态=空闲,暂停=0,删除=0,下次执行时间<=now+30),即未来30秒内即将到期的任务。
  
     若这些任务没有出现在prepare和executing集合中,则将这些任务的状态更新为"预备",设置任务启动唯一标识、归属会话改为进程会话ID。
     
     将这些任务放入内存中的任务prepare集合。
    
     (提交事务、释放任务锁) 
     (释放进程任务锁)
     
     
     定时检测prepare集合,若任务执行时间到期,则:
     
     (获取进程任务锁)
     (启动事务,获取任务锁)
     
     更新任务(状态=执行,任务启动时间=now)
     
     从内存prepare集合挪到executing集合
     
     (提交事务,释放任务锁)
     (释放进程任务锁)
     
     将任务交给单独的执行线程,通过子进程运行。

  • 执行线程

调度线程发起一个任务,启动独立执行线程调用Golang Exec包执行shell命令(bash -c "cmd"),阻塞等待进程退出。

子进程退出后,将结果发给结果处理线程。

  • 遗漏回补线程
    (获取进程任务锁)
    
    (启动事务、获取任务锁)
     周期性扫描任务表,找出(状态!=空闲,归属=节点会话ID)的任务。
  
     对于每个任务,如果是预备状态且不在prepare集合,或者是执行状态并且不在executing集合,则说明因为MYSQL异常访问导致了调度遗漏。

     更新每个任务(状态=空闲,下次调度时间=now)。
    (提交事务、释放任务锁)
    
    (释放进程任务锁)

  • 结果处理线程
    接收执行线程发来的任务结果。
      
    (获取进程任务锁)
    
    (启动事务,获取任务锁)
    
    获取对应任务,更新记录(状态=空闲,下次调度时间),插入一条历史执行日志。
    
    从executing集合删除对应记录。
    
    (提交事务,释放任务锁)
    
    (释放进程任务锁)
    
  • 会话线程(DONE)
    周期性心跳,
    
    (启动事务,获取会话锁)
    
    获取对应的会话,更新会话心跳时间。
    
    (提交事务,释放会话锁)

  • 会话检测线程(DONE)
    周期性检测,
    
    (启动事务,获取会话锁)

    找出过期的会话,
    
     (获取任务锁)
     重置过期会话的任务(状态=空闲,下次调度时间)
     
     (提交事务,释放任务锁和会话锁)

  • API
    对外暴露HTTP API,主要包括几个功能:
    
    1)添加任务
    2)删除任务
    3)暂停任务
    4)恢复任务
    5)查看任务
    6)查看历史
    7)查看集群
    
  • 异常处理
因为MYSQL可能访问异常,此时SQL执行是否成功是未知的(例如仅仅是response时网络中断)。

本程序最大难点在于数据库更新和内存更新的一致性问题。

实现不能做出假设,需对异常case考虑全面,下面是暂时想到的一些异常处理点:

1)空闲->预备 异常,不应该放入prepare集合。 

SQL异常忽略,由补偿线程去发现不在prepare集合中的"预备"任务。

2)预备->执行 异常,不应该从prepare集合转移至executing集合,不应该启动执行线程。

SQL异常忽略,由补偿线程去发现不在executing集合中的"执行"任务。

3)执行->空闲 异常,不应该从executing集合删除。

立即无限重试即可,若发现状态=空闲,则幂等处理,即从exeuting集合删除。

4)预备/执行->空闲 异常

这是补偿线程的逻辑,下次重新select再次执行即可

依赖