分庫分表中間件的高可用實踐


前言

分庫分表中間件在咱們一年多的錘鍊下,基本解決了可用性和高性能的問題(只能說基本,確定還有隱藏的坑要填),問題天然而然的就聚焦於高可用。本文就闡述了咱們在這方面作出的一些工做。mysql

哪些高可用的問題

做爲一個無狀態的中間件,高可用問題並無那麼困難。可是儘可能減小不可用期間的流量損失,仍是須要必定的工做的。這些流量損失主要分佈在:react

(1)某臺中間件所在的物理機忽然宕機。      
(2)中間件的升級和發佈。

因爲咱們的中間件是做爲數據庫的代理提供給應用的,即應用把咱們的中間件當作數據庫,以下圖所示:

因此出現上述問題後,業務上很難經過重試等操做去屏蔽這些影響。這就勢必須要咱們在底層作一些操做,可以自動的感知中間件的狀態從而有效避免流量的損失。sql

中間件所在物理機宕機的狀況

物理機宕機實際上是一種常見現象,這時候應用一瞬間就沒了響應。那麼跑在上面的sql確定也是失敗了的(準確來講是未知狀態,除非從新查詢後端數據庫,應用沒法得知準確的狀態)。這部分流量咱們確定是沒法挽救。咱們所作的是在client端(Druid數據源)可以快速的發現並剔除宕機的中間件節點。數據庫

發現並剔除不可用節點

經過心跳去發現不可用節點

天然而然的咱們經過心跳來探查後端中間件的存活狀態。咱們經過定時建立一個新鏈接ping(mysql的ping)一下而後立馬關閉來作心跳(這種作法便於咱們區分正常流量和心跳流量,若是經過保持一個鏈接而後一直髮送相似select '1'的sql這種方式的話區分流量會稍微麻煩點)。

爲了防止網絡抖動形成的偶發性connect失敗,咱們在三次connect都失敗後才斷定某臺中間件處於不可用狀態。而這三次的探活卻延長了錯誤感知時間,因此咱們三次connect的時間間隔是指數級衰減的,以下圖所示:

爲什麼不在第一次connect失敗後,連續發送兩次connect呢?可能考慮到網絡的抖動可能會有一個時間窗口,若是在時間窗口內連續發了3次,出了這個時間窗口網絡又okay了,那麼會錯誤的發現後端某節點不可用了,因此咱們就作了指數級衰減的折衷。後端

經過錯誤計數去發現不可用節點

上述的心跳感知始終有一個時間窗口,當流量很大的時候,在這個時間窗口內使用這個不可用節點的都會失敗,因此咱們可使用錯誤計數去輔助不可用節點的感知(固然這個手段的實現還在計劃中)。

這邊有一個注意的點是,只能經過建立鏈接異常來計數,並不能經過read timeout之類的來計算。緣由是,read timeout異常多是慢sql或者後端數據庫的問題致使,只有建立鏈接異常才能肯定是中間件的問題(connection closed也多是後端關閉了這個鏈接,並不表明總體不可用),以下圖所示:
服務器

一個請求使用若干個鏈接致使的問題

因爲咱們須要保證事務儘量小,因此在一個請求裏面多條sql並不使用同一個鏈接。在非事務(auto-commit)狀況下,運行多少條sql就從鏈接池裏面取出多少鏈接,並放回。保證事務小是很是重要的,可是這在中間件宕機的時候會致使一些問題,以下圖所示:

如上圖所示,在故障發現窗口期中(即尚未肯定某臺中間件不可用時),數據源是隨機選擇鏈接的。而這個鏈接就有必定1/N(N爲中間件個數)的機率命中不可用中間件致使一條sql失敗進而致使整個請求失敗。咱們作一個計算:網絡

假設N爲8,一個請求有20條sql,
那麼在這個期間每一個請求失敗的機率就爲(1-(7/8)的20次方)=0.93,
即有93%的機率會失敗!

更爲甚者,整個應用集羣都會經歷這個階段,即每臺應用都有93%的機率失敗。
一臺中間件宕機致使整個服務在十幾秒內基本全部請求基本都失敗,這是不可忍受的。ide

採用sticky數據源解決問題

因爲咱們不能瞬間發現並確認中間件不可用,因此這個故障發現窗口確定存在(固然,錯誤計數法會在很大程度上縮短髮現時間)。但理想情況下,宕機一臺,只損失1/N的流量就行了。咱們採用了sticky數據源解決了這個問題,使得在機率上大體只損失1/N的流量,以下圖所示:

而配合錯誤計數的話,總流量的損失會更小(由於故障窗口短)
如上圖所示,只有在故障時間內隨機選擇到中間件2(不可用)的請求才會失敗,再讓咱們看下整個應用集羣的狀況。

