網(wǎng)頁與網(wǎng)站的關(guān)系互聯(lián)網(wǎng)廣告代理可靠嗎
今天繼續(xù)優(yōu)化了bigpipe項目,核心目標就是解決重啟程序損失流量的問題。
背景
bigpipe作為一個消息中間件,其出現(xiàn)是為了給php程序提供方便的異步Http調(diào)用功能。然而php語言并不是常駐進程模型,當它請求bigpipe失敗后,頂多重試幾次,就必須盡快的向用戶返回應(yīng)答。因此,bigpipe服務(wù)的可用性是非常重要的。
bigpipe使用golang編寫,采用channel逐層緩沖流量和數(shù)據(jù),采用協(xié)程并發(fā)處理數(shù)據(jù)。因為bigpipe承接了若干業(yè)務(wù),經(jīng)常會對配置文件做一些修改,那么就必須重啟bigpipe。
思考
在最初的版本中,bigpipe提供了優(yōu)雅退出功能,也就是在退出前首先停止對外的Http服務(wù),然后將進程內(nèi)剩余的數(shù)據(jù)處理干凈,最后再退出,這樣不至于損失已經(jīng)接受到的數(shù)據(jù)請求。
優(yōu)雅退出存在一個問題,就是先要停止對外http服務(wù),這樣才不會有新的流量涌入,才有可能把緩沖在內(nèi)存里的剩余流量處理干凈。因為這個設(shè)計,導(dǎo)致在http停止服務(wù)后的一段時間內(nèi),客戶端是無法訪問bigpipe的,服務(wù)完全不可用。
最初的想法是,部署多個bigpipe,前端采用lvs/haproxy等負載均衡,這樣一旦http端口關(guān)閉,lvs會自動轉(zhuǎn)發(fā)流量。但是,這樣的缺點是要求bigpipe必須多點部署,而且lvs/haproxy并不能保證流量瞬時切換到正常節(jié)點,總要損失一些流量,而這就要求客戶端支持重試邏輯,總之不是一個完美的方案。
另外一個想法是,仍舊部署多個等價bigpipe組成集群,在bigpipe之前部署一個自研發(fā)的輕量級的proxy服務(wù),其支持多個bigpipe之間轉(zhuǎn)發(fā)重試,然而這樣不僅是帶來了更大的運維成本,其實還是沒有直面問題本質(zhì),在錯誤的路上越繞越遠。
方案
必須讓bigpipe支持配置熱加載,這一點實現(xiàn)起來并不是很簡單,下面我來說說難在哪里。
首先,在加載新的配置期間,不能停止http對外服務(wù),因此我決定Http模塊自身不支持熱加載(http監(jiān)聽地址,讀寫超時等簡單配置),它始終保持對外服務(wù)。
然而,請求的處理模塊等是需要加載新的配置的,在重新加載這些模塊期間,http接收的請求必須要緩沖起來,這樣才能做到流量0損失,因此我重新設(shè)計了模塊結(jié)構(gòu),在http接口層和業(yè)務(wù)處理層之間增加一個緩沖層,專門用來支撐熱加載期間的流量緩沖作用。
為了簡化設(shè)計,無論是否使用熱加載特性,這個緩沖層總是存在。
另外一個重要的變更點是,之前配置文件我采用了全局單例的模式,并假設(shè)了一旦加載就不會再變化。然而在golang這樣一個多線程并發(fā)的模型下,要支持熱加載配置,就不能讓配置自身成為單例了,否則各個模塊正在訪問單例的同時配置內(nèi)容加載成新的,那模塊就會崩潰。
因此,關(guān)于配置熱加載的正常的設(shè)計思路是,舊模塊使用舊配置,新模塊使用新配置,配置文件不再保存單例,而是解析成功后將副本傳入到各個模塊之內(nèi)保存。
一旦配置文件重新加載到內(nèi)存,那么接下來要做的就是和優(yōu)雅退出類似,先讓http模塊暫停向內(nèi)部模塊轉(zhuǎn)發(fā)流量,但是它仍舊接收外部流量,并緩存起來。
接下來,各個舊模塊開始消耗剩余的流量,最終銷毀自身。
當所有舊模塊退出后,將新的配置傳遞給各個模塊,啟動新的模塊實例,并恢復(fù)http模塊繼續(xù)向內(nèi)部模塊轉(zhuǎn)發(fā)流量,程序恢復(fù)運行。
不過,僅僅完成這些設(shè)計并不能解決整個問題,最棘手的是log和stats模塊,前者負責日志,后者負責程序計數(shù),它們一樣需要熱加載配置,比如:運維想把日志的輸出目錄或者日志級別變更一下。
這兩個模塊比較特殊,它們被其他各個模塊調(diào)用,并且是并發(fā)的調(diào)用,相當于”給天上的飛機換發(fā)動機”,非常難。按照設(shè)計,應(yīng)當在老模塊全部銷毀后,將log和stats銷毀并重建。但是問題來了,http服務(wù)模塊并沒有銷毀,它仍舊在實時的操作log和stats庫,那么又怎么重啟這2個模塊呢?
這里我使用了atomic庫,log和stats模塊都是單例模式,保存的是對象的指針。在程序仍舊在持續(xù)訪問2個模塊的情況下,想要銷毀這個單例并重建,必須對指針進行原子操作,好在golang提供了指針的atomic操作:
atomic.StorePointer
atomic.LoadPointer
1
2
atomic.StorePointer
atomic.LoadPointer
有了這2個api,我就可以原子的操作單例指針,完成瞬時的轉(zhuǎn)換。
當然,在銷毀之后到重建之間的這段時間,http模塊打印的log和stats統(tǒng)計都會無效,但是這個時間通常可以短到忽略。
以log庫為例,相應(yīng)的日志操作函數(shù)也首先通過atomic獲取log指針,如果存在則進行實際的操作,否則什么也不做:
Go
// 單例
var gLogger unsafe.Pointer = nil
func getLogger() *logger {
return (*logger)(atomic.LoadPointer(&gLogger))
}
func FATAL(format string, v ...interface{}) {
if logger := getLogger(); logger != nil {
userLog := fmt.Sprintf(format, v...)
logger.queueLog(LOG_LEVEL_FATAL, &userLog)
}
}
func ERROR(format string, v ...interface{}) {
if logger := getLogger(); logger != nil {
userLog := fmt.Sprintf(format, v...)
logger.queueLog(LOG_LEVEL_ERROR, &userLog)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 單例
vargLoggerunsafe.Pointer=nil
funcgetLogger()*logger{
return(*logger)(atomic.LoadPointer(&gLogger))
}
funcFATAL(formatstring,v...interface{}){
iflogger:=getLogger();logger!=nil{
userLog:=fmt.Sprintf(format,v...)
logger.queueLog(LOG_LEVEL_FATAL,&userLog)
}
}
funcERROR(formatstring,v...interface{}){
iflogger:=getLogger();logger!=nil{
userLog:=fmt.Sprintf(format,v...)
logger.queueLog(LOG_LEVEL_ERROR,&userLog)
}
}
這就是我在實現(xiàn)bigpipe熱加載期間遇到的一些問題,希望對大家設(shè)計熱加載時有所幫助。
如果文章幫助您解決了工作難題,您可以幫我點擊屏幕上的任意廣告,或者贊助少量費用來支持我的持續(xù)創(chuàng)作,謝謝~