先介紹下問題:
組內(nèi)有十來臺(tái)機(jī)器,上面用 cron 分別定時(shí)執(zhí)行著一些腳本和 shell 命令,一開始任務(wù)少的時(shí)候,大家都記得哪臺(tái)機(jī)器執(zhí)行著什么,隨著時(shí)間推移,人員幾經(jīng)變動(dòng),任務(wù)也越來越多,再也沒人能記得清哪些任務(wù)在哪些機(jī)器上執(zhí)行了,排查和解決后臺(tái)腳本的問題也越來越麻煩。
解決這個(gè)問題也不是沒有辦法:
除此之外,任務(wù)的修改也非常不方便,如果想給在 crontab 里修改某一項(xiàng)任務(wù),還需要找運(yùn)維操作。雖然解決這個(gè)問題也有辦法,使用 crontab cronfile.txt 直接讓 crontab 加載文件,但引入新的問題:任務(wù)文件加載的實(shí)時(shí)性不好控制。
為了解決以上問題,我結(jié)合 cron 和任務(wù)管理,每天下班后花一點(diǎn)時(shí)間,實(shí)現(xiàn)一個(gè)小功能,最后完成了 gotorch 的可用版。看著 GitHub 的 commit 統(tǒng)計(jì),還挺有成就感的~
這里放上 GitHub 鏈接地址: GitHub-zhenbianshu-gotorch ,歡迎 star/fork/issue。
介紹一下特色功能:
下面說一下功能實(shí)現(xiàn)的技術(shù)要點(diǎn):
文章歡迎轉(zhuǎn)載,但請(qǐng)帶上本文源地址:http://www.cnblogs.com/zhenbianshu/p/7905678.html,謝謝。
在實(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。
后來由于 “秒分時(shí)日月周” 的日期格式進(jìn)位不規(guī)則,代碼太復(fù)雜,實(shí)現(xiàn)出來效率也不比原來好,終于放棄了這種想法。。采用了跟 cron 一樣的執(zhí)行思路。
此外,我添加了三種限制任務(wù)執(zhí)行的方式:
而任務(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í)行了多次。
本服務(wù)是做成了一個(gè)類似 nginx 的服務(wù),我將進(jìn)程的 pid 保存在一個(gè)臨時(shí)文件中,對(duì)進(jìn)程操作時(shí)通過命令行給進(jìn)程發(fā)送信號(hào),只需要注意下異常情況下及時(shí)清理 pid 文件就好了。
這里說一下 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ì)被“過繼”給 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)程退出
將進(jìn)程制作為守護(hù)進(jìn)程之后,進(jìn)程與外界的通信就只好依靠信號(hào)了,Go 的 signal 包搭配 goroutine 可以方便地監(jiān)聽、處理信號(hào)。同時(shí)我們使用 syscall 包內(nèi)的 Kill 方法來向進(jìn)程發(fā)送信號(hào)。
我們監(jiān)聽 Kill 默認(rèn)發(fā)送的信號(hào)SIGTERM,用來處理服務(wù)退出前的清理工作,另外我還使用了用戶自定義信號(hào)SIGUSR2 用來作為終端通知服務(wù)重啟的消息。
一個(gè)信號(hào)從監(jiān)聽到捕捉再到處理的完整流程如下:
1.首先我們使用創(chuàng)建一個(gè)類型為 os.Sygnal 的無緩沖channel,來存放信號(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)通過 channel 傳遞過來,此時(shí) goroutine 便不會(huì)繼續(xù)阻塞。
5.通過后面的代碼處理對(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) } }()
gotorch 的開發(fā)共花了三個(gè)月,每天半小時(shí)左右,1~3 個(gè) commits,經(jīng)歷了三次大的重構(gòu),特別是在代碼格式上改得比較頻繁。 不過使用 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)文章!
標(biāo)簽:武漢 江西 延邊 新余 張掖 宜賓 嘉峪關(guān) 黑龍江
巨人網(wǎng)絡(luò)通訊聲明:本文標(biāo)題《詳解Gotorch多機(jī)定時(shí)任務(wù)管理系統(tǒng)》,本文關(guān)鍵詞 詳解,Gotorch,多機(jī),定時(shí),任務(wù),;如發(fā)現(xiàn)本文內(nèi)容存在版權(quán)問題,煩請(qǐng)?zhí)峁┫嚓P(guān)信息告之我們,我們將及時(shí)溝通與處理。本站內(nèi)容系統(tǒng)采集于網(wǎng)絡(luò),涉及言論、版權(quán)與本站無關(guān)。