一篇簡明扼要的分布式系統設計概要,可以好好參考。
原文鏈接:
上篇:http://www.cnblogs.com/ccdev/p/3338412.html
中篇:http://www.cnblogs.com/ccdev/p/3340484.html
下篇:http://www.cnblogs.com/ccdev/p/3341234.html
來源:張峻崇
鏈接:www.cnblogs.com/ccdev/p/3338412.html
寫這篇文章的目的,主要是把今年以來學習的一些東西積淀下來,同時作為之前文章《高性能分布式計算與存儲系統設計概要》的補充與提升,然而本人水平非常有限,回頭看之前寫的文章也有許多不足,甚至是錯誤,希望同學們看到了錯誤多多見諒,更歡迎與我討論並指正。
我大概是從2010年底起開始進入高並發、高性能服務器和分布式這一塊領域的研究,到現在也差不多有三年,但其實很多東西仍然是一知半解,我所提到的許許多多概念,也許任何一個我都不能講的很清楚,還需要繼續鑽研。但我們平時在工作和學習中,多半也只能從這種一知半解開始,慢慢琢磨,不斷改進。
好了,下面開始說我們今天要設計的系統。
這個系統的目標很明確,針對千萬級以上PV的網站,設計一套用於后台的高並發的分布式處理系統。這套系統包含業務邏輯的處理、各種計算、存儲、日志、備份等方面內容,可用於類微博,SNS,廣告推送,郵件等有大量線上並發請求的場景。
如何抗大流量高並發?(不要告訴我把服務器買的再好一點)說起來很簡單,就是“分”,如何“分”,簡單的說就是把不同的業務分拆到不同的服務器上去跑(垂直拆分),相同的業務壓力分拆到不同的服務器去跑(水平拆分),並時刻不要忘記備份、擴展、意外處理等討厭的問題。說起來都比較簡單,但設計和實現起來,就會比較困難。以前我的文章,都是“從整到零”的方式來設計一個系統,這次咱們就反着順序來。
那我們首先來看,我們的數據應該如何存儲和取用。根據我們之前確定的“分”的方法,先確定以下2點:
(1)我們的分布式系統,按不同的業務,存儲不同的數據;(2)同樣的業務,同一個數據應存儲多份,其中有的存儲提供讀寫,而有的存儲只提供讀。
好,先解釋下這2點。對於(1)應該容易理解,比如說,我這套系統用於微博(就假想我們做一個山寨的推特吧,給他個命名就叫“山推” 好了,以下都叫山推,Stwi),那么,“我關注的人”這一個業務的數據,肯定和“我發了的推文”這個業務的數據是分開存儲的,那么我們現在把,每一個業務所負責的數據的存儲,稱為一個group。即以group的方式,來負責各個業務的數據的存儲。接下來說(2),現在我們已經知道,數據按業務拆到group里面去存取,那么一個group里面又應該有哪些角色呢?自然的,應該有一台主要的機器,作為group的核心,我們稱它為Group Master,是的,它就是這個group的主要代表。這個group的數據,在Group Master上應該都能找到,進行讀寫。另外,我們還需要一些輔助角色,我們稱它們為Group Slaves,這些slave機器做啥工作呢?它們負責去Group Master處拿數據,並盡量保持和它同步,並提供讀服務。請注意我的用詞,“盡量”,稍后將會解釋。現在我們已經有了一個group的基本輪廓:

