一、背景
一年一度的雙十一又要來了,自2008年雙十一以來,在每年雙十一超大規模流量的衝擊上,螞蟻金服都會不斷突破現有技術的極限。2010年雙11的支付峰值為2萬筆/分鐘,全天1280萬筆支付,這個數字到2017雙11時變為了25.6萬筆/秒,全天14.8億筆。在如此之大的支付TPS背後除了削峰等錦上添花的應用級優化,最解渴最實質的招數當數基於分庫分表的單元化了,螞蟻技術稱之為LDC(邏輯資料中心)。
我想關心分散式系統設計的人都曾被下面這些問題所困擾過:
支付寶海量支付背後最解渴的設計是啥?換句話說,實現支付寶高TPS的最關鍵的設計是啥?LDC是啥?LDC怎麼實現異地多活和異地災備的?CAP魔咒到底是啥?P到底怎麼理解?什麼是腦裂?跟CAP又是啥關係?什麼是PAXOS,它解決了啥問題?PAXOS和CAP啥關係?PAXOS可以逃脫CAP魔咒麼?Oceanbase能逃脫CAP魔咒麼?如果你對這些感興趣,不妨看一場赤裸裸的論述,拒絕使用晦澀難懂的詞彙,直面最本質的邏輯。
二、LDC和單元化LDC(logic data center)是相對於傳統的(Internet Data Center-IDC)提出的,邏輯資料中心所表達的中心思想是無論物理結構如何的分佈,整個資料中心在邏輯上是協同和統一的。這句話暗含的是強大的體系設計,分散式系統的挑戰就在於整體協同工作(可用性,分割槽容忍性)和統一(一致性)。
單元化是大型網際網路系統的必然選擇趨勢,舉個最最通俗的例子來說明單元化。我們總是說TPS很難提升,確實任何一家網際網路(比如淘寶、攜程、新浪)它的交易TPS頂多以十萬計量(平均水平),很難往上串了,因為資料庫儲存層瓶頸的存在再多水平擴充套件的伺服器都無法繞開這個瓶頸,而從整個網際網路的視角看,全世界電商的交易TPS可以輕鬆上億。這個例子帶給我們一些思考:為啥幾家網際網路的TPS之和可以那麼大,服務的使用者數規模也極為嚇人,而單個網際網路的TPS卻很難提升?
究其本質,每家網際網路都是一個獨立的大型單元,他們各自服務自己的使用者互不干擾。
這就是單元化的基本特性,任何一家網際網路公司,其想要成倍的擴大自己系統的服務能力,都必然會走向單元化之路,它的本質是分治,我們把廣大的使用者分為若干部分,同時把系統複製多份,每一份都獨立部署,每一份系統都服務特定的一群使用者,以淘寶舉例,這樣之後,就會有很多個淘寶系統分別為不同的使用者服務,每個淘寶系統都做到十萬TPS的話,N個這樣的系統就可以輕鬆做到N*十萬的TPS了。
LDC實現的關鍵就在於單元化系統架構設計,所以在螞蟻內部,LDC和單元化是不分家的,這也是很多同學比較困擾的地方,看似沒啥關係,實則是單元化體系設計成就了LDC。
小結:分庫分表解決的最大痛點是資料庫單點瓶頸,這個瓶頸的產生是由現代二進位制資料儲存體系決定的(即I/O速度)。單元化只是分庫分表後系統部署的一種方式,這種部署模式在災備方面也發揮了極大的優勢。
2.1 系統架構演化史
幾乎任何規模的網際網路公司,都有自己的系統架構迭代和更新,大致的演化路徑都大同小異。最早一般為了業務快速上線,所有功能都會放到一個應用裡,系統架構如圖1所示。
這樣的架構顯然是有問題的,單機有著明顯的單點效應,單機的容量和效能都是很侷限的,而使用中小型機會帶來大量的浪費。 隨著業務發展,這個矛盾逐漸轉變為主要矛盾,因此工程師們採用了以下架構。
這是整個公司第一次觸碰到分散式,也就是對某個應用進行了水平擴容,它將多個微機的計算能力團結了起來,可以完勝同等價格的中小型機器。慢慢的,大家發現,應用伺服器CPU都很正常了,但是還是有很多慢請求,究其原因,是因為單點資料庫帶來了效能瓶頸。於是程式設計師們決定使用主從結構的資料庫叢集,如下圖所示。
其中大部分讀操作可以直接訪問從庫,從而減輕主庫的壓力。然而這種方式還是無法解決寫瓶頸,寫依舊需要主庫來處理,當業務量量級再次增高時,寫已經變成刻不容緩的待處理瓶頸。這時候,分庫分表方案出現了。
分庫分表不僅可以對相同的庫進行拆分,還可以進行對相同的表進行拆分,對錶進行拆分的方式叫做水平拆分。不同功能的表放到不同的庫裡,一般對應的是垂直拆分(按照業務功能進行拆分),此時一般還對應了微服務化。這種方法做到極致基本能支撐TPS在萬級甚至更高的訪問量了。
然而隨著相同應用擴充套件的越多,每個資料庫的連結數也巨量增長,這讓資料庫本身的資源成為了瓶頸。這個問題產生的本質是全量資料無差別的分享了所有的應用資源,比如A使用者的請求在負載均衡的分配下可能分配到任意一個應用伺服器上,因而所有應用全部都要連結A使用者所在的分庫,資料庫連線數就變成笛卡爾乘積了。
在本質點說,這種模式的資源隔離性還不夠徹底。要解決這個問題,就需要把識別使用者分庫的邏輯往上層移動,從資料庫層移動到路由閘道器層。這樣一來,從應用伺服器a進來的來自A客戶的所有請求必然落庫到DB-A,因此a也不用連結其他的資料庫例項了,這樣一個單元化的雛形就誕生了。
思考一下:應用間其實也存在互動(比如A轉賬給B),也就意味著,應用不需要連結其他的資料庫了,但是還需要連結其他應用。如果是常見的RPC框架如dubbo等,使用的是TCP/IP協議,那麼等同於把之前與資料庫建立的連結,換成與其他應用之間的連結了。為啥這樣就消除瓶頸了呢?首先由於合理的設計,應用間的資料互動並不巨量,其次應用間的互動可以共享TCP連結,比如A->B之間的Socket連結可以被A中的多個執行緒複用,而一般的資料庫如MySQL則不行,所以MySQL才需要資料庫連結池。
如上圖所示,但我們把整套系統打包為單元化時,每一類的資料從進單元開始就註定在這個單元被消化,由於這種徹底的隔離性,整個單元可以輕鬆的部署到任意機房而依然能保證邏輯上的統一。
下圖為一個三地五機房的部署方式。
2.2 螞蟻單元化架構實踐
螞蟻支付寶應該是國內最大的支付工具,其在雙十一等活動日當日的支付TPS可達幾十萬級,未來這個數字可能會更大,這決定了螞蟻單元化架構從容量要求上看必然從單機房走向多機房。另一方面,異地災備也決定了這些IDC機房必須是異地部署的。
整體上支付寶也採用了三地五中心(IDC機房)來保障系統的可用性,跟2.1中描述的有所不同的是,支付寶將單元分成了三類(也稱CRG架構):
RZone(Region Zone):直譯可能有點反而不好理解。實際上就是所有可以分庫分表的業務系統整體部署的最小單元。每個RZone連上資料庫就可以撐起一片天空,把業務跑的溜溜的。GZone(Global Zone):全域性單元,意味著全域性只有一份。部署了不可拆分的資料和服務,比如系統配置等。實際情況下,GZone異地也會部署,不過僅是用於災備,同一時刻,只有一地GZone進行全域性服務。GZone一般被RZone依賴,提供的大部分是讀取服務。CZone(City Zone):顧名思義,這是以城市為單位部署的單元。同樣部署了不可拆分的資料和服務,比如使用者賬號服務,客戶資訊服務等。理論上CZone會被RZone以比訪問GZone高很多的頻率進行訪問。CZone是基於特定的GZone場景進行優化的一種單元,它把GZone中有些有著”寫讀時間差現象”的資料和服務進行了的單獨部署,這樣RZone只需要訪問本地的CZone即可,而不是訪問異地的GZone。“寫讀時間差現象”是螞蟻架構師們根據實踐統計總結的,他們發現大部分情況下,一個數據被寫入後,都會過足夠長的時間後才會被訪問。生活中這種例子很常見,我們辦完銀行卡後可能很久才會存第一筆錢;我們建立微博賬號後,可能想半天才會發微博;我們下載建立淘寶賬號後,可能得瀏覽好幾分鐘才會下單買東西。當然了這些例子中的時間差遠遠超過了系統同步時間。一般來說異地的延時在100ms以內,所以只要滿足某地CZone寫入資料後100ms以後才用這個資料,這樣的資料和服務就適合放到CZone中。
相信大家看到這都會問:為啥分這三種單元?
其實其背後對應的是不同性質的資料,而服務不過是對資料的操作集。下面我們來根據資料性質的不同來解釋支付寶的CRG架構。當下幾乎所有網際網路公司的分庫分表規則都是根據使用者ID來制定的,而圍繞使用者來看整個系統的資料可以分為以下兩類:
使用者流水型資料:典型的有使用者的訂單、使用者發的評論、使用者的行為記錄等。這些資料都是使用者行為產生的流水型資料,具備天然的使用者隔離性,比如A使用者的App上絕對看不到B使用者的訂單列表。所以此類資料非常適合分庫分表後獨立部署服務。使用者間共享型資料:這種型別的資料又分兩類。一類共享型資料是像賬號、個人部落格等可能會被所有使用者請求訪問的使用者資料,比如A向B轉賬,A給B發訊息,這時候需要確認B賬號是否存在;又比如A想看B的個人部落格之類的。另外一類是使用者無關型資料,像商品、系統配置(匯率、優惠政策)、財務統計等這些非使用者緯度的資料,很難說跟具體的某一類使用者掛鉤,可能涉及到所有使用者。比如商品,假設按商品所在地來存放商品資料(這需要雙維度分庫分表),那麼上海的使用者仍然需要訪問杭州的商品,這就又構成跨地跨zone訪問了,還是達不到單元化的理想狀態,而且雙維度分庫分表會給整個LDC運維帶來複雜度提升。注:網上和支付寶內部有另外一些分法,比如流水型和狀態性,有時候還會分為三類:流水型、狀態型和配置型。個人覺得這些分法雖然嘗試去更高層次的抽象資料分類,但實際上邊界很模糊,適得其反。
直觀的類比,我們可以很輕易的將上述兩類資料對應的服務劃分為RZone和GZone,RZone包含的就是分庫分表後負責固定客戶群體的服務,GZone則包含了使用者間共享的公共資料對應的服務。到這裡為止,一切都很完美,這也是主流的單元化話題了。
對比支付寶的CRG架構,我們一眼就發現少了C(City Zone),CZone確實是螞蟻在單元化實踐領域的一個創新點。再來分析下GZone,GZone之所以只能單地部署,是因為其資料要求被所有使用者共享,無法分庫分表,而多地部署會帶來由異地延時引起的不一致,比如實時風控系統,如果多地部署,某個RZone直接讀取本地的話,很容易讀取到舊的風控狀態,這是很危險的。這時螞蟻架構師們問了自己一個問題——難道所有資料受不了延時麼?這個問題像是打開了新世界的大門,通過對RZone已有業務的分析,架構師們發現80%甚至更高的場景下,資料更新後都不要求立馬被讀取到。也就是上文提到的”寫讀時間差現象”,那麼這就好辦了,對於這類資料,我們允許每個地區的RZone服務直接訪問本地,為了給這些RZone提供這些資料的本地訪問能力,螞蟻架構師設計出了CZone。在CZone的場景下,寫請求一般從GZone寫入公共資料所在庫,然後同步到整個OB叢集,然後由CZone提供讀取服務。比如支付寶的會員服務就是如此。
即便架構師們設計了完美的CRG,但即便在螞蟻的實際應用中,各個系統仍然存在不合理的CRG分類,尤其是CG不分的現象很常見。
三、支付寶單元化的異地多活和災備3.1、流量挑撥技術探祕簡介
單元化後,異地多活只是多地部署而已。比如上海的兩個單元為ID範圍為[0019],[4059]的使用者服務,而杭州的兩個單元為ID為[20~39]和[60,79]的使用者服務,這樣上海和杭州就是異地雙活的。
支付寶對單元化的基本要求是每個單元都具備服務所有使用者的能力,即——具體的那個單元服務哪些使用者是可以動態配置的。所以異地雙活的這些單元還充當了彼此的備份。
發現工作中冷備熱備已經被用的很亂了。最早冷備是指資料庫在備份資料時需要關閉後進行備份(也叫離線備份),防止資料備份過程中又修改了,不需要關閉即在執行過程中進行資料備份的方式叫做熱備(也叫線上備份)[7]。也不知道從哪一天開始,冷備在主備系統裡代表了這臺備用機器是關閉狀態的,只有主伺服器掛了之後,備伺服器才會被啟動;而相同的熱備變成了備伺服器也是啟動的,只是沒有流量而已,一旦主伺服器掛了之後,流量自動打到備伺服器上。本文不打算用第二種理解,因為感覺有點野、、、
為了做到每個單元訪問哪些使用者變成可配置,支付寶要求單元化管理系統具備流量到單元的可配置以及單元到DB的可配置能力,如下圖所示:
其中spanner是螞蟻基於nginx自研的反向代理閘道器,也很好理解,有些請求我們希望在反向代理層就被轉發至其他IDC的spanner而無需進入後端服務,如圖箭頭2所示。那麼對於應該在本IDC處理的請求,就直接對映到對應的RZ即可,如圖箭頭1。進入後端服務後,理論上如果請求只是讀取使用者流水型資料,那麼一般不會再進行路由了。然而,對於有些場景來說,A使用者的一個請求可能關聯了對B使用者資料的訪問,比如A轉賬給B,A扣完錢後要呼叫賬務系統去增加B的餘額。這時候就涉及到再次的路由,同樣有兩個結果:跳轉到其他IDC(如圖箭頭3)或是跳轉到本IDC的其他RZone(如圖箭頭4)。
RZone到DB資料分割槽的訪問這是事先配置好的,上圖中RZ和DB資料分割槽的關係為:
RZ0* --> aRZ1* --> bRZ2* --> cRZ3* --> d下面我們舉個例子來說明整個流量挑撥的過程,假設C使用者所屬的資料分割槽是c,而C使用者在杭州訪問了cashier.alipay.com(隨便編的)。
(1)目前支付寶預設會按照地域來路由流量,具體的實現承載者是自研的GLSB(Global Server Load Balancing)
[https://developer.alipay.com/article/1889],它會根據請求者的IP,自動將cashier.alipay.com解析為杭州IDC的IP地址(或者跳轉到IDC所在的域名)。大家自己高過網站的化應該知道大部分DNS服務商的地址都是靠人去配置的,GLSB屬於動態配置域名的系統,網上也有比較火的類似產品,比如花生殼之類(建過私站的同學應該很熟悉)的。
(2)好了,到此為止,使用者的請求來到了IDC-1的Spanner叢集伺服器上,Spanner從記憶體中讀取到了路由配置,知道了這個請求的主體使用者C所屬的RZ3*不再本IDC,於是直接轉到了IDC-2進行處理。
(3)進入IDC-2之後,根據流量配比規則,該請求被分配到了RZ3B進行處理。
(4)RZ3B得到請求後對資料分割槽c進行訪問。
(5)處理完畢後原路返回。
大家應該發現問題所在了,如果再來一個這樣的請求,豈不是每次都要跨地域進行呼叫和返回體傳遞?確實是存在這樣的問題的,對於這種問題,支付寶架構師們決定繼續把決策邏輯往使用者終端推移。比如,每個IDC機房都會有自己的域名(真實情況可能不是這樣命名的): IDC-1對應cashieridc-1.alipay.com IDC-2對應cashieridc-2.alipay.com 那麼請求從IDC-1涮過一遍返回時會將前端請求跳轉到cashieridc-2.alipay.com去(如果是APP,只需要替換rest呼叫的介面域名),後面所有使用者的行為都會在這個域名上發生,就避免了走一遍IDC-1帶來的延時。
3.2、支付寶災備機制
流量挑撥是災備切換的基礎和前提條件,發生災難後的通用方法就是把陷入災難的單元的流量重新打到正常的單元上去,這個流量切換的過程俗稱切流。支付寶LDC架構下的災備有三個層次:
同機房單元間災備。同城機房間災備。異地機房間災備。3.2.1、同機房單元間災備
災難發生可能性相對最高(但其實也很小)。對LDC來說,最小的災難就是某個單元由於一些原因(區域性插座斷開、線路老化、人為操作失誤)宕機了。從3.1節裡的圖中可以看到每組RZ都有A,B兩個單元,這就是用來做同機房災備的,並且AB之間也是雙活雙備的,正常情況下AB兩個單元共同分擔所有的請求,一旦A單元掛了,B單元將自動承擔A單元的流量份額。這個災備方案是預設的。
3.2.2、同城機房間災備
災難發生可能性相對更小。這種災難發生的原因一般是機房電線網線被挖斷,或者機房維護人員操作失誤導致的。在這種情況下,就需要人工的制定流量挑撥(切流)方案了。下面我們舉例說明這個過程,如下圖所示為上海的兩個IDC機房。
整個切流配置過程分兩步,首先需要將陷入災難的機房中RZone對應的資料分割槽的訪問權配置進行修改;假設我們的方案是由IDC-2機房的RZ2和RZ3分別接管IDC-1中的RZ0和RZ1。那麼首先要做的是把資料分割槽a,b對應的訪問權從RZ0和RZ1收回,分配給RZ2和RZ3。即將(如上圖所示為初始對映):
RZ0* --> aRZ1* --> bRZ2* --> cRZ3* --> d變為:
RZ0* --> /RZ1* --> /RZ2* --> aRZ2* --> cRZ3* --> bRZ3* --> d然後再修改使用者ID和RZ之間的對映配置。假設之前為:
[00-24] --> RZ0A(50%),RZOB(50%)[25-49] --> RZ1A(50%),RZ1B(50%)[50-74] --> RZ2A(50%),RZ2B(50%)[75-99] --> RZ3A(50%),RZ3B(50%)那麼按照災備方案的要求,這個對映配置將變為:
[00-24] --> RZ2A(50%),RZ2B(50%)[25-49] --> RZ3A(50%),RZ3B(50%)[50-74] --> RZ2A(50%),RZ2B(50%)[75-99] --> RZ3A(50%),RZ3B(50%)這樣之後,所有流量將會被打到IDC-2中,期間部分已經向IDC-1發起請求的使用者會收到失敗並重試的提示。 實際情況中,整個過程並不是災難發生後再去做的,整個切換的流程會以預案配置的形式事先準備好,推送給每個流量挑撥客戶端(整合到了所有的服務和spanner中)。
這裡可以思考下,為何先切資料庫對映,再切流量呢?這是因為如果先切流量,意味著大量註定失敗的請求會被打到新的正常單元上去,從而影響系統的穩定性(資料庫還沒準備好)。
3.2.3、異地機房間災備
這個基本上跟同城機房間災備一致(這也是單元化的優點),不再贅述。
四、螞蟻單元化架構的CAP分析4.1、回顧CAP
4.1.1 CAP的定義
CAP原則是指任意一個分散式系統,同時最多隻能滿足其中的兩項,而無法同時滿足三項。所謂的分散式系統,說白了就是一件事一個人做的,現在分給好幾個人一起幹。我們先簡單回顧下CAP各個維度的含義[1]:
Consistency(一致性),這個理解起來很簡單,就是每時每刻每個節點上的同一份資料都是一致的。這就要求任何更新都是原子的,即要麼全部成功,要麼全部失敗。想象一下使用分散式事務來保證所有系統的原子性是多麼低效的一個操作。Availability(可用性),這個可用性看起來很容易理解,但真正說清楚的不多。我更願意把可用性解釋為:任意時刻系統都可以提供讀寫服務。那麼舉個例子,當我們用事務將所有節點鎖住來進行某種寫操作時,如果某個節點發生不可用的情況,會讓整個系統不可用。對於分片式的NoSQL中介軟體叢集(Redis,Memcached)來說,一旦一個分片歇菜了,整個系統的資料也就不完整了,讀取宕機分片的資料就會沒響應,也就是不可用了。需要說明一點,哪些選擇CP的分散式系統,並不是代表可用性就完全沒有了,只是可用性沒有保障了。為了增加可用性保障,這類中介軟體往往都提供了”分片叢集+複製集”的方案。Partition tolerance(分割槽容忍性),這個可能也是很多文章都沒說清楚的。P並不是像CA一樣是一個獨立的性質,它依託於CA來進行討論。參考文獻[1]中解釋道:”除非整個網路癱瘓,否則任何時刻系統都能正常工作”,言下之意是小範圍的網路癱瘓,節點宕機,都不會影響整個系統的CA。我感覺這個解釋聽著還是有點懵逼,所以個人更願意解釋為”當節點之間網路不通時(出現網路分割槽),可用性和一致性仍然能得到保障”。從個人角度理解,分割槽容忍性又分為”可用性分割槽容忍性”和”一致性分割槽容忍性”。”出現分割槽時會不會影響可用性”的關鍵在於”需不需要所有節點互相溝通協作來完成一次事務”,不需要的話是鐵定不影響可用性的,慶幸的是應該不太會有分散式系統會被設計成完成一次事務需要所有節點聯動,一定要舉個例子的話,全同步複製技術下的Mysql是一個典型案例[2]。”出現分割槽時會不會影響一致性”的關鍵則在於出現腦裂時有沒有保證一致性的方案,這對主從同步型資料庫(MySQL、SQL Server)是致命的,一旦網路出現分割槽,產生腦裂,系統會出現一份資料兩個值的狀態,誰都不覺得自己是錯的。需要說明的是,正常來說同一區域網內,網路分割槽的概率非常低,這也是為啥我們最熟悉的資料庫(MySQL、SQL Server等)也是不考慮P的原因。下圖為CAP之間的經典關係圖:
還有個需要說明的地方,其實分散式系統很難滿足CAP的前提條件是這個系統一定是有讀有寫的,如果只考慮讀,那麼CAP很容易都滿足,比如一個計算器服務,接受表示式請求,返回計算結果,搞成水平擴充套件的分散式,顯然這樣的系統沒有一致性問題,網路分割槽也不怕,可用性也是很穩的,所以可以滿足CAP。
4.1.2 CAP分析方法
先說下CA和P的關係,如果不考慮P的話,系統是可以輕鬆實現CA的。而P並不是一個單獨的性質,它代表的是目標分散式系統有沒有對網路分割槽的情況做容錯處理。如果做了處理,就一定是帶有P的,接下來再考慮分割槽情況下到底選擇了A還是C。所以分析CAP,建議先確定有沒有對分割槽情況做容錯處理。以下是個人總結的分析一個分散式系統CAP滿足情況的一般方法:
if( 不存在分割槽的可能性 || 分割槽後不影響可用性或一致性 || 有影響但考慮了分割槽情況-P){ if(可用性分割槽容忍性-A under P)) return "AP"; else if(一致性分割槽容忍性-C under P) return "CP";}else { //分割槽有影響但沒考慮分割槽情況下的容錯 if(具備可用性-A && 具備一致性-C){ return AC; }}這裡說明下,如果考慮了分割槽容忍性,就不需要考慮不分割槽情況下的可用性和一致性了(大多是滿足的)。
4.2 水平擴充套件應用+單資料庫例項的CAP分析
讓我們再來回顧下分散式應用系統的來由,早年每個應用都是單體的,跑在一個伺服器上,伺服器一掛,服務就不可用了。另外一方面,單體應用由於業務功能複雜,對機器的要求也逐漸變高,普通的微機無法滿足這種效能和容量的要求。所以要拆!還在IBM大賣小型商用機的年代,阿里巴巴就提出要以分散式微機替代小型機。所以我們發現,分散式系統解決的最大的痛點,就是單體單機系統的可用性問題。要想高可用,必須分散式。 一家網際網路公司的發展之路上,第一次與分散式相遇應該都是在單體應用的水平擴充套件上。也就是同一個應用啟動了多個例項,連線著相同的資料庫(為了簡化問題,先不考慮資料庫是否單點),如下圖所示。
這樣的系統天然具有的就是AP(可用性和分割槽容忍性),一方面解決了單點導致的低可用性問題,另一方面無論這些水平擴充套件的機器間網路是否出現分割槽,這些伺服器都可以各自提供服務,因為他們之間不需要進行溝通。然而,這樣的系統是沒有一致性可言的,想象一下每個例項都可以往資料庫insert和update(注意這裡還沒討論到事務),那還不亂了套。
於是我們轉向了讓DB去做這個事,這時候”資料庫事務”就被用上了。用大部分公司會選擇的Mysql來舉例,用了事務之後會發現資料庫又變成了單點和瓶頸。單點就像單機一樣(本例子中不考慮從庫模式),理論上就不叫分散式了,如果一定要分析其CAP的話,根據4.1.2的步驟分析過程應該是這樣的:
分割槽容忍性:先看有沒有考慮分割槽容忍性,或者分割槽後是否會有影響。單臺MySQL無法構成分割槽,要麼整個系統掛了,要麼就活著。可用性分割槽容忍性:分割槽情況下,假設恰好是該節點掛了,系統也就不可用了,所以可用性分割槽容忍性不滿足。一致性分割槽容忍性:分割槽情況下,只要可用,單點單機的最大好處就是一致性可以得到保障。因此這樣的一個系統,個人認為只是滿足了CP。A有但不出色,從這點可以看出,CAP並不是非黑即白的。包括常說的BASE[3](最終一致性)方案,其實只是C不出色,但最終也是達到一致性的,BASE在一致性上選擇了退讓。關於分散式應用+單點資料庫的模式算不算純正的分散式系統,這個可能每個人看法有點差異,上述只是我個人的一種理解,是不是分散式系統不重要,重要的是分析過程。其實我們討論分散式,就是希望系統的可用性是多個系統多活的,一個掛了另外的也能頂上,顯然單機單點的系統不具備這樣的高可用特性。所以在我看來,廣義的說CAP也適用於單點單機系統,單機系統是CP的。
說到這裡,大家似乎也發現了,水平擴充套件的服務應用+資料庫這樣的系統的CAP魔咒主要發生在資料庫層,因為大部分這樣的服務應用都只是承擔了計算的任務(像計算器那樣),本身不需要互相協作,所有寫請求帶來的資料的一致性問題下沉到了資料庫層去解決。想象一下,如果沒有資料庫層,而是應用自己來保障資料一致性,那麼這樣的應用之間就涉及到狀態的同步和互動了,Zookeeper就是這麼一個典型的例子。
4.3 水平擴充套件應用+主從資料庫叢集的CAP分析
上一節我們討論了多應用例項+單資料庫例項的模式,這種模式是分散式系統也好,不是分散式系統也罷,整體是偏CP的。現實中,技術人員們也會很快發現這種架構的不合理性——可用性太低了。於是如下圖所示的模式成為了當下大部分中小公司所使用的架構:
從上圖我可以看到三個資料庫例項中只有一個是主庫,其他是從庫。一定程度上,這種架構極大的緩解了”讀可用性”問題,而這樣的架構一般會做讀寫分離來達到更高的”讀可用性”,幸運的是大部分網際網路場景中讀都佔了80%以上,所以這樣的架構能得到較長時間的廣泛應用。”寫可用性”可以通過keepalived[4]這種HA(高可用)框架來保證主庫是活著的,但仔細一想就可以明白,這種方式並沒有帶來效能上的可用性提升。還好,至少系統不會因為某個例項掛了就都不可用了。可用性勉強達標了,這時候的CAP分析如下:
分割槽容忍性:依舊先看分割槽容忍性,主從結構的資料庫存在節點之間的通訊,他們之間需要通過心跳來保證只有一個Master。然而一旦發生分割槽,每個分割槽會自己選取一個新的Master,這樣就出現了腦裂,常見的主從資料庫(MySQL,Oracle等)並沒有自帶解決腦裂的方案。所以分割槽容忍性是沒考慮的。一致性:不考慮分割槽,由於任意時刻只有一個主庫,所以一致性是滿足的。可用性:不考慮分割槽,HA機制的存在可以保證可用性,所以可用性顯然也是滿足的。所以這樣的一個系統,我們認為它是AC的。我們再深入研究下,如果發生腦裂產生資料不一致後有一種方式可以仲裁一致性問題,是不是就可以滿足P了呢。還真有嘗試通過預先設定規則來解決這種多主庫帶來的一致性問題的系統,比如CouchDB,它通過版本管理來支援多庫寫入,在其仲裁階段會通過DBA配置的仲裁規則(也就是合併規則,比如誰的時間戳最晚誰的生效)進行自動仲裁(自動合併),從而保障最終一致性(BASE),自動規則無法合併的情況則只能依賴人工決策了。
4.4 螞蟻單元化LDC架構CAP分析
4.4.1 戰勝分割槽容忍性
在討論螞蟻LDC架構的CAP之前,我們再來想想分割槽容忍性有啥值得一提的,為啥很多大名鼎鼎的BASE(最終一致性)體系系統都選擇損失實時一致性,而不是丟棄分割槽容忍性呢?
分割槽的產生一般有兩種情況:
某臺機器宕機了,過一會兒又重啟了,看起來就像失聯了一段時間,像是網路不可達一樣。異地部署情況下,異地多活意味著每一地都可能會產生資料寫入,而異地之間偶爾的網路延時尖刺(網路延時曲線圖陡增)、網路故障都會導致小範圍的網路分割槽產生。前文也提到過,如果一個分散式系統是部署在一個區域網內的(一個物理機房內),那麼個人認為分割槽的概率極低,即便有複雜的拓撲,也很少會有在同一個機房裡出現網路分割槽的情況。而異地這個概率會大大增高,所以螞蟻的三地五中心必須需要思考這樣的問題,分割槽容忍不能丟!同樣的情況還會發生在不同ISP的機房之間(想象一下你和朋友組隊玩DOTA,他在電信,你在聯通)。為了應對某一時刻某個機房突發的網路延時尖刺活著間歇性失聯,一個好的分散式系統一定能處理好這種情況下的一致性問題。那麼螞蟻是怎麼解決這個問題的呢?我們在4.2的備註部分討論過,其實LDC機房的各個單元都由兩部分組成:負責業務邏輯計算的應用伺服器和負責資料持久化的資料庫。大部分應用伺服器就像一個個計算器,自身是不對寫一致性負責的,這個任務被下沉到了資料庫。所以螞蟻解決分散式一致性問題的關鍵就在於資料庫!
想必螞蟻的讀者大概猜到下面的討論重點了——OceanBase(下文簡稱OB),中國第一款自主研發的分散式資料庫,一時間也確實獲得了很多光環。在討論OB前,我們先來想想Why not MySQL?
首先,就像CAP三角圖中指出的,MySQL是一款滿足AC但不滿足P的分散式系統。試想一下,一個MySQL主從結構的資料庫叢集,當出現分割槽時,問題分割槽內的Slave會認為主已經掛了,所以自己成為本分割槽的master(腦裂),等分割槽問題恢復後,會產生2個主庫的資料,而無法確定誰是正確的,也就是分割槽導致了一致性被破壞。這樣的結果是嚴重的,這也是螞蟻寧願自研OceanBase的原動力之一。
(1) 可用性分割槽容忍性保障機制
可用性分割槽容忍的關鍵在於別讓一個事務以來所有節點來完成,這個很簡單,別要求所有節點共同同時參與某個事務即可。
(2) 一致性分割槽容忍性保障機制
老實說,都產生分割槽了,哪還可能獲得實時一致性。但要保證最終一致性也不簡單,一旦產生分割槽,如何保證同一時刻只會產生一份提議呢?換句話說,如何保障仍然只有一個腦呢?下面我們來看下PAXOS演算法是如何解決腦裂問題的。
這裡可以發散下,所謂的”腦”其實就是具備寫能力的系統,”非腦”就是隻具備讀能力的系統,對應了MySQL叢集中的從庫。
下面是一段摘自維基百科的PAXOS定義[5]:
Paxos is a family of protocols for solving consensus in a network of unreliable processors (that is, processors that may fail).
大致意思就是說,PAXOS是在一群不是特別可靠的節點組成的叢集中的一種共識機制。Paxos要求任何一個提議,至少有(N/2)+1的系統節點認可,才被認為是可信的,這背後的一個基礎理論是少數服從多數。想象一下,如果多數節點認可後,整個系統宕機了,重啟後,仍然可以通過一次投票知道哪個值是合法的(多數節點保留的那個值)。這樣的設定也巧妙的解決了分割槽情況下的共識問題,因為一旦產生分割槽,勢必最多隻有一個分割槽內的節點數量會大於等於(N/2)+1。通過這樣的設計就可以巧妙的避開腦裂,當然MySQL叢集的腦裂問題也是可以通過其他方法來解決的,比如同時Ping一個公共的IP,成功者繼續為腦,顯然這就又製造了另外一個單點。
如果你了解過比特幣或者區塊鏈,你就知道區塊鏈的基礎理論也是PAXOS。區塊鏈藉助PAXOS對最終一致性的貢獻來抵禦惡意篡改。而本文涉及的分散式應用系統則是通過PAXOS來解決分割槽容忍性。再說本質一點,一個是抵禦部分節點變壞,一個是防範部分節點失聯。
大家一聽說過這樣的描述——PAXOS是唯一能解決分散式一致性問題的解法。這句話越是理解越發覺得詭異,這會讓人以為PAXOS逃離於CAP約束了,所以個人更願意理解為——PAXOS是唯一一種保障分散式系統最終一致性的共識演算法(所謂共識演算法,就是大家都按照這個演算法來操作,大家最後的結果一定相同)。PAXOS並沒有逃離CAP魔咒,畢竟達成共識是(N/2)+1的節點之間的事,剩下的(N/2)-1的節點上的資料還是舊的,這時候仍然是不一致的,所以PAXOS對一致性的貢獻在於經過一次事務後,這個叢集裡已經有部分節點保有了本次事務正確的結果(共識的結果),這個結果隨後會被非同步的同步到其他節點上,從而保證最終一致性。以下摘自維基百科[5]:
Quorums express the safety (or consistency) properties of Paxos by ensuring at least some surviving processor retains knowledge of the results.
另外PAXOS不要求對所有節點做實時同步,實質上是考慮到了分割槽情況下的可用性,通過減少完成一次事務需要的參與者個數,來保障系統的可用性。
4.4.2 OceanBase的CAP分析
上文提到過,單元化架構中的成千山萬的應用就像是計算器,本身無CAP限制,其CAP限制下沉到了其資料庫層,也就是螞蟻自研的分散式資料庫OceanBase(本節簡稱OB)[6]。在OB體系中,每個資料庫例項都具備讀寫能力,具體是讀是寫可以動態配置(參考2.2部分)。實際情況下大部分時候,對於某一類資料(固定使用者號段的資料)任意時刻只有一個單元會負責寫入某個節點,其他節點要麼是實時庫間同步,要麼是非同步資料同步。OB也採用了PAXOS共識協議。實時庫間同步的節點(包含自己)個數至少需要(N/2)+1個,這樣就可以解決分割槽容忍性問題。
下面我們舉個馬老師改英文名的例子來說明OB設計的精妙之處。假設資料庫按照使用者ID分庫分表,馬老師的使用者ID對應的資料段在[0-9],開始由單元A負責資料寫入,假如馬老師(使用者ID假設為000)正在用支付寶APP修改自己的英文名,馬老師一開始打錯了,打成了Jason Ma,A單元收到了這個請求。這時候發生了分割槽(比如A網路斷開了),我們將單元A對資料段[0,9]的寫入許可權轉交給單元B(更改對映),馬老師這次寫對了,為Jack Ma。而在網路斷開前請求已經進入了A,寫許可權轉交給單元B生效後,A和B同時對[0,9]資料段進行寫入馬老師的英文名。假如這時候都允許寫入的話就會出現不一致,A單元說我看到馬老師設定了Jason Ma,B單元說我看到馬老師設定了Jack Ma。然而這種情況不會發生的,A提議說我建議把馬老師的英文名設定為Jason Ma時,發現沒人迴應它,因為出現了分割槽,其他節點對它來說都是不可達的,所以這個提議被自動丟棄,A心裡也明白是自己分割槽了,會有主分割槽替自己完成寫入任務的。同樣的,B提出了將馬老師的英文名改成Jack Ma後,大部分節點都響應了,所以B成功將Jack Ma寫入了馬老師的賬號記錄。假如在寫許可權轉交給單元B後A突然恢復了,也沒關係,兩筆寫請求同時要求獲得(N/2)+1個節點的事務鎖,通過no-wait設計,在B獲得了鎖之後,其他掙強該鎖的事務都會因為失敗而回滾。
下面我們分析下OB的CAP:
分割槽容忍性:OB節點之間是有互相通訊的(需要相互同步資料),所以存在分割槽問題,OB通過僅同步到部分節點來保證可用性。這一點就說明OB做了分割槽容錯。可用性分割槽容忍性:OB事務只需要同步到(N/2)+1個節點,允許其餘的一小半節點分割槽(宕機、斷網等),只要(N/2)+1個節點活著就是可用的。極端情況下,比如5個節點分成3份(2:2:1),那就確實不可用了,只是這種情況概率比較低。一致性分割槽容忍性:分割槽情況下意味著部分節點失聯了,一致性顯然是不滿足的。但通過共識演算法可以保證當下只有一個值是合法的,並且最終會通過節點間的同步達到最終一致性。所以OB仍然沒有逃脫CAP魔咒,產生分割槽的時候它變成AP+最終一致性(C)。整體來說,它是AP的,即高可用和分割槽容忍。五、結語個人感覺本文涉及到的知識面確實不少,每個點單獨展開都可以討論半天。回到我們緊扣的主旨來看,雙十一海量支付背後技術上大快人心的設計到底是啥?我想無非是以下幾點:
基於使用者分庫分表的RZone設計。每個使用者群獨佔一個單元給整個系統的容量帶來了爆發式增長。
RZone在網路分割槽或災備切換時OB的防腦裂設計(PAXOS)。我們知道RZone是單腦的(讀寫都在一個單元對應的庫),而網路分割槽或者災備時熱切換過程中可能會產生多個腦,OB解決了腦裂情況下的共識問題(PAXOS演算法)。基於CZone的本地讀設計。這一點保證了很大一部分有著“寫讀時間差”現象的公共資料能被高速本地訪問。剩下的那一丟丟不能本地訪問只能實時訪問GZone的公共配置資料,也興不起什麼風,作不了什麼浪。比如使用者建立這種TPS,不會高到哪裡去。再比如對於實時庫存資料,可以通過“頁面展示查詢走應用層快取”+“實際下單時再校驗”的方式減少其GZone呼叫量。而這就是螞蟻LDC的CRG架構,相信54.4萬筆/秒還遠沒到LDC的上限,這個數字可以做到更高。當然雙十一海量支付的成功不單單是這麼一套設計所決定的,還有預熱削峰等運營+技術的手段,以及成百上千的兄弟姐妹共同奮戰,特此在這向各位雙十一留守同學致敬。
-
1 #
-
2 #
第一句話有問題嗎?
順利的從頭看到尾,然後一句也沒看懂