《Redis 核心篇:唯快不破的祕密》中,「碼哥」揭祕了 Redis 五大資料型別底層的資料結構、IO 模型、執行緒模型、漸進式 rehash 掌握了 Redis 快的本質原因。

接著,在《Redis 日誌篇:無畏宕機與快速恢復的殺手鐗》中揭曉了當 Redis 發生宕機可以通過重新讀取 RDB 快照和執行 AOF 日誌實現快速恢復的高可用手段。

高可用有兩個含義:一是資料儘量不丟失,二是服務儘可能提供服務。 AOF 和 RDB 保證了資料持久化儘量不丟失,而主從複製就是增加副本,一份資料儲存到多個例項上。即使有一個例項宕機,其他例項依然可以提供服務。

本篇主要帶大家全方位吃透 Redis 高可用技術解決方案之一主從複製架構

本篇硬核,建議收藏慢慢品味,我相信讀者朋友會有一個質的提升。如有錯誤還望糾正,謝謝。關注「碼哥位元組」設定「星標」第一時間接收優質文章,謝謝讀者的支援。

核心知識點

開篇寄語

問題 = 機會。遇到問題的時候,內心其實是開心的,越大的問題意味著越大的機會。

任何事情都是有代價的,有得必有失,有失必有得,所以不必計較很多東西,我們只要想清楚自己要做什麼,並且想清楚自己願意為之付出什麼代價,然後就放手去做吧!

1. 主從複製概述

65 哥:有了 RDB 和 AOF 再也不怕宕機丟失資料了,但是 Redis 例項宕機了怎麼實現高可用呢?

既然一臺宕機了無法提供服務,那多臺呢?是不是就可以解決了。Redis 提供了主從模式,通過主從複製,將資料冗餘一份複製到其他 Redis 伺服器。

前者稱為主節點 (master),後者稱為從節點 (slave);資料的複製是單向的,只能由主節點到從節點。

預設情況下,每臺 Redis 伺服器都是主節點;且一個主節點可以有多個從節點 (或沒有從節點),但一個從節點只能有一個主節點。

65 哥:主從之間的資料如何保證一致性呢?

為了保證副本資料的一致性,主從架構採用了讀寫分離的方式。

  • 讀操作:主、從庫都可以執行;
  • 寫操作:主庫先執行,之後將寫操作同步到從庫;

65 哥:為何要採用讀寫分離的方式?

我們可以假設主從庫都可以執行寫指令,假如對同一份資料分別修改了多次,每次修改傳送到不同的主從例項上,就導致是例項的副本資料不一致了。

如果為了保證資料一致,Redis 需要加鎖,協調多個例項的修改,Redis 自然不會這麼幹!

65 哥:主從複製還有其他作用麼?

  1. 故障恢復:當主節點宕機,其他節點依然可以提供服務;
  2. 負載均衡:Master 節點提供寫服務,Slave 節點提供讀服務,分擔壓力;
  3. 高可用基石:是哨兵和 cluster 實施的基礎,是高可用的基石。

2. 搭建主從複製

主從複製的開啟,完全是在從節點發起的,不需要我們在主節點做任何事情。

65 哥:怎麼搭建主從複製架構呀?

可以通過 replicaof(Redis 5.0 之前使用 slaveof)命令形成主庫和從庫的關係。

在從節點開啟主從複製,有 3 種方式:

  1. 配置檔案

    在從伺服器的配置檔案中加入 replicaof <masterip> <masterport>

  2. 啟動命令

    redis-server 啟動命令後面加入 --replicaof <masterip> <masterport>

  3. 客戶端命令

    啟動多個 Redis 例項後,直接通過客戶端執行命令:replicaof <masterip> <masterport>,則該 Redis 例項成為從節點。

比如假設現在有例項 1(172.16.88.1)、例項 2(172.16.88.2)和例項 3 (172.16.88.3),在例項 2 和例項 3 上分別執行以下命令,例項 2 和 例項 3 就成為了例項 1 的從庫,例項 1 成為 Master。

replicaof 172.16.88.1 6379

3. 主從複製原理

主從庫模式一旦採用了讀寫分離,所有資料的寫操作只會在主庫上進行,不用協調三個例項。

主庫有了最新的資料後,會同步給從庫,這樣,主從庫的資料就是一致的。

65 哥:主從庫同步是如何完成的呢?主庫資料是一次性傳給從庫,還是分批同步?正常執行中又怎麼同步呢?要是主從庫間的網路斷連了,重新連線後資料還能保持一致嗎?

65 哥你問題咋這麼多,同步分為三種情況:

  1. 第一次主從庫全量複製;
  2. 主從正常執行期間的同步;
  3. 主從庫間網路斷開重連同步。

主從庫第一次全量複製

65 哥:我好暈啊,先從主從庫間第一次同步說起吧。

主從庫第一次複製過程大體可以分為 3 個階段:連線建立階段(即準備階段)、主庫同步資料到從庫階段、傳送同步期間新寫命令到從庫階段

直接上圖,從整體上有一個全域性觀的感知,後面具體介紹。

建立連線

該階段的主要作用是在主從節點之間建立連線,為資料全量同步做好準備。從庫會和主庫建立連線,從庫執行 replicaof 併發送 psync 命令並告訴主庫即將進行同步,主庫確認回覆後,主從庫間就開始同步了

65 哥:從庫怎麼知道主庫資訊並建立連線的呢?

在從節點的配置檔案中的 replicaof 配置項中配置了主節點的 IP 和 port 後,從節點就知道自己要和那個主節點進行連線了。

從節點內部維護了兩個欄位,masterhost 和 masterport,用於儲存主節點的 IP 和 port 資訊。

從庫執行 replicaof 併發送 psync 命令,表示要執行資料同步,主庫收到命令後根據引數啟動複製。命令包含了主庫的 runID複製進度 offset 兩個引數。

  • runID:每個 Redis 例項啟動都會自動生成一個 唯一標識 ID,第一次主從複製,還不知道主庫 runID,引數設定為 「?」。
  • offset:第一次複製設定為 -1,表示第一次複製,記錄複製進度偏移量。

主庫收到 psync 命令後,會用 FULLRESYNC 響應命令帶上兩個引數:主庫 runID 和主庫目前的複製進度 offset,返回給從庫。從庫收到響應後,會記錄下這兩個引數。

FULLRESYNC 響應表示第一次複製採用的全量複製,也就是說,主庫會把當前所有的資料都複製給從庫。

主庫同步資料給從庫

第二階段

master 執行 bgsave命令生成 RDB 檔案,並將檔案傳送給從庫,同時主庫為每一個 slave 開闢一塊 replication buffer 緩衝區記錄從生成 RDB 檔案開始收到的所有寫命令。

從庫收到 RDB 檔案後儲存到磁碟,並清空當前資料庫的資料,再載入 RDB 檔案資料到記憶體中。

傳送新寫命令到從庫

第三階段

從節點載入 RDB 完成後,主節點將 replication buffer 緩衝區的資料傳送到從節點,Slave 接收並執行,從節點同步至主節點相同的狀態。

65 哥:主庫將資料同步到從庫過程中,可以正常接受請求麼?

主庫不會被阻塞,Redis 作為唯快不破的男人,怎麼會動不動就阻塞呢。

在生成 RDB 檔案之後的寫操作並沒有記錄到剛剛的 RDB 檔案中,為了保證主從庫資料的一致性,所以主庫會在記憶體中使用一個叫 replication buffer 記錄 RDB 檔案生成後的所有寫操作。

65 哥:為啥從庫收到 RDB 檔案後要清空當前資料庫?

因為從庫在通過 replcaof命令開始和主庫同步前可能儲存了其他資料,防止主從資料之間的影響。

replication buffer 到底是什麼玩意?

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

1)master 執行 bgsave 產生 RDB 的期間的寫操作;

2)master 傳送 rdb 到 slave 網路傳輸期間的寫操作;

3)slave load rdb 檔案把資料恢復到記憶體的期間的寫操作。

Redis 和客戶端通訊也好,和從庫通訊也好,Redis 都分配一個記憶體 buffer 進行資料互動,客戶端就是一個 client,從庫也是一個 client,我們每個 client 連上 Redis 後,Redis 都會分配一個專有 client buffer,所有資料互動都是通過這個 buffer 進行的。

Master 先把資料寫到這個 buffer 中,然後再通過網路傳送出去,這樣就完成了資料互動。

不管是主從在增量同步還是全量同步時,master 會為其分配一個 buffer ,只不過這個 buffer 專門用來傳播寫命令到從庫,保證主從資料一致,我們通常把它叫做 replication buffer。

replication buffer 太小會引發的問題

replication buffer 由 client-output-buffer-limit slave 設定,當這個值太小會導致主從複製連線斷開

1)當 master-slave 複製連線斷開,master 會釋放連線相關的資料。replication buffer 中的資料也就丟失了,此時主從之間重新開始複製過程。

2)還有個更嚴重的問題,主從複製連線斷開,導致主從上出現重新執行 bgsave 和 rdb 重傳操作無限迴圈。

當主節點資料量較大,或者主從節點之間網路延遲較大時,可能導致該緩衝區的大小超過了限制,此時主節點會斷開與從節點之間的連線;

這種情況可能引起全量複製 -> replication buffer 溢位導致連線中斷 -> 重連 -> 全量複製 -> replication buffer 緩衝區溢位導致連線中斷……的迴圈。

具體詳情:[top redis headaches for devops – replication buffer]

因而推薦把 replication buffer 的 hard/soft limit 設定成 512M。

config set client-output-buffer-limit "slave 536870912 536870912 0"

65 哥:主從庫複製為何不使用 AOF 呢?相比 RDB 來說,丟失的資料更少。

這個問題問的好,原因如下:

  1. RDB 檔案是二進位制檔案,網路傳輸 RDB 和寫入磁碟的 IO 效率都要比 AOF 高。
  2. 從庫進行資料恢復的時候,RDB 的恢復效率也要高於 AOF。

增量複製

65 哥:主從庫間的網路斷了咋辦?斷開後要重新全量複製麼?

在 Redis 2.8 之前,如果主從庫在命令傳播時出現了網路閃斷,那麼,從庫就會和主庫重新進行一次全量複製,開銷非常大。

從 Redis 2.8 開始,網路斷了之後,主從庫會採用增量複製的方式繼續同步。

增量複製:用於網路中斷等情況後的複製,只將中斷期間主節點執行的寫命令傳送給從節點,與全量複製相比更加高效

repl_backlog_buffer

斷開重連增量複製的實現奧祕就是 repl_backlog_buffer 緩衝區,不管在什麼時候 master 都會將寫指令操作記錄在 repl_backlog_buffer 中,因為記憶體有限, repl_backlog_buffer 是一個定長的環形陣列,如果陣列內容滿了,就會從頭開始覆蓋前面的內容

master 使用 master_repl_offset記錄自己寫到的位置偏移量,slave 則使用 slave_repl_offset記錄已經讀取到的偏移量。

master 收到寫操作,偏移量則會增加。從庫持續執行同步的寫指令後,在 repl_backlog_buffer 的已複製的偏移量 slave_repl_offset 也在不斷增加。

正常情況下,這兩個偏移量基本相等。在網路斷連階段,主庫可能會收到新的寫操作命令,所以 master_repl_offset會大於 slave_repl_offset

當主從斷開重連後,slave 會先發送 psync 命令給 master,同時將自己的 runIDslave_repl_offset傳送給 master。

master 只需要把 master_repl_offsetslave_repl_offset之間的命令同步給從庫即可。

增量複製執行流程如下圖:

65 哥:repl_backlog_buffer 太小的話從庫還沒讀取到就被 Master 的新寫操作覆蓋了咋辦?

我們要想辦法避免這個情況,一旦被覆蓋就會執行全量複製。我們可以調整 repl_backlog_size 這個引數用於控制緩衝區大小。計算公式:

repl_backlog_buffer = second * write_size_per_second
  1. second:從伺服器斷開重連主伺服器所需的平均時間;
  2. write_size_per_second:master 平均每秒產生的命令資料量大小(寫命令和資料大小總和);

例如,如果主伺服器平均每秒產生 1 MB 的寫資料,而從伺服器斷線之後平均要 5 秒才能重新連線上主伺服器,那麼複製積壓緩衝區的大小就不能低於 5 MB。

為了安全起見,可以將複製積壓緩衝區的大小設為2 * second * write_size_per_second,這樣可以保證絕大部分斷線情況都能用部分重同步來處理。

基於長連線的命令傳播

65 哥:完成全量同步後,正常執行過程如何同步呢?

當主從庫完成了全量複製,它們之間就會一直維護一個網路連線,主庫會通過這個連線將後續陸續收到的命令操作再同步給從庫,這個過程也稱為基於長連線的命令傳播,使用長連線的目的就是避免頻繁建立連線導致的開銷。

在命令傳播階段,除了傳送寫命令,主從節點還維持著心跳機制:PING 和 REPLCONF ACK。

主->從:PING

每隔指定的時間,主節點會向從節點發送 PING 命令,這個 PING 命令的作用,主要是為了讓從節點進行超時判斷。

從->主:REPLCONF ACK

在命令傳播階段,從伺服器預設會以每秒一次的頻率,向主伺服器傳送命令:

REPLCONF ACK <replication_offset>

其中 replication_offset 是從伺服器當前的複製偏移量。傳送 REPLCONF ACK 命令對於主從伺服器有三個作用:

  1. 檢測主從伺服器的網路連線狀態。
  2. 輔助實現 min-slaves 選項。
  3. 檢測命令丟失, 從節點發送了自身的 slave_replication_offset,主節點會用自己的 master_replication_offset 對比,如果從節點資料缺失,主節點會從 repl_backlog_buffer緩衝區中找到並推送缺失的資料。注意,offset 和 repl_backlog_buffer 緩衝區,不僅可以用於部分複製,也可以用於處理命令丟失等情形;區別在於前者是在斷線重連後進行的,而後者是在主從節點沒有斷線的情況下進行的。

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

在 Redis 2.8 及以後,從節點可以傳送 psync 命令請求同步資料,此時根據主從節點當前狀態的不同,同步方式可能是全量複製部分複製。本文以 Redis 2.8 及之後的版本為例。

關鍵就是 psync的執行:

  1. 從節點根據當前狀態,傳送 psync命令給 master:

    • 如果從節點從未執行過 replicaof ,則從節點發送 psync ? -1,向主節點發送全量複製請求;
    • 如果從節點之前執行過 replicaof 則傳送 psync <runID> <offset>, runID 是上次複製儲存的主節點 runID,offset 是上次複製截至時從節點儲存的複製偏移量。
  2. 主節點根據接受到的psync命令和當前伺服器狀態,決定執行全量複製還是部分複製:
    • runID 與從節點發送的 runID 相同,且從節點發送的 slave_repl_offset 之後的資料在 repl_backlog_buffer 緩衝區中都存在,則回覆 CONTINUE,表示將進行部分複製,從節點等待主節點發送其缺少的資料即可;
    • runID 與從節點發送的 runID 不同,或者從節點發送的 slave_repl_offset 之後的資料已不在主節點的 repl_backlog_buffer 緩衝區中 (在佇列中被擠出了),則回覆從節點 FULLRESYNC <runid> <offset>,表示要進行全量複製,其中 runID 表示主節點當前的 runID,offset 表示主節點當前的 offset,從節點儲存這兩個值,以備使用。

一個從庫如果和主庫斷連時間過長,造成它在主庫 repl_backlog_buffer 的 slave_repl_offset 位置上的資料已經被覆蓋掉了,此時從庫和主庫間將進行全量複製。

總結下

每個從庫會記錄自己的 slave_repl_offset,每個從庫的複製進度也不一定相同。

在和主庫重連進行恢復時,從庫會通過 psync 命令把自己記錄的 slave_repl_offset 發給主庫,主庫會根據從庫各自的複製進度,來決定這個從庫可以進行增量複製,還是全量複製。

replication buffer 和 repl_backlog

  1. replication buffer 對應於每個 slave,通過 config set client-output-buffer-limit slave 設定。
  2. repl_backlog_buffer 是一個環形緩衝區,整個 master 程序中只會存在一個,所有的 slave 公用。repl_backlog 的大小通過 repl-backlog-size 引數設定,預設大小是 1M,其大小可以根據每秒產生的命令、(master 執行 rdb bgsave) +( master 傳送 rdb 到 slave) + (slave load rdb 檔案)時間之和來估算積壓緩衝區的大小,repl-backlog-size 值不小於這兩者的乘積。

總的來說,replication buffer 是主從庫在進行全量複製時,主庫上用於和從庫連線的客戶端的 buffer,而 repl_backlog_buffer 是為了支援從庫增量複製,主庫上用於持續儲存寫操作的一塊專用 buffer。

repl_backlog_buffer 是一塊專用 buffer,在 Redis 伺服器啟動後,開始一直接收寫操作命令,這是所有從庫共享的。主庫和從庫會各自記錄自己的複製進度,所以,不同的從庫在進行恢復時,會把自己的複製進度(slave_repl_offset)發給主庫,主庫就可以和它獨立同步。

如圖所示:

4. 主從應用問題

4.1 讀寫分離的問題

資料過期問題

65 哥:主從複製的場景下,從節點會刪除過期資料麼?

這個問題問得好,為了主從節點的資料一致性,從節點不會主動刪除資料。我們知道 Redis 有兩種刪除策略:

  1. 惰性刪除:當客戶端查詢對應的資料時,Redis 判斷該資料是否過期,過期則刪除。
  2. 定期刪除:Redis 通過定時任務刪除過期資料。

65 哥:那客戶端通過從節點讀取資料會不會讀取到過期資料?

Redis 3.2 開始,通過從節點讀取資料時,先判斷資料是否已過期。如果過期則不返回客戶端,並且刪除資料。

4.2 單機記憶體大小限制

如果 Redis 單機記憶體達到 10GB,一個從節點的同步時間在幾分鐘的級別;如果從節點較多,恢復的速度會更慢。如果系統的讀負載很高,而這段時間從節點無法提供服務,會對系統造成很大的壓力。

如果資料量過大,全量複製階段主節點 fork + 儲存 RDB 檔案耗時過大,從節點長時間接收不到資料觸發超時,主從節點的資料同步同樣可能陷入全量複製->超時導致複製中斷->重連->全量複製->超時導致複製中斷……的迴圈。

此外,主節點單機記憶體除了絕對量不能太大,其佔用主機記憶體的比例也不應過大:最好只使用 50% - 65% 的記憶體,留下 30%-45% 的記憶體用於執行 bgsave 命令和建立複製緩衝區等。

總結

  1. 主從複製的作用:AOF 和 RDB 二進位制檔案保證了宕機快速恢復資料,儘可能的防止丟失資料。但是宕機後依然無法提供服務,所以便演化出主從架構、讀寫分離。
  2. 主從複製原理:連線建立階段、資料同步階段、命令傳播階段;資料同步階段又分為 全量複製和部分複製;命令傳播階段主從節點之間有 PING 和 REPLCONF ACK 命令互相進行心跳檢測。
  3. 主從複製雖然解決或緩解了資料冗餘、故障恢復、讀負載均衡等問題,但其缺陷仍很明顯:故障恢復無法自動化;寫操作無法負載均衡;儲存能力受到單機的限制;這些問題的解決,需要哨兵和叢集的幫助,我將在後面的文章中介紹,歡迎關注。

65 哥:碼哥你的圖畫的真好看,內容好,跟著你的文章我收穫了很多,我要收藏、點贊、在看和分享。讓更多的優秀開發者看到共同進步!

謝謝讀者支援,另外讀者技術群也開通了,關注公眾號回覆「加群」,群裡 N 多大廠的大佬,跟我一塊交流,共同進步!

參考資料:

[1] redis 設計與實現(黃健巨集)

[2] redis replication (http://redis.io/topics/replication)

[3] designing redis replication partial resync (http://antirez.com/news/31)

(4) Redis 核心技術與實戰(https://time.geekbang.org/column/intro/329)