寫這篇文章的目的,主要是把今年以來學習的一些東西積澱下來,同時作為之前文章《高效能分散式計算與儲存系統設計概要》的補充與提升,然而本人水平非常有限,回頭看之前寫的文章也有許多不足,甚至是錯誤,希望同學們看到了錯誤多多見諒,更歡迎與我討論並指正。
我大概是從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等,以加深理解。