本篇文章謝絕轉載,歡迎轉發

幾年之前,曾不自量力的想要寫一個兼容RDBMS和NoSQL的資料庫,結果僅實現了一個Raft協議,寫了一棵BTree,就放棄了。使用Golang寫這個算是比較簡單的了,但過程難以言訴,有點螞蟻撼大樹了。

而個人,由於工作的關係,也已經有四五年沒有和SQL打交道了。最近重拾,感慨良多。

MySQL這種RDBMS,天生是存在分散式缺陷的,在海量數據的今天,很容易就達到瓶頸。過去這麼多年,一點長進都沒有。所以經常的操作就是換存儲引擎、分庫分表、引入中間件,閹割功能。

分散式存儲特徵

能讓你不忍割捨的,一個就是MySQL協議,你用慣了;一個就是事務,你怕丟數據。

幸運的是,大多數互聯網業務不需要強事務,甚至連MySQL協議都不需要。接下來我們看一下要將一個傳統的MySQL改造成分散式的存儲,是有多麼的困難。

CAP理論應該是人盡皆知的事情了,在此不多提。

單機上的任何數據都是不可信的,因為硬碟會壞,會斷電,會被挖光纜。所以一般通過冗餘多個副本來保證數據的安全。副本的另外一個作用,就是提供額外的計算能力,比如某些請求,會落到副本上。副本越多,可用性越高

而加入副本以後,就涉及到數據的同步問題。即使是最快的區域網,也會存在延遲,更不用說機器性能差異引起的同步延遲。這就存在一個問題,讀副本的請求讀到的數據,可能不是最新的,這就是數據的一致性發生了改變。當然有些手段能保證數據的一致性,但副本越多,延遲越大

副本的加入還會引入主從的問題。主節點死掉以後,要有副本節點頂上去,這個過程的協調需要時間,其間部分不可用。


而當一類數據足夠大(比如說某張表),在其上的操作已經非常耗時的情況下,就需要對此類數據進行切割,將其分佈到多臺機器上。這個切割過程就是Sharding,通過一定規則的分片來減少單次查詢數據的規模,增加集羣容量。

當某些查詢涉及到多個分片,這個過程就比較緩慢了。協調節點需要與每個節點進行溝通,然後聚合查詢的結果,分片數越多,時間越長

一般,在一個維度上的分庫都會遇到上述問題,可怕的是你可能會有多個維度的需求。對於一些NoSQL來說,每一個維度,都需要冗餘一份數據,這一般是膨脹性的。


集羣的規劃並不是一成不變的,你的集羣可能會加入新的節點;也可能有節點因為事故離線;也可能因為分片維度的問題,數據發生了傾斜。當這種情況發生,集羣間的數據會發生遷移,以便達到平衡。這個過程有些是自動的,也有些是手動進行觸發。這個過程也是最困難的:既要保證數據的增量遷移,又要保證集羣的正確服務。

如果你想要事務(很多情況是你不懂技術的Leader決定),那就集中存儲,不要分片。事務是很多性能場景和擴展場景的萬惡之源,流量大了你會急著去掉它。

副本

針對一個分片的數據,只能有一個寫入的地方,這就是master,其他副本都是從master複製數據。

副本能夠增加讀操作的並行讀,但會讀到臟數據。如果你想要讀到的數據是一致的,可以採用同步寫副本的方式,比如KAFKA的ack=-1,只有全部同步成功了,才認為本次提交成功。

但如果你的副本太多,這個過程會非常的慢。你可能想要通過分配寫入和讀取的副本個數來協調寫入和讀取的效率,QuorumR+W>N就是一個權衡策略。

這個過程可以簡單的用抽屜原理來解釋。

上面的這個過程比較簡單,所以需要有點複雜的壓下軸。一個名門就是Paxos,複雜的很,以前看了一個星期也沒全部搞懂 -.-。

ZAB協議是ZooKeeperPaxos協議的基礎上進行擴展而來的,說實話也沒看懂,而且ZK的源代碼也非常的…

唯一看得懂的就是Raft協議,這個是EtcdConsul的基礎,是簡化版的Paxos,目前來看是高效且可靠的。

副本是用來做HA的,所以master死了,要有副本頂上來。這個過程就涉及到master的選舉。

kafka,藉助zookeeper來進行主分區的選舉。而ES是使用Bully演算法,通過選出ID最大的節點當作master。無論什麼方式,都是要從一堆機器中,找到一個唯一的master節點,而且在選舉的過程中,都需要注意一個腦裂問題(也就是不小心找到倆了)。master選舉通常都是投票機制,所以最小組集羣的臺數一般都設置成n/2+1

這也是為什麼很多集羣推薦奇數臺的原因!

cassandra採用了另外一種協議來維護集羣的狀態,那就是gossip,是最終一致性的典範。

副本機制在傳統的DB上也工作的很好。比如MySQL通過binlog完成副本的同步;Postgresql採用WAL日誌完成同步。但涉及到主從的切換,尤其是有多個從庫的情況下,一般都不能夠自動化執行。

分片

分片就是對資料的切割,也就是一套主從已經裝不下了。分片的邏輯可以放在客戶端,比如驅動層的資料庫中間件,Memcache等;也可以放在服務端,比如ES、Mongo等。

分片的信息組成了一組元數據,存放了切割的規則。這些信息可以藉助外部的存儲比如KAFKA;也有的直接同步在集羣每個節點的內存中,比如ES。比較流行的NoSQL主從信息最小維度一般都是分片,一個節點上同時會有master分片和其他分片的副本。


分片的規則一般有下面幾種:

Round-Robin 資料輪流落進不同的機器,數據比較平均,適合弱相關性的數據存儲。壞處是聚合查詢可能會非常慢,擴容、縮容難。

Hash 使用某些信息的Hash進行尋路,客戶端依照同樣的規則可以方便的找到服務端數據。問題與輪詢類似,數據過於分散且擴容、縮容難。Hash同樣適合弱相關的數據,並可通過一致性哈希來解決數據的遷移問題。

Range 根據範圍來分片數據,比如日期範圍。可以將一類數據歸檔到特定的節點,以增加查詢速度。此類分片會遇到熱點問題,會冷落很多機器。

自定義 自定義一些分片規則。比如通過用戶的年齡,區域等進行切分。你需要維護大量的路由表,然後自己控制數據和訪問的傾斜問題。

嵌套 屬於自定義的一種,路由規則可以嵌套。比如首先使用Range進行虛擬分片,然後再使用Hash進行實際分片。在實際操作中,這很有用,需要客戶端和服務端的結合才能完成。

路由的元數據不能太多,否則它本身就是一個訪問瓶頸;也不能夠太複雜,否則數據的去向將成為謎底。分散式系統的數據驗證和測試是困難的,原因就在於此。


可惜的是,使用用戶的年齡,和使用用戶的地域進行分片,數據的分佈完全不同。增加了一個維度的查詢速度,會減慢另一個維度的性能,這是不可避免的。切分欄位的選擇非常重要,如果幾個維度都很必要,解決的方式就是冗餘—-按照每個切分維度,都寫一份數據。

大部分互聯網業務一般通過用戶ID即可找到用戶的所有相關信息,規劃一個分層的路由結構即能滿足需求。但數據統計類的需求就困難的多,你看到的很多年度報告,可能是算了個把月纔出來的。

一般組成結構

數據寫入簡單,因為是按條寫的。但數據的讀取就複雜多了,因為可能涉及到大量分片,尤其是AGG查詢業務。一般會引入中間節點負責數據的聚合,因為大量的計算會影響master的穩定,這是不能忍受的。

通過區分節點的職責,可以保證集羣的穩定。根據不同的需要,會有更多的協調節點被加入。

在做分散式之前,先要確保在單機場景能夠最優。除了一些緩衝區優化,還有索引。但分散式是一直缺少一個索引的,曾經想設計一種基於內存的分散式索引,但還是賺錢養家要緊。

存儲要有一個強大的查詢語法引擎,目前來看非SQL引擎莫屬。抽象成一棵巨大但語法樹,然後在其上編程。像Redis這樣簡單的文本協議,是一個特定領域的特例。

分散式事務

ACID是強事務的單機RDBMS的特性。涉及到跨庫,會有二階段提交、三階段提交之類的分散式事務處理。

資料庫的分散式事務實現叫做XA,也是一種2PC,MySQL5.5版本開始已經支持這種協議。

2PC會嚴重影響性能,並不是和高並發的場景,而且其實現複雜,犧牲了一部分可用性。


另一種常用的方式就是TCC(補償事務)。TCC的本質是:對於每一個操作,都需要一個與之對應的確認和撤銷操作。但可惜的是,在確認和撤銷階段,也有一定概率發生問題,需要TCC的TCC;很多業務根本沒有相應的逆操作,比如刪除某些數據,TCC就沒法玩了。

TCC需要大量編碼,適合在框架層統一處理。


還有一種思路是將分散式事務合併成本地事務來處理。也就是一個事務包含一條消息+一堆資料庫操作,成功執行完畢後再設置消息的狀態,失敗後會重試。 此種方式將消息強制耦合到業務中,且消息系統本身的事務問題也是一個需要考慮的因素。


分散式事務除了要寫多個分片的協調問題,還有並發讀寫某一個值的問題。

比如有很多請求同時在修改一個餘額。常用的方法就是加鎖,但是效率太低。我們回憶一下java如何保證這種衝突。

對於讀遠大於小的操作,可以使用CopyOnWrite這種方式優化;對於原子操作,可以使用CompareAndSet的方式先比較再賦值。要想保證餘額的安全,使用後者是很有必要的。

MVCC是行級別鎖的一種妥協,他用來保證一個值在某個事務中是一致的,避免了臟讀和幻讀,但並不能保證數據的安全,這點一定要注意。

最終一致性

舉個栗子:你的家庭資金共有500w,你私自借給好基友500萬。使出了洪荒之力在年底討回了借款,並追加了利息。在老婆查帳的時候,原封不動的展示給她看。這就是最終一致性。

我習慣性這樣描述:在可忍受的時間內,輕過程、重結果,達成一致即可。雖然回味起來心有餘悸。

在這種情況下,不需要過多的使用分散式事務來控制。你只管寫你的數據,不用管別人是否寫成功。我們通過其他的手段來保證數據的一致性。

一種方式是常見的定時任務,不斷的掃描最近生成的數據,進行補齊。如果程序實在無法判斷,則寫入到異常表中人工介入。

另外一種方式就是重放數據,將這個過程重新執行一遍,要求業務邏輯是可重入的(冪等)。如果依然有問題,還是需要人工介入。

比較幸運的是,良好的設計下,這些異常狀況產生的幾率是比較小的,投入和產出會超出期望。採用了BASE的系統,選擇的是弱一致性,高度依賴業務監控組件來及時的發現問題。

這種思想已經被大多數研發所接受,除非你的老闆可忍受時間很短!

哦,BASE的全稱是:

Basically Available(基本可用),

Soft state(軟狀態),

Eventually consistent(最終一致性)

總結

作為研發人員,是不能對軟體有好惡傾向的,只有合適與不合適的區別。沒有精力去改進這些系統,只能通過不斷的取捨,組合它們的優點。

Greenplum和ElasticSearch,在分散式DB領域,是兩個典型實現,它們都以強大的分散式能力著稱。

Greenplum代表了RDBMS是如何向分散式發展的,當然它是建立在強大的Postgresql基礎上的。

ES是建立在Lucene上的全文檢索搜索引擎,但好像大家也拿它當資料庫使用。源碼是java的,有很多值得推敲的地方。

緩慢的I/O設備,再也無法壓榨單機的性能,註定了要走向分散式。但前路依然漫漫,看看五花八門的分散式資料庫就知道了。

沒有誰,能一統江湖。

推薦閱讀:

相關文章