GO语言-调度中心与定时任务实践

1,949 阅读4分钟

Background

我们开发的过程中,总是免不了接触一些定时任务,希望系统每隔一段时间自动一项任务。

这篇文章主要是想总结一下Go语言中,我用到过的一些实现定时任务的方法,以及在分布式系统中,定时任务可能遇到的一些问题。

Timer

最简单的定时任务实现那一定是用定时器实现了,设置一个定时器,时间到了便执行任务,简单方便。

func TestTicker(t *testing.T) {
   ticker := time.NewTicker(2 * time.Second)
   defer ticker.Stop()
   for {
      select {
      case <-ticker.C:
         t.Logf("exec time:%v", time.Now())
      }
   }
}

由于timer是一个一次性的定时器,设置一次,触发一次,时间到了后需要重新设置时间。所以这里选择周期性触发的Ticker。

定时器执行定时任务的缺点也显而易见了,它只能设置一个固定的interval,使用不够灵活。

实际生产中的定时更多的是一些固定时间的跑批任务,每天凌晨0点的数据库备份任务,凌晨1点用户特征计算任务等。定时器不能做到设置几点几分开始,每隔多久执行一次,设置结束时间等功能,在生产中一般不会用来做定时任务。

cron定时任务

linux下有一个crontab 命令,可以在固定的间隔时间执行指定的系统指令或 shell script脚本。

crontab [-u username]    //省略用户表表示操作当前用户的crontab
    -e      (编辑工作表)
    -l      (列出工作表里的命令)
    -r      (删除工作作)

crontab 的结构为 cron表达式+命令

执行命令 crontab -e
编辑cron表达式,每分钟向~/test_cron.sh文件输入一句test cron
* * * * * echo "test cron\n" >> ~/test_cron.sh

image.png

image.png 设置好后就会按你的配置执行定时任务

cron表达式的每个位置分别代表分、时、日、月、周,表达式的具体规则可以参考菜鸟教程

cron表达式的功能比定时器好用的多,很多公司的运维工作也都基于cron展开。

Golang也提供一个比较常用cron框架:

github.com/robfig/cron/v3

linux 中的 cron 只能精确到分钟。Go 提供的 cron 框架可以精确到秒。

i := 0
// 返回一个支持至 秒 级别的 cron
c := cron.New(cron.WithSeconds())
spec := "0/1 * * * * ?" //一分钟运行一次
c.AddFunc(spec, func() {
   i++
   fmt.Println("cron running:", i)
})
c.Start()

select {} // 阻塞进程以免直接退出了

精确到秒级的cron与linux的cron表达式有些许区别,每个位置分别代表秒、分、时、日、月、周。

是一个占位符,没有实际意义,只能用于周几和日期上。

image.png

贴个cron表达式字段含义的图

image.png cron也提供了一些预表达式使用,防止写错cron表达式

image.png

使用方式和cron表达式也基本一致

i := 0
// 返回一个支持至 秒 级别的 cron
c := cron.New(cron.WithSeconds())
//spec := "0/1 * * * * ?" //一分钟运行一次
c.AddFunc("@every 1s", func() {
   i++
   fmt.Println("cron running:", i)
})
c.Start()

select {}

cron用于定时任务非常好用,但使用起来也有它的局限性,非常重要的一点就是他不支持调度。

定时任务总有执行失败的时候,这时我们希望进行补偿,重新执行。或者发版时错过了某些任务的执行,也需要进行重跑,这些需求是cron定时任务做不到的。

在分布式系统下,我们一个服务部署在多台机器上,这时我们希望只有一台机器执行任务,这也是cron定时任务做不到的。

xxl-job调度中心

xxl-job是一个轻量级的分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。开箱即用。 官方文档

xxl-job的架构如图所示,提供了执行器注册,任务管理,日志等一系列功能。 image.png

下面来简单使用一下这个调度中心

首先利用docker部署一个xxl-job服务器

