1. 程式人生 > 其它 >06 | 資料同步:主從庫如何實現資料一致?

06 | 資料同步:主從庫如何實現資料一致?

06 | 資料同步:主從庫如何實現資料一致?

前兩節課,我們學習了 AOF 和 RDB,如果 Redis 發生了宕機,它們可以分別通過回放日 志和重新讀入 RDB 檔案的方式恢復資料,從而保證儘量少丟失資料,提升可靠性。 不過,即使用了這兩種方法,也依然存在服務不可用的問題。比如說,我們在實際使用時 只運行了一個 Redis 例項,那麼,如果這個例項宕機了,它在恢復期間,是無法服務新來 的資料存取請求的。 那我們總說的 Redis 具有高可靠性,又是什麼意思呢?其實,這裡有兩層含義:一是資料 儘量少丟失,二是服務儘量少中斷。AOF 和 RDB 保證了前者,而對於後者,Redis 的做 法就是增加副本冗餘量
,將一份資料同時儲存在多個例項上。即使有一個例項出現了故 障,需要過一段時間才能恢復,其他例項也可以對外提供服務,不會影響業務使用。 多例項儲存同一份資料,聽起來好像很不錯,但是,我們必須要考慮一個問題:這麼多副 本,它們之間的資料如何保持一致呢?資料讀寫操作可以發給所有的例項嗎? 實際上,Redis 提供了主從庫模式,以保證資料副本的一致,主從庫之間採用的是讀寫分 離的方式。 Redis主從庫和讀寫分離 那麼,為什麼要採用讀寫分離的方式呢? 你可以設想一下,如果在上圖中,不管是主庫還是從庫,都能接收客戶端的寫操作,那 麼,一個直接的問題就是:如果客戶端對同一個資料(例如 k1)前後修改了三次,每一次 的修改請求都發送到不同的例項上,在不同的例項上執行,那麼,這個資料在這三個例項 讀操作:主庫、從庫都可以接收;
寫操作:首先到主庫執行,然後,主庫將寫操作同步給從庫。

 

 

那麼,為什麼要採用讀寫分離的方式呢? 你可以設想一下,如果在上圖中,不管是主庫還是從庫,都能接收客戶端的寫操作,那 麼,一個直接的問題就是:如果客戶端對同一個資料(例如 k1)前後修改了三次,每一次 的修改請求都發送到不同的例項上,在不同的例項上執行,那麼,這個資料在這三個例項  上的副本就不一致了(分別是 v1、v2 和 v3)。在讀取這個資料的時候,就可能讀取到舊 的值。 如果我們非要保持這個資料在三個例項上一致,就要涉及到加鎖、例項間協商是否完成修 改等一系列操作,但這會帶來鉅額的開銷,當然是不太能接受的。 而主從庫模式一旦採用了讀寫分離,所有資料的修改只會在主庫上進行,不用協調三個實
例。主庫有了最新的資料後,會同步給從庫,這樣,主從庫的資料就是一致的。 那麼,主從庫同步是如何完成的呢?主庫資料是一次性傳給從庫,還是分批同步?要是主 從庫間的網路斷連了,資料還能保持一致嗎?這節課,我就和你聊聊主從庫同步的原理, 以及應對網路斷連風險的方案。 好了,我們先來看看主從庫間的第一次同步是如何進行的,這也是 Redis 例項建立主從庫 模式後的規定動作。

主從庫間如何進行第一次同步?

當我們啟動多個 Redis 例項的時候,它們相互之間就可以通過 replicaof(Redis 5.0 之前 使用 slaveof)命令形成主庫和從庫的關係,之後會按照三個階段完成資料的第一次同步。 例如,現在有例項 1(ip:172.16.19.3)和例項 2(ip:172.16.19.5),我們在例項 2 上 執行以下這個命令後,例項 2 就變成了例項 1 的從庫,並從例項 1 上覆制資料:
replicaof 172.16.19.3 6379

 

接下來,我們就要學習主從庫間資料第一次同步的三個階段了。你可以先看一下下面這張 圖,有個整體感知,接下來我再具體介紹

 

 

