| |||||||||
日 | 一 | 二 | 三 | 四 | 五 | 六 | |||
---|---|---|---|---|---|---|---|---|---|
25 | 26 | 27 | 28 | 29 | 30 | 31 | |||
1 | 2 | 3 | 4 | 5 | 6 | 7 | |||
8 | 9 | 10 | 11 | 12 | 13 | 14 | |||
15 | 16 | 17 | 18 | 19 | 20 | 21 | |||
22 | 23 | 24 | 25 | 26 | 27 | 28 | |||
29 | 30 | 1 | 2 | 3 | 4 | 5 |
Two-phase commit(http://en.wikipedia.org/wiki/Two-phase_commit_protocol)是分布式事務(wù)最基礎(chǔ)的協(xié)議,Three-phase commit(http://en.wikipedia.org/wiki/Three-phase_commit_protocol)主要解決Two-phase commit中協(xié)調(diào)者宕機問題。
Two-phase commit的算法實現(xiàn) (from <<Distributed System: Principles and Paradigms>>):
協(xié)調(diào)者(Coordinator):
write START_2PC to local log;
multicast VOTE_REQUEST to all participants;
while not all votes have been collected {
wait for any incoming vote;
if timeout {
write GLOBAL_ABORT to local log;
multicast GLOBAL_ABORT to all participants;
exit;
}
record vote;
}
if all participants sent VOTE_COMMIT and coordinator votes COMMIT {
write GLOBAL_COMMIT to local log;
multicast GLOBAL_COMMIT to all participants;
} else {
write GLOBAL_ABORT to local log;
multicast GLOBAL_ABORT to all participants;
}
參與者(Participants)
write INIT to local log;
wait for VOTE_REQUEST from coordinator;
if timeout {
write VOTE_ABORT to local log;
exit;
}
if participant votes COMMIT {
write VOTE_COMMIT to local log;
send VOTE_COMMIT to coordinator;
wait for DECISION from coordinator;
if timeout {
multicast DECISION_REQUEST to other participants;
wait until DECISION is received; /* remain blocked*/
write DECISION to local log;
}
if DECISION == GLOBAL_COMMIT
write GLOBAL_COMMIT to local log;
else if DECISION == GLOBAL_ABORT
write GLOBAL_ABORT to local log;
} else {
write VOTE_ABORT to local log;
send VOTE_ABORT to coordinator;
}
另外,每個參與者維護一個線程專門處理其它參與者的DECISION_REQUEST請求,處理線程流程如下:
while true {
wait until any incoming DECISION_REQUEST is received;
read most recently recorded STATE from the local log;
if STATE == GLOBAL_COMMIT
send GLOBAL_COMMIT to requesting participant;
else if STATE == INIT or STATE == GLOBAL_ABORT;
send GLOBAL_ABORT to requesting participant;
else
skip; /* participant remains blocked */
}
從上述的協(xié)調(diào)者與參與者的流程可以看出,如果所有參與者VOTE_COMMIT后協(xié)調(diào)者宕機,這個時候每個參與者都無法單獨決定全局事務(wù)的最終結(jié)果(GLOBAL_COMMIT還是GLOBAL_ABORT),也無法從其它參與者獲取,整個事務(wù)一直阻塞到協(xié)調(diào)者恢復(fù);如果協(xié)調(diào)者出現(xiàn)類似磁盤壞這種永久性錯誤,該事務(wù)將成為被永久遺棄的孤兒。問題的解決有如下思路:
1. 協(xié)調(diào)者持久化數(shù)據(jù)定期備份。為了防止協(xié)調(diào)者出現(xiàn)永久性錯誤,這是一種代價最小的解決方法,不容易引入bug,但是事務(wù)被阻塞的時間可能特別長,比較適合銀行這種正確性高于一切的系統(tǒng)。
2. Three-phase Commit。這是理論上的一種方法,實現(xiàn)起來復(fù)雜且效率低。思路如下:假設(shè)參與者機器不可能出現(xiàn)超過一半同時宕機的情況,如果協(xié)調(diào)者宕機,我們需要從活著的超過一半的參與者中得出事務(wù)的全局結(jié)果。由于不可能知道已經(jīng)宕機的參與者的狀態(tài),所以引入一個新的參與者狀態(tài)PRECOMMIT,參與者成功執(zhí)行一個事務(wù)需要經(jīng)過INIT, READY, PRECOMMIT,最后到COMMIT狀態(tài);如果至少有一個參與者處于PRECOMMIT或者COMMIT,事務(wù)成功;如果至少一個參與者處于INIT或者ABORT,事務(wù)失??;如果所有的參與者都處于READY(至少一半?yún)⑴c者活著),事務(wù)失敗,即使原先宕機的參與者恢復(fù)后處于PRECOMMIT狀態(tài),也會因為有其它參與者處于ABORT狀態(tài)而回滾。PRECOMMIT狀態(tài)的引入給了宕機的參與者回滾機會,所以Three-phase commit在超過一半的參與者活著的時候是不阻塞的。不過,Three-phase Commit只能算是是理論上的探索,效率低并且沒有解決網(wǎng)絡(luò)分區(qū)問題。
3. Paxos解決協(xié)調(diào)者單點問題。Jim Gray和Lamport合作了一篇論文講這個方法,很適合互聯(lián)網(wǎng)公司的超大規(guī)模集群,Google的Megastore事務(wù)就是這樣實現(xiàn)的,不過問題在于Paxos和Two-phase Commit都不簡單,需要有比較靠譜(代碼質(zhì)量高)的小團隊設(shè)計和編碼才行。后續(xù)的blog將詳細闡述該方法。
總之,分布式事務(wù)只能是系統(tǒng)開發(fā)者的烏托邦式理想,Two-phase commit的介入將導(dǎo)致涉及多臺機器的事務(wù)之間完全串行,沒有代價的分布式事務(wù)是不存在的。
前面我的一篇文章http://hi.baidu.com/knuthocean/blog/item/12bb9f3dea0e400abba1673c.html引用了對Google App Engine工程師關(guān)于Bigtable/Megastore replication的文章。當(dāng)時留下了很多疑問,比如:為什么Google Bigtable 是按照column family級別而不是按行執(zhí)行replication的?今天重新思考了Bigtable replication問題,有如下體會:
1. Bigtable/GFS的設(shè)計屬于分層設(shè)計,和文件系統(tǒng)/數(shù)據(jù)庫分層設(shè)計原理一致,通過系統(tǒng)隔離解決工程上的問題。這種分層設(shè)計帶來了兩個問題,一個是性能問題,另外一個就是Replication問題。由于存儲節(jié)點和服務(wù)節(jié)點可能不在一臺機器,理論上總是存在性能問題,這就要求我們在加載/遷移Bigtable子表(Bigtable tablet)的時候考慮本地化因素;另外,GFS有自己的replication機制保證存儲的可靠性,Bigtable通過分離服務(wù)節(jié)點和存儲節(jié)點獲得了很大的靈活性,且Bigtable的宕機恢復(fù)時間可以做到很短。對于很多對實時性要求不是特別高的應(yīng)用Bigtable由于服務(wù)節(jié)點同時只有一個,既節(jié)約資源又避免了單點問題。然后,Bigtable tablet服務(wù)過于靈活導(dǎo)致replication做起來極其困難。比如,tablet的分裂和合并機制導(dǎo)致多個tablet(一個只寫,其它只讀)服務(wù)同一段范圍的數(shù)據(jù)變得幾乎不可能。
2. Google replication分為兩種機制,基于客戶端和基于Tablet Server。分述如下:
2-1). 基于客戶端的replication。這種機制比較簡單,實現(xiàn)如下:客戶端讀/寫操作均為異步操作,每個寫操作都嘗試寫兩個Bigtable集群,任何一個寫成功就返回用戶,客戶端維護一個retry list,不斷重試失敗的寫操作。讀操作發(fā)到兩個集群,任何一個集群讀取成功均可。然后,這樣做有兩個問題:
a. 客戶端不可靠,可能因為各種問題,包括程序問題退出,retry list丟失導(dǎo)致兩個集群的數(shù)據(jù)不一致;
b. 多個客戶端并發(fā)操作時無法保證順序性。集群A收到的寫操作可能是"DEL item; PUT item";集群B的可能是"PUT item; DEL item"。
2-2). 基于Tablet Server的replication。這種機制實現(xiàn)較為復(fù)雜,目的是為了保證讀服務(wù),寫操作的延時仍然可能比較長。兩個集群,一個為主集群,提供讀/寫服務(wù);一個為slave集群,提供只讀服務(wù),兩個集群維持最終一致性。對于一般的讀操作,盡量讀取主集群,如果主集群不可以訪問則讀取slave集群;對于寫操作,首先將寫操作提交到主集群的Tablet Server,主集群的Tablet Server維護slave集群的元數(shù)據(jù)信息,并維護一個后臺線程不斷地將積攢的用戶表格寫操作提交到slave集群進行日志回放(group commit)。對于一般的tablet遷移,操作邏輯和Bigtable論文中的完全一致;主集群如果發(fā)生了機器宕機,則除了回放commit log外,還需要完成宕機的Tablet Server遺留的后臺備份任務(wù)。之所以要按照column family級別而不是按行復(fù)制,是為了提高壓縮率從而提高備份效率。如果主集群寫操作日志的壓縮率大于備份數(shù)據(jù)的壓縮率,則可能出現(xiàn)備份不及時,待備份數(shù)據(jù)越來越多的問題。
假設(shè)集群A為主集群,集群B是集群A的備份,集群切換時先停止集群A的寫服務(wù),將集群A余下的備份任務(wù)備份到集群B后切換到集群B;如果集群A不可訪問的時間不可預(yù)知,可以選擇直接切換到集群B,這樣會帶來一致性問題。且由于Bigtable是按列復(fù)制的,最后寫入的一些行的事務(wù)性無法保證。不過由于寫操作數(shù)據(jù)還是保存在集群A的,所以用戶可以知道丟了哪些數(shù)據(jù),很多應(yīng)用可以通過重新執(zhí)行A集群遺留的寫操作進行災(zāi)難恢復(fù)。Google的App Engine也提供了這種查詢及重做丟失的寫操作的工具。
想法不成熟,有問題聯(lián)系:knuthocean@163.com
負載平衡策略
Dynamo的負載平衡取決于如何給每臺機器分配虛擬節(jié)點號。由于集群環(huán)境的異構(gòu)性,每臺物理機器包含多個虛擬節(jié)點。一般有如下兩種分配節(jié)點號的方法:
1. 隨機分配。每臺物理節(jié)點加入時根據(jù)其配置情況隨機分配S個Token(節(jié)點號)。這種方法的負載平衡效果還是不錯的,因為自然界的數(shù)據(jù)大致是比較隨機的,雖然可能出現(xiàn)某段范圍的數(shù)據(jù)特別多的情況(如baidu, sina等域名下的網(wǎng)頁特別多),但是只要切分足夠細,即S足夠大,負載還是比較均衡的。這個方法的問題是可控性較差,新節(jié)點加入/離開系統(tǒng)時,集群中的原有節(jié)點都需要掃描所有的數(shù)據(jù)從而找出屬于新節(jié)點的數(shù)據(jù),Merkle Tree也需要全部更新;另外,增量歸檔/備份變得幾乎不可能。
2. 數(shù)據(jù)范圍等分+隨機分配。為了解決方法1的問題,首先將數(shù)據(jù)的Hash空間等分為Q = N * S份 (N=機器個數(shù),S=每臺機器的虛擬節(jié)點數(shù)),然后每臺機器隨機選擇S個分割點作為Token。和方法1一樣,這種方法的負載也比較均衡,且每臺機器都可以對屬于每個范圍的數(shù)據(jù)維護一個邏輯上的Merkle Tree,新節(jié)點加入/離開時只需掃描部分數(shù)據(jù)進行同步,并更新這部分數(shù)據(jù)對應(yīng)的邏輯Merkle Tree,增量歸檔也變得簡單。該方法的一個問題是對機器規(guī)模需要做出比較合適的預(yù)估,隨著業(yè)務(wù)量的增長,可能需要重新對數(shù)據(jù)進行劃分。
不管采用哪種方法,Dynamo的負載平衡效果還是值得擔(dān)心的。
客戶端緩存及前后臺任務(wù)資源分配
客戶端緩存機器信息可以減少一次在DHT中定位目標(biāo)機器的網(wǎng)絡(luò)交互。由于客戶端數(shù)量不可控,這里緩存采用客戶端pull的方式更新,Dynamo中每隔10s或者讀/寫操作發(fā)現(xiàn)緩存信息不一致時客戶端更新一次緩存信息。
Dynamo中同步操作、寫操作重試等后臺任務(wù)較多,為了不影響正常的讀寫服務(wù),需要對后臺任務(wù)能夠使用的資源做出限制。Dynamo中維護一個資源授權(quán)系統(tǒng)。該系統(tǒng)將整個機器的資源切分成多個片,監(jiān)控60s內(nèi)的磁盤讀寫響應(yīng)時間,事務(wù)超時時間及鎖沖突情況,根據(jù)監(jiān)控信息算出機器負載從而動態(tài)調(diào)整分配給后臺任務(wù)的資源片個數(shù)。
Dynamo的優(yōu)點
1. 設(shè)計簡單,組合利用P2P的各種成熟技術(shù),模塊劃分好,代碼復(fù)用程度高。
2. 分布式邏輯與單機存儲引擎邏輯基本隔離。很多公司有自己的單機存儲引擎,可以借鑒Dynamo的思想加入分布式功能。
3. NWR策略可以根據(jù)應(yīng)用自由調(diào)整,這個思想已經(jīng)被Google借鑒到其下一代存儲基礎(chǔ)設(shè)施中。
4. 設(shè)計上天然沒有單點,且基本沒有對系統(tǒng)時鐘一致性的依賴。而在Google的單Master設(shè)計中,Master是單點,需要引入復(fù)雜的分布式鎖機制來解決,且Lease機制需要對機器間時鐘同步做出假設(shè)。
Dynamo的缺陷
1. 負載平衡相比單Master設(shè)計較不可控;負載平衡策略一般需要預(yù)估機器規(guī)模,不能無縫地適應(yīng)業(yè)務(wù)動態(tài)增長。
2. 系統(tǒng)的擴展性較差。由于增加機器需要給機器分配DHT算法所需的編號,操作復(fù)雜度較高,且每臺機器存儲了整個集群的機器信息及數(shù)據(jù)文件的Merkle Tree信息,機器最大規(guī)模只能到幾千臺。
3. 數(shù)據(jù)一致性問題。多個客戶端的寫操作有順序問題,而在GFS中可以通過只允許Append操作得到一個比較好的一致性模型。
4. 數(shù)據(jù)存儲不是有序,無法執(zhí)行Mapreduce;Mapreduce是目前允許機器故障,具有強擴展性的最好的并行計算模型,且有開源的Hadoop可以直接使用,Dynamo由于數(shù)據(jù)存儲依賴Hash無法直接執(zhí)行Mapreduce任務(wù)。
異常處理
Dynamo中把異常分為兩種類型,臨時性的異常和永久性異常。服務(wù)器程序運行時一般通過類似supervise的監(jiān)控daemon啟動,出現(xiàn)core dump等異常情況時自動重啟。這種異常是臨時性的,其它異常如硬盤報修或機器報廢等由于其持續(xù)時間太長,稱之為永久性的?;仡橠ynamo的設(shè)計,一份數(shù)據(jù)被寫到N, N+1, ... N+K-1這K臺機器上,如果機器N+i (0 <= i <= K-1)宕機,原本寫入該機器的數(shù)據(jù)轉(zhuǎn)移到機器N+K,機器N+K定時ping機器N+i,如果在指定的時間T內(nèi)N+i重新提供服務(wù),機器N+K將啟動傳輸任務(wù)將暫存的數(shù)據(jù)發(fā)送給機器N+i;如果超過了時間T機器N+i還是處于宕機狀態(tài),這種異常被認為是永久性的,這時需要借助Merkle Tree機制進行數(shù)據(jù)同步。這里的問題在于時間T的選擇,所以Dynamo的開發(fā)人員后來干脆把所有程序檢測出來的異常認為是臨時性的,并提供給管理員一個utility工具,用來顯示指定一臺機器永久性下線。由于數(shù)據(jù)被存儲了K份,一臺機器下線將導(dǎo)致后續(xù)的K臺機器出現(xiàn)數(shù)據(jù)不一致的情況。這是因為原本屬于機器N的數(shù)據(jù)由于機器下線可能被臨時寫入機器N+1, ... N+K。如果機器N出現(xiàn)永久性異常,后續(xù)的K臺機器都需要服務(wù)它的部分數(shù)據(jù),這時它們都需要選擇冗余機器中較為空閑的一臺進行同步。Merkle Tree同步的原理很簡單,每個非葉子節(jié)點對應(yīng)多個文件,為其所有子節(jié)點值組合以后的Hash值,葉子節(jié)點對應(yīng)單個數(shù)據(jù)文件,為文件內(nèi)容的Hash值。這樣,任何一個數(shù)據(jù)文件不匹配都將導(dǎo)致從該文件對應(yīng)的葉子節(jié)點到根節(jié)點的所有節(jié)點值不同。每臺機器維護K棵Merkle Tree,機器同步時首先傳輸Merkle Tree信息,并且只需要同步從根到葉子的所有節(jié)點值均不相同的文件。
讀/寫流程
客戶端的讀/寫請求首先傳輸?shù)骄彺娴囊慌_機器,根據(jù)預(yù)先配置的K、W和R值,對于寫請求,根據(jù)DHT算法計算出數(shù)據(jù)所屬的節(jié)點后直接寫入后續(xù)的K個節(jié)點,等到W個節(jié)點返回成功時返回客戶端,如果寫請求失敗將加入retry_list不斷重試。如果某臺機器發(fā)生了臨時性異常,將數(shù)據(jù)寫入后續(xù)的備用機器并在備用機器中記錄臨時異常的機器信息。對于讀請求,根據(jù)DHT算法計算出數(shù)據(jù)所屬節(jié)點后根據(jù)負載策略選擇R個節(jié)點,從中讀取R份數(shù)據(jù),如果數(shù)據(jù)一致,直接返回客戶端;如果數(shù)據(jù)不一致,采用vector clock的方法解決沖突。Dynamo系統(tǒng)默認的策略是選擇最新的數(shù)據(jù),當(dāng)然用戶也可以自定義沖突處理方法。每個寫入系統(tǒng)的<key, value>對都記錄一個vector lock信息,vector lock就是一系列<機器節(jié)點號, 版本號/時間戳>對,記錄每臺機器對該數(shù)據(jù)的最新更新版本信息。如下圖:
讀取時進行沖突解決,如果一臺機器讀到的數(shù)據(jù)的vector lock記錄的所有版本信息都小于另一臺機器,直接返回vector lock較大的數(shù)據(jù);如果二者是平行版本,根據(jù)時間戳選擇最新的數(shù)據(jù)或者通過用戶自定義策略解決沖突。讀請求除了返回數(shù)據(jù)<key, value>值以外還返回vector lock信息,后續(xù)的寫操作需要帶上該信息。
問題1:垃圾數(shù)據(jù)如何回收?
Dynamo的垃圾回收機制主要依賴每個節(jié)點上的存儲引擎,如Berkely db存儲引擎,merge-dump存儲引擎等。其它操作,如Merkle Tree同步產(chǎn)生的垃圾文件回收可以和底層存儲引擎配合完成。
問題2:Dynamo有沒有可能丟數(shù)據(jù)?
關(guān)鍵在于K, W, R的設(shè)置。假設(shè)一個讀敏感應(yīng)用設(shè)置K=3, W=3, R=1,待處理的數(shù)據(jù)原本屬于節(jié)點A, B, C,節(jié)點B出現(xiàn)臨時性故障的過程中由節(jié)點D代替。在節(jié)點B出現(xiàn)故障到節(jié)點B同步完成節(jié)點D暫存的修改這段時間內(nèi),如果讀請求落入節(jié)點B或者D都將出現(xiàn)丟數(shù)據(jù)的問題。這里需要適當(dāng)處理下,對于B節(jié)點下線的情況,由于其它機器要么緩存了B節(jié)點已下線信息,要么讀取時將發(fā)現(xiàn)B節(jié)點處于下線狀態(tài),這是只需要將請求轉(zhuǎn)發(fā)其它節(jié)點即可;對于B節(jié)點上線情況,可以等到B節(jié)點完全同步以后才開始提供讀服務(wù)。對于設(shè)置W<K的應(yīng)用,Dynamo讀取時需要解決沖突,可能丟數(shù)據(jù)??傊珼ynamo中可以保證讀取的機器都是有效的(處于正常服務(wù)狀態(tài)),但W != K時不保證所有的有效機器均同步了所有更新操作。
問題3:Dynamo的寫入數(shù)據(jù)有沒有順序問題?
假設(shè)要寫入兩條數(shù)據(jù)"add item"和"delete item",如果寫入的順序不同,將導(dǎo)致完全不同的結(jié)果。如果設(shè)置W=K,對于同一個客戶端,由于寫入所有的機器以后才返回,可以保證順序;而多個客戶端的寫操作可能被不同的節(jié)點處理,不能保證順序性。如果設(shè)置W < K,Dynamo不保證順序性。
問題4:沖突解決后是否需要將結(jié)果值更新存儲節(jié)點?
讀操作解決沖突后不需要將結(jié)果值更新存儲節(jié)點。產(chǎn)生沖突的情況一般有機器下線或者多個客戶端導(dǎo)致的順序問題。機器下線時retry_list中的操作將丟失,某些節(jié)點不能獲取所有的更新操作。對于機器暫時性或者永久性的異常,Dynamo中內(nèi)部都有同步機制進行處理,但是對于retry_list中的操作丟失或者多個客戶端引發(fā)的順序問題,Dynamo內(nèi)部根本無法分辨數(shù)據(jù)是否正確。唯一的沖突解決機器在讀操作,Dynamo可以設(shè)計成讀操作將沖突解決結(jié)果值更新存儲節(jié)點,但是這樣會使讀操作變得復(fù)雜和不高效。所以,比較好的做法是每個寫操作都帶上讀操作返回的多個版本數(shù)據(jù),寫操作將沖突處理的結(jié)果更新存儲節(jié)點。