寫在前面
理論上,有了可靠的負載均衡機制,咱們就能將 1 臺服務器輕鬆擴展到 n 臺,然而,若是這 n 臺機器仍然使用同一數據庫的話,很快數據庫就會成爲系統的性能瓶頸和可靠性瓶頸數據庫
那麼,如何提高數據庫的處理能力?服務器
從資源的角度來看,無非兩種思路:網絡
縱向擴展:提高單機配置(硬盤、內存、CPU 等等),但一樣會遭遇單機性能瓶頸app
橫向擴展:增長機器,數量上從單數據庫實例擴展到多實例負載均衡
這樣看來,彷佛只要加幾個數據庫,共同分擔來自應用層的流量就完成了從單庫到多庫的擴展:less
果然如此簡單嗎?異步
一.一致性問題
若是同一數據存在多份拷貝,那麼就須要考慮如何保證其一致性async
(摘自一致性模式)ide
數據庫與應用服務最大的區別在於,應用服務能夠是無狀態的(或者能夠將共享狀態抽離出去,好比放到數據庫),而數據庫操做必定是有狀態的,在擴展數據庫時必需要考慮數據的一致性函數
具體的,一致性分爲 3 種,嚴格程度依次遞減:
強一致性(Strong consistency):寫完以後,當即就能讀到
最終一致性(Eventual consistency):寫完以後,保證最終能讀到
弱一致性(Weak consistency):寫完以後,不必定能讀到
二.Replication
因此,從單庫擴展成多庫,至少要有一種數據更新同步機制,稱之爲Replication(複製):
Replication in computing involves sharing information so as to ensure consistency between redundant resources, such as software or hardware components, to improve reliability, fault-tolerance, or accessibility.
(摘自Replication (computing))
即,經過複製(寫操做)來保證多份數據拷貝的信息一致性。例如,向數據庫實例 A 寫入數據時,也要把相同的數據寫入到實例 B、C、D 等
三.複製方式
異步複製
具體的,能夠在寫完以後,再告知其它實例更新數據,即異步複製(Asynchronous replication):
這種模式下,客戶端無需等待複製操做完成,不存在額外的性能影響。但問題在於:
有數據丟失風險
沒法保證強一致性,由於存在複製延遲(Replication lag)
若是實例 A 在寫完以後,還沒來得及告知其它實例,本身卻 down 掉了,就會出現數據丟失:
另外一方面,因爲複製操做是異步完成的,數據更新其實是滯後的:
從當前實例上一個寫操做完成,到該操做被應用到其它實例的時間差稱爲複製延遲(Replication lag)。在這期間,客戶端從其它實例上讀到的仍然是舊數據,顯然不知足強一致性的要求(僅能保證最終一致性)
同步複製
想要達到嚴格的一致性要求,不得不考慮同步複製(Synchronous replication):
發生寫操做時,當即將操做同步到其它全部實例,複製完成以後纔算寫完,以確保嚴格的一致性
但同步複製會影響性能和可用性,代價頗高:
性能影響:須要等待整個複製過程完成
可用性影響:只要有一個實例出現故障(網絡等緣由),整個寫操做就會失敗
而且數據庫實例數量越多,這兩方面的影響越大
半同步複製
特殊地,能夠將兩種方式結合使用,稱之爲半同步複製(Semi-synchronous replication):
Some databases and replication tools allow us to define a number of followers to replicate synchronously, and the others just use the asynchronous approach. This is sometimes called semi-synchronous replication.
即要求一部分數據庫實例同步複製,其他的異步複製
P.S.PostgreSQL支持這種模式
四.拓撲結構
拓撲結構上,複製能夠分爲 3 類:
單主結構(Single leader replication)
多主結構(Multi leader replication)
無主結構(Leaderless replication)
單主結構
即最多見的一主多從結構:
這種結構下,寫操做(增/刪/改)只容許發生在主庫,由主庫將寫操做複製到其它全部從庫,從庫只支持讀操做(查)
因爲全部客戶端都寫同一個庫,成功避免了寫操做衝突的大麻煩。但要注意的是:
承載寫操做壓力的仍然是單庫:不適用於寫密集(write-intensive)的應用,但好在大多數應用都是讀密集的
訪問主庫的延遲問題:主庫只有一個,只能放在某個肯定的地理位置,意味着在某些區域發起寫操做(訪問主庫)可能要承擔較高的延遲
更糟糕的狀況,若是主庫 down 掉了,須要當即在從庫中選出一個接班人,擔起主庫的職責,保證這套機制正常運轉
然而,這種故障轉移策略卻不那麼容易實現,難點在於:
如何肯定主庫真的 down 掉了?
如何選擇新任主庫?
如何將寫操做轉到新任主庫上?
實際上,咱們沒法區分高延遲和不可用,一般認爲超時就算不可用(不管是否是真的 down 掉了),接着啓動故障轉移預案,開始選擇新任主庫
選出一個不難,關鍵在於所選的新任主庫要被其它全部從庫承認其地位纔算(即共識問題),好比預先定好接班次序
新任主庫選出來以後,要將全部寫操做轉發過來,好比增長一層分發機制,以容許路由控制
另外,若是採用的是異步複製,舊主庫恢復以後,還沒有複製到其它從庫的數據與掉線期間新任主庫寫入的數據可能會出現衝突,此時一般採用 LWW(last-write-win)策略,直接丟棄舊數據,但一樣存在風險
特殊的,一種有意思的狀況是舊主庫恢復過來覺得本身仍是主庫,出現分裂(Split-brain):
P.S.網絡故障也會致使這樣的狀況,例如兩個集羣之間出現網絡故障,沒法互相訪問,都覺得另外一隊人馬掛掉了,因而各自開始大選
簡單的處理辦法是 STONITH(Shoot The Other Node In The Head),一旦發現存在多個主庫,直接停掉一個
多主結構
如今有了多個可寫的主庫,能夠分擔寫操做,也能夠多地部署,單主結構的 2 個問題迎刃而解。然而,大麻煩卻出現了
因爲寫操做可以同時發生在(異步複製的)多個庫,咱們必須考慮如何解決寫入衝突。通常有 3 種思路:
避免衝突:好比按內容特徵分庫存儲,互不相干,好比對於國內國外兩個主庫,若是可以保證全部對國內數據的寫操做都能落到國內主庫上,全部對國外數據的寫操做都能落在國外主庫上,就不存在衝突了
LWW(last-write-win)策略:給每一個寫操做帶上時間戳,只保留最新版本
交由用戶來解決:記下衝突,應用程序提示給用戶,由用戶決定保留哪一份
P.S.有些數據庫(如CouchDB)支持將全部衝突值都寫下來,並在讀取時返回一系列值
此外,多主結構下的另外一個難題是複製 DDL(Data Definition Language),即針對 Schema 的寫操做,具體見DDL replication
無主結構
固然,還有一種不區分主庫的結構,全部庫均可讀可寫
看起來像是「全主結構」,那麼可預見的,寫衝突將變得很是廣泛,因此咱們須要調整策略,避免使之成爲「全主結構」:
寫:客戶端同時向多個數據庫寫,只要有一些成功了就算寫完
讀:客戶端同時從多個數據庫讀,各個庫返回數據及其對應的版本號,客戶端根據版本號來決定採用哪一個
沒有主庫,意味着不須要考慮故障轉移,單庫故障不影響總體,選擇新任主庫的各類麻煩問題都不復存在了
同時,沒有主庫也意味着沒有了數據同步機制,讀到的舊值沒法自動更正:
因此須要額外的糾錯機制,客戶端在讀到舊值時將新值寫回去(稱爲Read repair),或者由獨立的進程專門負責找出舊值並糾正回來
另外一個關鍵因素是讀/寫操做的目標庫數量,至少幾個庫寫入成功後,至少從幾個庫成功讀取才能保證必定能讀到新值?
若是w個庫寫入成功,接着成功讀到了r個庫的數據,那麼必須知足w + r > 庫的總數
五.具體實現
具體的,把一些數據從一個庫拷貝到另外一個庫有 3 種方式:
基於語句的複製:將寫操做語句原樣發一份給其它庫執行
日誌傳送式複製:也叫物理複製,將數據庫日誌傳遞給其它庫,從日誌恢復出徹底一致的數據。例如 PostgreSQL 提供的Streaming Replication
基於行的複製:也叫邏輯複製,傳遞專門用於複製的日誌,按行復制。例如MySQL提供的的Mixed Binary Logging Format
按語句複製的問題在於,並非全部語句的執行結果都是肯定的,例如CURRENT_TIME()、RANDOM(),雖然一些數據庫會在複製時對這些值進行替換,但仍沒法保證觸發器,以及用戶定義的函數有肯定的執行結果。另外一方面,還要確保事務操做在全部數據庫上的原子性,要麼全都完成了,要麼全都一點兒沒作
日誌傳送式複製可以保證數據徹底一致,但(面向存儲引擎的)日誌一般沒法跨數據庫版本使用,由於在不一樣版本的數據庫下,數據的物理存儲方式可能會發生變化。而且,日誌傳送不適用於多主結構,由於沒法把多份日誌合併成一份
而基於行的複製是前兩種方式的結合,採用一種專門用於複製的日誌,再也不與存儲引擎耦合,於是可以跨數據庫版本使用。與按語句複製相比,按行復制須要記錄更多的信息(好比一個語句影響了 100 行,須要按行都記下)
參考資料A Primer on Database Replication