分庫分表中間件的高可用實踐
前言
分庫分表中間件在我們一年多的錘煉下,基本解決了可用性和高性能的問題(只能說基本,肯定還有隱藏的坑要填),問題自然而然的就聚焦于高可用。本文就闡述了我們在這方面做出的一些工作。
哪些高可用的問題
作為一個無狀態(tài)的中間件,高可用問題并沒有那么困難。但是盡量減少不可用期間的流量損失,還是需要一定的工作的。這些流量損失主要分布在:
- (1)某臺中間件所在的物理機突然宕機。
- (2)中間件的升級和發(fā)布。
由于我們的中間件是作為數(shù)據(jù)庫的代理提供給應(yīng)用的,即應(yīng)用把我們的中間件當(dāng)做數(shù)據(jù)庫,如下圖所示:
所以出現(xiàn)上述問題后,業(yè)務(wù)上很難通過重試等操作去屏蔽這些影響。這就勢必需要我們在底層做一些操作,能夠自動的感知中間件的狀態(tài)從而有效避免流量的損失。
中間件所在物理機宕機的情況
物理機宕機其實是一種常見現(xiàn)象,這時候應(yīng)用一瞬間就沒了響應(yīng)。那么跑在上面的sql肯定也是失敗了的(準(zhǔn)確來說是未知狀態(tài),除非重新查詢后端數(shù)據(jù)庫,應(yīng)用無法得知準(zhǔn)確的狀態(tài))。這部分流量我們肯定是無法挽救。我們所做的是在client端(Druid數(shù)據(jù)源)能夠快速的發(fā)現(xiàn)并剔除宕機的中間件節(jié)點。
發(fā)現(xiàn)并剔除不可用節(jié)點
通過心跳去發(fā)現(xiàn)不可用節(jié)點
自然而然的我們通過心跳來探查后端中間件的存活狀態(tài)。我們通過定時創(chuàng)建一個新連接ping(mysql的ping)一下然后立馬關(guān)閉來做心跳(這種做法便于我們區(qū)分正常流量和心跳流量,如果通過保持一個連接然后一直發(fā)送類似select ‘1’的sql這種方式的話區(qū)分流量會稍微麻煩點)。
為了防止網(wǎng)絡(luò)抖動造成的偶發(fā)性connect失敗,我們在三次connect都失敗后才判定某臺中間件處于不可用狀態(tài)。而這三次的探活卻延長了錯誤感知時間,所以我們?nèi)蝐onnect的時間間隔是指數(shù)級衰減的,如下圖所示:
為何不在第一次connect失敗后,連續(xù)發(fā)送兩次connect呢?可能考慮到網(wǎng)絡(luò)的抖動可能會有一個時間窗口,如果在時間窗口內(nèi)連續(xù)發(fā)了3次,出了這個時間窗口網(wǎng)絡(luò)又okay了,那么會錯誤的發(fā)現(xiàn)后端某節(jié)點不可用了,所以我們就做了指數(shù)級衰減的折衷。
通過錯誤計數(shù)去發(fā)現(xiàn)不可用節(jié)點
上述的心跳感知始終有一個時間窗口,當(dāng)流量很大的時候,在這個時間窗口內(nèi)使用這個不可用節(jié)點的都會失敗,所以我們可以使用錯誤計數(shù)去輔助不可用節(jié)點的感知(當(dāng)然這個手段的實現(xiàn)還在計劃中)。
這邊有一個注意的點是,只能通過創(chuàng)建連接異常來計數(shù),并不能通過read timeout之類的來計算。原因是,read timeout異常可能是慢sql或者后端數(shù)據(jù)庫的問題導(dǎo)致,只有創(chuàng)建連接異常才能確定是中間件的問題(connection closed也可能是后端關(guān)閉了這個連接,并不代表整體不可用),如下圖所示:
一個請求使用若干個連接導(dǎo)致的問題
由于我們需要保證事務(wù)盡可能小,所以在一個請求里面多條sql并不使用同一個連接。在非事務(wù)(auto-commit)情況下,運行多少條sql就從連接池里面取出多少連接,并放回。保證事務(wù)小是非常重要的,但是這在中間件宕機的時候會導(dǎo)致一些問題,如下圖所示:
如上圖所示,在故障發(fā)現(xiàn)窗口期中(即還沒有確定某臺中間件不可用時),數(shù)據(jù)源是隨機選擇連接的。而這個連接就有一定1/N(N為中間件個數(shù))的概率命中不可用中間件導(dǎo)致一條sql失敗進(jìn)而導(dǎo)致整個請求失敗。我們做一個計算:
假設(shè)N為8,一個請求有20條sql,
那么在這個期間每個請求失敗的概率就為(1-(7/8)的20次方)=0.93,
即有93%的概率會失敗!
更為甚者,整個應(yīng)用集群都會經(jīng)歷這個階段,即每臺應(yīng)用都有93%的概率失敗。
一臺中間件宕機導(dǎo)致整個服務(wù)在十幾秒內(nèi)基本所有請求基本都失敗,這是不可忍受的。
采用sticky數(shù)據(jù)源解決問題
由于我們不能瞬間發(fā)現(xiàn)并確認(rèn)中間件不可用,所以這個故障發(fā)現(xiàn)窗口肯定存在(當(dāng)然,錯誤計數(shù)法會在很大程度上縮短發(fā)現(xiàn)時間)。但理想狀況下,宕機一臺,只損失1/N的流量就好了。我們采用了sticky數(shù)據(jù)源解決了這個問題,使得在概率上大致只損失1/N的流量,如下圖所示:
而配合錯誤計數(shù)的話,總流量的損失會更小(因為故障窗口短)
如上圖所示,只有在故障時間內(nèi)隨機選擇到中間件2(不可用)的請求才會失敗,再讓我們看下整個應(yīng)用集群的情況。
只有sticky到中間件2的請求流量才有損失,由于是隨機選擇,所以這個流量的損失應(yīng)用在1/N。
中間件升級發(fā)布過程中的高可用
分庫分表中間件的升級發(fā)布不可避免。例如bug fix以及新功能添加等都需要重啟中間件。而重啟的時間也會導(dǎo)致不可用,與物理機宕機的情況相比是其不可用的時間點是可知的,重啟的動作也是可控的,那么我們就可以利用這些信息去做到流量的平滑無損。
讓client端感知即將下線
在筆者所知的很多做法中,讓client端感知下線是引入一個第三方協(xié)調(diào)者(例如zookeeper/etcd)。而我們并不想引入第三方的組件去做這個操作,因為這又會引入zookeeper的高可用問題,而且會讓client端的配置更加復(fù)雜。平滑無損的大致思路(狀態(tài)機)如下圖所示:
讓心跳流量感知下線而正常流量保持
我們可以復(fù)用之前client端檢測不可用的邏輯,即讓心跳的新建連接失敗,而正常請求的新建連接成功。這樣,client端就會認(rèn)為Server不可用,而在內(nèi)部剔除調(diào)這個server。由于我們只是模擬不可用,所以已經(jīng)建立的連接和正常新建的連接(非心跳)都是正??捎玫模缦聢D所示:
心跳連接的創(chuàng)建在server端可以通過其第一條執(zhí)行的是mysql的ping而正常流量第一條執(zhí)行的是一條sql來區(qū)分(當(dāng)然我們采用的Druid連接池在新建連接成功以后也會ping一下,所以采用了另一種方式區(qū)分,這個細(xì)節(jié)在這里就不闡述了)。
三次心跳失敗后,client端判定Server1失敗,需要將連接到server1的連接銷毀。其思路是,業(yè)務(wù)層用完連接返回連接池的時候,直接給close掉(當(dāng)然這個是簡單的描述,實際操作到Druid數(shù)據(jù)源也是有細(xì)微的差別的)。
由于配置了一個connection最長保持時間,所以在這個時間之后肯定會對Server1的連接數(shù)為0
由于線上流量也不低,這個收斂時間是比較快的(進(jìn)一步的做法,其實是主動去銷毀,不過我們尚未做這個操作)。
如何判定下線Server再也沒有流量
在上述小心翼翼的操作之后,在Server1下線的過程中,是不會有流量損失的。但是我們在Server端還需要判定何時不會再有新的流量,這個判定標(biāo)準(zhǔn)即是Server1沒有任何一個client端的連接。
這也是上面我們在執(zhí)行完sql后銷毀連接從而可以讓連接數(shù)變?yōu)?的原因,如下圖所示:
當(dāng)連接數(shù)為0后,我們就可以重新發(fā)布Server1(分庫分表中間件)了。對于這一點,我們寫了個腳本,其偽代碼如下所示:
- while(true){
- count =`netstat -anp | grep port | grep ESTABLISHED | wc -l`
- if(0 == count){
- // 流量已經(jīng)為0,關(guān)掉服務(wù)器
- kill Server
- // 發(fā)布升級服務(wù)器
- public Server
- break
- }else{
- Sleep(30s)
- }
- }
將這個腳本接入發(fā)布平臺,即可進(jìn)行滾動式上下線了。
現(xiàn)在可以解釋下recover_time為何要較長了,因為新建連接也會導(dǎo)致腳本計算出來的 connection count數(shù)量增加,所以需要一個時間窗口不去建立心跳,從而能讓這個腳本順利運行。
recover_time其實是非必要的
如果我們將心跳創(chuàng)建的端口號和正常流量的端口號分開,是不需要recover_time的,如下圖所示:
圖片
采用這種方案的話,會在很大程度上降低我們client端代碼的復(fù)雜度。
但是這樣無疑又給client端增加了一個新的配置,對使用人員就又多了一個負(fù)擔(dān),還得在網(wǎng)絡(luò)上多一次開墻的操作,所以我們采取了recover_time的方案。
中間件的啟動順序問題
前面的過程是一個優(yōu)雅下線的過程,但我們發(fā)現(xiàn)我們的中間件才上線的時候在某些情況下也不會優(yōu)雅。即在中間件啟動時候,如果對后端數(shù)據(jù)庫剛建立的連接建立上去后由于某些原因斷開了,會導(dǎo)致中間件的reactor線程卡住一分鐘左右,這段時間無法服務(wù),造成流量損失。所以我們在后端數(shù)據(jù)庫連接全部創(chuàng)建成功后,再啟動reactor的accept線程從而接收新的流量,從而解決這一問題,如下圖所示:
總結(jié)
筆者個人感覺高可用比高性能還要復(fù)雜。因為高性能可以在線下反復(fù)的去壓測,通過壓測的數(shù)據(jù)去分析瓶頸,提高性能。而高可用需要應(yīng)付線上各種千奇百怪的現(xiàn)象,本篇博客講述的高可用方案只是我們工作的一小部分,還有很大一部分精力是處理中間件本身的問題上。但只要不放過任何一個點,將問題都能夠分析清楚并解決,就會讓系統(tǒng)越來越好。
本文轉(zhuǎn)載自微信公眾號「解Bug之路」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系解Bug之路公眾號。