第一階段是主從庫間建立連線、協商同步的過程,主要是為全量複製做準備。在這一步, 從庫和主庫建立起連線,並告訴主庫即將進行同步,主庫確認回覆後,主從庫間就可以開 始同步了。 具體來說,從庫給主庫傳送 psync 命令,表示要進行資料同步,主庫根據這個命令的引數 來啟動複製。psync 命令包含了主庫的 runID 和複製進度 offset 兩個引數
runID,是每個 Redis 例項啟動時都會自動生成的一個隨機 ID,用來唯一標記這個實 例。當從庫和主庫第一次複製時,因為不知道主庫的 runID,所以將 runID 設 為“?”。 offset,此時設為 -1,表示第一次複製。
  主庫收到 psync 命令後,會用 FULLRESYNC 響應命令帶上兩個引數:主庫 runID 和主庫 目前的複製進度 offset,返回給從庫。從庫收到響應後,會記錄下這兩個引數。 這裡有個地方需要注意,FULLRESYNC 響應表示第一次複製採用的全量複製,也就是說, 主庫會把當前所有的資料都複製給從庫。 在第二階段,主庫將所有資料同步給從庫。從庫收到資料後,在本地完成資料載入。這個 過程依賴於記憶體快照生成的 RDB 檔案 具體來說,主庫執行 bgsave 命令,生成 RDB 檔案,接著將檔案發給從庫。從庫接收到 RDB 檔案後,會先清空當前資料庫,然後載入 RDB 檔案。這是因為從庫在通過 replicaof 命令開始和主庫同步前,可能儲存了其他資料。為了避免之前資料的影響,從庫需要先把 當前資料庫清空。 在主庫將資料同步給從庫的過程中,主庫不會被阻塞,仍然可以正常接收請求。否則, Redis 的服務就被中斷了。但是,這些請求中的寫操作並沒有記錄到剛剛生成的 RDB 檔案 中。為了保證主從庫的資料一致性,主庫會在記憶體中用專門的 replication buffer,記錄 RDB 檔案生成後收到的所有寫操作。 最後,也就是第三個階段,主庫會把第二階段執行過程中新收到的寫命令,再發送給從 庫。具體的操作是,當主庫完成 RDB 檔案傳送後,就會把此時 replication buffer 中的修 改操作發給從庫,從庫再重新執行這些操作。這樣一來,主從庫就實現同步了。 主從級聯模式分擔全量複製時的主庫壓力 通過分析主從庫間第一次資料同步的過程,你可以看到,一次全量複製中,對於主庫來 說,需要完成兩個耗時的操作:生成 RDB 檔案和傳輸 RDB 檔案。 如果從庫數量很多,而且都要和主庫進行全量複製的話,就會導致主庫忙於 fork 子程序生 成 RDB 檔案,進行資料全量同步。fork 這個操作會阻塞主執行緒處理正常請求,從而導致 主庫響應應用程式的請求速度變慢。此外,傳輸 RDB 檔案也會佔用主庫的網路頻寬,同樣 會給主庫的資源使用帶來壓力。那麼,有沒有好的解決方法可以分擔主庫壓力呢? 其實是有的,這就是“主 - 從 - 從”模式。 在剛才介紹的主從庫模式中,所有的從庫都是和主庫連線,所有的全量複製也都是和主庫 進行的。現在,我們可以通過“主 - 從 - 從”模式將主庫生成 RDB 和傳輸 RDB 的壓力, 以級聯的方式分散到從庫上。 簡單來說,我們在部署主從叢集的時候,可以手動選擇一個從庫(比如選擇記憶體資源配置 較高的從庫),用於級聯其他的從庫。然後,我們可以再選擇一些從庫(例如三分之一的 從庫),在這些從庫上執行如下命令,讓它們和剛才所選的從庫,建立起主從關係。 
replicaof 所選從庫的IP 6379
這樣一來,這些從庫就會知道,在進行同步時,不用再和主庫進行互動了,只要和級聯的 從庫進行寫操作同步就行了,這就可以減輕主庫上的壓力,如下圖所示:  

 

 