version: "3.9"
services:
  xxl-job-admin:
    restart: always
    # docker 镜像
    image: xuxueli/xxl-job-admin:2.3.1
    # 容器名称
    container_name: xxl-job-admin
    volumes:
      # 日志目录映射到主机目录
      - /Users/weizhifeng/GolandProjects/xxl-job-admin/logs:/data/applogs
    ports:
      # 端口映射
      - "8800:8800"
    environment:
      # 设置启动参数
      PARAMS: '
      --server.port=8800
      --server.servlet.context-path=/xxl-job-admin
      --spring.datasource.url=jdbc:mysql://docker.for.mac.host.internal:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
      --spring.datasource.username=root
      --spring.datasource.password=
      --spring.mail.host=smtp.qq.com
      --spring.mail.port=465
      --spring.mail.username=test_mail@qq.com
      --spring.mail.from=test_mail@qq.com
      --spring.mail.password=password123456
      --spring.mail.properties.mail.smtp.starttls.enable=true
      --spring.mail.properties.mail.smtp.starttls.required=true
      --xxl.job.accessToken=Lpoms_xxljob_default_token'

前置条件是在本地装好mysql数据库并执行下面链接中的sql语句初始化数据库:

github.com/xuxueli/xxl…

执行完后命令行执行 docker-compose up启动服务。

访问http://localhost:8800/xxl-job-admin/ 即可看到调度中心页面,默认登陆密码为admin/123456

image.png

服务器是java写的,先暂时不管它的实现原理,后续再深入了解一下。

下面使用go创建一个注册器,将任务注册到调度中心

1、注册之前需要在调度中心先注册执行器

image.png

2、代码进行自动注册

package main

import (
   "fmt"
   xxl "github.com/xxl-job/xxl-job-executor-go"
   "github.com/xxl-job/xxl-job-executor-go/example/task"
   "log"
)

//xxl.Logger接口实现
type logger struct{}

func (l *logger) Info(format string, a ...interface{}) {
   fmt.Println(fmt.Sprintf("自定义日志 - "+format, a...))
}

func (l *logger) Error(format string, a ...interface{}) {
   log.Println(fmt.Sprintf("自定义日志 - "+format, a...))
}
func main() {
   exec := xxl.NewExecutor(
      xxl.ServerAddr("http://127.0.0.1:8800/xxl-job-admin"),
      xxl.AccessToken("Lpoms_xxljob_default_token"), //请求令牌(默认为空)
      xxl.RegistryKey("golang-jobs"),                //执行器名称
      xxl.SetLogger(&logger{}),                      //自定义日志
   )
   exec.Init()
   //设置日志查看handler
   exec.LogHandler(func(req *xxl.LogReq) *xxl.LogRes {
      return &xxl.LogRes{Code: 200, Msg: "", Content: xxl.LogResContent{
         FromLineNum: req.FromLineNum,
         ToLineNum:   2,
         LogContent:  "这个是自定义日志handler",
         IsEnd:       true,
      }}
   })
   //注册任务handler
   exec.RegTask("task.test", task.Test)
   exec.RegTask("task.test2", task.Test2)
   exec.RegTask("task.panic", task.Panic)
   log.Fatal(exec.Run())
}

这段代码主要是向调度中心注册了一个执行器,然后向中心注册三个定时任务

3、在调度中心配置定时任务

任务需要提前在注册中心配置上才能成功注册。

注意选择正确的执行器,jobhandler是你要注册的任务,cron是希望定时任务的执行频率,新增成功点击执行一次,即可调用一次任务 image.png

image.png

调度日志中可以查看定时任务的执行情况。

image.png

启动任务后,定时任务就会按照cron表达式定时执行,可以通过执行一次对意外情况进行补偿,也可以灵活的启动和停止任务。

image.png

在分布式的架构下,xxl-job可以配置任务的路由规则进行调度,调度第一个服务,最后一个服务或者轮询服务。有的公司在这基础上进行改造也可以完成可靠的父子任务,分片任务的调度。

关于xxl-job更深入的介绍,还是需要参考官方文档www.xuxueli.com/xxl-job/