Golang中的自动伸缩和自防御设计
Raygun服務(wù)由許多活動組件構(gòu)成,每個組件用于特定的任務(wù)。其中一個模塊是用Golang編寫的,負責對iOS崩潰報告進行處理。簡而言之,它接受本機iOS崩潰報告,查找相關(guān)的dSYM文件,并生成開發(fā)者可以閱讀并理解的堆棧跟蹤信息。
?
dSYM-worker進程的操作非常簡單,它通過Redis隊列接收作業(yè)并執(zhí)行,然后不斷重復。這個dSYN-worker進程在一臺機器上運行,作業(yè)處理的速率及負載相對合理,但仍有一些情況需要運維人員隨時待命維護:
-
負載飆升。每隔一段時間,通常是在周末,用戶更多地使用iOS設(shè)備,iOS崩潰報告的數(shù)量可能會遠遠超過預(yù)設(shè)的數(shù)量。這種場景下,需要手動啟動更多dSYM-worker進程來處理任務(wù)并降低平均負載。每個新啟動的進程作為消費者消費任務(wù)隊列,它使用Golang ? ? Redis隊列庫分布式并發(fā)地完成多個作業(yè)。
-
不響應(yīng)(進程假死)。也就是說,進程仍然在運行,但是沒有執(zhí)行任何操作。大多數(shù)情況下,可能是由于死鎖造成的。糟糕的是,從監(jiān)控上看它仍在運行(但沒有消費隊列中的任務(wù)),因此只有當隊列達到閾值時才會發(fā)出警報。這種情況下,則需要手動終止假死進程,并啟動一個新的進程。(也可能需要啟動更多的進程,為了加速消費隊列中的任務(wù))。
-
意外終止。進程崩潰并完全關(guān)閉。這種情況從未發(fā)生在dSYM-worker上,但在更新代碼并發(fā)布時還是有可能發(fā)生。一旦發(fā)生,監(jiān)控會發(fā)出進程已死的警報,需要再次手動啟動該進程。
在深夜或凌晨處理這些問題非常痛苦,不論是對運維還是相關(guān)的負責人。以上這些人工操作理論上都應(yīng)當是自動化的,因此有了以下的這些實踐。
?
原理(Theory)
我們需要某種自動伸縮的能力來處理變化的、突增的負載,以某種方式檢測并重啟無響應(yīng)的、假死的進程。是時候制定一個計劃來解決問題了。
?
最初想法非常簡單。我們使用Golang Redis隊列庫在單個進程中將多個Consumer關(guān)聯(lián)到隊列。通過添加多個Consumer,可以一次性完成更多的工作,這有助于實現(xiàn)自動伸縮。此外,如果每個Consumer都跟蹤它們上一次完成工作的時間,那么就可以定期檢查其是否已經(jīng)長時間沒有運行。這可以用來實現(xiàn)對無響應(yīng)的Consumer的簡單檢測。于是開始著手研究這個計劃的可行性。
?
?
沒過多久就發(fā)現(xiàn)這種策略不會奏效——至少對Golang不起作用。每個Consumer都在Golang Redis隊列庫中的goroutine中進行管理。如果檢測到一個反應(yīng)遲鈍的Consumer,那么就需要關(guān)閉它;但事實證明,這并不是簡單地關(guān)閉一個goroutine。為了結(jié)束goroutine,通常要等到它完成工作,或者使用Channels或其他一些機制來打破循環(huán)。但如果Consumer陷入死循環(huán),很難控制goroutine關(guān)閉。即使有,也意味著需要修改Golang Redis隊列庫。因此這個策略變得越來越復雜,于是不得不思考其他的解決方案。
?
?
緊接著的下一個想法是編寫一個新的程序,生成并管理幾個Worker進程。每個Worker進程仍然可以將單個Consumer關(guān)聯(lián)到隊列上,但是運行的進程越多,意味著一次完成的工作就越多。Golang具有啟動和關(guān)閉子進程的能力,因此這對自動伸縮有很大幫助。不同的進程之間有不同的通信方式,因此Worker進程可以告訴Master進程它們上一次完成任務(wù)是什么時候。如果Master進程發(fā)現(xiàn)某個Worker已經(jīng)很長時間沒有反饋了,那么就會觸發(fā)響應(yīng)遲鈍和死亡檢測——稍后將對此進行更多介紹。?
?
?
?
另一種方法是建立hivemind。單個Worker進程可以同時處理作業(yè),并且生成和管理其他Worker進程。如果檢測到一個無響應(yīng)或假死進程,則另一個正在運行的進程可以負責啟動一個新進程。總的來說,它們可以確保始終有大量的進程在運行以處理隊列中的任務(wù)。最終采用了第一種方法——Master-Worker方法。
?
?
自動伸縮(Autoscaling)
Master進程首先針對一個goroutine自旋,該goroutine定時檢查Worker進程的數(shù)量。然后這個goroutine根據(jù)實際的進程數(shù)量來啟動或停止Worker進程,確保負載均勻。所需Worker進程的計算非常簡單,一般可以根據(jù)隊列的當前長度,隊列的生產(chǎn)/消費速率來計算。隊列越長,或生產(chǎn)作業(yè)的速度越快,對應(yīng)的Worker就越多。以下是對主要goroutine的簡單介紹:
funcwatch() {
procs:=?make(map[int]*Worker)
for?{
//?檢查每個Worker的健康狀況
checkProcesses(&procs)
queueLength,?rate:=queueStats()
//?計算所需的Worker進程數(shù),根據(jù)實際情況增減
desiredWorkerCount:=calculateDesiredWorkerCount(queueLength,?rate, len(procs))
if?len(procs)?!=desiredWorkerCount?{
manageWorkers(&procs,?desiredWorkerCount)
}
time.Sleep(30000*time.Millisecond)
}
}
?
Master進程需要跟蹤它啟動的子進程。這有助于自動伸縮,并定期檢查每個子進程的健康狀態(tài)。最簡單的方法是使用一個Map,以整型鍵作為鍵值(keys),以及一個Worker結(jié)構(gòu)(Worker struct)的實例作為值。這個Map的長度用于確定在添加Worker時使用的下一個鍵(keys),以及在刪除Worker時刪除哪個鍵。
?
funcmanageWorkers(procs*map[int]*Worker,?desiredWorkerCountint) {
currentCount:=?len(*procs)
ifcurrentCount?<?desiredWorkerCount?{
//?添加Workers:
forcurrentCount?<?desiredWorkerCount?{
StartWorker(procs,?currentCount)
currentCount++
}
}?elseifcurrentCount?>?desiredWorkerCount?{
//?移除Workers:
forcurrentCount?>?desiredWorkerCount?{
StopWorker(procs,?currentCount-1)
currentCount--
}
}
}
?
Golang為進程管理提供了一個os/exec包。Master進程使用這個包生成新的Worker進程。Master進程和Worker進程被部署到相同的文件夾中,因此”./dSYM-worker"可以用來啟動Worker。但是,Master進程作為守護進程運行時將不起作用。下面StartWorker函數(shù)中的第一行是如何獲得正在運行的進程的工作目錄。這樣就可以創(chuàng)建Worker可執(zhí)行文件的完整路徑來可靠地運行它。一旦進程運行,將從Worker結(jié)構(gòu)創(chuàng)建一個對象并將其存儲在Map中。
?
funcStartWorker(procs*map[int]*Worker,?indexint) {
dir,?err:=filepath.Abs(filepath.Dir(os.Args[0]))
iferr!=nil?{
//?錯誤及異常處理邏輯
}
cmd:=exec.Command(dir+"/dSYM-worker")
//?此處執(zhí)行實際業(yè)務(wù)邏輯并將結(jié)果存入Worker對象
//?例如檢索標準的輸入和輸出管道等,后面將對此進行詳細解釋
cmd.Start()
worker:=NewWorker(cmd,?index)
(*procs)[index] =?worker
}
?
確定作業(yè)負載所需的Worker數(shù)量,然后啟動/停止Worker以滿足,這是在本例中自動伸縮的實現(xiàn)。接下來將介紹如何阻止Worker進程假死。
?
Golang進程間通信(Inter process communication)
如上所述,檢測一個Worker進程是否有響應(yīng),簡單方法是由每個Worker報告上一次完成作業(yè)的時間。如果Master進程發(fā)現(xiàn)某個Worker時間周期太長,則可以認為它沒有響應(yīng),并將其替換為新的Worker。要實現(xiàn)這一點,Worker進程需要以某種方式與Master進程通信,以便在它完成任務(wù)時保存并通知Master當前時間。實現(xiàn)這一點的方式有很多:
-
讀取和寫入文件
-
設(shè)置本地隊列系統(tǒng),如Redis或RabbitMQ
-
使用Golang rpc包
-
通過本地網(wǎng)絡(luò)連接傳輸數(shù)據(jù)
-
利用共享內(nèi)存
-
設(shè)置命名管道(named pipes)
在我們的例子中,所傳輸?shù)闹皇菚r間戳,而不是重要的客戶數(shù)據(jù),因此上述這些方式大多數(shù)都有些殺雞用牛刀了。因此最終采取了簡單的解決方案——通過Worker進程的標準輸出管道進行通信。
通過exec.Command啟動新進程后,進程的標準輸出管道可通過如下方式得到:
stdoutPipe,?err:=cmd.StdoutPipe()
?
一旦有了標準的輸出管道,就可以運行g(shù)oroutine并發(fā)地監(jiān)聽它。在goroutine中,可以使用掃描器從管道讀取數(shù)據(jù),如下所示。在Worker進程將數(shù)據(jù)寫入標準輸出管道時,將執(zhí)行scanner.Text()并調(diào)用之后的業(yè)務(wù)代碼。
scanner:=bufio.NewScanner(stdoutPipe)
forscanner.Scan() {
line:=scanner.Text()
//?此處可處理讀取到的具體業(yè)務(wù)數(shù)據(jù)
}
?
無響應(yīng)監(jiān)測(Unresponsivenessdetection)
現(xiàn)在進程間通信已經(jīng)就緒,可以使用它來實現(xiàn)對無響應(yīng)的Worker進程進行檢測。更新現(xiàn)有的Worker邏輯,使之在完成工作時使用Golang fmt包打印出當前時間。而時間戳將被掃描器捕獲,并使用與打印時相同的格式解析時間。然后將time對象設(shè)置到相關(guān)Worker對象的LastJob字段中用于跟蹤。
t,?err:=time.Parse(time.RFC3339,?line)
iferr!=nil?{
//?此處為錯誤異常處理邏輯
}?else?{
worker.LastJob?=?t
}
?
回到定時掃描包含Worker進程的Map的goroutine程序中,現(xiàn)在可以比較每個Worker的當前時間和上一個作業(yè)時間。如果這個時間間隔太長,則終止該進程并重新啟動新的Worker進程。
funcCheckWorker(procs*map[int]*Worker,?worker*Worker,?indexint) {
//?如果Worker進程響應(yīng)時間間隔太長,則終止并重啟新的進程
duration:=time.Now().Sub(worker.LastJob)
ifduration.Minutes() >?4?{
KillWorker(procs,?index)
StartWorker(procs,?index)
}
}
?
可以通過調(diào)用進程對象的Kill函數(shù)來殺死進程。這是由生成進程時獲得的Command對象提供的。另一件需要做的事是從映射的Map中刪除Worker對象。在終止異常的Worker之后,可以通過調(diào)用StartWorker函數(shù)來啟動一個新的Worker。新Worker在映射的Map中使用的key與被終止的Worker進程相同——從而完成了Worker的替換邏輯。
funcKillWorker(procs*map[int]*Worker,?indexint) {
worker:=?(*procs)[index]
ifworker!=nil?{
process:=worker.Command.Process
delete(*procs,?index)?//?從Map中刪除無效Worker進程
err:=process.Kill()?//?終止進程
iferr!=nil?{
//?錯誤及異常處理
}
}
}
?
終止檢測(Terminationdetection)
從技術(shù)上講,對無響應(yīng)進程的檢測和解析還應(yīng)包括意外終止的進程。假死進程將無法報告它們正在執(zhí)行作業(yè),因此最終它們將由于響應(yīng)超時而被替換。針對這類進程,越早發(fā)現(xiàn)越好。
?
嘗試1
當啟動一個進程時,可以得到分配給它的pid。Golang os包有一個名為FindProcess的函數(shù),它返回給定pid的一個進程對象。如果能定期檢查跟蹤的每個進程,那么就知道某個特定的Worker是否處于假死狀態(tài)。但問題在于FindProcess函數(shù)總是會返回一些東西,即使給定的pid不存在任何進程(走進了死胡同?)
?
嘗試2
通過鍵入“kill – s 0 {pid}”,則不會向進程發(fā)送信號,但仍將執(zhí)行錯誤檢查。如果給定pid沒有進程,則會發(fā)生錯誤。這可以用Golang快速實現(xiàn),但不幸的是,在Golang中運行它不會產(chǎn)生任何錯誤。類似地,向不存在的進程發(fā)送0信號也不表示進程的存在。(另一個死胡同?)
?
最終方案
幸運的是實際上已經(jīng)有了一種機制來檢測假死進程。還記得用來監(jiān)聽Worker進程的scanner程序嗎?scanner監(jiān)聽的標準輸出管道對象是名為ReadCloser的類型。顧名思義,可以關(guān)閉它,如果另一端的Worker進程以任何方式停止,就會關(guān)閉它。如果管道關(guān)閉,scanner將停止偵聽并跳出循環(huán)。因此可以在循環(huán)邏輯之后,在代碼中插入邏輯以捕獲該Worker進程停止的信號。
?
現(xiàn)在需要確定的是Master進程的正常操作導致了Worker的關(guān)閉(例如殺死沒有響應(yīng)的Worker,或者由于負載減少而收縮),還是意外終止。在Master進程以任何原因殺死/停止一個Worker之前,它會從進程映射的Map中刪除它。因此,如果scanner掃描程序發(fā)現(xiàn)某進程已經(jīng)終止,但Worker進程的引用仍然登記在Map中,那表示它沒有在Master進程的控制下關(guān)閉。如果是這樣的話,需要進行替換。
if?(*procs)[worker.Index]?==worker?{
StartWorker(procs,?worker.Index)
}
通過在終端中使用kill命令來終止Worker進程,可以很容易地測試它的功能。
?
優(yōu)雅關(guān)閉(Gracefulshut down)
當?shù)谝淮问褂肎olang構(gòu)建自動伸縮行為的原型時,調(diào)用了上面列出的KillWorker函數(shù),以便于在負載較低時終止部分進程。設(shè)想如果一個Worker正在處理作業(yè)時被KillWorker函數(shù)終止,那么該作業(yè)會發(fā)生什么?在作業(yè)完成之前,它將安全地保存在Redis的隊列中。只有當Worker確認作業(yè)已完成時,它才會被移除。Master進程定期檢查已假死的Redis連接,并將未響應(yīng)的作業(yè)移回就緒隊列。這都是由Golang Redis隊列庫管理的。
?
這意味著當Worker進程意外終止時,不會丟失任何作業(yè)。這也意味著手動關(guān)閉進程完全可以正常工作。然而這感覺有點low,意味著這些作業(yè)將被延遲執(zhí)行。一種更好的解決方案是實現(xiàn)優(yōu)雅關(guān)閉—即允許Worker進程完成當前正在處理的工作,然后自然退出。
?
步驟1 - Master進程通知Worker停止
To start off, we need away for the master process to tell a particular worker process to begingraceful shut down. I’ve read that a common way of doing this is to send an OSsignal such as ‘interrupt’ to the worker process, and then have the worker handlethose signals to perform graceful shut down. For now though, I preferred toleave the OS signals to their default behaviours, and instead have the masterprocess send “stop” through the standard in pipe of a worker process.
首先需要一種方法讓Master進程告訴特定的Worker進程開始優(yōu)雅地關(guān)閉。一種常見的方法是向Worker進程發(fā)送一個OS信號,比如“interrupt”,然后讓Worker處理這些信號,執(zhí)行優(yōu)雅關(guān)閉。這里讓Master進程通過Worker進程管道中的標準輸出發(fā)送“stop”指令。
funcStopWorker(procs*map[int]*Worker,?indexint) {
worker:=?(*procs)[index]
stdinPipe:=worker.StdinPipe
_,?err:=?(*stdinPipe).Write([]byte("stop\n"))
iferr!=nil?{
//?錯誤及異常處理
}
}
?
步驟2 - Worker進程優(yōu)雅關(guān)閉
當Worker進程接收到“stop”消息時,它使用Golang Redis隊列庫來停止消費,并設(shè)置一個布爾字段來指示它已經(jīng)準備好優(yōu)雅地關(guān)閉。另一個布爾字段用于跟蹤作業(yè)當前是否在進行中。只要其中一個布爾值為真,程序就會保持活動。直到它們都為false,則意味著沒有作業(yè)要處理,可以被標記為優(yōu)雅關(guān)閉,此時程序自然終止。
funcscan(consumer*Consumer) {
reader:=bufio.NewReader(os.Stdin)
for?{
text,?_:=reader.ReadString('\n')
ifstrings.Contains(text,?"stop") {
stop(consumer)
}
}
}
?
步驟3 – Worker進程反饋Master已完成
在Master流程中需要從映射Map中刪除已關(guān)閉Worker。可以在給Worker發(fā)送“stop”消息后執(zhí)行此操作,但是如果上一個作業(yè)碰巧導致該Worker陷入一個意外的死循環(huán),會發(fā)生什么?為了更好地理清這個問題,當一個Worker進程完成了它的上一個任務(wù)并關(guān)閉時,它將打印一個“stop”消息。就像時間戳一樣,這個消息在之前設(shè)置的scanner程序中獲取。當Master看到這條消息時,可以停止跟蹤該Worker并將其從Map中刪除。
// Worker進程:
funcstop(consumer*Consumer) {
consumer.queue.StopConsuming()
consumer.running?=?false
if?!consumer.doingJob?{
fmt.Println("stop")
}
}
// Master進程的掃描流程:
ifstrings.Contains(line,?"stop") {
delete(*procs,?worker.Index)
break
}
?
誰來監(jiān)視監(jiān)控者?
此時dSYM-worker進程可以自動伸縮以處理突增的負載,并具有針對意外終止和無響應(yīng)性的自衛(wèi)機制。但是Master進程本身是否存在單點故障的可能性呢?它比Worker進程簡單得多,但是仍然有崩潰的危險。如果它發(fā)生故障,一切就又回到了老路上了。此時也可以設(shè)計一個自動重啟Master進程的機制。
?
有幾種方法可以確保Master進程在失敗時重新啟動。一種方法是在啟動守護進程配置中使用“KeepAlive”選項。另一種選擇是編寫一個腳本來檢查進程的健康狀況,如果發(fā)生故障就啟動它。類似的腳本可以通過Cron作業(yè)來實現(xiàn),每5分鐘左右運行一次。
?
最后創(chuàng)建了另一個Golang程序,它首先啟動Master進程,然后在檢測到終止時重新啟動它。這是采用與Master進程類似方式實現(xiàn)的。總的來說,它小而美且運行至今情況良好。
?
孤立的Worker
如果Master進程通過中斷信號被關(guān)閉,它關(guān)聯(lián)的所有Worker進程也將自動被妥善關(guān)閉。這對于部署新版本非常方便,因為只需要通知頂層Master進程關(guān)閉即可。但是如果Master進程崩潰了,那就另當別論了。所有的Worker進程都在不停地進行作業(yè),但卻沒有人監(jiān)督它們。當一個新的Master進程啟動時,會產(chǎn)生更多的Worker,如果這種情況持續(xù)發(fā)生而不進行任何清理,這時候就悲劇了。
?
這是一個需要處理的重要場景,有一個簡單的解決方案。Golang中的os包提供了一個Getppid()的方法。它不接受任何參數(shù),因此可以隨時調(diào)用它并獲得父進程的pid。如果父進程死亡,子進程將成為孤兒,函數(shù)將返回1(初始值)。因此,在Worker進程中,可以很容易地檢測它是否是孤立的。當一個Worker第一次啟動時,可以獲取并保存其初始父進程的pid。然后,定期調(diào)用Getppid函數(shù)并將結(jié)果與初始父pid進行比較。如果父pid發(fā)生變化,則表示該Worker已成為孤兒,此時可以優(yōu)雅關(guān)閉它。
ppid:=os.Getppid()
ifppid!=initialParentId?{
stop(consumer)?//?開始優(yōu)雅關(guān)閉
}
?
結(jié)束語
本文介紹了如何在Golang中實現(xiàn)自動伸縮和自防御服務(wù),到目前為止,它運行良好。
?
原文作者:Jason Fauchelle? 譯者:江瑋
原文鏈接:https://raygun.com/blog/2016/03/golang-auto-scaling/
版權(quán)聲明:本文版權(quán)歸作者(譯者)及公眾號所有,歡迎轉(zhuǎn)載,但未經(jīng)作者(譯者)同意必須保留此段聲明,且在文章頁面明顯位置給出,本文鏈接如有問題,可留言咨詢。
轉(zhuǎn)載于:https://www.cnblogs.com/davidwang456/p/10381816.html
總結(jié)
以上是生活随笔為你收集整理的Golang中的自动伸缩和自防御设计的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 增量架构方法与系统构建
- 下一篇: 你的响应阻塞了没有?--Spring-W