技術(shù)干貨總結(jié):分布式系統(tǒng)常見同步機(jī)制
分布式系統(tǒng)為保證數(shù)據(jù)高可用,需要為數(shù)據(jù)保存多個(gè)副本,隨之而來的問題是如何在不同副本間同步數(shù)據(jù)?不同的同步機(jī)制有不同的效果和代價(jià),本文嘗試對(duì)常見分布式組件的同步機(jī)制做一個(gè)小結(jié)。
常見機(jī)制
有一些常用的同步機(jī)制,對(duì)它們也有許多評(píng)價(jià)的維度,先看看大神的 經(jīng)典總結(jié) :

上圖給出了常用的同步方式(個(gè)人理解,請(qǐng)批評(píng)指正):
- Backup,即定期備份,對(duì)現(xiàn)有的系統(tǒng)的性能基本沒有影響,但節(jié)點(diǎn)宕機(jī)時(shí)只能勉強(qiáng)恢復(fù)
- Master-Slave,主從復(fù)制,異步復(fù)制每個(gè)指令,可以看作是粒度更細(xì)的定期備份
- Multi-Muster,多主,也稱“主主”,MS 的加強(qiáng)版,可以在多個(gè)節(jié)點(diǎn)上寫,事后再想辦法同步
- 2 Phase-Commit,二階段提交,同步先確保通知到所有節(jié)點(diǎn)再寫入,性能容易卡在“主”節(jié)點(diǎn)上
- Paxos,類似 2PC,同一時(shí)刻有多個(gè)節(jié)點(diǎn)可以寫入,也只需要通知到大多數(shù)節(jié)點(diǎn),有更高的吞吐
同步方式分兩類,異步的性能好但可能有數(shù)據(jù)丟失,同步的能保證不丟數(shù)據(jù)但性能較差。同種方式的算法也能有所提升(如 Paxos 對(duì)于 2PC),但實(shí)現(xiàn)的難度又很高。實(shí)現(xiàn)上只能在這幾點(diǎn)上進(jìn)行權(quán)衡。
考慮同步算法時(shí),需要考慮節(jié)點(diǎn)宕機(jī)、網(wǎng)絡(luò)阻斷等故障情形。下面,我們來看看一些分布式組件的數(shù)據(jù)同步機(jī)制,主要考慮數(shù)據(jù)寫入請(qǐng)求如何被處理,期間可能會(huì)涉及如何讀數(shù)據(jù)。
Redis
Redis 3.0 開始引入 Redis Cluster 支持集群模式,個(gè)人認(rèn)為它的設(shè)計(jì)很漂亮,大家可以看看 官方文檔 。
- 采用的是主從復(fù)制,異步同步消息,極端情況會(huì)丟數(shù)據(jù)
- 只能從主節(jié)點(diǎn)讀寫數(shù)據(jù),從節(jié)點(diǎn)只會(huì)拒絕并讓客戶端重定向,不會(huì)轉(zhuǎn)發(fā)請(qǐng)求
- 如果主節(jié)點(diǎn)宕機(jī)一段時(shí)間,從節(jié)點(diǎn)中會(huì)自動(dòng)選主
- 如果期間有數(shù)據(jù)不一致,以最新選出的主節(jié)點(diǎn)的數(shù)據(jù)為準(zhǔn)。
一些設(shè)計(jì)細(xì)節(jié):
- HASH_SLOT = CRC16(Key) mod 16384
- MEET
- WAIT
Kafka
Kafka 的分片粒度是 Partition,每個(gè) Partition 可以有多個(gè)副本。副本同步設(shè)計(jì)參考 官方文檔
- 類似于 2PC,節(jié)點(diǎn)分主從,同步更新消息,除非節(jié)點(diǎn)全掛,否則不會(huì)丟消息
- 消息發(fā)到主節(jié)點(diǎn),主節(jié)點(diǎn)寫入后等待“所有”從節(jié)點(diǎn)拉取該消息,之后通知客戶端寫入完成
- “所有”節(jié)點(diǎn)指的是 In-Sync Replica(ISR),響應(yīng)太慢或宕機(jī)的從節(jié)點(diǎn)會(huì)被踢除
- 主節(jié)點(diǎn)宕機(jī)后,從節(jié)點(diǎn)選舉成為新的主節(jié)點(diǎn),繼續(xù)提供服務(wù)
- 主節(jié)點(diǎn)宕機(jī)時(shí)正在提交的修改沒有做保證(消息可能沒有 ACK 卻提交了)
一些設(shè)計(jì)細(xì)節(jié):
- 當(dāng)前消費(fèi)者只能從主節(jié)點(diǎn)讀取數(shù)據(jù),未來可能會(huì)改變
- 主從的粒度是 partition,每個(gè) broker 對(duì)于某些 Partition 而言是主節(jié)點(diǎn),對(duì)于另一些而言是從節(jié)點(diǎn)
- Partition 創(chuàng)建時(shí),Kafka 會(huì)盡量讓 preferred replica 均勻分布在各個(gè) broker
- 選主由一個(gè) controller 跟 zookeeper 交互后“內(nèi)定”,再通過 RPC 通知具體的主節(jié)點(diǎn) ,此舉能防止 partition 過多,同時(shí)選主導(dǎo)致 zk 過載。
ElasticSearch
ElasticSearch 對(duì)數(shù)據(jù)的存儲(chǔ)需求和 Kafka 很類似,設(shè)計(jì)也很類似,詳細(xì)可見 官方文檔 。
ES 中有 master node 的概念,它實(shí)際的作用是對(duì)集群狀態(tài)進(jìn)行管理,跟數(shù)據(jù)的請(qǐng)求無關(guān)。為了上下文一致性,我們稱它為管理節(jié)點(diǎn),而稱 primary shard 為“主節(jié)點(diǎn)”, 稱 replica shard 為從節(jié)點(diǎn)。ES 的設(shè)計(jì):
- 類似于 2PC,節(jié)點(diǎn)分主從,同步更新消息,除非節(jié)點(diǎn)全掛,否則不會(huì)丟消息
- 消息發(fā)到主節(jié)點(diǎn),主節(jié)點(diǎn)寫入成功后并行發(fā)給從節(jié)點(diǎn),等到從節(jié)點(diǎn)全部寫入成功,通知客戶端寫入完成
- 管理節(jié)點(diǎn)會(huì)維護(hù)每個(gè)分片需要寫入的從節(jié)點(diǎn)列表,稱為 in-sync copies
- 主節(jié)點(diǎn)宕機(jī)后,從節(jié)點(diǎn)選舉成為新的主節(jié)點(diǎn),繼續(xù)提供服務(wù)
- 提交階段從節(jié)點(diǎn)不可用的話,主節(jié)點(diǎn)會(huì)要求管理節(jié)點(diǎn)將從節(jié)點(diǎn)從 in-sync copies 中移除
一些設(shè)計(jì)細(xì)節(jié):
- 寫入只能通過只主節(jié)點(diǎn)進(jìn)行,讀取可以從任意從節(jié)點(diǎn)進(jìn)行
- 每個(gè)節(jié)點(diǎn)均可提供服務(wù),它們會(huì)轉(zhuǎn)發(fā)請(qǐng)求到數(shù)據(jù)分片所在的節(jié)點(diǎn),但建議循環(huán)訪問各個(gè)節(jié)點(diǎn)以平衡負(fù)載
- 數(shù)據(jù)做分片: shard = hash(routing) % number_of_primary_shards
- primary shard 的數(shù)量是需要在創(chuàng)建 index 的時(shí)候就確定好的
- 主從的粒度是 shard,每個(gè)節(jié)點(diǎn)對(duì)于某些 shard 而言是主節(jié)點(diǎn),對(duì)于另一些而言是從節(jié)點(diǎn)
- 選主算法使用了 ES 自己的 Zen Discovery
Hadoop
Hadoop 使用的是鏈?zhǔn)綇?fù)制,參考 Replication Pipelining
- 數(shù)據(jù)的多個(gè)復(fù)本寫入多個(gè) datanode,只要有一個(gè)存活數(shù)據(jù)就不會(huì)丟失
- 數(shù)據(jù)拆分成多個(gè) block,每個(gè) block 由 namenode 決定數(shù)據(jù)寫入哪幾個(gè) datanode
- 鏈?zhǔn)綇?fù)制要求數(shù)據(jù)發(fā)往一個(gè)節(jié)點(diǎn),該節(jié)點(diǎn)發(fā)往下一節(jié)點(diǎn),待下個(gè)節(jié)點(diǎn)返回及本地寫入成 功后返回,以此類推形成一條寫入鏈。
- 寫入過程中的宕機(jī)節(jié)點(diǎn)會(huì)被移除 pineline,不一致的數(shù)據(jù)之后由 namenode 處理。
實(shí)現(xiàn)細(xì)節(jié):
- 實(shí)現(xiàn)中優(yōu)化了鏈?zhǔn)綇?fù)制:block 拆分成多個(gè) packet,節(jié)點(diǎn) 1 收到 packet, 寫入本地 的同時(shí)發(fā)往節(jié)點(diǎn) 2,等待節(jié)點(diǎn) 2 完成及本地完成后返回 ACK。節(jié)點(diǎn) 2 以此類推將 packet 寫入本地及發(fā)往節(jié)點(diǎn) 3……
TiKV
TiKV 使用的是 Raft 協(xié)議來實(shí)現(xiàn)寫入數(shù)據(jù)時(shí)的一致性。參考 三篇文章了解 TiDB 技術(shù)內(nèi)幕——說存儲(chǔ)
- 使用 Raft,寫入時(shí)需要半數(shù)以上的節(jié)點(diǎn)寫入成功才返回,宕機(jī)節(jié)點(diǎn)不超過半數(shù)則數(shù)據(jù)不丟失。
- TiKV 將數(shù)據(jù)的 key 按 range 分成 region,寫入時(shí)以 region 為粒度進(jìn)行同步。
- 寫入和讀取都通過 leader 進(jìn)行。每個(gè) region 形成自己的 raft group,有自己的 leader。
Zookeeper
Zookeeper 使用的是 Zookeeper 自己的 Zab 算法(Paxos 的變種?),參考 Zookeeper Internals
- 數(shù)據(jù)只可以通過主節(jié)點(diǎn)寫入(請(qǐng)求會(huì)被轉(zhuǎn)發(fā)到主節(jié)點(diǎn)進(jìn)行),可以通過任意節(jié)點(diǎn)讀取
- 主節(jié)點(diǎn)寫入數(shù)據(jù)后會(huì)廣播給所有節(jié)點(diǎn),超過半數(shù)節(jié)點(diǎn)寫入后返回客戶端
- Zookeeper 不保證數(shù)據(jù)讀取為最新,但通過“單一視圖”保證讀取的數(shù)據(jù)版本不“回退”
小結(jié)
如果系統(tǒng)對(duì)性能要求高以至于能容忍數(shù)據(jù)的丟失(Redis),則顯然異步的同步方式是一種好的選擇。
而當(dāng)系統(tǒng)要保證不丟數(shù)據(jù),則幾乎只能使用同步復(fù)制的機(jī)制,看到 Kafka 和 Elasticsearch 不約而同地使用了 PacificA 算法(個(gè)人認(rèn)為可以看成是 2PC 的變種),當(dāng)然這種方法的響應(yīng)制約于最慢的副本,因此 Kafka 和 Elasticsearch 都有相關(guān)的機(jī)制將慢的副本移除。
當(dāng)然看起來 Paxos, Raft, Zab 等新的算法比起 2PC 還是要好的:一致性保證更強(qiáng),只要半數(shù)節(jié)點(diǎn)寫入成功就可以返回,Paxos 還支持多點(diǎn)寫入。只不過這些算法也很難正確實(shí)現(xiàn)和優(yōu)化。