最近在学习 Go 语言,遵循着 “学一门语言最好的方式是使用它” 的理念,想着用 Go 来实现些什么,恰好工做中一直有一个比较让我烦恼的问题,因而用 Go 解决一下,即便不在生产环境使用,也能够做为 Go 语言学习的一种方式。css
先介绍下问题:html
组内有十来台机器,上面用 cron 分别定时执行着一些脚本和 shell 命令,一开始任务少的时候,你们都记得哪台机器执行着什么,随着时间推移,人员几经变更,任务也愈来愈多,再也没人能记得清哪些任务在哪些机器上执行了,排查和解决后台脚本的问题也愈来愈麻烦。linux
解决这个问题也不是没有办法:nginx
除此以外,任务的修改也很是不方便,若是想给在 crontab 里修改某一项任务,还须要找运维操做。虽然解决这个问题也有办法,使用 crontab cronfile.txt
直接让 crontab 加载文件,但引入新的问题:任务文件加载的实时性很差控制。git
为了解决以上问题,我结合 cron 和任务管理,天天下班后花一点时间,实现一个小功能,最后完成了 gotorch 的可用版。看着 GitHub 的 commit 统计,还挺有成就感的~github
这里放上 GitHub 连接地址: GitHub-zhenbianshu-gotorch
,欢迎 star/fork/issue
。web
介绍一下特点功能:shell
下面说一下功能实现的技术要点:数据库
文章欢迎转载,但请带上本文源地址:http://www.cnblogs.com/zhenbianshu/p/7905678.html,谢谢。编程
在实现相似 cron 的功能以前,我简单地看了一下 cron 的源码,源码在 https://busybox.net/downloads/ 能够下载,解压后文件在miscutils > crond.c
。
cron 的实现设计得很巧妙的,大概以下:
数据结构:
true
,例如 在天天的 3 点执行则 cl_Hrs[3]=true
;执行方式:
while(true)
式的长循环,每次 sleep
到下一分钟的开始。第 n 分/时/日/月/周
,并判断 cal_Time[n] 全为 true 则执行任务。明白了 cron 的执行方式后,感受每一个时间单位都遍历任务进行判断于性能有损耗,并且我实现的是秒级执行,遍历判断的性能损耗更大,因而考虑优化成:
给每一个任务设置一个 next_time 的时间戳,在一次执行后更新此时间戳,每一个时间单位只须要判断
task.next_time == current_time
。
后来因为 “秒分时日月周” 的日期格式进位不规则,代码太复杂,实现出来效率也不比原来好,终于放弃了这种想法。。采用了跟 cron 同样的执行思路。
此外,我添加了三种限制任务执行的方式:
而任务启动方式,则直接使用 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
用来做为终端通知服务重启的消息。
一个信号从监听到捕捉再处处理的完整流程以下:
os.Sygnal
的无缓冲channel,来存放信号。signal.Notify()
函数注册要监听的信号,传入刚建立的 channel,在捕捉到信号时接收信号。signal := <-channel
会阻塞。对应的代码以下:
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 火热得这么快了。
关于本文有什么问题能够在下面留言交流,若是您以为本文对您有帮助,能够点击下面的 推荐
支持一下我,博客一直在更新,欢迎 关注
。
参考: