115

Gotorch - 多机定时任务管理系统 - 枕边书

 6 years ago
source link: http://www.cnblogs.com/zhenbianshu/p/7905678.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Gotorch - 多机定时任务管理系统

最近在学习 Go 语言,遵循着 “学一门语言最好的方式是使用它” 的理念,想着用 Go 来实现些什么,刚好工作中一直有一个比较让我烦恼的问题,于是用 Go 解决一下,即使不在生产环境使用,也可以作为 Go 语言学习的一种方式。

先介绍下问题:

组内有十来台机器,上面用 cron 分别定时执行着一些脚本和 shell 命令,一开始任务少的时候,大家都记得哪台机器执行着什么,随着时间推移,人员几经变动,任务也越来越多,再也没人能记得清哪些任务在哪些机器上执行了,排查和解决后台脚本的问题也越来越麻烦。

解决这个问题也不是没有办法:

  • 维护一个 wiki,一旦任务有变动就更新 wiki,但一旦忘记更新 wiki,任务就会变成孤儿,什么时候出了问题更不好查。
  • 布置一台机器,定时拉取各机器的 cron 配置文件,进行对比统计,再将结果汇总展示,但命令的写法各式各样,对比命令也是个没头脑的事。
  • 使用开源分布式任务调度任务,比较重型,而且一般要布置数据库、后台,比较麻烦。

除此之外,任务的修改也非常不方便,如果想给在 crontab 里修改某一项任务,还需要找运维操作。虽然解决这个问题也有办法,使用 crontab cronfile.txt 直接让 crontab 加载文件,但引入新的问题:任务文件加载的实时性不好控制。

为了解决以上问题,我结合 cron 和任务管理,每天下班后花一点时间,实现一个小功能,最后完成了 gotorch 的可用版。看着 GitHub 的 commit 统计,还挺有成就感的~

 

819496-20171127192520409-705608500.png

这里放上 GitHub 链接地址: GitHub-zhenbianshu-gotorch ,欢迎 star/fork/issue

介绍一下特色功能:

  • cron+,秒级定时,使任务执行更加灵活;
  • 任务列表文件路径可以自定义,建议使用版本控制系统;
  • 内置日志和监控系统,方便各位同学任意扩展;
  • 平滑重加载配置文件,一旦配置文件有变动,在不影响正在执行的任务的前提下,平滑加载;
  • IP、最大执行数、任务类型配置,支持更灵活的任务配置;

下面说一下功能实现的技术要点:

文章欢迎转载,但请带上本文源地址:http://www.cnblogs.com/zhenbianshu/p/7905678.html,谢谢。


cron+

在实现类似 cron 的功能之前,我简单地看了一下 cron 的源码,源码在 https://busybox.net/downloads/ 可以下载,解压后文件在miscutils > crond.c

cron 的实现设计得很巧妙的,大概如下:

数据结构:

  1. cron 拥有一个全局结构体 global ,保存着各个用户的任务列表;
  2. 每一个任务列表是一个结构体 CronFile, 保存着用户名和任务链表等;
  3. 每一个任务 CronLine 有 shell 命令、执行 pid、执行时间数组 cl_Time 等属性;
  4. 执行时间数组的最大长度根据 “分时日月周” 的最大值确定,将可执行时间点的值置为 true,例如 在每天的 3 点执行则 cl_Hrs[3]=true

执行方式:

  1. cron是一个 while(true) 式的长循环,每次 sleep 到下一分钟的开始。
  2. cron 在每分钟的开始会依次遍历检查用户 cron 配置文件,将更新后的配置文件解析成任务存入全局结构体,同时它也定期检查配置文件是否被修改。
  3. 然后 cron 会将当前时间解析为 第 n 分/时/日/月/周,并判断 cal_Time[n] 全为 true 则执行任务。
  4. 执行任务时将 pid 写入防止重复执行;
  5. 后续 cron 还会进行一些异常检测和错误处理操作。

明白了 cron 的执行方式后,感觉每个时间单位都遍历任务进行判断于性能有损耗,而且我实现的是秒级执行,遍历判断的性能损耗更大,于是考虑优化成:

给每个任务设置一个 next_time 的时间戳,在一次执行后更新此时间戳,每个时间单位只需要判断 task.next_time == current_time

后来由于 “秒分时日月周” 的日期格式进位不规则,代码太复杂,实现出来效率也不比原来好,终于放弃了这种想法。。采用了跟 cron 一样的执行思路。

