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
设置好后就会按你的配置执行定时任务
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表达式有些许区别,每个位置分别代表秒、分、时、日、月、周。
?是一个占位符,没有实际意义,只能用于周几和日期上。
贴个cron表达式字段含义的图
cron也提供了一些预表达式使用,防止写错cron表达式
使用方式和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的架构如图所示,提供了执行器注册,任务管理,日志等一系列功能。
下面来简单使用一下这个调度中心
首先利用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语句初始化数据库:
执行完后命令行执行 docker-compose up启动服务。
访问http://localhost:8800/xxl-job-admin/ 即可看到调度中心页面,默认登陆密码为admin/123456
服务器是java写的,先暂时不管它的实现原理,后续再深入了解一下。
下面使用go创建一个注册器,将任务注册到调度中心
1、注册之前需要在调度中心先注册执行器
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是希望定时任务的执行频率,新增成功点击执行一次,即可调用一次任务
调度日志中可以查看定时任务的执行情况。
启动任务后,定时任务就会按照cron表达式定时执行,可以通过执行一次对意外情况进行补偿,也可以灵活的启动和停止任务。
在分布式的架构下,xxl-job可以配置任务的路由规则进行调度,调度第一个服务,最后一个服务或者轮询服务。有的公司在这基础上进行改造也可以完成可靠的父子任务,分片任务的调度。
关于xxl-job更深入的介绍,还是需要参考官方文档www.xuxueli.com/xxl-job/