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