好了,到這裡,我們瞭解了主從庫間通過全量複製實現資料同步的過程,以及通過“主 - 從 - 從”模式分擔主庫壓力的方式。那麼,一旦主從庫完成了全量複製,它們之間就會一 直維護一個網路連線,主庫會通過這個連線將後續陸續收到的命令操作再同步給從庫,這 個過程也稱為基於長連線的命令傳播,可以避免頻繁建立連線的開銷。 聽上去好像很簡單,但不可忽視的是,這個過程中存在著風險點,最常見的就是網路斷連 或阻塞。如果網路斷連,主從庫之間就無法進行命令傳播了,從庫的資料自然也就沒辦法 和主庫保持一致了,客戶端就可能從從庫讀到舊資料。 接下來,我們就來聊聊網路斷連後的解決辦法。

主從庫間網路斷了怎麼辦?

在 Redis 2.8 之前,如果主從庫在命令傳播時出現了網路閃斷,那麼,從庫就會和主庫重 新進行一次全量複製,開銷非常大。 從 Redis 2.8 開始,網路斷了之後,主從庫會採用增量複製的方式繼續同步。聽名字大概 就可以猜到它和全量複製的不同:全量複製是同步所有資料,而增量複製只會把主從庫網 絡斷連期間主庫收到的命令,同步給從庫。 那麼,增量複製時,主從庫之間具體是怎麼保持同步的呢?這裡的奧妙就在於 repl_backlog_buffer 這個緩衝區。我們先來看下它是如何用於增量命令的同步的。 當主從庫斷連後,主庫會把斷連期間收到的寫操作命令,寫入 replication buffer,同時也 會把這些操作命令也寫入 repl_backlog_buffer 這個緩衝區。 repl_backlog_buffer 是一個環形緩衝區,主庫會記錄自己寫到的位置,從庫則會記錄自己 已經讀到的位置。 剛開始的時候,主庫和從庫的寫讀位置在一起,這算是它們的起始位置。隨著主庫不斷接 收新的寫操作,它在緩衝區中的寫位置會逐步偏離起始位置,我們通常用偏移量來衡量這 個偏移距離的大小,對主庫來說,對應的偏移量就是 master_repl_offset。主庫接收的新 寫操作越多,這個值就會越大。 同樣,從庫在複製完寫操作命令後,它在緩衝區中的讀位置也開始逐步偏移剛才的起始位 置,此時,從庫已複製的偏移量 slave_repl_offset 也在不斷增加。正常情況下,這兩個偏 移量基本相等。 主從庫的連線恢復之後,從庫首先會給主庫傳送 psync 命令,並把自己當前的 slave_repl_offset 發給主庫,主庫會判斷自己的 master_repl_offset 和 slave_repl_offset 之間的差距。 在網路斷連階段,主庫可能會收到新的寫操作命令,所以,一般來說,master_repl_offset 會大於 slave_repl_offset。此時,主庫只用把 master_repl_offset 和 slave_repl_offset 之間的命令操作同步給從庫就行。 就像剛剛示意圖的中間部分,主庫和從庫之間相差了 put d e 和 put d f 兩個操作,在增量 複製時,主庫只需要把它們同步給從庫,就行了。 說到這裡,我們再借助一張圖,回顧下增量複製的流程。  不過,有一個地方我要強調一下,因為 repl_backlog_buffer 是一個環形緩衝區,所以在 緩衝區寫滿後,主庫會繼續寫入,此時,就會覆蓋掉之前寫入的操作。如果從庫的讀取速 度比較慢,就有可能導致從庫還未讀取的操作被主庫新寫的操作覆蓋了,這會導致主從庫 間的資料不一致。 因此,我們要想辦法避免這一情況,一般而言,我們可以調整 repl_backlog_size 這個參 數。這個引數和所需的緩衝空間大小有關。緩衝空間的計算公式是:緩衝空間大小 = 主庫 寫入命令速度 * 操作大小 - 主從庫間網路傳輸命令速度 * 操作大小。在實際應用中,考慮 到可能存在一些突發的請求壓力,我們通常需要把這個緩衝空間擴大一倍,即 repl_backlog_size = 緩衝空間大小 * 2,這也就是 repl_backlog_size 的最終值。 舉個例子,如果主庫每秒寫入 2000 個操作,每個操作的大小為 2KB,網路每秒能傳輸 1000 個操作,那麼,有 1000 個操作需要緩衝起來,這就至少需要 2MB 的緩衝空間。否 則,新寫的命令就會覆蓋掉舊操作了。為了應對可能的突發壓力,我們最終把 repl_backlog_size 設為 4MB。 這樣一來,增量複製時主從庫的資料不一致風險就降低了。不過,如果併發請求量非常 大,連兩倍的緩衝空間都存不下新操作請求的話,此時,主從庫資料仍然可能不一致。 針對這種情況,一方面,你可以根據 Redis 所在伺服器的記憶體資源再適當增加 repl_backlog_size 值,比如說設定成緩衝空間大小的 4 倍,另一方面,你可以考慮使用切 片叢集來分擔單個主庫的請求壓力。關於切片叢集,我會在第 9 講具體介紹。

