前言
先介紹下問題:
組內(nèi)有十來臺機器,上面用 cron 分別定時執(zhí)行著一些腳本和 shell 命令,一開始任務(wù)少的時候,大家都記得哪臺機器執(zhí)行著什么,隨著時間推移,人員幾經(jīng)變動,任務(wù)也越來越多,再也沒人能記得清哪些任務(wù)在哪些機器上執(zhí)行了,排查和解決后臺腳本的問題也越來越麻煩。
解決這個問題也不是沒有辦法:
- 維護一個 wiki,一旦任務(wù)有變動就更新 wiki,但一旦忘記更新 wiki,任務(wù)就會變成孤兒,什么時候出了問題更不好查。
- 布置一臺機器,定時拉取各機器的 cron 配置文件,進行對比統(tǒng)計,再將結(jié)果匯總展示,但命令的寫法各式各樣,對比命令也是個沒頭腦的事。
- 使用開源分布式任務(wù)調(diào)度任務(wù),比較重型,而且一般要布置數(shù)據(jù)庫、后臺,比較麻煩。
除此之外,任務(wù)的修改也非常不方便,如果想給在 crontab 里修改某一項任務(wù),還需要找運維操作。雖然解決這個問題也有辦法,使用 crontab cronfile.txt 直接讓 crontab 加載文件,但引入新的問題:任務(wù)文件加載的實時性不好控制。
為了解決以上問題,我結(jié)合 cron 和任務(wù)管理,每天下班后花一點時間,實現(xiàn)一個小功能,最后完成了 gotorch 的可用版??粗?GitHub 的 commit 統(tǒng)計,還挺有成就感的~
這里放上 GitHub 鏈接地址: GitHub-zhenbianshu-gotorch ,歡迎 star/fork/issue。
介紹一下特色功能:
- cron+,秒級定時,使任務(wù)執(zhí)行更加靈活;
- 任務(wù)列表文件路徑可以自定義,建議使用版本控制系統(tǒng);
- 內(nèi)置日志和監(jiān)控系統(tǒng),方便各位同學任意擴展;
- 平滑重加載配置文件,一旦配置文件有變動,在不影響正在執(zhí)行的任務(wù)的前提下,平滑加載;
- IP、最大執(zhí)行數(shù)、任務(wù)類型配置,支持更靈活的任務(wù)配置;
下面說一下功能實現(xiàn)的技術(shù)要點:
文章歡迎轉(zhuǎn)載,但請帶上本文源地址:http://www.cnblogs.com/zhenbianshu/p/7905678.html,謝謝。
cron+
在實現(xiàn)類似 cron 的功能之前,我簡單地看了一下 cron 的源碼,源碼在 https://busybox.net/downloads/ 可以下載,解壓后文件在miscutils > crond.c。
cron 的實現(xiàn)設(shè)計得很巧妙的,大概如下:
數(shù)據(jù)結(jié)構(gòu):
1.cron 擁有一個全局結(jié)構(gòu)體 global ,保存著各個用戶的任務(wù)列表;
2.每一個任務(wù)列表是一個結(jié)構(gòu)體 CronFile, 保存著用戶名和任務(wù)鏈表等;
3.每一個任務(wù) CronLine 有 shell 命令、執(zhí)行 pid、執(zhí)行時間數(shù)組 cl_Time 等屬性;
4.執(zhí)行時間數(shù)組的最大長度根據(jù) “分時日月周” 的最大值確定,將可執(zhí)行時間點的值置為 true,例如 在每天的 3 點執(zhí)行則 cl_Hrs[3]=true;
執(zhí)行方式:
1.cron是一個 while(true) 式的長循環(huán),每次 sleep 到下一分鐘的開始。
2.cron 在每分鐘的開始會依次遍歷檢查用戶 cron 配置文件,將更新后的配置文件解析成任務(wù)存入全局結(jié)構(gòu)體,同時它也定期檢查配置文件是否被修改。
3.然后 cron 會將當前時間解析為 第 n 分/時/日/月/周,并判斷 cal_Time[n] 全為 true 則執(zhí)行任務(wù)。
4.執(zhí)行任務(wù)時將 pid 寫入防止重復(fù)執(zhí)行;
5.后續(xù) cron 還會進行一些異常檢測和錯誤處理操作。
明白了 cron 的執(zhí)行方式后,感覺每個時間單位都遍歷任務(wù)進行判斷于性能有損耗,而且我實現(xiàn)的是秒級執(zhí)行,遍歷判斷的性能損耗更大,于是考慮優(yōu)化成:
給每個任務(wù)設(shè)置一個 next_time 的時間戳,在一次執(zhí)行后更新此時間戳,每個時間單位只需要判斷 task.next_time == current_time。
后來由于 “秒分時日月周” 的日期格式進位不規(guī)則,代碼太復(fù)雜,實現(xiàn)出來效率也不比原來好,終于放棄了這種想法。。采用了跟 cron 一樣的執(zhí)行思路。
此外,我添加了三種限制任務(wù)執(zhí)行的方式:
- IP:在服務(wù)啟動時獲取本地內(nèi)網(wǎng) IP,執(zhí)行前校驗是否在任務(wù)的 IP 列表中;
- 任務(wù)類型:任務(wù)為 daemon 的,當任務(wù)沒有正在執(zhí)行時則中斷判斷直接啟動;
- 最大執(zhí)行數(shù):在每個任務(wù)上設(shè)置一個執(zhí)行中任務(wù)的 pid 構(gòu)成的 slice,每次執(zhí)行前校驗當前執(zhí)行數(shù)。
而任務(wù)啟動方式,則直接使用 goroutine 配合 exec 包,每次執(zhí)行任務(wù)都啟動一個新的 goroutine,保存 pid,同時進行錯誤處理。由于服務(wù)可能會在一秒內(nèi)多次掃描任務(wù),我給每個任務(wù)添加了一個進程上次執(zhí)行時間戳的屬性,待下次執(zhí)行時對比,防止任務(wù)在一秒內(nèi)多次掃描執(zhí)行了多次。
守護進程
本服務(wù)是做成了一個類似 nginx 的服務(wù),我將進程的 pid 保存在一個臨時文件中,對進程操作時通過命令行給進程發(fā)送信號,只需要注意下異常情況下及時清理 pid 文件就好了。
這里說一下 Go 守護進程的創(chuàng)建方式:
由于 Go 程序在啟動時 runtime 可能會創(chuàng)建多個線程(用于內(nèi)存管理,垃圾回收,goroutine管理等),而 fork 與多線程環(huán)境并不能和諧共存,所以 Go 中沒有 Unix 系統(tǒng)中的 fork 方法;于是啟動守護進程我采用 exec 之后立即執(zhí)行,即 fork and exec
的方式,而 Go 的 exec 包則支持這種方式。
在進程最開始時獲取并判斷進程 ppid 是否為1 (守護進程的父進程退出,進程會被“過繼”給 init 進程,其進程號為1),在父進程的進程號不為1時,使用原進程的所有參數(shù) fork and exec 一個跟自己相同的進程,關(guān)閉新進程與終端的聯(lián)系,并退出原進程。
filePath, _ := filepath.Abs(os.Args[0]) // 獲取服務(wù)的命令路徑
cmd := exec.Command(filePath, os.Args[1:]...) // 使用自身的命令路徑、參數(shù)創(chuàng)建一個新的命令
cmd.Stdin = nil
cmd.Stdout = nil
cmd.Stderr = nil // 關(guān)閉進程標準輸入、標準輸出、錯誤輸出
cmd.Start() // 新進程執(zhí)行
return // 父進程退出
信號處理
將進程制作為守護進程之后,進程與外界的通信就只好依靠信號了,Go 的 signal 包搭配 goroutine 可以方便地監(jiān)聽、處理信號。同時我們使用 syscall 包內(nèi)的 Kill 方法來向進程發(fā)送信號。
我們監(jiān)聽 Kill 默認發(fā)送的信號SIGTERM,用來處理服務(wù)退出前的清理工作,另外我還使用了用戶自定義信號SIGUSR2 用來作為終端通知服務(wù)重啟的消息。
一個信號從監(jiān)聽到捕捉再到處理的完整流程如下:
1.首先我們使用創(chuàng)建一個類型為 os.Sygnal 的無緩沖channel,來存放信號。
2.使用 signal.Notify() 函數(shù)注冊要監(jiān)聽的信號,傳入剛創(chuàng)建的 channel,在捕捉到信號時接收信號。
3.創(chuàng)建一個 goroutine,在 channel 中沒有信號時 signal := -channel 會阻塞。
4.Go 程序一旦捕捉到正在監(jiān)聽的信號,就會把信號通過 channel 傳遞過來,此時 goroutine 便不會繼續(xù)阻塞。
5.通過后面的代碼處理對應(yīng)的信號。
對應(yīng)的代碼如下:
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)
}
}()
小結(jié)
gotorch 的開發(fā)共花了三個月,每天半小時左右,1~3 個 commits,經(jīng)歷了三次大的重構(gòu),特別是在代碼格式上改得比較頻繁。 不過使用 Go 開發(fā)確實是挺舒心的,Go 的代碼很簡潔, gofmt 用著非常方便。另外 Go 的學習曲線也挺平滑,熟悉各個常用標準包后就能進行簡單的開發(fā)了。 簡單易學、高效快捷,難怪 Go 火熱得這么快了。
以上就是詳解Gotorch多機定時任務(wù)管理系統(tǒng)的詳細內(nèi)容,更多關(guān)于Gotorch多機定時任務(wù)管理系統(tǒng)的資料請關(guān)注腳本之家其它相關(guān)文章!
您可能感興趣的文章:- go 實現(xiàn)簡易端口掃描的示例
- go xorm框架的使用
- 解析Go的Waitgroup和鎖的問題
- Go語言快速入門圖文教程
- go語言基礎(chǔ) seek光標位置os包的使用
- Go語言獲取文件的名稱、前綴、后綴
- Go語言 如何實現(xiàn)RSA加密解密
- Go 自定義package包設(shè)置與導入操作