基于GO语言实现分布式定时任务学习(五)---- worker 代码开发与实现

worker 功能点

  1. 获取ETCD中,被master写入的cmd任务;
  2. 根据corn表达式,确定任务调度列表;
  3. 根据任务列表,进行任务执行;
  4. 对Job做分布式锁,防止集群并发调用。
  5. 执行日志存储;

worker 启动

同master ( init args(flags解析) -> init config -> init job(etcd) ) //todo master 连接


worker 功能实现

在这里插入图片描述
解释

  1. worker中的JobMgr负责向etcd进行任务监听,当其监听到任务后,根据任务类型进行简单封装,并将其投递到核心对象Scheduler进行调度;
  2. Scheduler 构建为单例,调度计划、执行计划与相应的任务执行结果都由该对象进行维护;
  3. Scheduler计算定时任务执行间隔,以便获得执行步长去做任务,而相应执行结果会通过channel的方式在投递回该对象;
  4. 其中有两点在设计的时候需要考虑 :
    4.1 某任务若仍旧处于执行中,但该任务新一轮次的定时任务已经到点,则该任务不应该执行。
    解决方式:通过构建一张任务执行表避免该问题,既任务执行前后对该任务执行表进行维护。
    4.2 如何避免一个任务被多节点并行了?
    解决方式:类似于elastic-job中呢样,建立一个分布式锁,去做任务的临界资源。

实体类介绍

common.Protocol.go

// 任务调度计划 
type JobSchedulePlan struct {
	Job *Job					// 要调度的任务信息
	Expr *cronexpr.Expression	// 解析好的cronexpr表达式
	NextTime time.Time			// 下次调度时间
}

// 任务执行状态
type JobExecuteInfo struct {
	Job *Job // 任务信息
	PlanTime time.Time // 理论上的调度时间
	RealTime time.Time // 实际的调度时间
	CancelCtx context.Context // 任务command的context
	CancelFunc context.CancelFunc//  用于取消command执行的cancel函数
}

// 任务执行结果
type JobExecuteResult struct {
	ExecuteInfo *JobExecuteInfo	// 执行状态
	Output []byte // 脚本输出
	Err error // 脚本错误原因
	StartTime time.Time // 启动时间
	EndTime time.Time // 结束时间
}

worker.Scheduler.go

// 任务调度
type Scheduler struct {
	jobEventChan      chan *common.JobEvent              //  etcd任务事件队列
	jobPlanTable      map[string]*common.JobSchedulePlan // 任务调度计划表
	jobExecutingTable map[string]*common.JobExecuteInfo  // 任务执行表
	jobResultChan     chan *common.JobExecuteResult      // 任务结果队列
}

worker.JobMgr.go

// 任务管理器
type JobMgr struct {
	client  *clientv3.Client
	kv      clientv3.KV
	lease   clientv3.Lease
	watcher clientv3.Watcher
}

worker.Executor.go

// 任务执行器
type Executor struct {

}

var (
	G_executor *Executor
)

功能实现

根据设计,master将任务被广播到etcd中,任务调度与执行基于worker来做。
呢么worker将会watch etcd中指定的prefix k,去领取任务,随手列入执行表,并最终执行;

JobMgr.go

//单例结构体赋值如下:

// 任务管理器
type JobMgr struct {
	client  *clientv3.Client
	kv      clientv3.KV
	lease   clientv3.Lease
	watcher clientv3.Watcher
}

var (
	// 单例
	G_jobMgr *JobMgr
)
// ------------------------------

