分布式任务调度
知识储备
任务执行原理

涉及的系统调用
pipe():创建2个文件描述符,fd[0]可读,fd[1]可写
fork():创建子进程,fd[1]被继承到子进程
dup2():重定向子进程stdout/stderr到fd[1]
exec():在当前进程内,加载并执行二进制程序
通过Command类执行任务
在golang中通过os/exec
可以提交执行任务
Cron表达式
Cron表达式语法
开源Cronexpr库
go get github.com/gorhill/cronexpr
调度多个Cron

etcd
核心特性
将数据存储在集群中的高可用K-V存储
允许应用实时监听存储中的K-V的变化
能够容忍单点故障,能够应对网络分区
传统存储模型
单点存储:节点宕机,服务将不可用
主从模式:主库宕机,从库暂时还能提供读功能,但是由于同步延迟会有一部分主库信息未能及时同步到从库,导致信息丢失(因为主库宕机了,无法继续同步)。同时从库身份切换成主库需要一定时间,会有段时间写功能异常。
抽屉原理
一个班级有60人,如果将一个秘密告诉其中的31人,那么随便挑选31个人,一定有一个人知道秘密
etcd与Raft
raft是强一致的集群日志同步算法,一个分布式协议
etcd是一个分布式KV存储
etcd利用raft算法在集群中同步key-value
quorum模型(大多数模型)
集群中有2N+1个节点(N>=1)

raft日志概念
replication:日志在leader生产,向follower复制,达到各个节点的日志序列最终一致
term:任期,重新选举产生的leader,其term单调递增
log index:日志行在日志序列的下标
raft异常场景

a~g都可能是第一任leader
f是第二任leader
f是第三任leader
e是第四任leader
a、c、d、g都可能是第四任leader
c是第六任leader
d是第七任leader
raft异常安全
选举leader需要半数以上节点参与
节点commit日志最多的允许选举为leader
commit日志同样多,则term、index越大的允许被选举为leader
raft工作示例
raft保证
提交成功的请求,一定不会丢
各个节点的数据将最终一致
交互协议
SDK内置gRPC协议,性能高效(一般的服务是基于HTTP+JSON协议实现的,性能低效)
重要特性
底层存储是按key有序排列的,可以顺序遍历
因为key有序,所以etcd天然支持按目录结构高效遍历
支持复杂事务,提供类似if...then...else...的事务能力
基于租约机制实现key的TTL过期
MVCC多版本控制
提交版本(revision)在etcd中单调递增。同key维护多个历史版本,用于实现watch机制(可以通过key+version在etcd中访问到旧值)
历史版本过多,可以通过执行compact命令完成删减
监听KV变化
通过watch机制,可以监听某个key,或者某个目录(key前缀)的连续变化
常用于分布式系统的配置分发、状态同步。
watch工作原理

lease租约

架构
传统crontab痛点
机器故障,任务停止调度,甚至crontab配置都找不回来
任务数量多,单机的硬件资源耗尽,需要人工迁移到其他机器
需要人工去机器上配置corn,任务执行状态不方便查看
分布式架构 核心要素
调度器:需要高可用,确保不会因为单点故障停止调度
执行器:需要扩展性,提供大量任务的并行处理能力
场景开源调度架构

伪分布式设计
分布式网络环境不可靠,RPC异常属于常态
Master下发任务RPC异常,导致Master与Worker状态不一致
Worker上报任务RPC异常,导致Master状态信息落后
异常CASE举例
状态不一致:Master下发任务给node1异常,实际上node1收到并开始执行
并发执行:Master重试下发任务给node2,结果node1与node2同时执行一个任务
状态丢失:Master更新zookeeper中任务状态异常,此时Master宕机切换Standby,任务仍旧处于旧状态
分布式伪命题
但凡需要经过网络的操作,都可能出现异常
将应用状态放在存储中,必然会出现内存与存储状态不一致
应用直接利用raft管理状态,可以确保最终一致性,但是成本太高
CAP理论(常用于分布式存储)
如:zookeeper、etcd等都应用了该理论

对于应用来说必须要具备P 分区容错性
,然后在C和A之间取舍,也就是说只能实现CP或AP
MySQL实现的就是CP(通过主从复制提供可用性,但是主节点挂掉,只能提供读,写服务异常),对于写入的数据可以立即被读取到。
BASE理论(常用于应用架构)

架构师能做什么?
架构简化,减少有状态服务,秉承最终一致性
架构折衷,确保异常可以被程序自我治愈
整体架构(概要设计)

架构思路
利用etcd同步全量任务列表到所有worker节点
每个worker独立调度全量任务,无需与Master通信(单worker调度全量任务会不会很慢?不会,因为在程序中遍历任务很快)
各个worker利用分布式锁抢占,解决并发调度导致相同任务同时执行的问题(抢到锁的就执行,没有就跳过)
Master功能点与实现(详细设计)
Master的功能点
任务管理HTTP接口:新建、修改、查看、删除
任务日志HTTP接口:查看任务执行历史日志
任务控制HTTP接口:提供强制结束任务的接口
实现web管理界面:基于jquery+bootstrap的Web控制台,前后端分离
管理后台

Master内部架构

任务管理
etcd结构

保存到etcd的任务会被实时同步到所以worker
任务日志
MongoDB结构

请求MongoDB,按任务名查看最近的执行日志
任务控制
etcd结构
/cron/killer/任务名 -> "" (key部分是将要杀死的任务名写入到/corn/killer中,value部分可以填写任意值)
worker监听/corn/killer
目录下put修改操作
master将要结束的任务名put到/cron/killer目录下,触发worker立即结束shell任务
Worker功能点与实现
Worker功能点
任务同步:监听etcd中/cron/jobs目录变化
任务调度:基于corn表达式计算,触发过期任务
任务执行:协程池并发执行多任务,基于etcd分布式锁抢占
日志保存:捕获任务执行输出,保存到MongoDB
Worker内部架构图

监听协程
利用watch API,监听/corn/jobs和/cron/killer目录的变化
将变化事件通过channel推送给调度协程,更新内存中的任务信息
调度协程
监听任务变更event,更新内存中维护的任务列表
检查任务cron表达式,扫描到期任务,交给执行协程运行
监听任务控制event,强制中断正在执行中的子进程
监听任务执行result,更新内存中任务状态,投递执行日志
执行协程
在etcd中抢占分布式乐观锁:/cron/lock/任务名
抢占成功则通过Command类执行Shell任务
捕获Command输出并等待子进程结束,将任务执行结果投递给调度协程
日志协程
监听调度发来的执行日志,放入一个batch中
对新batch启动定时器,超时未满自动提交
若batch被放满,那么立即提交,并取消自动提交定时器
最后更新于
这有帮助吗?