libi / dcron Goto Github PK
View Code? Open in Web Editor NEW轻量分布式定时任务库 a lightweight distributed job scheduler library
License: MIT License
轻量分布式定时任务库 a lightweight distributed job scheduler library
License: MIT License
启动新的节点,如果任务中有小于 10s(defaultDuration) 的定时任务, 可能会导致有的任务会同时在两个节点中运行。
解决方案: 在 dcron.Start() 中至少等待 defaultDuration 时间后再运行 d.cr.Start()。但是此方案可能会导致有的任务停滞运行一段时间。
目前每个服务节点是有状态的(job 存储),且状态不同步(每个节点内保存的 jobs 不同),并未实现分布式系统的高可用特性。与单机节点的风险无异。
当前的driver接口有一些冗余设计,现在提出新的设计
type DriverV2 interface {
Init(serviceName string, timeout time.Duration, logger dlog.Logger)
GetServiceNodeList() ([]string, error)
Start() error
}
旧版driver对每个driver的新建,都要使用特定的Conf,这其实是没必要的,我们完全可以只传入对应的client即可。
如此设计的原因:
redis的keys命令会影响redis性能,线上使用keys命令非常危险,建议替换成用scan实现
比如间隔5秒执行一次,当任务启动的时候服务器时间是
00:02 下一次执行应该是 00:07,而并非Cron的00:05
vendor/github.com/libi/dcron/dlog/logger.go:8:20: undefined: any
vendor/github.com/libi/dcron/dlog/logger.go:12:18: undefined: any
vendor/github.com/libi/dcron/dlog/logger.go:17:19: undefined: any
vendor/github.com/libi/dcron/dlog/logger.go:18:19: undefined: any
vendor/github.com/libi/dcron/dlog/logger.go:19:20: undefined: any
vendor/github.com/libi/dcron/dlog/logger.go:27:50: undefined: any
vendor/github.com/libi/dcron/dlog/logger.go:34:50: undefined: any
vendor/github.com/libi/dcron/dlog/logger.go:38:51: undefined: any
vendor/github.com/libi/dcron/dlog/logger.go:42:51: undefined: any
vendor/github.com/libi/dcron/dlog/logger.go:50:65: undefined: any
vendor/github.com/libi/dcron/dlog/logger.go:50:65: too many errors
note: module requires Go 1.19
看issue中对redis 7的支持是2月份增加的,但是releas还是去年的,希望能发布一个新的release,方便go mod引用
在运行中有A,B,C三个定时任务,请问怎样删除指定的那个定时,例如要删除B任务怎样删?
版本:master 本地测试
leaseRespChan 满了
func (e *EtcdDriver) SetHeartBeat(nodeID string) {
leaseID, err := e.putKeyWithLease(nodeID, nodeID)
if err != nil {
log.Printf("putKeyWithLease error: %v", err)
return
}
leaseRespChan, err := e.cli.KeepAlive(context.Background(), leaseID)
if err != nil {
log.Printf("keepalive error:%v", err)
return
}
// 尝试修复
go func (){
for {
select {
case resp := <-leaseRespChan:
if resp == nil {
log.Printf("ectd cli keepalive unexpected nil")
}
case <-time.After(businessTimeout):
log.Printf("ectd cli keepalive timeout")
}
}
} ()
}
建议将Duration放到可配置项中,不然
func (np *NodePool) tickerUpdatePool() { tickers := time.NewTicker(time.Second * defaultDuration) for range tickers.C { if np.dcron.isRun { np.updatePool() } } }
直接for循环扫描redis,对性能有影响.
为什么不直接用分布式锁实现?
通过各个节点在定时任务内抢锁方式实现,需要依赖各个节点系统时间完全一致,当系统时间有误差时可能会导致以下问题:
如果任务的执行时间小于系统时间差,任务仍然会被重复执行(某个节点定时执行完毕释放锁,又被另一个因为系统时间之后到达定时时间的节点取得锁)。
即使有极小的误差,因为某个节点的时间会比其他节点靠前,在抢锁时能第一时间取得锁,所以导致的结果是所有任务都只会被该节点执行,无法均匀分布到多节点。
dcron 是如何解决这个问题的呢,我看核心代码:
通过一致性hash 获取到的节点为当前节点的话即进行执行,好像也会出现时间误差的情况吧
//Run is run job
func (job JobWarpper) Run() {
//如果该任务分配给了这个节点 则允许执行
if job.Dcron.allowThisNodeRun(job.Name) {
if job.Func != nil {
job.Func()
}
if job.Job != nil {
job.Job.Run()
}
}
}
cron的stop是有返回一个context,可以判断,context.Done正在执行的任务是否都完成了
Dcron的stop忽略了这个参数,应该优化一下,支持等待任务完成
If two machines start the cron tab at the same time, the cron tab will stop after a while, the code is as follows
func main() {
drv, _ := dredis.NewDriver(&dredis.Conf{
Host: "10.203.169.19",
Port: 1841,
Password: "005f4ce0b221abf",
}, redis.DialConnectTimeout(time.Second*10))
dcronDemo := dcron.NewDcron("server1", drv, cron.WithSeconds())
//添加多个任务 启动多个节点时 任务会均匀分配给各个节点
dcronDemo.AddFunc("s1 test1", "0 */1 * * * *", func() {
fmt.Println("执行 service1 test1 任务", time.Now().Format("15:04:05"))
})
dcronDemo.AddFunc("s1 test2", "0 */1 * * * *", func() {
fmt.Println("执行 service1 test2 任务", time.Now().Format("15:04:05"))
})
dcronDemo.AddFunc("s1 test3", "0 */1 * * * *", func() {
fmt.Println("执行 service1 test3 任务", time.Now().Format("15:04:05"))
})
dcronDemo.Start()
//测试120分钟后退出
time.Sleep(120 * time.Minute)
}
在集群增减节点,哈希环同步时间中执行的任务可能会丢失或者被重复执行,这个可能需要有一个可行的稳定性维护方案。
讨论来源:
#25 (comment)
实现方式:
增加一种新的任务模式,可以将任务本身序列化后存储在redis/etcd中,在重启之后,获取当前副本所属服务类型的所有任务。
Dcron.Start() //启动分布式定时任务
Dcron.Stop()//停止定时任务
会产生数据竞争
代码
`
//分布式定时任务路由
Dcron.Start() //启动分布式定时任务
quit := make(chan os.Signal)
signal.Notify(quit, syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM)
sign := <-quit //阻塞等待结束信号
utils.Logger().Info().Msgf("%v %v %v %+v", "SERVER_RUN", "server_close_sig", "", sign)
//停止定时任务
Dcron.Stop()
==================
WARNING: DATA RACE
Read at 0x00c000594038 by goroutine 46:
github.com/libi/dcron.(*NodePool).tickerUpdatePool()
C:/Users/yunwe/go/pkg/mod/github.com/libi/[email protected]/node_pool.go:77 +0xfb
github.com/libi/dcron.(*NodePool).StartPool.func1()
C:/Users/yunwe/go/pkg/mod/github.com/libi/[email protected]/node_pool.go:57 +0x39
Previous write at 0x00c000594038 by main goroutine:
github.com/libi/dcron.(*Dcron).Stop()
C:/Users/yunwe/go/pkg/mod/github.com/libi/[email protected]/dcron.go:159 +0x39
main.main()
D:/code_qifan/jump_live/main.go:81 +0x486
`
error output:
panic: got 4 elements in cluster info address, expected 2 or 3
我的代码:
drvRedis, _ := redis.NewDriver(&redis.Conf{
Addr: conf.RedisSetting.RedisHost,
Password: conf.RedisSetting.RedisPassword,
//Wait: true,
})
if err := drvRedis.Ping(); err != nil {
log.Fatal(err.Error())
return
}
log.Println("init redis ======")
dCronEngine := dcron.NewDcron("cron_server1", drvRedis, cron.WithSeconds())
tg := new(product_platform.TaskGroup)
//每小时执行一次
//dcr.AddFunc("TestTask", "0 0 * * * *", tg.TestTask)
dCronEngine.AddFunc("GetBipData", "0 0 * * * *", tg.GetBipData)
dCronEngine.AddFunc("GetTeaData", "0 0 * * * *", tg.GetTeaData)
dCronEngine.Start()
返回的错误日志:
2022-05-27 20:55:06.386 INFO runtime/proc.go:255 init redis ======
[dcron] 2022/05/27 20:55:06 INFO: addJob 'GetBipData' : 0 0 * * * *
[dcron] 2022/05/27 20:55:06 INFO: addJob 'GetTeaData' : 0 0 * * * *
[dcron] 2022/05/27 20:55:06 ERR: dcron start node pool error ERR unkown command or protocal error
部署到linux服务器上就出现错误了,本地没有什么事,请问这是什么问题,执行到start方法的时候就出错了?
@libi PTLA
大部分服务节点都部署在同一子网内,节点间通信不需要经过多级路由转换,通信效率会非常高。
所以可以考虑直接通过节点间通信来同步状态,大概思路如下:
这么做的优势在于可以去掉存储依赖,并且状态同步将更精准,不会产生心跳周期导致的瞬间状态不一致情况。
当然如果存在节点间跨机房等情况,不适用该 driver。
最近使用这个库的时候发现, 当redis数据量比较大的时候,在 GetNodes
时,会阻塞在 rd.scan(ctx, mathStr)
的位置, 请问我的推测合理吗, 我提一个fix代码修复一下?
@libi
环境变量设定支持 / 命令宏替换支持就不考虑了,不适合dcron
可以 补充一下bash命令支持
func (d *Dcron) AddCmds(jobName, cronStr string, cmds []string) (err error)
//JobWarpper is a job warpper
type JobWarpper struct {
ID cron.EntryID
Dcron *Dcron
Name string
CronStr string
Func func()
Job Job
Type string // command, http, rpc etc. 任务类型
Commands []string // 要执行的shell命令
// 用于存储分隔后的任务
cmd []string
}
func (job *JobWarpper) splitCmd(i int) {
job.cmd = strings.SplitN(job.Command[i], " ", 2)
}
func (job *JobWarpper) CmdRun() {
var cmd *exec.Cmd
var stdOut bytes.Buffer
var stdErr bytes.Buffer
var cmdStr string
for i := range job.Commands {
job.splitCmd(i)
// 超时控制
c := strings.Join(job.cmd, " ")
fmt.Println(c)
if job.Timeout > 0 {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(job.Timeout)*time.Second)
defer cancel()
cmd = exec.CommandContext(ctx, "sh", "-c", c)
} else {
cmd = exec.Command("sh", "-c", c)
}
//cmd.SysProcAttr = sysProcAttr
cmd.Stdout = &stdOut
cmd.Stderr = &stdErr
if i > 0 {
cmdStr = cmdStr + "\n" + c
} else {
cmdStr = c
}
if err := cmd.Start(); err != nil {
// 错误处理
break
}
if err := cmd.Wait(); err != nil {
// 错误处理
if p, _ := os.FindProcess(cmd.Process.Pid); p != nil {
// 无法杀死任务启动的子进程
p.Kill()
}
// 告警之类
break
}
}
result := "------ " + cmdStr + " ----\n" + stdOut.String() + " ----\n" + stdErr.String()
fmt.Println(result)
}
顺便提一嘴,可以加一下系统信号监控
func (np *NodePool) monitorExit() {
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
<-quit
// 一些退出前 要做的节点处理逻辑 如关闭redis 连接 通知其他节点我退出了==
}
问题说明:
当设置的标准日志输出,在addFunc中的func想要日志输出,实际上并没有输出日志。设置WithPrintLogInfo只是打印了dcron的日志,cron的日志还是无法打印。
问题原因:
默认设置日志格式的时候
// WithLogger both set dcron and cron logger.
func WithLogger(logger interface{ Printf(string, ...interface{}) }) Option {
return func(dcron *Dcron) {
//set dcron logger
dcron.logger = logger
//set cron logger,这里设置的是标准日志输出,很多信息无法打印
f := cron.WithLogger(cron.PrintfLogger(logger))
dcron.crOptions = append(dcron.crOptions, f)
}
}
// PrintfLogger wraps a Printf-based logger (such as the standard library "log")
// into an implementation of the Logger interface which logs errors only.
func PrintfLogger(l interface{ Printf(string, ...interface{}) }) Logger {
return printfLogger{l, false}
}
// VerbosePrintfLogger wraps a Printf-based logger (such as the standard library
// "log") into an implementation of the Logger interface which logs everything.
func VerbosePrintfLogger(l interface{ Printf(string, ...interface{}) }) Logger {
return printfLogger{l, true}
}
建议设置dcron.loginfo设置为true的时候,则cron改成,f := cron.VerbosePrintfLogger(cron.PrintfLogger(logger)) ?
需要提交新feature可以在develop分支的基础上进行开发,然后提交到develop分支;
需要提交bugfix可以在master分支的基础上进行开发,经过完整测试之后和入master,保证master分支的稳定性。
1676531774.933469 [0 127.0.0.1:57462] "scan" "1759" "match" "distributed-cron:mole_show:"
1676531774.933515 [0 127.0.0.1:57462] "scan" "2623" "match" "distributed-cron:mole_show:"
1676531774.933555 [0 127.0.0.1:57462] "scan" "127" "match" "distributed-cron:mole_show:"
1676531774.933595 [0 127.0.0.1:57462] "scan" "2815" "match" "distributed-cron:mole_show:"
一秒执行redis命令200多次?这有点太多了吧,我设置的是秒级定时任务
可以考虑将 robfig/cron 完全内置:
对内可以实现更精准的任务执行时机判断。
对外用户调用接口将与robfig/cron完全一致,只通过替换包名即可无缝替换单机模式。 如果需要分布式支持,只需要配置额外的 option 选项开启分布式。
例如开启某个定时任务:
cron 用法
c := cron.New(cron.WithSeconds())
c.AddJob("* * * * * ?", job)
c.Start()
dcron用法
c := dcron.New(dcron.WithSeconds())
c.AddJob("* * * * * ?", job)
c.Start()
如需开启分布式时:
c := dcron.New(dcron.WithSeconds(),dcron.WithDistributed(dcron.Driver))
c.AddJob("* * * * * ?", job)
c.Start()
https://github.com/libi/dcron/blob/master/driver/etcd/etcd_driver.go#L147
是否需要用etcd.Status这个接口来实现ping?
redis_driver.go:95:21: multiple-value uuid.NewV4() in single-value context
我想问下:
文档中提到 "负载均衡:根据任务数据和节点数据均衡分发任务。",
兄弟,既然是分布式定时器库,我觉得想法可以进一步展开。
本地启动三个实例,会一直在其中一个实例运行,不会分配到其他实例
dcron在和gorm/gen一起使用的时候
go.mod
module dcron-test
go 1.21
require (
github.com/go-redis/redis/v8 v8.11.5
github.com/libi/dcron v0.5.1
gorm.io/gen v0.3.23
)
go mod tidy
go: finding module for package go.opentelemetry.io/otel/internal/metric
go: finding module for package go.opentelemetry.io/otel/semconv
go: finding module for package go.opentelemetry.io/otel/unit
go: found go.opentelemetry.io/otel/internal/metric in go.opentelemetry.io/otel/internal/metric v0.27.0
go: finding module for package go.opentelemetry.io/otel/metric/registry
go: finding module for package go.opentelemetry.io/otel/semconv
go: dcron-test/jobx imports
github.com/libi/dcron tested by
github.com/libi/dcron.test imports
go.etcd.io/etcd/tests/v3/integration imports
go.etcd.io/etcd/server/v3/embed imports
go.opentelemetry.io/otel/semconv: module go.opentelemetry.io/otel@latest found (v1.19.0), but does not contain package go.opentelemetry.io/otel/semconv
go: dcron-test/jobx imports
github.com/libi/dcron tested by
github.com/libi/dcron.test imports
go.etcd.io/etcd/tests/v3/integration imports
go.etcd.io/etcd/server/v3/embed imports
go.opentelemetry.io/otel/exporters/otlp imports
go.opentelemetry.io/otel/sdk/metric/controller/basic imports
go.opentelemetry.io/otel/metric/registry: module go.opentelemetry.io/otel/metric@latest found (v1.19.0), but does not contain package go.opentelemetry.io/otel/metric/registry
有啥办法解决吗
如题,感谢
https://docs.codecov.com/docs/quick-start
@libi
我觉得可以按照这个教程配置一下codecov的看板(由于需要owner权限所以我做不了这个事情),这样子用户对库也会更有信心。
项目中的定时任务越来越多,为了防止任务重复执行曾经使用过的方案:
第一种方案没有容错机制,当单个节点宕机,所有定时任务都无法正常执行。
第二种方案不能跟cron一样灵活设定时间,比如需要设定每天1点执行就必须借助数据库或者其他存储手段去轮询,非常低效。
在对比了市面上主流的分布式定时任务库后,发现要不就是过重,要不就是使用复杂或者不能使用golang无缝接入。所以萌生了开发一个分布式定时任务库。
要解决的痛点主要包括:
在参考了gojob源码后想到了全新的解决思路:
将所有节点存入公共存储(目前基本所有项目都使用redis作为缓存库,所以首先开发了redis支持)后使用一致性hash算法来选举出执行单个任务的节点来保证唯一性,所有节点都按照写入的cron预执行,在任务执行入口处根据一致性hash算法来判断该任务是否应该由当前节点执行。
最近一直遇到报错:[DCron]2023/11/29 04:38:00 ERR: node pool is empty;
使用的版本
github.com/libi/dcron v0.2.2
github.com/robfig/cron/v3 v3.0.1
[root@dev-node1-ops1 log]# go version
go version go1.19.10 linux/amd64
初始化和相关代码,分钟级别的cron有50+,秒级的cron有一个:
`drv, _ := redis.NewDriver(&redis.Conf{
Host: g.Config().RedisStandalone.Host,
Port: g.Config().RedisStandalone.Port,
})
logger := log.New(os.Stdout, "[DCron]", log.LstdFlags)
rec := dcron.CronOptionChain(cron.Recover(cron.PrintfLogger(logger)))
dc := dcron.NewDcronWithOption("HbsCron", drv, rec, dcron.WithLogger(logger), dcron.WithHashReplicas(10), dcron.WithNodeUpdateDuration(time.Second*10))
dc.AddFunc("cache.WarmUPForCMDB()", g.Config().DCron.WarmUPForCMDB, func() {
fmt.Println("DCron execute task: cache.WarmUPForCMDB()", time.Now().Format("15:04:05"))
cache.WarmUPForCMDB()
})
dc.AddFunc("cache.LoopInit()", g.Config().DCron.LoopInit, func() {
fmt.Println("DCron execute task: cache.LoopInit()", time.Now().Format("15:04:05"))
cache.MonitoredHosts.Init()
cache.MonitoredHosts.CorrectInitHost()
})
......
dc.Start()
secondLevelCron := dcron.NewDcron("secondLevelCron", drv, cron.WithSeconds())
secondLevelCron.AddFunc("cache.DeliveryTimeoutCheck()", g.Config().DCron.DeliveryTimeoutCheck, func() {
fmt.Println("DCron execute secondLevelCron task: cache.DeliveryTimeoutCheck()", time.Now().Format("15:04:05"))
cmd_tunnel.DeliveryTimeoutCheck()
})
secondLevelCron.Start()
`
具体报错
Nov 29 04:38:00 dev-node1-ops1 hbs: DCron execute secondLevelCron task: cache.DeliveryTimeoutCheck() 04:38:00
Nov 29 04:38:00 dev-node1-ops1 hbs: [DCron]2023/11/29 04:38:00 INFO: job 'cache.AutoAddStatusForMaintenance()' running in node
Nov 29 04:38:00 dev-node1-ops1 hbs: [DCron]2023/11/29 04:38:00 ERR: node pool is empty
Nov 29 04:38:00 dev-node1-ops1 hbs: [DCron]2023/11/29 04:38:00 INFO: job 'cache.PopulateWorkersByTeamManagers()' running in node
Nov 29 04:38:00 dev-node1-ops1 hbs: [DCron]2023/11/29 04:38:00 ERR: node pool is empty
Nov 29 04:38:00 dev-node1-ops1 hbs: [DCron]2023/11/29 04:38:00 INFO: job 'cache.SyncSpecifiedRecords()' running in node
Nov 29 04:38:00 dev-node1-ops1 hbs: [DCron]2023/11/29 04:38:00 INFO: job 'cache.SetStaleSysProperty()' running in node
Nov 29 04:38:00 dev-node1-ops1 hbs: [DCron]2023/11/29 04:38:00 ERR: node pool is empty
Nov 29 04:38:00 dev-node1-ops1 hbs: [DCron]2023/11/29 04:38:00 ERR: node pool is empty
Nov 29 04:38:00 dev-node1-ops1 hbs: [DCron]2023/11/29 04:38:00 INFO: job 'cache.CallIncident()' running in node
Nov 29 04:38:00 dev-node1-ops1 hbs: [DCron]2023/11/29 04:38:00 ERR: node pool is empty
Nov 29 04:38:00 dev-node1-ops1 hbs: [DCron]2023/11/29 04:38:00 INFO: job 'cache.GetCache2mongo()' running in node
Nov 29 04:38:00 dev-node1-ops1 hbs: [DCron]2023/11/29 04:38:00 ERR: node pool is empty
从mysql批量加入定时,是加一个for然后
for (⋯⋯) {
dcron.AddFunc("test n+1","*/3 * * * *",func(){
fmt.Println("执行 test n+1 任务",time.Now().Format("15:04:05"))
})
}
这样子添加吗?
driver不支持redis集群?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.