// 监听任务变化
func (jobMgr *JobMgr) watchJobs() (err error) {
	var (
		getResp            *clientv3.GetResponse
		kvpair             *mvccpb.KeyValue
		job                *common.Job
		watchStartRevision int64
		watchChan          clientv3.WatchChan
		watchResp          clientv3.WatchResponse
		watchEvent         *clientv3.Event
		jobName            string
		jobEvent           *common.JobEvent
	)

	// 1, get一下/cron/jobs/目录下的所有任务,并且获知当前集群的revision,用于监听该reversion之后的变化
	if getResp, err = jobMgr.kv.Get(context.TODO(), common.JOB_SAVE_DIR, clientv3.WithPrefix()); err != nil {
		return
	}

	// 当前有哪些任务
	for _, kvpair = range getResp.Kvs {
		// 反序列化json得到Job
		if job, err = common.UnpackJob(kvpair.Value); err == nil {
			//将当前监听到的任务放置到jobScheduler
			jobEvent = common.BuildJobEvent(common.JOB_EVENT_SAVE, job)
			 同步给scheduler(调度协程,下文介绍)
			G_scheduler.PushJobEvent(jobEvent)
		}
	}

	// 2, 从该revision向后监听变化事件
	go func() { // 监听协程
		// 从GET时刻的后续版本开始监听变化
		watchStartRevision = getResp.Header.Revision + 1
		// 监听/cron/jobs/目录的后续变化
		watchChan = jobMgr.watcher.Watch(context.TODO(),
			common.JOB_SAVE_DIR, clientv3.WithRev(watchStartRevision), clientv3.WithPrefix())
		// 处理监听事件
		for watchResp = range watchChan {

			for _, watchEvent = range watchResp.Events {

				switch watchEvent.Type {

				case mvccpb.PUT: // 任务保存事件

					if job, err = common.UnpackJob(watchEvent.Kv.Value); err != nil {
						continue
					}
					// 构建一个更新Event
					jobEvent = common.BuildJobEvent(common.JOB_EVENT_SAVE, job)

				case mvccpb.DELETE: // 任务被删除了
					// Delete 若key为 /cron/jobs/job10 ,则需要删除的任务为 job10 
					jobName = common.ExtractJobName(string(watchEvent.Kv.Key))

					job = &common.Job{Name: jobName}

					// 构建一个删除Event
					jobEvent = common.BuildJobEvent(common.JOB_EVENT_DELETE, job)

				}

				// 变化推给scheduler(下文介绍)
				G_scheduler.PushJobEvent(jobEvent)

			}
		}
	}()
	return
}

common.Protocol.go.

// 反序列化Job字符串  (不知道怎么用Java呢样的泛型提供一个通用的方法)
func UnpackJob(value []byte) (ret *Job, err error) {
	var (
		job *Job
	)

	job = &Job{}
	if err = json.Unmarshal(value, job); err != nil {
		return
	}
	ret = job
	return
}

//----------
// 定时任务
type Job struct {
	Name     string `json:"name"`     //  任务名
	Command  string `json:"command"`  // shell命令
	CronExpr string `json:"cronExpr"` // cron表达式
}
//对Job进行一次包装,添加任务类型 
type JobEvent struct {
	EventType int //  SAVE, DELETE
	Job       *Job
}
//-------

Scheduler.go

// 初始化调度器
func InitScheduler() (err error) {
	G_scheduler = &Scheduler{
		//当大于1000的时候,无法继续放进来,除非里面有被取走的元素
		jobEventChan: make(chan *common.JobEvent, 1000),
		//key - > jobName
		jobPlanTable: make(map[string]*common.JobSchedulePlan), //调度计划表
		// key - > jobName
		jobExecutingTable: make(map[string]*common.JobExecuteInfo), //调度任务表
		jobResultChan:     make(chan *common.JobExecuteResult, 1000),
	}
	// 启动调度协程
	go G_scheduler.scheduleLoop()
	return
}


// 调度协程,检测是否有相应的任务需要执行
func (scheduler *Scheduler) scheduleLoop() {

	var (
		jobEvent      *common.JobEvent
		scheduleAfter time.Duration
		scheduleTimer *time.Timer
		//jobResult     *common.JobExecuteResult
	)

	// 初始化一次(1秒)
	scheduleAfter = scheduler.TrySchedule()

	// 调度的延迟定时器
	scheduleTimer = time.NewTimer(scheduleAfter)

	// 定时任务common.Job
	for {

		//监听任务变化事件
		select {

		// 对内存中维护的任务列表做增删改查
		case jobEvent = <-scheduler.jobEventChan:
			//既对Scheduler.jobPlan map 进行修改 ;并且当任务需要执行时,将任务分发至执行器
			scheduler.handleJobEvent(jobEvent)
		// 最近的任务到期了
		case <-scheduleTimer.C:
			// 监听任务执行结果
		case jobResult = <-scheduler.jobResultChan:
			scheduler.handleJobResult(jobResult)
		}

		// 调度一次任务
		scheduleAfter = scheduler.TrySchedule()
		// 重置调度间隔
		scheduleTimer.Reset(scheduleAfter)

	}

}

// 推送任务变化事件
func (scheduler *Scheduler) PushJobEvent(jobEvent *common.JobEvent) {
	scheduler.jobEventChan <- jobEvent
}