只有sticky到中間件2的請求流量纔有損失,因爲是隨機選擇,因此這個流量的損失應用在1/N。性能

中間件升級發佈過程當中的高可用

分庫分表中間件的升級發佈不可避免。例如bug fix以及新功能添加等都須要重啓中間件。而重啓的時間也會致使不可用,與物理機宕機的狀況相比是其不可用的時間點是可知的,重啓的動做也是可控的,那麼咱們就能夠利用這些信息去作到流量的平滑無損。ui

讓client端感知即將下線

在筆者所知的不少作法中,讓client端感知下線是引入一個第三方協調者(例如zookeeper/etcd)。而咱們並不想引入第三方的組件去作這個操做,由於這又會引入zookeeper的高可用問題,並且會讓client端的配置更加複雜。平滑無損的大體思路(狀態機)以下圖所示:

讓心跳流量感知下線而正常流量保持

咱們能夠複用以前client端檢測不可用的邏輯,即讓心跳的新建鏈接失敗,而正常請求的新建鏈接成功。這樣,client端就會認爲Server不可用,而在內部剔除調這個server。因爲咱們只是模擬不可用,因此已經創建的鏈接和正常新建的鏈接(非心跳)都是正常可用的,以下圖所示:

心跳鏈接的建立在server端能夠經過其第一條執行的是mysql的ping而正常流量第一條執行的是一條sql來區分(固然咱們採用的Druid鏈接池在新建鏈接成功之後也會ping一下,因此採用了另外一種方式區分,這個細節在這裏就不闡述了)。

三次心跳失敗後,client端斷定Server1失敗,須要將鏈接到server1的鏈接銷燬。其思路是,業務層用完鏈接返回鏈接池的時候,直接給close掉(固然這個是簡單的描述,實際操做到Druid數據源也是有細微的差異的)。

因爲配置了一個connection最長保持時間,因此在這個時間以後確定會對Server1的鏈接數爲0
因爲線上流量也不低,這個收斂時間是比較快的(進一步的作法,實際上是主動去銷燬,不過咱們還沒有作這個操做)。

如何斷定下線Server再也沒有流量

在上述當心翼翼的操做以後,在Server1下線的過程當中,是不會有流量損失的。可是咱們在Server端還須要斷定什麼時候不會再有新的流量,這個斷定標準便是Server1沒有任何一個client端的鏈接。
這也是上面咱們在執行完sql後銷燬鏈接從而可讓鏈接數變爲0的緣由,以下圖所示:

當鏈接數爲0後,咱們就能夠從新發布Server1(分庫分表中間件)了。對於這一點,咱們寫了個腳本,其僞代碼以下所示:

while(true){
	count =`netstat -anp | grep port | grep ESTABLISHED | wc -l`
	if(0 == count){		// 流量已經爲0,關掉服務器
		kill Server		// 發佈升級服務器
		public Server		break
	}else{
		Sleep(30s)
	}
}

將這個腳本接入發佈平臺,便可進行滾動式上下線了。
如今能夠解釋下recover_time爲什麼要較長了,由於新建鏈接也會致使腳本計算出來的 connection count數量增長,因此須要一個時間窗口不去創建心跳,從而能讓這個腳本順利運行。

recover_time實際上是非必要的

若是咱們將心跳建立的端口號和正常流量的端口號分開,是不須要recover_time的,以下圖所示:

採用這種方案的話,會在很大程度上下降咱們client端代碼的複雜度。
可是這樣無疑又給client端增長了一個新的配置,對使用人員就又多了一個負擔,還得在網絡上多一次開牆的操做,因此咱們採起了recover_time的方案。

中間件的啓動順序問題

前面的過程是一個優雅下線的過程,但咱們發現咱們的中間件才上線的時候在某些狀況下也不會優雅。即在中間件啓動時候,若是對後端數據庫剛創建的鏈接創建上去後因爲某些緣由斷開了,會致使中間件的reactor線程卡住一分鐘左右,這段時間沒法服務,形成流量損失。因此咱們在後端數據庫鏈接所有建立成功後,再啓動reactor的accept線程從而接收新的流量,從而解決這一問題,以下圖所示:

總結

筆者我的感受高可用比高性能還要複雜。由於高性能能夠在線下反覆的去壓測,經過壓測的數據去分析瓶頸,提升性能。而高可用須要應付線上各類千奇百怪的現象,本篇博客講述的高可用方案只是咱們工做的一小部分,還有很大一部分精力是處理中間件自己的問題上。但只要不放過任何一個點,將問題都可以分析清楚並解決,就會讓系統愈來愈好。

相關文章
相關標籤/搜索