一個group提供對外的接口(廢話否則怎么存取數據),group的底層可以是實際的File System,甚至是HDFS。Group Master和Group Slave可以共享同一個File System(用於不能丟數據的強一致性系統),也可以分別指向不同的File System(用於弱一致性,允許停寫服務和系統宕機時丟數據的系統),但總之應認為這個"File System"是無狀態,有狀態的是Group Master和各個Group Slave。
下面來說一個group如何工作,同步等核心問題。首先,一個group的Group Master和Group Slave間應保持強一致性還是弱一致性(最終一致性)應取決於具體的業務需求,以我們的“山推”來說,Group Master和Group Slave並不要求保持強一致性,而弱一致性(最終一致性)即能滿足要求,為什么?因為對於“山推”來講,一個Group Master寫了一個數據,而另一個Group Slave被讀到一個“過期”(因為Group Master已經寫,但此Group Slave還未更新此數據)的數據通常並不會帶來大問題,比如,我在“山推”上發了一個推文,“關注我的人”並沒有即時同步地看到我的最新推文,並沒有太大影響,只要“稍后”它們能看到最新的數據即可,這就是所謂的最終一致性。但當Group Master掛掉時,寫服務將中斷一小段時間由其它Group Slave來頂替,稍后還要再講這個問題。假如我們要做的系統不是山推,而是淘寶購物車,支付寶一類的,那么弱一致性(最終一致性)則很難滿足要求,同時寫服務掛掉也是不能忍受的,對於這樣的系統,應保證“強一致性”,保證不能丟失任何數據。
接下來還是以我們的“山推“為例,看看一個group如何完成數據同步。假設,現在我有一個請求要寫一個數據,由於只有Group Master能寫,那么Group Master將接受這個寫請求,並加入寫的隊列,然后Group Master將通知所有Group Slave來更新這個數據,之后這個數據才真正被寫入File System。那么現在就有一個問題,是否應等所有Group Slave都更新了這個數據,才算寫成功了呢?這里涉及一些NWR的概念,我們作一個取舍,即至少有一個Group Slave同步成功,才能返回寫請求的成功。這是為什么呢?因為假如這時候Group Master突然掛掉了,那么我們至少可以找到一台Group Slave保持和Group Master完全同步的數據並頂替它繼續工作,剩下的、其它的Group Slave將“異步”地更新這個新數據,很顯然,假如現在有多個讀請求過來並到達不同的Group Slave節點,它們很可能讀到不一樣的數據,但最終這些數據會一致,如前所述。我們做的這種取舍,叫“半同步”模式。那之前所說的強一致性系統應如何工作呢?很顯然,必須得等所有Group Slave都同步完成才能返回寫成功,這樣Group Master掛了,沒事,其它Group Slave頂上就行,不會丟失數據,但是付出的代價就是,等待同步的時間。假如我們的group是跨機房、跨地區分布的,那么等待所有Group Slave同步完成將是很大的性能挑戰。所以綜合考慮,除了對某些特別的系統,采用“最終一致性”和“半同步”工作的系統,是符合高並發線上應用需求的。而且,還有一個非常重要的原因,就是通常線上的請求都是讀>>寫,這也正是“最終一致性”符合的應用場景。
好,繼續。剛才我們曾提到,如果Group Master宕機掛掉,至少可以找到一個和它保持同不的Group Slave來頂替它繼續工作,其它的Group Slave則“盡量”保持和Group Master同步,如前文所述。那么這是如何做到的呢?這里涉及到“分布式選舉”的概念,如Paxos協議,通過分布式選舉,總能找到一個最接近Group Master的Group Slave,來頂替它,從而保證系統的可持續工作。當然,在此過程中,對於最終一致性系統,仍然會有一小段時間的寫服務中斷。現在繼續假設,我們的“山推”已經有了一些規模,而負責“山推”推文的這個group也有了五台機器,並跨機房,跨地區分布,按照上述設計,無論哪個機房斷電或機器故障,都不會影響這個group的正常工作,只是會有一些小的影響而已。
那么對於這個group,還剩2個問題,一是如何知道Group Master掛掉了呢?二是在圖中我們已經看到Group Slave是可擴展的,那么新加入的Group Slave應如何去“偷”數據從而逐漸和其它節點同步呢?對於問題一,我們的方案是這樣的,另外提供一個類似“心跳”的服務(由誰提供呢,后面我們將講到的Global Master將派上用場),group內所有節點無論是Group Master還是Group Slave都不停地向這個“心跳”服務去申請一個證書,或認為是一把鎖,並且這個鎖是有時間的,會過期。“心跳”服務定期檢查Group Master的鎖和其有效性,一旦過期,如果Group Master工作正常,它將鎖延期並繼續工作,否則說明Group Master掛掉,由其它Group Slave競爭得到此鎖(分布式選舉),從而變成新的Group Master。對於問題二,則很簡單,新加入的Group Slave不斷地“偷”老數據,而新數據總由於Group Master通知其更新,最終與其它所有結點同步。(當然,“偷”數據所用的時間並不樂觀,通常在小時級別)
我們完成了在此分布式系統中,一個group的設計。那么接下來,我們設計系統的其他部分。如前文所述,我們的業務及其數據以group為單位,顯然在此系統中將存在many many的groups(別告訴我你的網站總共有一個業務,像我們的“山推”,那業務是一堆一堆地),那么由誰來管理這些groups呢?由Web過來的請求,又將如何到達指定的group,並由該group處理它的請求呢?這就是我們要討論的問題。
我們引入了一個新的角色——Global Master,顧名思義,它是管理全局的一個節點,它主要完成如下工作:(1)管理系統全局配置,發送全局控制信息;(2)監控各個group的工作狀態,提供心跳服務,若發現宕機,通知該group發起分布式選舉產生新的Group Master;(3)處理Client端首次到達的請求,找出負責處理該請求的group並將此group的信息(location)返回,則來自同一個前端請求源的該類業務請求自第二次起不需要再向Global Master查詢group信息(緩存機制);(4)保持和Global Slave的強一致性同步,保持自身健康狀態並向全局的“心跳”服務驗證自身的狀態。
現在我們結合圖來逐條解釋上述工作,顯然,這個系統的完整輪廓已經初現。