小結

這節課,我們一起學習了 Redis 的主從庫同步的基本原理,總結來說,有三種模式:全量 複製、基於長連線的命令傳播,以及增量複製。 全量複製雖然耗時,但是對於從庫來說,如果是第一次同步,全量複製是無法避免的,所 以,我給你一個小建議:一個 Redis 例項的資料庫不要太大,一個例項大小在幾 GB 級別 比較合適,這樣可以減少 RDB 檔案生成、傳輸和重新載入的開銷。另外,為了避免多個從 庫同時和主庫進行全量複製,給主庫過大的同步壓力,我們也可以採用“主 - 從 - 從”這 一級聯模式,來緩解主庫的壓力。 長連線複製是主從庫正常執行後的常規同步階段。在這個階段中,主從庫之間通過命令傳 播實現同步。不過,這期間如果遇到了網路斷連,增量複製就派上用場了。我特別建議你 留意一下 repl_backlog_size 這個配置引數。如果它配置得過小,在增量複製階段,可能 會導致從庫的複製進度趕不上主庫,進而導致從庫重新進行全量複製。所以,通過調大這 個引數,可以減少從庫在網路斷連時全量複製的風險。 不過,主從庫模式使用讀寫分離雖然避免了同時寫多個例項帶來的資料不一致問題,但是 還面臨主庫故障的潛在風險。主庫故障了從庫該怎麼辦,資料還能保持一致嗎,Redis 還 能正常提供服務嗎?在接下來的兩節課裡,我會和你具體聊聊主庫故障後,保證服務可靠 性的解決方案。   按照慣例,我給你提一個小問題。這節課,我提到,主從庫間的資料複製同步使用的是 RDB 檔案,前面我們學習過,AOF 記錄的操作命令更全,相比於 RDB 丟失的資料更少。 那麼,為什麼主從庫間的複製不使用 AOF 呢? 

1、RDB檔案內容是經過壓縮的二進位制資料(不同資料型別資料做了針對性優化),檔案很小。而AOF檔案記錄的是每一次寫操作的命令,寫操作越多檔案會變得很大,其中還包括很多對同一個key的多次冗餘操作。在主從全量資料同步時,傳輸RDB檔案可以儘量降低對主庫機器網路頻寬的消耗,從庫在載入RDB檔案時,一是檔案小,讀取整個檔案的速度會很快,二是因為RDB檔案儲存的都是二進位制資料,從庫直接按照RDB協議解析還原資料即可,速度會非常快,而AOF需要依次重放每個寫命令,這個過程會經歷冗長的處理邏輯,恢復速度相比RDB會慢得多,所以使用RDB進行主從全量同步的成本最低。

2、假設要使用AOF做全量同步,意味著必須開啟AOF功能,開啟AOF就要選擇檔案刷盤的策略,選擇不當會嚴重影響Redis效能。而RDB只有在需要定時備份和主從全量同步資料時才會觸發生成一次快照。而在很多丟失資料不敏感的業務場景,其實是不需要開啟AOF的。