原文在我自己的Blog,這里只是同步發(fā)布。
對于使用SQL或者NoSQL的童鞋來說,replication都是一個避不開的話題,通過replication,能極大地保證你的數(shù)據(jù)安全性。畢竟誰都知道,不要把雞蛋放在一個籃子里,同理,也不要把數(shù)據(jù)放到一臺機器上面,不然機器當機了你就happy了。
在分布式環(huán)境下,對于任何數(shù)據(jù)存儲系統(tǒng),實現(xiàn)一套好的replication機制是很困難的,畢竟CAP的限制擺在那里,我們不可能實現(xiàn)出一套完美的replication機制,只能根據(jù)自己系統(tǒng)的實際情況來設計和對CAP的取舍。
對于replication更詳細的說明與解釋,這里推薦Distributed systems
for fun and profit,后面,我會根據(jù)LedisDB的實際情況,詳細的說明我在LedisDB里面使用的replication是如何實現(xiàn)的。
BinLog
最開始的時候,Ledisdb采用的是類似MySQL通用binlog的replication機制,即通過binlog的filename + position來決定需要同步的數(shù)據(jù)。這套方式實現(xiàn)起來非常簡單,但是仍然有一些不足,主要就在于hierarchical replication情況下如果master當?shù)簦x擇合適的slave提升為master是比較困難的。舉個最簡單的例子,假設A為master,B,C為slave,如果A當?shù)袅耍覀儠贐,C里面選擇同步數(shù)據(jù)最多的那個,但是是哪一個呢?這個問題,在MySQL的replication中也會碰到。
MySQL GTID
在MySQL 5.6之后,引入了GTID(Global transaction ID)的概念來解決上述問題,它通過Source:ID
的方式來在binlog里面表示一個唯一的transaction。Source為當前server的uuid,這個是全局唯一的,而ID則是該server內部的transaction ID(采用遞增保證唯一)。具體到上面那個問題,采用GTID,如果A當?shù)袅耍覀冎恍枰贐和C的binlog里面查找比較最后一個A這個uuid的transaction id的大小,譬如B的為uuid:10,而C的為uuid:30,那么鐵定我們會選擇C為新的master。
當然使用GTID也有相關的限制,譬如slave也必須寫binlog等,但它仍然足夠強大,解決了早期MySQL replication的時候一大攤子的棘手問題。但LedisDB并不準備使用,主要就在于應用場景沒那么復雜的情況,我需要的是一個更加簡單的解決方案。
Google Global Transaction ID
早在MySQL的GTID之前,google的一個MySQL版本就已經使用了global transaction id,在binlog里面,它對于任何的transaction,使用了group id來唯一標示。group id是一個全局的遞增ID,由master負責維護生成。當master當?shù)糁螅覀冎恍枰磗lave的binlog里面誰的group id最大,那么那一個就是能被選為master了。
可以看到,這套方案非常簡單,但是限制更多,譬如slave端的binlog只能由replication thread寫入,不支持Multi-Masters,不支持circular replication等。但我覺得它已經足夠簡單高效,所以LedisDB準備參考它來實現(xiàn)。
Raft
弄過分布式的童鞋應該都或多或少的接觸過Paxos(至少我是沒完全弄明白的),而Raft則號稱是一個比Paxos簡單得多的分布式一致性算法。
Raft通過replicated log來實現(xiàn)一致性,假設有A,B,C三臺機器,A為Leader,B和C為follower,(其實也就是master和slave的概念)。A的任何更新,都必須首先寫入Log(每個Log有一個LogID,唯一標示,全局遞增),然后將其Log同步到Follower,最后才能在A上面提交更新。如果A當?shù)袅耍珺和C重新選舉,如果哪一臺機器當前的LogID最大,則成為Leader。看到這里,是不是有了一種很熟悉的感覺?
LedisDB在支持consensus replication上面,參考了Raft的相關做法。
名詞解釋
在詳細說明LedisDB replication的實現(xiàn)前,有必要解釋一些關鍵字段。
- LogID:log的唯一標示,由master負責生成維護,全局遞增。
- LastLogID:當前程序最新的logid,也就是記錄著最后一次更新的log。
- FirstLogID:當前程序最老的logid,之前的log已經被清除了。
- CommitID:當前程序已經處理執(zhí)行的log。譬如當前LastLogID為10,而CommitID為5,則還有6,7,8,9,10這幾個log需要執(zhí)行處理。如果CommitID = LastLogID,則證明程序已經處于最新狀態(tài),不再需要處理任何log了。
LedisDB Replication
LedisDB的replication實現(xiàn)很簡單,當master有任何更新,master會做如下事情:
- 記錄該更新到log,logid = LastLogID + 1,LastLogID = logid
- 同步該log到slaves,等待slaves的確認返回,或者超時
- 提交更新
- 更新CommitID = logid
上面還需要考慮到錯誤處理的情況。
- 如果1失敗,記錄錯誤日志,然后我們會認為該次更新操作失敗,直接返回。
- 如果3失敗,不更新CommitID返回,因為這時候CommitID小于LastLogID,master進入read only模式,replication thread嘗試執(zhí)行l(wèi)og,如果能執(zhí)行成功,則更新CommitID,變成可寫模式。
- 如果4失敗,同上,因為LedisDB采用的是Row-Base Format的log格式,所以一次更新操作能夠冪等多次執(zhí)行。
對于slave
如果是首次同步,則進入全同步模式:
- master生成一個snapshot,連同當前的LastLogID一起發(fā)送給slave。
- slave收到該dump文件之后,load載入,同時更新CommitID為dump文件里面的LastLogID。
然后進入增量同步模式,如果slave已經有相關log,則直接進入增量同步模式。
在增量模式下面,slave向master發(fā)送sync命令,sync的參數(shù)為下一個需要同步的log,如果slave當前沒有binlog(譬如上面提到的全同步情況),則logid = CommitID + 1, 否則logid = LastLogID + 1。
master收到sync請求之后,有如下處理情況:
- sync的logid小于FirstLogID,master沒有該log,slave收到該錯誤重新進入全同步模式。
- master有該sync的log,于是將log發(fā)送給slave,slave收到之后保存,并再次發(fā)送sync獲取下一個log,同時該次請求也作為ack告知master同步該log成功。
- sync的log id已經大于LastLogID了,表明master和slave的狀態(tài)已經到達一致,沒有l(wèi)og可以同步了,slave將會等待新的log直到超時再次發(fā)送sync。
在slave端,對于接受到的log,由replication thread負責執(zhí)行,并更新CommitID。
如果master當機,我們只需要選擇具有最大LastLogID的那個slave為新的master就可以了。
Limitation
總的來說,這套replication機制很簡單,易于實現(xiàn),但是仍然有許多限制。
- 不支持Multi-Master,因為同時只能有一個地方進行全局LogID的生成。不過我真的很少見到Multi-Master這樣的架構模式,即使在MySQL里面。
- 不支持Circular-Replication,slave寫入的log id不允許小于當前的LastLogID,這樣才能保證只同步最新的log。
- 沒有自動master選舉機制,不過我覺得放到外部去實現(xiàn)更好。
Async/Sync Replication
LedisDB是支持強一致性的同步replication的,如果配置了該模式,那么master會等待slave同步完成log之后再提交更新,這樣我們就能保證當master當機之后,一定有一臺slave具有跟master一樣的數(shù)據(jù)。但在實際中,可能因為網絡環(huán)境或者slave當機等問題,master不可能一直等待slave同步完成log,所以通常都會有一個超時機制。從這點來看,我們仍然不能保證數(shù)據(jù)的強一致性,但是還是能夠達成最終一致性。
使用同步replication機制會極大地降低master的寫入性能,如果對數(shù)據(jù)一致性不敏感的業(yè)務,其實采用異步replication就可以了。
Failover
LedisDB現(xiàn)在沒有自動的failover機制,master當機之后,我們仍然需要人工的介入來選擇合適的slave(具有最大LastLogID那個),提升為master,并將其他slave重新指向該master。后續(xù)考慮使用外部的keeper程序來處理。而對于keeper的單點問題,則考慮使用raft或者zookeeper來處理。
后記
雖然LedisDB現(xiàn)在已經支持replication,但仍然需要在生產環(huán)境中檢驗完善。
LedisDB是一個采用Go實現(xiàn)的高性能NoSQL,接口類似Redis,現(xiàn)在已經用于生產環(huán)境,歡迎大家使用。