// 处理任务事件,维护任务计划表
func (scheduler *Scheduler) handleJobEvent(jobEvent *common.JobEvent) {

	var (
		jobSchedulePlan *common.JobSchedulePlan
		jobExecuteInfo  *common.JobExecuteInfo
		jobExecuting 	bool
		jobExisted 		bool
		err        		error
	)

	switch jobEvent.EventType {

	// 保存任务事件
	case common.JOB_EVENT_SAVE:

		if jobSchedulePlan, err = common.BuildJobSchedulePlan(jobEvent.Job); err != nil {
			return
		}

		scheduler.jobPlanTable[jobEvent.Job.Name] = jobSchedulePlan

	// 删除任务事件
	case common.JOB_EVENT_DELETE:

		//判断map中是否有这个元素
		if jobSchedulePlan, jobExisted = scheduler.jobPlanTable[jobEvent.Job.Name]; jobExisted {
			delete(scheduler.jobPlanTable, jobEvent.Job.Name)
		}

	// 强杀任务事件
	case common.JOB_EVENT_KILL:

		//取消掉Command执行, 判断任务是否在执行中
		if jobExecuteInfo, jobExecuting = scheduler.jobExecutingTable[jobEvent.Job.Name]; jobExecuting {
			jobExecuteInfo.CancelFunc() // 触发command杀死shell子进程, 任务得到退出
		}

	}

}

//遍历定时任务计划表
//1 : 如果定时任务已经到期需要执行了,呢么执行任务并设置该任务的下次执行时间;
//2 : 从计划任务表找出即将执行的下一个定时任务。并以与当前时间的时间差作为可休眠的时间。
func (scheduler *Scheduler) TrySchedule() (scheduleAfter time.Duration) {

	var (
		jobPlan  *common.JobSchedulePlan
		now      time.Time
		nearTime *time.Time
	)

	// 如果任务表为空话,随便睡眠多久
	if len(scheduler.jobPlanTable) == 0 {
		scheduleAfter = 1 * time.Second
		return
	}

	// 当前时间
	now = time.Now()

	// 遍历所有任务
	for _, jobPlan = range scheduler.jobPlanTable {

		//任务到期,需要执行任务并且更新该任务的下次到期时间
		if jobPlan.NextTime.Before(now) || jobPlan.NextTime.Equal(now) {
			scheduler.TryStartJob(jobPlan)
			jobPlan.NextTime = jobPlan.Expr.Next(now)
		}

		// 统计最近一个要过期的任务时间
		if nearTime == nil || jobPlan.NextTime.Before(*nearTime) {
			nearTime = &jobPlan.NextTime
		}

	}

	// 下次调度间隔(最近要执行的任务调度时间 - 当前时间)
	if nil != nearTime {
		scheduleAfter = (*nearTime).Sub(now)
	}

	return
}

// 尝试执行任务,若任务仍在执行但又被安插了新一轮的任务过来,则应该规避
func (scheduler *Scheduler) TryStartJob(jobPlan *common.JobSchedulePlan) {
	// 调度 和 执行 是2件事情
	var (
		jobExecuteInfo *common.JobExecuteInfo
		jobExecuting bool
	)

	// 执行的任务可能运行很久, 1分钟会调度60次,但是只能执行1次, 防止并发!

	// 如果任务正在执行,跳过本次调度
	if jobExecuteInfo, jobExecuting = scheduler.jobExecutingTable[jobPlan.Job.Name]; jobExecuting {
		// fmt.Println("尚未退出,跳过执行:", jobPlan.Job.Name)
		return
	}

	// 构建执行状态信息
	jobExecuteInfo = common.BuildJobExecuteInfo(jobPlan)

	// 保存执行状态
	scheduler.jobExecutingTable[jobPlan.Job.Name] = jobExecuteInfo

	// 执行任务
	fmt.Println("执行任务:", jobExecuteInfo.Job.Name, jobExecuteInfo.PlanTime, jobExecuteInfo.RealTime)
	G_executor.ExecuteJob(jobExecuteInfo)
}

//回执任务结果
func (scheduler *Scheduler) PushJobResult(jobResult *common.JobExecuteResult) {
	scheduler.jobResultChan <- jobResult
}

worker.Executor.go

