Redis系列3:高可用之主從架構

2022-07-12 18:04:10

Redis系列1:深刻理解高效能Redis的本質
Redis系列2:資料持久化提高可用性

1 主從複製介紹

上一篇《Redis系列2:資料持久化提高可用性》中,我們介紹了Redis中的資料持久化技術,包括 RDB快照 和 AOF紀錄檔 。有了這兩個利器,我們再也不用擔心機器宕機,資料丟失了。
但是持久化技術只是解決了Redis服務故障之後,快速資料恢復的問題。並沒有從根本上提升Redis的可用性,我們需要的是保障Redis的高可用,減少甚至避免Redis服務發生宕機的可能。

目前實現Redis高可用的模式主要有三種: 主從模式、哨兵模式、叢集模式。這些我們後面會分成三篇介紹,今天我們先來聊一下主從模式。
Redis 提供的主從模式,是通過複製的方式,將主伺服器上的Redis的資料同步複製一份到從 Redis 伺服器,這種做法很常見,MySQL的主從也是這麼做的。
主節點的Redis我們稱之為master,從節點的Redis我們稱之為slave,主從複製為單向複製,只能由主到從,不能由從到主。可以有多個從節點,比如1主3從甚至n從,從節點的多少根據實際的業務需求來判斷。

2 主從資料一致性保證

為了保證主伺服器Redis的資料和從伺服器Redis的資料的一致性,也為了分擔存取壓力,均衡負載,應用層面一般採取讀寫分離的模式。
讀操作:主、從庫都可以執行,一般是在從庫上讀資料,對實時性和準確性有100%高真要求的部分業務,可以謹慎評估之後讀主庫;
寫操作:只在主庫上寫資料,寫完之後將寫操作指令同步到從庫
如下圖:

2.1 為何採用讀寫分離模式?

讀寫分離模式的使用跟MySQL做讀寫分離的初衷是一樣的。因為我們已經劃分了主從庫,而且從庫的資料是由主庫單向複製的。如果主從庫都可以執行寫指令,那麼在高頻並行場景下對不同的副本資料做修改,操作會具有無序性,極易導致各副本產生資料不一致,這是分散式模式的弊病。 如果非要保證資料的強一致性,Redis 需要加鎖處理,或者使用佇列順序執行,這樣勢必降低Redis的效能,降低服務的吞吐能力,這就不是高效能Redis所能接受的。

2.2 主從複製還有其他作用麼?

  • 故障隔離和恢復:無論主節點或者從節點宕機,其他節點依然可以保證服務的正常執行,並可以手動切換主從。
  • 讀寫隔離:Master 節點提供寫服務,Slave 節點提供讀服務,分攤流量壓力,均衡流量的負載。
  • 提供高可用保障:主從模式是高可用的最基礎版本,也是哨兵模式和 cluster模式實施的前置條件。

3 搭建主從複製

主從複製的開啟,完全是在從節點設定和發起的,不需要我們在主節點做任何事情。
可以通過 replicaof(Redis 5.0 之前使用 slaveof)命令形成主庫和從庫的關係。在從節點開啟主從複製,有 3 種方式:

說明:masterip:主機IP,masterport:主機埠號

3.1 組態檔方式

在從伺服器的組態檔中加入

replicaof <masterip> <masterport>

3.2 啟動命令方式

redis-server 啟動命令後面加入

--replicaof <masterip> <masterport>

3.3 通過在使用者端使用命令

啟動多個 Redis 範例後,直接通過使用者端執行命令:

replicaof <masterip> <masterport>

則該 Redis 範例成為從節點。

假設現在有主範例 (192.168.0.1:6379)、從範例 A(192.168.0.2:6380)和 從範例 B (192.168.0.3:6381),在從範例上分別執行以下命令,就成為了Slave,主範例成為 Master。

# redis 5.0之前
slaveof 192.168.0.1 6379

# redis 5.0之後
replicaof 192.168.0.1 6379

4 主從複製原理

主從庫模式開啟之後,應用層面採用讀寫分離,所有資料的寫操作只會在主庫上進行,而讀操作基本會在從庫上面進行(特殊情況下部分讀業務允許走主庫)。
主從會保持最終一致性:主庫有了資料更新之後,會立即同步給從庫,來保證主從庫的資料的一致的。

4.1 主從庫的同步步驟

那主從庫同步是如何完成的呢?一次性傳輸麼,那樣資料會不會太大?分批傳遞麼,那樣時效性會不會有問題?故障時候資料會不會丟失?重新連線之後中間產生的差額資料怎麼補充才能保證一致性?帶著這些疑問我們繼續來分析下。

綜合上面的問題來看同步,會有三種重要場景:

  • 首次設定完成主從庫之後的全量複製
  • 主從正常執行期間,準實時同步
  • 主從庫間網路斷開重連,Append增量資料 + 準實時同步

4.1.1 主從庫第一次全量複製

主從庫第一次複製過程大體可以分為 3 個階段:準備階段(即建立連線準備)、主庫同步資料到從庫階段、傳送同步期間增量指令到從庫的階段。
我們來看這張完整的流轉圖,從整體上有個認識。

4.1.1.1 建立連線

這個階段的主要作用是建立主從之間的連線,連線成立之後,才能夠做資料全量同步。主要包含如下步驟:

  • 從節點的組態檔中的 replicaof 設定項中設定了主節點的 IP 和 port ,設定完成之後,從節點就知道要跟哪個主節點進行連線。
  • 當連線成功之後,從庫開啟replicaof 操作,同時傳送psync指令告訴主庫,我準備開始同步了。命令包含了主庫的 runID 和 複製進度 offset 兩個引數。
    • runID:每個 Redis 範例啟動都會自動生成一個 唯一標識 ID,第一次主從複製,還不知道主庫 runID,所以引數會預設設定為:?。
    • offset:因為第一次複製,沒有偏移量,所以預設設定為 -1,這樣就預設從第1條指令開始複製。
  • 主庫收到 psync 命令後根據引數啟動複製,使用 FULLRESYNC 響應命令,同時帶上兩個引數:主庫 runID 和主庫目前的複製進度 offset,返回給從庫。
  • 從庫收到響應後,記錄下這兩個引數。

4.1.1.2 主庫同步資料給從庫

第二階段

master 執行 bgsave命令生成 RDB 檔案,並將檔案傳送給從庫,從庫收到 RDB 檔案後儲存到磁碟,清空當前Redis庫中的資料,再將 RDB 檔案資料載入到記憶體中。
同時主庫為每一個 slave 開闢一塊 replication buffer 緩衝區記錄,用於記錄主庫生成 RDB 檔案後那段時間(那段時間的產生的寫命令沒有被記錄到RDB檔案中,但是主庫又會源源不斷的接收到新的請求指令,記錄緩衝區是為了保證資料不丟失)產生的所有寫指令。

4.1.1.3 傳送新寫命令到從庫

第三階段

從第二階段我們可以知道,生成 RDB 檔案之後,後續的操作指令並沒有被記錄,為了保證Redis主從庫資料的一致性,主庫會在記憶體中建立 replication buffer ,記錄 RDB 檔案生成後的所有操作指令。
從庫在接收完RDB主資料,先清空當前從庫資料,然後完成資料初始化。整個初始化工作完成之後,繼續執行從replication buffer 緩衝區傳送過來的資料,避免資料斷層。
★ 主資料同步到從庫的過程中,主庫不會被阻塞,可以正常處理其他任意操作,這也是Redis保證高效能的必備條件。

replication buffer 緩衝區建立在 master 主庫上,存放的資料是下面三個時間內 master 資料的所有寫操作。

  • master 執行 bgsave 生產 rdb 的期間的寫操作;
  • master 傳輸 rdb 檔案到 slave 期間的寫操作;
  • slave 載入 rdb 檔案將資料初始化到記憶體期間的寫操作。

三個步驟完成了Redis主從的全量複製。這邊需要注意的是,Redis中的通訊,無論是主庫跟從庫之間,還是與使用者端之間的資料互動。本質上都是通過分配記憶體buffer來進行的,Master 會先把資料寫到 buffer 中,再通過網路傳送出去,從而完成資料互動。
RDB 檔案作為二進位制檔案,無論是網路傳輸還是寫入時的磁碟IO,效率都要比 AOF 高很多。同樣的,從庫進行資料恢復的時候,效率也會高一些。所以我們會選擇RDB檔案做同步而不是AOF模式。

4.1.2 增量複製

4.1.2.1 主從網路斷開之後的同步方式

高版本的Redis,在網路斷開之後或者從範例服務故障恢復之後,主從庫會採用增量複製的方式繼續同步,而不是全量同步的模式,這樣會大大降低開銷,提升效率。
增量複製: 就是指網路中斷或者從庫重啟等情況後的複製,只將中斷期間主節點執行的寫命令傳送給從節點,與全量複製相比更加高效。

repl_backlog_buffer

主從庫重新連線之後可以實現增量複製。關鍵就在 repl_backlog_buffer 緩衝區 上面。
因為 master 會將寫指令操作記錄在 repl_backlog_buffer 緩衝區中,並使用 master_repl_offset 記錄master寫入的位置偏移量,slave 則使用 slave_repl_offset 記錄讀的偏移量。master 新增寫操作的時候,偏移量則會增加。從庫持續執行同步的寫指令後,slave_repl_offset 也會不斷增加。一般情況下,這兩個偏移量會保持同步,如下圖左。
但是網路斷開或者從庫故障期間,主範例Redis一般會收到新的寫操作命令,但從範例則暫停執行,所以 master_repl_offset 會大於 slave_repl_offset。如下圖右。

需要注意的是, repl_backlog_buffer 並不是如圖中顯示的貌似無限佇列的模式,而是一個類似環形陣列,如果陣列內容滿了,就會從頭開始覆蓋前面的內容,因為給到的記憶體空間是有限的。

在主從之間重新連線之後,slave 會先傳送 psync 命令給 master,同時將自己的 {runID,slave_repl_offset} 兩個引數傳送給 master。master 只需要把 master_repl_offsetslave_repl_offset 之間的命令同步給從庫即可。增量複製的流程類似如下:

在設定repl_backlog_buffer 的時候,需要綜合考慮各種因素,太大了會導致增量執行週期比較長,還不如RDB全量覆蓋。太小了,有可能從庫還沒讀取到就被 Master 的新寫操作覆蓋了,那樣也只能執行全量複製。
所以我們需要給出一個合理 緩衝區Size。一般有如下的計算公式共參考:

repl_backlog_buffer_size = seconds * write_size_per_second

seconds:正常情況下從庫斷開,到重連主庫所需的平均時間,秒為單位。
write_size_per_second:主庫平均每秒產生的寫命令資料量大小。
如主伺服器大約每秒產生 0.5 MB 的寫指令資料,而斷開到重連一般需要30s,那麼緩衝區的大小就是 0.5 * 30s = 15 MB。
但是我們一般會保留一點buffer,比如 預留 0.5 倍,那就是 : 1.5 * 15 MB = 22.5 MB

4.1.2.2 基於長連線的命令傳播

上面的工作都是為了完成完整複製,那在完成全量複製之後,主從開始進入正常有序的同步了,具體應該怎麼做呢?

主從完成全量複製之後,他們之間需要保持連線。當主庫收到操作指令的時候,通過這個連線同步給從庫,這個過程稱之為 基於長連線的命令傳播
為了保證傳播的有效性和穩定性,從節點採用心跳機制進行偵測,傳送命令:PING 和 REPLCONF ACK。

  • 主->從:PING

每隔指定的時間(比如 1 分鐘,可設定),主節點會向從節點傳送 PING 命令,偵測從節點有無超時來判斷從節點的健康情況。

  • 從->主:REPLCONF ACK

命令執行傳播的階段,從伺服器預設會以每秒一次的頻率,向主伺服器傳送命令,將複製的偏移量傳送過去。

REPLCONF ACK <replication_offset>

replication_offset 的屬性指的是當前從範例伺服器的複製偏移量。

從範例傳送 REPLCONF ACK 命令對於主要範例,主要有以下作用:

  1. 檢測主從伺服器的網路通路是否正常。
  2. 輔助實現 min-slaves 選項,使用Redis的 min-slaves-to-write(少於n個從範例時,拒絕執行寫命令) 和 min-slaves-max-lag(主從延遲大於等於n秒時,拒絕執行寫命令)兩個選項可以防止主伺服器在不安全的情況下執行寫命令。
  3. 檢測命令丟失, 從節點傳送了 slave_replication_offset,主節點會對比 master_replication_offset ,如果不一致,說明從節點資料缺失,主節點會從 repl_backlog_buffer緩衝區中找到並推播缺失的資料。

4.1.2.3 如何確定執行全量同步還是部分同步?

從節點可以傳送 psync 命令給主節點請求同步資料,主節點判斷從節點的當前狀態,看看具體同步是採用全量複製還是部分複製。核心的地方就是psync的引數,這個我們前面也已經提到過了:

下面我們來拆解下步驟:

  1. 從節點根據自身狀態,傳送 psync命令給 master:
  • 如果從範例從未執行過 replicaof ,則從節點傳送 psync ? -1,代表全量,從 -1 處開始複製。
  • 如果從節點之前執行過 replicaof,則取當前範例中記錄下 runID和 offset,執行命令 psync <runID> <offset>, runID 是主節點 runID,offset 複製偏移量。
  1. 主節點根據接收到的psync命令及當前伺服器狀態,決定執行全量複製還是部分複製:
  • 對比主、從節點的 runID 一致,且從節點傳送 slave_repl_offset 之後的資料在 repl_backlog_buffer緩衝區中均存在(佇列是環形的,有可能被擦除重寫了),則回覆 CONTINUE,代表以追加模式進行部分複製。
  • runID 與從節點傳送的 runID 不同,或者從節點傳送的 slave_repl_offset 之後的資料已不在主節點的 repl_backlog_buffer緩衝區中 (因為佇列是環形的,所以等待時間太長或者有斷連的情況,有可能被擦除重寫了),則回覆從節點 FULLRESYNC <runid> <offset>,表示要進行全量複製,同時記下主節點的 runID 和offset。

4.2 1主n從同步的理解

從上面的內容,我們得到以下兩點:

  • 多個從庫情況下,每個從庫都會記錄自己的 slave_repl_offset,各自複製的進度也不相同。
  • 重連主庫進行恢復時,從庫會通過 psync 命令將 slave_repl_offset 告知主庫,主庫判斷從庫的狀態,來決定進行增量複製,還是全量複製。
  • replication buffer 和 repl_backlog 的說明

replication buffer 是主從庫在進行全量複製時,主庫上用於和從庫連線的使用者端的 buffer,而 repl_backlog_buffer 是為了支援從庫增量複製,主庫上用於持續儲存寫操作的一塊專用 buffer,所有從庫共用的。
主庫和從庫會各自記錄自己的複製進度,所以,不同的從庫在進行恢復時,需要將自己的複製進度(slave_repl_offset)發給主庫,主庫才可以按照偏移量取資料跟它同步。

如圖所示:

5 總結

  • 主從複製的作用一個是為分擔讀寫壓力,均衡負載,另一個是為了保證部分範例宕機之後服務的持續可用性,所以Redis演變出主從架構和讀寫分離。
  • 主從複製的步驟包括:建立連線的階段、資料同步的階段、基於長連線的命令傳播階段。
  • 資料同步可以分為全量複製和部分複製,全量複製一般為第一次全量或者長時間主從連線斷開。
  • 命令傳播階段主從節點之間有 PING(主到從的的探測) 和 REPLCONF ACK(從到主的ack應答) 命令,這種互相確認心跳的模式保證資料同步的穩定性。
  • 主從模式是比較低階的可用性優化,要做到故障自動轉移,異常預警,高保活,還需要更為複雜的哨兵或者叢集模式,這個後面我們會有專門的文章進行介紹。