分佈式數據庫的取捨——Cassandra的選擇及其後果

分佈式數據庫的取捨——Cassandra的選擇及其後果

作者: yangzhe1991 分類: 我是搞技術的 發佈時間: 2015-10-07 20:02 ė 62條評論

想寫的Cassandra系列文章的第一篇。

本文的中心思想是:不談需求場景只談利弊是耍流氓;只談利不談弊是臭流氓。

分佈式數據庫,自然是相對於傳統的單機數據庫。從傳統的單機數據庫到多機分佈式數據庫無非是有兩大類需求:單機在性能或數據容量上扛不住;單機有單點問題,一旦掛了系統就掛了甚至數據都丟了。這兩個核心需求決定了分佈式數據庫的根本目標有兩個:多個節點承接讀寫請求,從而加大整體性能;多個節點存儲數據,每個數據存不止一份從而防止丟數據並且提高整體的數據上限。

最簡單直接的辦法是,把單機數據搭很多個。每個節點處理一部分數據,從而加大整體讀寫吞吐和數據上限;同時提出“主從”概念,多個節點處理同一批數據,這樣其中一個節點掛了另一個節點馬上頂上。無論Mysql、redis、mongodb都有類似的方式可以將單機數據庫擴展成集羣。

另一種新思路繞開了單機數據庫,從設計上便實現分佈式,是天生的分佈式數據庫。Google基於GFS設計了BigTable,因爲GFS是分佈式文件系統因此只要把數據持久化在GFS上便實現了第二個目標既數據存多份;因爲GFS可以通過rpc調用讀寫整個文件系統上的任何數據所以每個BitTable的節點理論上都可以處理整個數據庫的請求,從而實現了第一個目標既多個節點可以承接讀寫請求。開源的HBase在總體思路上山寨了BigTable,後文用HBase代指這種設計。

還有一種思路,不基於分佈式文件系統,把數據寫在節點本地,但是client一次讀寫請求可能要去讀寫多個節點,從而同時實現兩個目標。這就是Amazon提出的Dynamo。而開源的Cassandra在分佈式模型上山寨了Dynamo,並且有所區別,本文不看重Dynamo的設計,只介紹Cassandra的設計。

三種模式的利弊先不說,但無論哪種模式針對第一個目標時都需要處理一個問題——客戶端想讀寫某個數據時應該請求集羣中的哪個節點?一般來說數據庫都是以行爲基本單元,通過對行的partition key來定位一行數據應該放在哪。而key->node的映射,每個key單獨存儲映射關係顯然存不下,因此一般將key分組,滿足同一個條件的key作爲一組,每組key都在一個節點上,也就是key->group->node。具體key->group的映射分爲兩大類——pre-sharding和resharding。

Pre-sharding的意思是key->group的映射關係不會動態修改,在啓動的時候就確立了,一般這種情況的group也叫“slot”,通過哈希取模的方式來確定一個key落在哪個slot上。通常單機數據庫擴展出來的分佈式數據庫都是pre-sharding的。

而resharding(一般也稱爲auto-sharding)是可以動態修改的,比如發現某個group的key太多,就修改下分配方式從而讓每個group均勻些。當然即使是動態修改,也一樣要確保每個group的key滿足同一種規律,否則還是要每個key存一個路由信息。而sharding的修改就是修改key->group的規律,而具體規律的不同決定了sharding算法的不同。比如把字節序相鄰的key分在一個group,resharding負責記錄每個group的起止key;也可以先哈希成一個數字,把相鄰數字的key分在一個group,resharding 負責記錄每個group的起止hash。字節序的好處是可以按key順序讀取,hash的好處是防止冷熱不均。一般天生的分佈式數據庫都是sharding的,HBase和Cassandra都同時支持兩種sharding算法,但大多數情況下,HBase使用第一種,Cassandra使用第二種。但HBase用第二種也還不錯無非是不能順序掃行,而Cassandra用第一種就容易悲劇了,原因後面再說。

知道一個key在哪個group後,group->node又分爲三種思路:在數據庫上層提供一層proxy,client可以全部請求proxy而proxy知道每個數據該讀寫哪個節點;在一個外部系統存儲這個信息(如zookeeper),拿到這個信息後直接請求數據庫節點;數據庫節點自己維護這個路由信息,任何一個節點都知道某個數據應該放在哪個節點。Codis、各種MySQL的proxy都是第一種方案;HBase是第二種方案;Cassandra和redis cluster是第三個方案。

因爲group->node的路由信息是可能隨時改變的(如增減節點、有節點掛了等等),所以需要有一個邏輯來分配key->node的映射關係來保證每個key都有節點能服務並可以根據集羣信息動態調整。這又分兩種:一個master節點來進行分配,這個master只有一個從而確保不會分亂;數據庫節點自己掌控全局,一起判斷,一起調整到收斂狀態。HBase、Codis屬於第一種;Cassandra屬於第二種;Redis Cluster介於兩者之間,主從切換的邏輯是自動的通過第二種來實現,而一個slot該存哪個group是外部腳本人工控制所以是第一種。

通過實現方式的不同把分佈式數據庫按各種維度進行分類之後,總結一下Cassandra在分佈式架構上的設計思路:數據存本地,多個節點都可以讀寫同一個數據;對key做sharding而非pre-sharding來判斷一行數據該放在哪(幾)個節點;每個節點都存儲這個路由信息;這個路由表是整個集羣共同維護、修改的而非某個master修改。

