在更新服務(wù)的時(shí)候,怎么能做到讓自己的服務(wù)不斷掉,又拍云做更新的時(shí)候,不允許有失敗,如果說(shuō)因?yàn)槲覀兊氖《鴮?dǎo)致請(qǐng)求失敗,即使你的請(qǐng)求非常少,首先從口碑上就很不好;另外一個(gè)原因:如果造成了事故,是要賠錢(qián)的。這也是我們做動(dòng)態(tài)服務(wù)路由的重要原因。
說(shuō)到服務(wù)路由,大家都會(huì)想到三個(gè)方面:
服務(wù)發(fā)現(xiàn)有很多方案,ETC跟Consul算是后起之秀,比較常見(jiàn)。ZooKeeper是一個(gè)比較老牌的開(kāi)源項(xiàng)目,比較成熟,對(duì)資源的要求比較高,相對(duì)比較強(qiáng)大一點(diǎn)。Consul不但支持KV存儲(chǔ),還有原生的服務(wù)監(jiān)控、多數(shù)據(jù)中心、DNS功能等,所以我們選了Consul這個(gè)方案。
負(fù)載均衡也是有很多方案,比如說(shuō)Nginx,LVS擴(kuò)展起來(lái)非常難;再高級(jí)一點(diǎn)的有HA_PROXY,它可以做到高層的,也可以做到基層的,Nginx專(zhuān)注于做HTTP,后續(xù)也支持了TCP。從負(fù)載均衡出發(fā),選擇了Nginx。
如上圖,我們把Nginx和Consul放在一張圖里。為了突出服務(wù)這一塊,我把一些跟服務(wù)不太有關(guān)系的都省略掉了。我們基于Mesos、Docker還有Marathon做了服務(wù)管理。其中有一個(gè)服務(wù)是特殊的,就是Registrator,它會(huì)通過(guò)Docker API在每個(gè)物理機(jī)上起一個(gè)容器,通過(guò)Docker API,把容器的狀態(tài)定時(shí)的匯報(bào)給Consul,上面的Nginx是做負(fù)載均衡的,因?yàn)槲覀兊姆?wù)目前來(lái)說(shuō)都是基于Nginx直接到容器里面。
在這個(gè)圖里面,Nginx到容器這一步是沒(méi)有問(wèn)題的,服務(wù)注冊(cè)到配置文件也是沒(méi)有問(wèn)題的,但是從Consul到Nginx是有問(wèn)題的,因?yàn)镃onsul有所有的信息,但是這些信息要如何通知給Nginx,一個(gè)新的服務(wù)起來(lái)了,或者是一個(gè)服務(wù)掛掉了,這些信息Consul知道了,怎么讓Nginx把有一些有問(wèn)題的給刪掉,再把一些新寫(xiě)的給加進(jìn)去,這就是我們要解決的一個(gè)問(wèn)題。
面臨的問(wèn)題就是Consul里的服務(wù)如何更新到Nginx,如果解決了這個(gè)問(wèn)題,剛才那個(gè)圖就已經(jīng)圓滿。
市場(chǎng)上有很多方案來(lái)解決這個(gè)問(wèn)題:
1. Consul_template
監(jiān)聽(tīng)Consul里的key,會(huì)觸發(fā)執(zhí)行一個(gè)腳本,利用這個(gè)特性的服務(wù),服務(wù)發(fā)生變動(dòng),會(huì)根據(jù)預(yù)先配置好的模板去重新生成配置,這個(gè)就是后要執(zhí)行的一個(gè)腳本。原理就是這樣:
上圖是一個(gè)例子,比如說(shuō)一個(gè)模板是這樣的,然后中間都是將來(lái)要被渲染的一些變量,如果K/v發(fā)生變動(dòng),模板化生成一份真實(shí)的配置文件,然后再執(zhí)行一個(gè)本地的命令,Nginx -s reload,重新生成配置文件,Reload一下,這樣新的服務(wù)就已經(jīng)生效了。當(dāng)然這樣也有一些問(wèn)題:
(1) 如果你頻繁的Reload會(huì)有性能損耗;
(2) 長(zhǎng)時(shí)間處于Shutting down的狀態(tài),如果連接里頭有長(zhǎng)連接,舊的進(jìn)程會(huì)一直處于一個(gè)中間進(jìn)程,這個(gè)時(shí)間是不定的,就是說(shuō)你不知道到底什么時(shí)候Reload真正完成;
(3) 進(jìn)程內(nèi)緩存失效,我們會(huì)把數(shù)據(jù)庫(kù)的一些信息,一些代碼全部緩存進(jìn)本地,這樣緩存就全部失效了。
當(dāng)然前面三點(diǎn)也不是非常的嚴(yán)重,畢Reload的操作不是特別的頻繁。
后一點(diǎn)是與設(shè)計(jì)初衷不符,這也是我們關(guān)心的一點(diǎn),它設(shè)計(jì)的初衷是做什么呢?就是方便運(yùn)維不去影響當(dāng)前的請(qǐng)求,就相當(dāng)于我們拿Docker做虛擬機(jī)用一樣走歪了,走歪了之后后很可能會(huì)碰到很多奇怪的坑,所以當(dāng)時(shí)沒(méi)有用這個(gè)方案。
2. 內(nèi)部DNS方案
DNS的方案也是比較常用的,比如我把之前是一個(gè)IP地址的Server,現(xiàn)在改成一個(gè)域名,只要把它解析掉一批IP就好了,這個(gè)聽(tīng)起來(lái)已經(jīng)很完美了,而且Consul本身支持DNS,我們也不用維護(hù)另外的DNS了,只要把這個(gè)ID換成域名就好了。
這樣做的話,我們感覺(jué)還不如做Reload,因?yàn)槭紫?span style="box-sizing:border-box;font-weight:700;">多了一層DNS解析時(shí)間,再怎么快都是需要解析時(shí)間的,第二個(gè)是有DNS緩存,這是主要的原因,因?yàn)榫彺娴拇嬖冢瑳](méi)辦法立即把一臺(tái)有問(wèn)題的機(jī)器切掉,如果你要緩解這個(gè)問(wèn)題,就要把緩存設(shè)得短一點(diǎn),但這樣解析次數(shù)就多了。還有一個(gè)就是端口號(hào)會(huì)改變,物理機(jī)一般我們會(huì)配置同一個(gè)端口,在Docker里面也可以這么做,但對(duì)于一些對(duì)網(wǎng)絡(luò)不是很敏感的應(yīng)用,比如說(shuō)一些強(qiáng)CPU的應(yīng)用,我們會(huì)直接把容器的網(wǎng)絡(luò),用橋接的方式連接起來(lái),而這時(shí)候端口是隨機(jī)分配的,可能每個(gè)容器分配的都不一樣,所以就不行。
那我們到底想要怎么樣呢?我們想要的非常簡(jiǎn)單,就是要通過(guò)HTTP接口,動(dòng)態(tài)修改Nginx的上游服務(wù)列表。這樣的方案我們找了之后發(fā)現(xiàn)有一個(gè)現(xiàn)成的,叫ngx_http_dyups_module。
3. Ngx_http_dyups_module
它能干什么事情呢?可以通過(guò)GET接口查詢當(dāng)前的一些信息;POST可以更新上游;也能通過(guò)Ddelete刪除上游。
上圖是一個(gè)例子,這個(gè)例子有三個(gè)請(qǐng)求,也就是發(fā)了三個(gè)指令:
在這個(gè)過(guò)程里頭沒(méi)有任何的Reload的操作,也沒(méi)有改配置,它就完成了一個(gè)功能。
這個(gè)模塊寫(xiě)得非常好,我們用了一段時(shí)間,但一段時(shí)間后把它下掉了,主要原因不是因?yàn)樗缓茫饕俏覀兘Y(jié)合了一些自身的情況,發(fā)現(xiàn)了一些問(wèn)題:
基于以上這些原因,我們開(kāi)始造自己的輪子。
這個(gè)輪子是這樣的,有四個(gè)部分:
lua_resty_checkups,這是我們Lua版的管理模塊,實(shí)現(xiàn)了動(dòng)態(tài)的upstream管理,這個(gè)模塊實(shí)現(xiàn)了大概30%的功能,而且還有一些主動(dòng)的健康檢查功能,它的代碼量大概也就是1500左右,那C模塊估計(jì)至少有1萬(wàn)行;
簡(jiǎn)單介紹一下lua_resty_checkups這個(gè)模板,它有幾個(gè)功能:
以Host區(qū)分服務(wù):比如說(shuō)這兩個(gè)curl往同一個(gè)地址去發(fā),這兩者之間是不一樣的。
這個(gè)圖簡(jiǎn)單講一下,它是一個(gè)請(qǐng)求的流程,可以分為三個(gè)部分,上面是接收請(qǐng)求,我們會(huì)加載一個(gè)worker代碼,在worker代碼執(zhí)行完之后,會(huì)根據(jù)這個(gè)host找對(duì)應(yīng)的列表,然后把這個(gè)請(qǐng)求代理給服務(wù)端。
這個(gè)跟dyups的C模塊一樣,也是通過(guò)HTTP接口動(dòng)態(tài)更新upstream列表,加完之后,可以在管理頁(yè)面看一下,就可以看到剛剛加進(jìn)去的兩個(gè)服務(wù),這里面有server地址,一些健康檢查的消息,還有它的狀態(tài)變更的時(shí)間,以及它失敗的次數(shù),這是主動(dòng)健康檢查的一個(gè)記錄。那為什么會(huì)有主動(dòng)健康檢查呢?我稍微介紹一下,大家平時(shí)用的就是一些被動(dòng)的健康檢查,就是說(shuō)我這個(gè)請(qǐng)求發(fā)出去之后失敗了才知道失敗了,主動(dòng)的就是我發(fā)心跳包,在請(qǐng)求之前,我就可以知道你這個(gè)服務(wù)是不是出問(wèn)題了。
動(dòng)態(tài)Lua加載,這個(gè)在做游戲的時(shí)候會(huì)經(jīng)常用到,在一開(kāi)始的時(shí)候,我們的程序里面跑了一些Lua的代碼,給后端的程序做參數(shù)轉(zhuǎn)化和做兼容用,比如有一個(gè)小調(diào)整不樂(lè)意去改,就拿前面的路由去做,首先我可以對(duì)請(qǐng)求做改寫(xiě),因?yàn)槲铱梢阅玫秸麄€(gè)的請(qǐng)求的,它的請(qǐng)求體可以做任意的事情,這樣的話,我可以跟一些權(quán)限控制結(jié)合起來(lái),還有一個(gè)就是可以做一些簡(jiǎn)單的參數(shù)檢查。據(jù)我們的統(tǒng)計(jì),我們大概有至少10%是重復(fù)的請(qǐng)求,那這些重復(fù)請(qǐng)求如果都去做的話就是無(wú)謂的消耗,我們會(huì)返一個(gè)304,表示結(jié)果跟之前的一樣,用之前的結(jié)果就好了。在返304的時(shí)候,如果說(shuō)我們是需要后端的服務(wù)去判斷,勢(shì)必會(huì)把整個(gè)請(qǐng)求給收下來(lái),然后再往后面發(fā),相當(dāng)于是內(nèi)網(wǎng)帶寬要增加一些,這樣其實(shí)就已經(jīng)節(jié)省了帶寬,可以不往后面發(fā)了,主要是這幾個(gè)原因。
這是一個(gè)動(dòng)態(tài)負(fù)載加載的例子,我如果把這段代碼推到Slardar里面的話,它會(huì)執(zhí)行,如果你進(jìn)行一個(gè)刪除操作,它會(huì)返403,也就是說(shuō)可以立即通過(guò)這個(gè)代碼禁掉這個(gè)操作,那還有什么功能呢?你可以想象到的功能都可以做,而且這個(gè)過(guò)程是動(dòng)態(tài)的,如果代碼加載,也可以從狀態(tài)頁(yè)里看到它的信息。
剛剛講的都是這個(gè)項(xiàng)目的特性,接下來(lái)想簡(jiǎn)單介紹一下實(shí)現(xiàn)過(guò)程,有一些可能比較深入,我盡量把一些深入的地方帶過(guò)去,動(dòng)態(tài)upstream管理,分三個(gè)部分,三個(gè)步驟。
1. 動(dòng)態(tài)upstream管理
啟動(dòng)時(shí)從Consul加載配置文件,如果你沒(méi)有任何理由的掛了,掛了之后你剛起來(lái)時(shí),你怎么知道你剛剛怎么了呢?所以得有一個(gè)地方去固化這些東西,而我們選的就是Consul,所以它啟動(dòng)的時(shí)候必須從Consul加載,啟動(dòng)之后一個(gè)就是監(jiān)聽(tīng)管理的端口,還有一個(gè)就是要啟動(dòng)一個(gè)定時(shí)器,這個(gè)定時(shí)器做worker間同步的,定時(shí)從共享內(nèi)存看一下有沒(méi)有更新,有更新的話可以同步在自己的worker里頭。
這是一個(gè)簡(jiǎn)單的流程圖,開(kāi)始的時(shí)候從Consul加載,在完成fork之后,就到了worker進(jìn)程,也就是剛剛你初始化加載的那些每個(gè)worker都有了,另外一部分啟動(dòng)定時(shí)器,一旦有更新就會(huì)進(jìn)入到這個(gè)里面。
2. 負(fù)載均衡
我們主要用到了balance_by_lua_,一個(gè)請(qǐng)求過(guò)來(lái),通過(guò)upstream的C模塊,然后把這個(gè)請(qǐng)求往這里發(fā),下圖這個(gè)配置文件,剛剛也有一個(gè)類(lèi)似的,就是在這里寫(xiě)了地址。通過(guò)balance_by_lua_指令,我們會(huì)把它攔到這個(gè)文件里,就可以在這個(gè)Lua文件里頭用Lua代碼選一個(gè),這就是自身的一個(gè)checkups的選擇的過(guò)程。
大概的流程見(jiàn)下圖,可以先看下邊部分,一開(kāi)始的時(shí)候,checkups.select_peer是我們的模塊,然后根據(jù)這個(gè)host再到當(dāng)前的peer,就跳出去了,這樣就實(shí)現(xiàn)了用Lua控制。上面部分是要知道它是成功還是失敗的,如果它失敗了,我要對(duì)這個(gè)狀態(tài)進(jìn)行反饋。
3. 動(dòng)態(tài)Lua加載
這個(gè)主要是用到Lua的三個(gè)函數(shù),分別是loadfile、loadstring和setfenv。loadfile是加載本地Lua代碼,loadstring是從Consul或HTTP請(qǐng)求body加載代碼,setfenv設(shè)置代碼的執(zhí)行環(huán)境,通過(guò)這三個(gè)函數(shù)就可以加載,具體的實(shí)踐細(xì)節(jié)我就不再介紹。
這是我們做的輪子,主要用到checkups的模塊和balance_by_lua_,它有這些優(yōu)勢(shì):
首先,純Lua實(shí)現(xiàn),不依賴(lài)第三方C模塊,二次開(kāi)發(fā)非常高效,減少維護(hù)負(fù)擔(dān)。
第二是可以用Nginx原生的Proxy,因?yàn)槲覀冎辉谡?qǐng)求的選peer的那個(gè)階段做,peer選完之后,發(fā)數(shù)據(jù)的那個(gè)階段是直接走Nginx自己的指令的,所以它可以用到Nginx原生的Proxy指令。
后,它適用于幾乎任何的ngx_lua項(xiàng)目。
我們目前也在把之前的一些服務(wù)改造成微服務(wù)模式。微服務(wù)其實(shí)就是源于一個(gè)比較大的服務(wù),把它拆分成一些小的服務(wù),它的擴(kuò)容跟遷移也不一樣,微服務(wù)的擴(kuò)容可以只擴(kuò)容其中一部分,如果需要的功能比較多,就擴(kuò)得多一點(diǎn),需要少的,就擴(kuò)得少一點(diǎn)。
我們現(xiàn)在正在嘗試的一個(gè)方案,這個(gè)方案背景是這樣,我們有一些做圖的需求,做圖這個(gè)功能有很多,比如美化等各種需求,如果要對(duì)這個(gè)做圖的服務(wù)進(jìn)行優(yōu)化是非常困難的,因?yàn)樗δ芴嗔耍绻覀儼阉鸪晌⒎?wù)就不一樣了,比如說(shuō)這個(gè)虛線上面的是我們現(xiàn)在的服務(wù),這個(gè)是微服務(wù)的一個(gè)網(wǎng)關(guān),下面是一些小的服務(wù)。
比如說(shuō)美化,它的運(yùn)算比較復(fù)雜,耗CPU比較多,我們肯定選擇一些CPU比較好的機(jī)器;用GPU來(lái)做縮略圖,這個(gè)性能可能提高幾十倍;后是一個(gè)中規(guī)中矩的做圖,那就普通的一些就夠了。
還有一些比較偏門(mén)的,比如說(shuō)梯度,可能只要保證下服務(wù)可以用就行了,通過(guò)這個(gè)微服務(wù)的路由,我們根據(jù)后面的區(qū)分把之前的一個(gè)服務(wù),以及它的參數(shù)拆成三個(gè)小的服務(wù),這樣通過(guò)三個(gè)步驟可以完成一個(gè)做圖的服務(wù)。
當(dāng)然我們?cè)趪L試的這個(gè)方案其實(shí)也有很多問(wèn)題,比如一個(gè)服務(wù)原來(lái)用一個(gè)程序就可以做了,現(xiàn)在變成了三個(gè),勢(shì)必內(nèi)網(wǎng)的帶寬要增加了,中間的圖片要被導(dǎo)來(lái)導(dǎo)去,這怎么辦?我們現(xiàn)在想到的辦法就是做一些本地優(yōu)先的調(diào)度策略,就是說(shuō)你做完之后,本地有一些水印的,那就優(yōu)先用本地的。
套用大師的一句話:Talk is cheap,Show me the code。開(kāi)源!
本站文章版權(quán)歸原作者及原出處所有 。內(nèi)容為作者個(gè)人觀點(diǎn), 并不代表本站贊同其觀點(diǎn)和對(duì)其真實(shí)性負(fù)責(zé),本站只提供參考并不構(gòu)成任何投資及應(yīng)用建議。本站是一個(gè)個(gè)人學(xué)習(xí)交流的平臺(tái),網(wǎng)站上部分文章為轉(zhuǎn)載,并不用于任何商業(yè)目的,我們已經(jīng)盡可能的對(duì)作者和來(lái)源進(jìn)行了通告,但是能力有限或疏忽,造成漏登,請(qǐng)及時(shí)聯(lián)系我們,我們將根據(jù)著作權(quán)人的要求,立即更正或者刪除有關(guān)內(nèi)容。本站擁有對(duì)此聲明的最終解釋權(quán)。