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