詳解Gotorch多機(jī)定時(shí)任務(wù)管理系統(tǒng)
前言
先介紹下問(wèn)題:
組內(nèi)有十來(lái)臺(tái)機(jī)器,上面用 cron 分別定時(shí)執(zhí)行著一些腳本和 shell 命令,一開始任務(wù)少的時(shí)候,大家都記得哪臺(tái)機(jī)器執(zhí)行著什么,隨著時(shí)間推移,人員幾經(jīng)變動(dòng),任務(wù)也越來(lái)越多,再也沒人能記得清哪些任務(wù)在哪些機(jī)器上執(zhí)行了,排查和解決后臺(tái)腳本的問(wèn)題也越來(lái)越麻煩。
解決這個(gè)問(wèn)題也不是沒有辦法:
- 維護(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é)果匯總展示,但命令的寫法各式各樣,對(duì)比命令也是個(gè)沒頭腦的事。
- 使用開源分布式任務(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ì),還挺有成就感的~
這里放上 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 到下一分鐘的開始。
2.cron 在每分鐘的開始會(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 寫入防止重復(fù)執(zhí)行;
5.后續(xù) cron 還會(huì)進(jìn)行一些異常檢測(cè)和錯(cuò)誤處理操作。
明白了 cron 的執(zhí)行方式后,感覺每個(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ù)沒有正在執(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 中沒有 Unix 系統(tǒng)中的 fork 方法;于是啟動(dòng)守護(hù)進(jìn)程我采用 exec 之后立即執(zhí)行,即 fork and exec
的方式,而 Go 的 exec 包則支持這種方式。
在進(jìn)程最開始時(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)聽、處理信號(hào)。同時(shí)我們使用 syscall 包內(nèi)的 Kill 方法來(lái)向進(jìn)程發(fā)送信號(hào)。
我們監(jiān)聽 Kill 默認(rèn)發(fā)送的信號(hào)SIGTERM,用來(lái)處理服務(wù)退出前的清理工作,另外我還使用了用戶自定義信號(hào)SIGUSR2 用來(lái)作為終端通知服務(wù)重啟的消息。
一個(gè)信號(hào)從監(jiān)聽到捕捉再到處理的完整流程如下:
1.首先我們使用創(chuàng)建一個(gè)類型為 os.Sygnal 的無(wú)緩沖channel,來(lái)存放信號(hào)。
2.使用 signal.Notify() 函數(shù)注冊(cè)要監(jiān)聽的信號(hào),傳入剛創(chuàng)建的 channel,在捕捉到信號(hào)時(shí)接收信號(hào)。
3.創(chuàng)建一個(gè) goroutine,在 channel 中沒有信號(hào)時(shí) signal := <-channel 會(huì)阻塞。
4.Go 程序一旦捕捉到正在監(jiān)聽的信號(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) // 開啟一個(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 的開發(fā)共花了三個(gè)月,每天半小時(shí)左右,1~3 個(gè) commits,經(jīng)歷了三次大的重構(gòu),特別是在代碼格式上改得比較頻繁。 不過(guò)使用 Go 開發(fā)確實(shí)是挺舒心的,Go 的代碼很簡(jiǎn)潔, gofmt 用著非常方便。另外 Go 的學(xué)習(xí)曲線也挺平滑,熟悉各個(gè)常用標(biāo)準(zhǔn)包后就能進(jìn)行簡(jiǎn)單的開發(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)文章!
相關(guān)文章
使用Lumberjack+zap進(jìn)行日志切割歸檔操作
這篇文章主要介紹了使用Lumberjack+zap進(jìn)行日志切割歸檔操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-12-12GO語(yǔ)言對(duì)數(shù)組切片去重的實(shí)現(xiàn)
本文主要介紹了GO語(yǔ)言對(duì)數(shù)組切片去重的實(shí)現(xiàn),主要介紹了幾種方法,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-04-04Golang實(shí)現(xiàn)DFA算法對(duì)敏感詞過(guò)濾功能
DFA算法是確定性有限自動(dòng)機(jī),其特征是,有一個(gè)有限狀態(tài)集合和一些從一個(gè)狀態(tài)通向另一個(gè)狀態(tài)的邊,每條邊上標(biāo)記有一個(gè)符號(hào),通俗的講DFA算法就是把你要匹配的做成一顆字典樹,然后對(duì)你輸入的內(nèi)容進(jìn)行匹配的過(guò)程,本文將利用DFA算法實(shí)現(xiàn)敏感詞過(guò)濾,需要的可以參考一下2023-10-10一鍵定位Golang線上服務(wù)內(nèi)存泄露的秘籍
內(nèi)存泄露?別讓它拖垮你的Golang線上服務(wù)!快速掌握Go語(yǔ)言服務(wù)內(nèi)存泄漏排查秘籍,從此問(wèn)題無(wú)處遁形,一文讀懂如何精準(zhǔn)定位與有效解決Golang應(yīng)用中的內(nèi)存問(wèn)題,立即閱讀,讓性能飛升!2024-01-01Go語(yǔ)言實(shí)現(xiàn)ssh&scp的方法詳解
這篇文章主要為大家詳細(xì)介紹了如何利用Go語(yǔ)言實(shí)現(xiàn)ssh&scp,文中的示例代碼講解詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴可以了解一下2022-10-10