Redis 主從複製的原理及演進

語言: CN / TW / HK

本文作者:百度基礎架構部工程師,王鈺

Redis 的主從複製經歷了多次演進,本文將從最基本的原理和實現講起,並層層遞進,逐步呈現 Redis 主從複製的演進歷史。大家將瞭解到 Redis 主從複製的原理,以及各個改進版本解決了什麼問題,並最終看清 Redis 7.0 主從複製原理的全貌。

什麼是主從複製?

在資料庫語境下,複製(replication)就是將資料從一個數據庫複製到另一個數據庫中。主從複製,是將資料庫分為主節點和從節點,主節點源源不斷地將資料複製給從節點,保證主從節點中存有相同的資料。

有了主從複製,資料可以有多份副本,這帶來了多種好處:

第一,提升資料庫系統的請求處理能力。單個節點能夠支撐的讀流量有限,部署多個節點,並構成主從關係,用主從複製保持主從節點資料一致,如此主從節點可以一起提供服務。

第二,提升整個系統的可用性。因為從節點中有主節點資料的副本,當主節點宕機後,可以立刻提升其中一個從節點為主節點,繼續提供服務。

1.png

Redis 主從複製原理

實現主從複製,直觀的思路是產生一份主節點資料的快照發送給從節點,並以此做為基準,隨後將快照時刻之後的增量資料傳送給從節點,如此就能保證主從資料的一致。總體來看,主從複製一般包含全量資料同步、增量同步兩個階段。

在 Redis 的主從複製實現中,包含兩個類似階段:全量資料同步和命令傳播。

  • 全量資料同步:主節點產生一份全量資料的快照,即 RDB 檔案,並將此快照發送給從節點。且從產生快照時刻起,記錄新接收到的寫命令。當快照發送完成後,將累積的寫命令傳送給從節點,從節點執行這些寫命令。此時基準已經建立完成,主從節點間資料已經大體一致。
  • 命令傳播:全量資料同步完成後,主節點將執行過的寫命令源源不斷地傳送給從節點,從節點執行這些命令,保證主從節點中資料有相同的變更,如此保證主從資料持續一致。

下圖中給出了 Redis 主從複製的整個過程:

2.png 1. 主從關係建立後,從節點向主節點發送一個 SYNC 命令請求進行主從同步。 1. 主節點收到 SYNC 命令後,執行 fork 建立一個子程序,子程序中將所有的資料按特定編碼儲存到 RDB(Redis Database) 檔案中,這就產生了資料庫的快照。 1. 主節點將此快照發送給從節點,從節點接收並載入快照。 1. 主節點接著將生成快照、傳送快照期間積壓的寫命令傳送給從節點,從節點接收這些命令並執行,命令執行後,從節點中的資料也就有了同樣的變更。 1. 此後,主節點源源不斷地新執行的寫命令同步到從節點,從節點執行傳播來的命令。如此,主從資料保持一致。需要說明的是,命令傳播存在時延的,所以任意時刻,不能保證主從節點間資料完全一致。

以上就是 Redis 主從複製的基本原理,很簡單很容易理解,Redis 最初就採用這種方案,但這種方案存在一些問題:fork 耗時過長,阻塞主程序執行fork 時,需要拷貝大量的記憶體頁表,這是一個耗時較多的操作,尤其當記憶體使用量較大的時候。組內同學曾做過測試,記憶體佔用 10GB 時,fork 需要消耗 100 多毫秒。fork 的時候主程序阻塞 100 多毫秒,這對 Redis 而言,實在太長了。另外fork 之後,如果主庫中有不少的寫入,那麼由於寫時複製機制,會額外消耗不少的記憶體,還會增大響應時間。主從間網路閃斷會觸發全量同步假如主從之間的網路出現了故障,連線意外斷開,主節點無法繼續傳播命令至該從節點。之後網路恢復,從節點重新連線上主節點後,主節點不能再繼續傳播新接收到的命令了,因為從節點已經漏掉了一些命令。此時,從節點需要從頭再來,再次執行全部的同步過程,而這要付出很高的代價。網路閃斷是常發生的事情,閃斷期間主節點中可能只寫入了比較少的資料,但就因為這很少的一部分資料,需要讓從節點進行一次代價高昂的全量同步。這種做法是非常低效的,該如何解決這問題呢?下一節 Redis 部分重同步給你答案。

Redis 部分重同步

網路短暫斷開後,從節點需要重新同步,這很浪費資源,很不環保。從節點為什麼需要重新同步呢?因為主從斷開期間有部分命令沒有同步到從節點上去。如果忽略這些命令繼續傳播後續的命令,則會導致資料的錯亂,因為丟失掉的命令是不能忽略的。為什麼不將那些命令儲存下來呢?這樣當從節點重新連線後,就可以將斷連期間的命令補充給它了,這樣就不需要重新全量同步了。Redis 2.8 版本後,引入了部分同步。它在主節點中維護了一個複製積壓緩衝區,命令一方面會傳播到從節點,另外還會記錄在這個緩衝區中。儲存所有的命令是不必要的,Redis 中使用了一個環形的緩衝區,這樣就可以只保留最近的一些命令了。

3.png

命令是儲存下來了,但從節點重新連線後,主節點該從什麼地方開始給從節點發送命令呢?如果能給所有命令編一個號,則從節點只需要告訴主節點自己最後收到的命令的編號,主節點就知道該從什麼位置傳送命令了。Redis 的實現中是對位元組進行編號,這個編號在 Redis 的語境中叫做複製偏移量。 有了部分同步後,主從複製的流程變成了下面這樣:

4.png

主從複製的時候不再使用SYNC命令,而是使用PSYNC,意思的Partial SYNC,部分同步。PSYNC的語法如下:

`PSYNC <master id> <replication offset>`

命令中的兩個引數,一個是主節點的編號,一個是複製偏移量。每個Redis節點都有一個 40 位元組的編號,PSYNC 命令中攜帶的編號是期望進行同步的主節點的編號。複製偏移量則表示當前從節點想要從什麼地方開始部分同步。如果是第一次進行主從複製,自然是不知道主節點的編號,複製偏移量也無意義,此時使用 PSYNC ? -1 來進行全量同步。另外,如果從節點指定的複製偏移量不在主節點的複製積壓緩衝區的範圍內,部分同步會失敗,會轉向全量同步。有了部分同步,網路閃斷後就可以避免全量同步了。但是因為主節點只能保留最近的部分命令,儲存多少取決於複製積壓緩衝區的大小。如果從節點斷開時間過長,或者斷開期間主節點新執行的寫命令足夠多,漏掉的命令就無法全部儲存到複製積壓緩衝區中了。加大複製積壓緩衝區可以儘可能多地避免全量同步,但這同時會造成額外的記憶體消耗。部分同步消耗了部分記憶體來儲存最近執行的寫命令,避免閃斷後的全同步,這是很直觀、很容易想象的解決方案。這種方案很好,是否還存在其他問題呢?考慮以下問題:假如從節點重啟了怎麼辦?部分同步依賴主節點的編號和複製偏移量,從節點在初次同步的時候會獲取到主節點的編號,並在之後的同步中不斷調整複製偏移量,這些資訊都儲存在記憶體中。當從節點意外重啟後,儘管本地存有 RDB 或 AOF 檔案,還是需要進行一次全量同步。但實際上完全可以載入本地資料,並執行部分同步即可。假如主從切換了怎麼辦?假如主節點意外宕機,外圍監控元件執行了主從切換。此時其他從節點對應的主節點就變化了,從節點中記錄的主節點編號就匹配不上新的主節點了,此時會進行一次全量同步。但實際上所有的從節點在主從切換之前同步進度應該是差不多的,而且新提升的從節點包含的資料應該最全,切主後所有從節點都執行一次全量同步,這實在不合理。

5.png 以上問題如何解決,請繼續往後看。

同源增量同步 從節點重啟後丟失了原主節點編號和複製偏移量,這導致重啟後需要全量同步,這很好辦,把這些資訊存下來就可以了。主從切換後,主節點資訊變化了,導致從節點需要全量同步,這也容易解決,只需能確認新主節點上的資料是從原主節點複製來的,那就可以繼續從新的主節點上進行復制。Redis 4.0 以後,對 PSYNC 進行了改進,提出了同源增量複製的解決方案,該方案解決了前面提到的兩個問題。從節點重啟後,需要跟主節點全量同步,這本質上是因為從節點丟失了主節點的編號資訊,在 Redis 4.0 後,主節點的編號資訊被寫入到 RDB 中持久化儲存。切主後,從節點需要和新主節點全量同步,本質原因是新的主節點不認原主節點的編號。從節點發送 PSYNC <原主節點編號> <複製偏移量> 給新的主節點,如果新的主節點能夠認識 <原主節點編號>,並明白自己的資料就是從該節點複製來的。那麼新的主節點就應該清楚它和該從節點師出同門,應該接受部分同步。如何才能識別呢,只需要讓從節點在切換為主節點時,將自己之前的主節點的編號記錄下來即可。Redis 4.0 以後,主從切換後,新的主節點會將先前的主節點記錄下來,觀察 info replication 的結果,可以可以看到 master_replid 和 master_replid2 兩個編號,前者是當前主節點的編號,後者為先前主節點的編號:

```` 127.0.0.1:6379> slaveof no one OK 127.0.0.1:6379> info replication

Replication

role:master ... master_replid:b34aff08d983991b3feb4567a2ac0308984a892a master_replid2:a3f2428d31e096a99d87affa6cc787cceb6128a2 master_repl_offset:38599 second_repl_offset:38600 ... repl_backlog_histlen:5180` ```

Redis 中目前值保留了兩個主節點編號,但完全可以實現一個連結串列,將過往的主節點的編號資訊都記錄下來,這樣就可以追溯的更遠了。這樣以來,如果一個從節點斷開後,執行了多次主從切換,該從節重新連線後,依然可以識別出它們的資料是同源的。但 Redis 沒有這麼做,這是因為沒有必要,因為就算資料是同源的,但複製積壓緩衝區中儲存的資料是有限的,多次主從切換後,複製積壓緩衝區中儲存的命令已經無法滿足部分同步了。有了同源增量複製後,主節點切換後,其他從節點可以基於新的主節點繼續增量同步。此時,主從複製看起來已經不存在太大的問題了。但做 Redis 的那幫傢伙,總挖空心思想著能不能再做些優化。下面我將描述 Redis 主從複製的一些優化策略。\ 無盤全量同步和無盤載入\ Redis 執行全量複製,需要生成當前資料庫的一份快照,具體做法是執行 fork 建立子程序,子程序遍歷所有資料並編碼後寫入 RDB 檔案中。RDB 生成後,在主程序中,會讀取此檔案併發送給從節點。讀寫磁碟上的 RDB 檔案是比較耗資源的,在主程序中執行勢必會導致 Redis 的響應時間變長。因此一個優化方案是 dump 後直接將資料直接傳送資料給從節點,不需要將資料先寫入到 RDB 。Redis 6.0 中實現了這種無盤全量同步和無盤載入的策略。採用無盤全量同步,避免了對磁碟的操作,但也有缺點。一般情況下,在子程序中直接使用網路傳送資料,這比在子程序中生成 RDB 要慢,這意味著子程序需要存活的時間相對較長。子程序存在的時間越長,寫時複製造成的影響就越大,進而導致消耗的記憶體會更多。在全量複製時候,從節點一般是先接收 RDB 將其存在本地,接收完成後再載入 RDB。同樣地,從節點也可以直接載入主節點發來的資料,避免將其存入本地的 RDB 檔案中,而後再從磁碟載入。\ 共享主從複製緩衝區\ 在主節點的視角中,從節點就是一個客戶端,從節點發送了 PSYNC 命令後,主節點就要與它們完成全量同步,並不斷地把寫命令同步給從節點。Redis 的每個客戶端連線上存在一個傳送緩衝區。主節點執行了寫命令後,就會將命令內容寫入到各個連線的傳送緩衝區中。傳送緩衝區儲存的是待傳播的命令,這意味著多個傳送緩衝區中的內容其實是相同的。而且,這些命令還在複製積壓緩衝區中存了一份呢。這就造成了大量的記憶體浪費,尤其是存在很多從節點的時候。

6.png 在 Redis 7.0 中,我們團隊的同學提出並實現了共享主從複製緩衝區的方案解決了這個問題。該方案讓傳送緩衝區與複製積壓緩衝區共享,避免了資料的重複,可有效節省記憶體。\

總結

本文回顧並總結了 Redis 主從複製的演化過程,並解釋了各次演化所解決的問題。最後,描述了對 Redis 主從複製進行優化的一些策略。

下面是對全文的總結:

  1. 巨集觀來看 Redis 的主從複製分為全量同步和命令傳播兩個階段。主節點先發送快照給從節點,然後源源不斷地將命令傳播給從節點,以此保證主從資料的一致。
  2. Redis 2.8 之前的主從複製存在閃斷後需要重新全量同步的問題,Redis 2.8 引入了複製積壓緩衝區解決了這一問題。
  3. 在 Redis 4.0 中,同源增量複製的策略被提出,解決了主從切換後從節點需要全量同步的問題。至此,Redis 的主從複製整體上已經比較完善了。
  4. Redis 6.0 中,為進一步優化主從複製的效能,無盤同步和載入被提出,避免全量同步時讀寫磁碟,提高主從同步的速度。
  5. 在 Redis 7.0 rc1 中,採用了共享主從複製緩衝區的策略,降低了主從複製帶來的記憶體開銷。

希望本文能幫助大家回顧 Redis 主從複製的原理,並對其建立更加深刻的印象。