// 执行一个任务
func (executor *Executor) ExecuteJob(info *common.JobExecuteInfo) {
	go func() {
		var (
			cmd *exec.Cmd
			err error
			output []byte
			result *common.JobExecuteResult
			jobLock *JobLock
		)

		// 任务结果
		result = &common.JobExecuteResult{
			ExecuteInfo: info,
			Output: make([]byte, 0),
		}

		// 初始化分布式锁
		jobLock = G_jobMgr.CreateJobLock(info.Job.Name)

		// 记录任务开始时间
		result.StartTime = time.Now()

		// 上锁
		// 随机睡眠(0~1s)
		time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)

		err = jobLock.TryLock()
		defer jobLock.Unlock()

		if err != nil { // 上锁失败
			result.Err = err
			result.EndTime = time.Now()
		} else {
			// 上锁成功后,重置任务启动时间
			result.StartTime = time.Now()

			// 执行shell命令
			cmd = exec.CommandContext(info.CancelCtx, "/bin/bash", "-c", info.Job.Command)

			// 执行并捕获输出
			output, err = cmd.CombinedOutput()

			// 记录任务结束时间
			result.EndTime = time.Now()
			result.Output = output
			result.Err = err
		}
		// 任务执行完成后,把执行的结果返回给Scheduler,Scheduler会从executingTable中删除掉执行记录
		G_scheduler.PushJobResult(result)
	}()
}


jobLock.go

// 初始化一把锁
func InitJobLock(jobName string, kv clientv3.KV, lease clientv3.Lease) (jobLock *JobLock) {
	jobLock = &JobLock{
		kv: kv,
		lease: lease,
		jobName: jobName,
	}
	return
}

// 尝试上锁
func (jobLock *JobLock) TryLock() (err error) {
	var (
		leaseGrantResp *clientv3.LeaseGrantResponse
		cancelCtx context.Context
		cancelFunc context.CancelFunc
		leaseId clientv3.LeaseID
		keepRespChan <- chan *clientv3.LeaseKeepAliveResponse
		txn clientv3.Txn
		lockKey string
		txnResp *clientv3.TxnResponse
	)

	// 1, 创建租约(5秒)
	if leaseGrantResp, err = jobLock.lease.Grant(context.TODO(), 5); err != nil {
		return
	}

	// context用于取消自动续租
	cancelCtx, cancelFunc = context.WithCancel(context.TODO())

	// 租约ID
	leaseId = leaseGrantResp.ID

	// 2, 自动续租
	if keepRespChan, err = jobLock.lease.KeepAlive(cancelCtx, leaseId); err != nil {
		goto FAIL
	}

	// 3, 处理续租应答的协程
	go func() {
		var (
			keepResp *clientv3.LeaseKeepAliveResponse
		)
		for {
			select {
			case keepResp = <- keepRespChan:	// 自动续租的应答
				if keepResp == nil {
					goto END
				}
			}
		}
		END:
	}()

	// 4, 创建事务txn
	txn = jobLock.kv.Txn(context.TODO())

	// 锁路径
	lockKey = common.JOB_LOCK_DIR + jobLock.jobName

	// 5, 事务抢锁
	txn.If(clientv3.Compare(clientv3.CreateRevision(lockKey), "=", 0)).
		Then(clientv3.OpPut(lockKey, "", clientv3.WithLease(leaseId))).
		Else(clientv3.OpGet(lockKey))

	// 提交事务
	if txnResp, err = txn.Commit(); err != nil {
		goto FAIL
	}

	// 6, 成功返回, 失败释放租约
	if !txnResp.Succeeded {	// 锁被占用
		err = common.ERR_LOCK_ALREADY_REQUIRED
		goto FAIL
	}

	// 抢锁成功
	jobLock.leaseId = leaseId
	jobLock.cancelFunc = cancelFunc
	jobLock.isLocked = true
	return

FAIL:
	cancelFunc() // 取消自动续租
	jobLock.lease.Revoke(context.TODO(), leaseId) //  释放租约
	return
}

// 释放锁
func (jobLock *JobLock) Unlock() {
	if jobLock.isLocked {
		jobLock.cancelFunc()     // 取消我们程序自动续租的协程
		jobLock.lease.Revoke(context.TODO(), jobLock.leaseId) // 释放租约
	}
}

感悟与思考:

通过这一章节的慕课,对go里面的channel以及程序设计角度有了一定的认识。
案例中,是使用一个全局的单例对象对外开出channel,使相关handler做完工作后,将结果再通过channel告知回来,这样的耦合性自然而然的就下来了。

如同一个消息队列一样,发消息时候不关注订阅者会做什么,订阅者处理完了消息也不必着着急急的告知投递;

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值