首先要明確,不管我們的系統如何“分布式”,總之會有至少一個最主要的節點,術語可稱為primary node,如圖所示,我們的系統中,這個節點叫Global Master,也許讀過GFS + Bigtable論文的同學知道,在GFS + Bigtable里,這樣的節點叫Config Master,雖然名稱不一樣,但所做的事情卻差不多。這個主要的Global Master可認為是系統狀態健康的標志之一,只要它在正常工作,那么基本可以保證整個系統的狀態是基本正常的(什么?group或其他結點會不正常不工作?前面已經說過,group內會通過“分布式選舉”來保證自己組內的正常工作狀態,不要告訴我group內所有機器都掛掉了,那個概率我想要忽略它),假如Global Master不正常了,掛掉了,怎么辦?顯然,圖中的Global Slave就派上用場了,在我們設計的這個“山推”系統中,至少有一個Global Slave,和Global Master保持“強一致性”的完全同步,當然,如果有不止一個Global Slave,它們也都和Global Master保持強一致性完全同步,這樣有個好處,假如Global Master掛掉,不用停寫服務,不用進行分布式選舉,更不會讀服務,隨便找一個Global Slave頂替Global Master工作即可。這就是強一致性最大的好處。那么有的同學就會問,為什么我們之前的group,不能這么搞,非要搞什么最終一致性,搞什么分布式選舉(Paxos協議屬於既難理解又難實現的坑爹一族)呢?我告訴你,還是壓力,壓力。我們的系統是面向日均千萬級PV以上的網站(“山推”嘛,推特是億級PV,我們千萬級也不過分吧),但系統的壓力主要在哪呢?細心的同學就會發現,系統的壓力並不在Global Master,更不會在Global Slave,因為他們根本不提供數據的讀寫服務!是的,系統的壓力正是在各個group,所以group的設計才是最關鍵的。同時,細心的同學也發現了,由於Global Master存放的是各個group的信息和狀態,而不是用戶存取的數據,所以它更新較少,也不能認為讀>>寫,這是不成立的,所以,Global Slave和Global Master保持強一致性完全同步,正是最好的選擇。所以我們的系統,一台Global Master和一台Global Slave,暫時可以滿足需求了。
好,我們繼續。現在已經了解Global Master的大概用途,那么,一個來自Client端的請求,如何到達真正的業務group去呢?在這里,Global Master將提供“首次查詢”服務,即,新請求首次請求指定的group時,通過Global Master獲得相應的group的信息,以后,Client將使用該信息直接嘗試訪問對應的group並提交請求,如果group信息已過期或是不正確,group將拒絕處理該請求並讓Client重新向Global Master請求新的group信息。顯然,我們的系統要求Client端緩存group的信息,避免多次重復地向Global Master查詢group信息。這里其實又挖了許多爛坑等着我們去跳,首先,這樣的工作模式滿足基本的Ddos攻擊條件,這得通過其他安全性措施來解決,避免group總是收到不正確的Client請求而拒絕為其服務;其次,當出現大量“首次”訪問時,Global Master盡管只提供查詢group信息的讀服務,仍有可能不堪重負而掛掉,所以,這里仍有很大的優化空間,比較容易想到的就是采用DNS負載均衡,因為Global Master和其Global Slave保持完全同步,所以DNS負載均衡可以有效地解決“首次”查詢時Global Master的壓力問題;再者,這個工作模式要求Client端緩存由Global Master查詢得到的group的信息,萬一Client不緩存怎么辦?呵呵,不用擔心,Client端的API也是由我們設計的,之后才面向Web前端。
之后要說的,就是圖中的“Global Heartbeat”,這又是個什么東西呢?可認為這是一個管理Global Master和Global Slave的節點,Global Master和各個Global Slave都不停向Global Heartbeat競爭成為Global Master,如果Global Master正常工作,定期更新其狀態並延期其獲得的鎖,否則由Global Slave替換之,原理和group內的“心跳”一樣,但不同的是,此處Global Master和Global Slave是強一致性的完全同步,不需要分布式選舉。有同學可能又要問了,假如Global Heartbeat掛掉了呢?我只能告訴你,這個很不常見,因為它沒有任何壓力,而且掛掉了必須人工干預才能修復。在GFS + Bigtable里,這個Global Heartbeat叫做Lock Service。
現在接着設計我們的“山推”系統。有了前面兩篇的鋪墊,我們的系統現在已經有了五臟六腑,剩下的工作就是要讓其羽翼豐滿。那么,是時候,放出我們的“山推”系統全貌了:

前面啰嗦了半天,也許不少同學看的不明不白,好了,現在開始看圖說話環節:
(1)整個系統由N台機器組合而成,其中Global Master一台,Global Slave一台到多台,兩者之間保持強一致性並完全同步,可由Global Slave隨時頂替Global Master工作,它們被Global Heartbeat(一台)來管理,保證有一個Global Master正常工作;Global Heartbeat由於無壓力,通常認為其不能掛掉,如果它掛掉了,則必須人工干預才能恢復正常;
(2)整個系統由多個groups合成,每一個group負責相應業務的數據的存取,它們是數據節點,是真正抗壓力的地方,每一個group由一個Group Master和一個到多個Group Slave構成,Group Master作為該group的主節點,提供讀和寫,而Group Slave則只提供讀服務且保證這些Group Slave節點中,至少有一個和Group Master保持完全同步,剩余的Group Slave和Group Master能夠達到最終一致,它們之間以“半同步”模式工作保證最終一致性;
(3)每一個group的健康狀態由Global Master來管理,Global Master向group發送管理信息,並保證有一個Group Master正常工作,若Group Master宕機,在該group內通過分布式選舉產生新的Group Master頂替原來宕機的機器繼續工作,但仍然有一小段時間需要中斷寫服務來切換新的Group Master;
(4)每一個group的底層是實際的存儲系統,File system,它們是無狀態的,即,由分布式選舉產生的Group Master可以在原來的File system上繼續工作;
(5)Client的上端可認為是Web請求,Client在“首次”進行數據讀寫時,向Global Master查詢相應的group信息,並將其緩存,后續將直接與相應的group進行通信;為避免大量“首次”查詢沖垮Global Master,在Client與Global Master之間增加DNS負載均衡,可由Global Slave分擔部分查詢工作;
(6)當Client已經擁有足夠的group信息時,它將直接與group通信進行工作,從而真正的壓力和流量由各個group分擔,並處理完成需要的工作。
好了,現在我們的“山推”系統設計完成了,但是要將它編碼實現,還有很遠的路要走,細枝末節的問題也會暴露更多。如果該系統用於線上計算,如有大量的Map-Reduce運行於group中,系統將會更復雜,因為此時不光考慮的數據的存儲同步問題,操作也需要同步。現在來檢驗下我們設計的“山推”系統,主要分布式指標:
一致性:如前文所述,Global機器強一致性,Group機器最終一致性;
可用性:Global機器保證了HA(高可用性),Group機器則不保證,但滿足了分區容錯性;
備份Replication:Global機器采用完全同步,Group機器則是半同步模式,都可以進行橫向擴展;
故障恢復:如前文所述,Global機器完全同步,故障可不受中斷由slave恢復工作,但Group機器采用分布式選舉和最終一致性,故障時有較短時間的寫服務需要中斷並切換到slave機器,但讀服務可不中斷。
還有其他一些指標,這里就不再多說了。還有一些細節,需要提一下,比如之前的評論中有同學提到,group中master掛時,由slave去頂替,但這樣一來該group內其他所有slave需要分擔之前成這新master的這個slave的壓力,有可能繼續掛掉而造成雪崩。針對此種情況,可采用如下做法:即在一個group內,至少還存在一個真正做“備份”用途的slave,平時不抗壓力,只同步數據,這樣當出現上述情況時,可由該備份slave來頂替成為新master的那個slave,從而避免雪崩效應。不過這樣一來,就有新的問題,由於備份slave平時不抗壓力,加入抗壓力后必然產生一定的數據遷移,數據遷移也是一個較麻煩的問題。常采用的分攤壓力做法如一致性Hash算法(環狀Hash),可將新結點加入對整個group的影響降到較小的程度。
另外,還有一個較為棘手的問題,就是系統的日志處理,主要是系統宕機后如何恢復之前的操作日志。比較常見的方法是對日志作快照(Snapshot)和回放點(checkpoint),並采用Copy-on-write方式定期將日志作snapshot存儲,當發現宕機后,找出對應的回放點並恢復之后的snapshot,但此時仍可能有新的寫操作到達,並產生不一致,這里主要依靠Copy-on-write來同步。
最后再說說圖中的Client部分。顯然這個模塊就是面向Web的接口,后面連接我們的“山推”系統,它可以包含諸多業務邏輯,最重要的,是要緩存group的信息。在Client和Web之間,還可以有諸如Nginx之類的反向代理服務器存在,做進一步性能提升,這已經超出了本文的范疇,但我們必須明白的是,一個高並發高性能的網站,對性能的要求是從起點開始的,何為起點,即用戶的瀏覽器。
現在,讓我們來看看GFS的設計:

很明顯,這么牛的系統我是設計不出來的,我們的“山推”,就是在學習GFS + Bigtable的主要思想。說到這,也必須提一句,可能我文章中,名詞擺的有點多了,如NWR,分布式選舉,Paxos包括Copy-on-write等,有興趣的同學可自行google了解。因為說實在的,這些概念我也沒法講透徹,只是一知半解。另外,大家可參考一些分布式項目的設計,如Cassandra,包括淘寶的Oceanbase等,以加深理解。