知道了Cassandra是如何在設計上做選擇的,就知道了其利弊了,當然一些特性的利弊不明顯,而一些特性的利弊可以說是不得不做的抉擇。

每個節點都存路由信息、共同維護無中心節點——client變得很好寫,隨便請求一個節點都能轉發請求,沒有中心節點更容易誇機房;整個集羣保存、調整集羣信息,用gossip協議協調一致,實現邏輯比有中心節點的複雜。

數據存本地,不依賴分佈式文件系統——可以少維護一個系統,維護成本低一些;但是不同節點存的數據的一致性不能保證,因此需要額外的邏輯保證一致性。

其中前者gossip等部分後文再單獨來寫,但是後者還是需要着重強調一下,並且也會單獨一篇來寫。

HBase的設計依賴HDFS,讓HBase實現起來比較簡單,只需要master確保一個group(region)在同一個時間內只有一個節點來讀寫,那麼實際上就把HBase的讀寫變成了單機數據庫的讀寫,只是把內存+磁盤變成了內存+HDFS的rpc,而單機讀寫的一致性很容易保證。而Cassandra每個group的數據是要寫在多個節點的。就會有一些額外的問題。

稍微知道分佈式系統的都知道傳說中的CAP,這是一種“不可能三角”,三個因素註定最多做到同時兩個。經濟學上還有個“蒙代爾三角”也是類似,不知道其他領域還有沒有不可能三角。CAP簡單來說就是要可用性就沒一致性,或者反過來,因爲P肯定不能放棄的。HBase是嚴格保證強一致性,放棄了A,一旦一個節點掛了那麼短時間內這個節點所服務的那些region是不可讀寫的,各種優化只是儘可能的保證這個不可用的時間更短。而Cassandra因爲有多個節點可以讀寫一行數據,理論上只要有一個節點還能用,可用性就有保證。

因爲rpc調用實際上是兩次網絡傳輸(傳過去一個request再傳回來一個response),如果成功當然沒問題,但是發了request沒收到response,也就是“無響應”,屬於一種很麻煩的中間狀態,因爲你不確定server是否處理了這條請求,甚至server也不確定它是否成功返回了response所以不確定client是否知道自己是否成功。所以普遍的做法是讓client認爲這次請求“超時”從而讓client去重試或者放棄(但放棄不代表這條記錄沒寫成)。HBase因爲只有一個節點讀寫這行,所以行就是行,不行就是不行。而Cassandra因爲有多個節點(配置上來說是N個節點,可自行設置),那麼同時寫N個節點的話,如果一部分成功一部分不成功算是成功還是不成功呢?Cassandra選擇的做法是讓client自己指定。寫的時候,W個節點成功了算作成功,不到W個節點在timeout時間內成功算做這次請求整體失敗。“整體失敗”就是這次請求client收到一個timeout的錯誤,但其實不代表每個節點都沒處理這個寫請求。也就是說,N個節點中有0到W-1個節點是有數據的,另外那些節點是可能有數據的(因爲可能只是response沒發回來)。讀的時候,R個節點返回數據或者返回沒有數據算作成功。如果R或者W等於N,那麼意味着只要有一個節點不響應,就會導致client請求失敗,失去了可用性。所以一般R和W都小於N。此外如果W和R相對於N設置的太小,那麼有可能寫的時候和讀的時候請求的節點恰好錯開,發現沒數據,相當於讀不到數據了。因此需要保證W和R是一定能有重疊部分的,也就是W+R>N。這樣就保證了每次讀數據都能讀到每次寫的數據。那這是強一致性嗎?並不是。(當然不是了,不然不就破了CAP了……)

具體原因要看讀數據時是如何處理的。如果R個節點返回的數據是不一致的,比如一個節點說a=3,一個節點說a=2,分佈式系統的時間戳是無法一致的,所以不能靠時間戳來判定先後,而Cassandra的做法是,照樣看誰時間戳大,誰大聽誰的。而時間戳以接受client請求的節點的系統時間戳或client自行提供爲準。也就是說這裏是存在誤差的,也因此Cassandra必須要配套NTP來降低時間戳的誤差。但畢竟時間戳是不可能完全沒有誤差的,所以理論上是可能出現後寫入的數據被先寫入的覆蓋,連最終一致性都不是,而我自己把這種一致性命名爲“NTP一致性”。但是因爲Cassandra不僅是NoSQL而且是和BigTable一樣的Wide Column,並且支持用CQL定義column,因此實際上只有不同client在NTP誤差範圍的時間(同機房內小於1ms)內同時寫同一個row key+column key+column纔會出現這種衝突。所以這就是個看需求的事情了,短時間內同時寫一個字段的話讀到哪個都行、或者完全不會短時間重複寫一個字段的話就能忍,能忍就可以用,忍不了千萬不要用。此外Cassandra從2.0開始支持輕量級事務——一個用paxos實現的CAS寫操作,能在一定程度上解決這個問題,但是因爲paxos的邏輯比單純一次寫大很多,所以性能差了很多。

發佈了177 篇原創文章 · 獲贊 147 · 訪問量 166萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章