此外,我添加了三种限制任务执行的方式:

  • IP:在服务启动时获取本地内网 IP,执行前校验是否在任务的 IP 列表中;
  • 任务类型:任务为 daemon 的,当任务没有正在执行时则中断判断直接启动;
  • 最大执行数:在每个任务上设置一个执行中任务的 pid 构成的 slice,每次执行前校验当前执行数。

而任务启动方式,则直接使用 goroutine 配合 exec 包,每次执行任务都启动一个新的 goroutine,保存 pid,同时进行错误处理。由于服务可能会在一秒内多次扫描任务,我给每个任务添加了一个进程上次执行时间戳的属性,待下次执行时对比,防止任务在一秒内多次扫描执行了多次。


本服务是做成了一个类似 nginx 的服务,我将进程的 pid 保存在一个临时文件中,对进程操作时通过命令行给进程发送信号,只需要注意下异常情况下及时清理 pid 文件就好了。

这里说一下 Go 守护进程的创建方式:

由于 Go 程序在启动时 runtime 可能会创建多个线程(用于内存管理,垃圾回收,goroutine管理等),而 fork 与多线程环境并不能和谐共存,所以 Go 中没有 Unix 系统中的 fork 方法;于是启动守护进程我采用 exec 之后立即执行,即 fork and exec 的方式,而 Go 的 exec 包则支持这种方式。

在进程最开始时获取并判断进程 ppid 是否为1 (守护进程的父进程退出,进程会被“过继”给 init 进程,其进程号为1),在父进程的进程号不为1时,使用原进程的所有参数 fork and exec 一个跟自己相同的进程,关闭新进程与终端的联系,并退出原进程。

    filePath, _ := filepath.Abs(os.Args[0]) // 获取服务的命令路径
    cmd := exec.Command(filePath, os.Args[1:]...) // 使用自身的命令路径、参数创建一个新的命令
    cmd.Stdin = nil
    cmd.Stdout = nil 
    cmd.Stderr = nil // 关闭进程标准输入、标准输出、错误输出
    cmd.Start() // 新进程执行
    return // 父进程退出
    

将进程制作为守护进程之后,进程与外界的通信就只好依靠信号了,Go 的 signal 包搭配 goroutine 可以方便地监听、处理信号。同时我们使用 syscall 包内的 Kill 方法来向进程发送信号。

我们监听 Kill 默认发送的信号 SIGTERM,用来处理服务退出前的清理工作,另外我还使用了用户自定义信号 SIGUSR2 用来作为终端通知服务重启的消息。

一个信号从监听到捕捉再到处理的完整流程如下:

  1. 首先我们使用创建一个类型为 os.Sygnal 的无缓冲channel,来存放信号。
  2. 使用 signal.Notify() 函数注册要监听的信号,传入刚创建的 channel,在捕捉到信号时接收信号。
  3. 创建一个 goroutine,在 channel 中没有信号时 signal := <-channel 会阻塞。
  4. Go 程序一旦捕捉到正在监听的信号,就会把信号通过 channel 传递过来,此时 goroutine 便不会继续阻塞。
  5. 通过后面的代码处理对应的信号。

对应的代码如下:

    c := make(chan os.Signal)
    signal.Notify(c, syscall.SIGTERM, syscall.SIGUSR2) 

    // 开启一个goroutine异步处理信号
    go func() {
        s := <-c
        if s == syscall.SIGTERM {
            task.End()
            logger.Debug("bootstrap", "action: end", "pid "+strconv.Itoa(os.Getpid()), "signal "+fmt.Sprintf("%d", s))
            os.Exit(0)
        } else if s == syscall.SIGUSR2 {
            task.End()
            bootStrap(true)
        }
    }()

gotorch 的开发共花了三个月,每天半小时左右,1~3 个 commits,经历了三次大的重构,特别是在代码格式上改得比较频繁。 不过使用 Go 开发确实是挺舒心的,Go 的代码很简洁, gofmt 用着非常方便。另外 Go 的学习曲线也挺平滑,熟悉各个常用标准包后就能进行简单的开发了。 简单易学、高效快捷,难怪 Go 火热得这么快了。

关于本文有什么问题可以在下面留言交流,如果您觉得本文对您有帮助,可以点击下面的 推荐 支持一下我,博客一直在更新,欢迎 关注

论fork()函数与Linux中的多线程编程

linux 信号量之SIGNAL详解


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK