本篇文章謝絕轉載,歡迎轉發
幾年之前,曾不自量力的想要寫一個相容RDBMS和NoSQL的資料庫,結果僅實現了一個Raft
協議,寫了一棵BTree
,就放棄了。使用Golang
寫這個算是比較簡單的了,但過程難以言訴,有點螞蟻撼大樹了。
而個人,由於工作的關係,也已經有四五年沒有和SQL打交道了。最近重拾,感慨良多。
像MySQL
這種RDBMS
,天生是存在分散式缺陷的,在海量資料的今天,很容易就達到瓶頸。過去這麼多年,一點長進都沒有。所以經常的操作就是換儲存引擎、分庫分表、引入中介軟體,閹割功能。
分散式儲存特徵
能讓你不忍割捨的,一個就是MySQL協議,你用慣了;一個就是事務,你怕丟資料。
幸運的是,大多數網際網路業務不需要強事務,甚至連MySQL協議都不需要。接下來我們看一下要將一個傳統的MySQL改造成分散式的儲存,是有多麼的困難。
CAP理論應該是人盡皆知的事情了,在此不多提。
單機上的任何資料都是不可信的,因為硬碟會壞,會斷電,會被挖光纜。所以一般通過冗餘多個副本來保證資料的安全。副本的另外一個作用,就是提供額外的計算能力,比如某些請求,會落到副本上。副本越多,可用性越高。
而加入副本以後,就涉及到資料的同步問題。即使是最快的區域網,也會存在延遲,更不用說機器效能差異引起的同步延遲。這就存在一個問題,讀副本的請求讀到的資料,可能不是最新的,這就是資料的一致性發生了改變。當然有些手段能保證資料的一致性,但副本越多,延遲越大。副本的加入還會引入主從的問題。主節點死掉以後,要有副本節點頂上去,這個過程的協調需要時間,其間部分不可用。
而當一類資料足夠大(比如說某張表),在其上的操作已經非常耗時的情況下,就需要對此類資料進行切割,將其分佈到多臺機器上。這個切割過程就是Sharding,通過一定規則的分片來減少單次查詢資料的規模,增加叢集容量。
當某些查詢涉及到多個分片,這個過程就比較緩慢了。協調節點需要與每個節點進行溝通,然後聚合查詢的結果,分片數越多,時間越長。
一般,在一個維度上的分庫都會遇到上述問題,可怕的是你可能會有多個維度的需求。對於一些NoSQL
來說,每一個維度,都需要冗餘一份資料,這一般是膨脹性的。
叢集的規劃並不是一成不變的,你的叢集可能會加入新的節點;也可能有節點因為事故離線;也可能因為分片維度的問題,資料發生了傾斜。當這種情況發生,叢集間的資料會發生遷移,以便達到平衡。這個過程有些是自動的,也有些是手動進行觸發。這個過程也是最困難的:既要保證資料的增量遷移,又要保證叢集的正確服務。
如果你想要事務(很多情況是你不懂技術的Leader決定),那就集中儲存,不要分片。事務是很多效能場景和擴充套件場景的萬惡之源,流量大了你會急著去掉它。
副本
針對一個分片的資料,只能有一個寫入的地方,這就是master
,其他副本都是從master
複製資料。
副本能夠增加讀操作的並行讀,但會讀到髒資料。如果你想要讀到的資料是一致的,可以採用同步寫副本的方式,比如KAFKA的ack=-1
,只有全部同步成功了,才認為本次提交成功。
但如果你的副本太多,這個過程會非常的慢。你可能想要通過分配寫入和讀取的副本個數來協調寫入和讀取的效率,Quorum
的R+W>N
就是一個權衡策略。
這個過程可以簡單的用抽屜原理來解釋。
上面的這個過程比較簡單,所以需要有點複雜的壓下軸。一個名門就是Paxos
,複雜的很,以前看了一個星期也沒全部搞懂 -.-。
ZAB
協議是ZooKeeper
在Paxos
協議的基礎上進行擴充套件而來的,說實話也沒看懂,而且ZK的原始碼也非常的...
唯一看得懂的就是Raft
協議,這個是Etcd
和Consul
的基礎,是簡化版的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裝置,再也無法壓榨單機的效能,註定了要走向分散式。但前路依然漫漫,看看五花八門的分散式資料庫就知道了。
沒有誰,能一統江湖。