Redis 學習筆記(五)高可用之主從模式

Ethan_Wong發表於2022-02-11

上一節提到了 Redis 的永續性,也就是在伺服器例項當機或故障時,擁有再恢復的能力。但是在這個伺服器例項當機恢復期間,是無法接受新的資料請求。對於整體服務而言這是無法容忍的,因此我們可以使用多個伺服器例項,在一個例項當機中斷時,另外的伺服器例項可以繼續對外提供服務,從而不中斷業務。Redis 是如何做的呢?Redis 做法是增加冗餘副本將一份資料同時儲存在多個例項上。那麼如何儲存各個例項之間的資料一致性呢?Redis 採用主從庫讀寫分離模式來保證資料副本的一致性。

一、主從複製介紹

在 Redis 中提供複製的伺服器稱為主伺服器(master),被主伺服器進行復制的伺服器稱為從伺服器(slave )。但是在複製方式上有所區別,主從庫之間採用的是讀寫分離的方式:

  • 讀操作:主庫和從庫都可接收
  • 寫操作:寫操作先在主庫中執行,然後再將操作同步給從庫

為何要採用讀寫分離,因為如果在不同例項上執行修改操作,要保證例項之間的一致性就必須加鎖、例項間的協商等操作,會帶來鉅額的開銷。如果採用讀寫分離,資料的修改遷移到主庫上進行,然後再同步到從庫上,就可以達到不使用鎖達到資料一致性的效果。

二、主從複製原理

主從複製是如何進行復制的,是一次性全部複製,還是分批一批一批的複製?而且如果複製中網路中斷,資料還能保持一致性嗎,其內部原理是怎樣的?Redis 的複製功能主要有兩個操作:

  • 同步(sync):同步操作是將從庫的資料狀態更新至主庫當前所處的狀態,主要有全量複製和增量複製兩種

  • 命令傳播(command propagate):命令傳播是作用在主庫的資料庫狀態被修改後,主從庫之間的資料庫狀態出現不一致,讓主從伺服器的資料庫狀態重新回到一致性。

2.1 同步操作

2.1.1 全量複製

在同步操作中,我們首先來看看全量複製,顧名思義也就是主庫將所有的資料傳送給從庫。因為主從庫初始化需要傳輸全部資料,所以第一次同步其實就是一次全量複製。當啟動多個 Redis 例項時,主從庫間就可以通過 replicaof(Redis 5.0 前使用的是 slaveof)命令形成主從庫的關係,在這期間從庫就會全量複製當前主庫的資料庫狀態。主要分為三個階段:

  1. 主從庫建立連線:主要是為全量複製做準備,在這一步,從庫和主庫建立起連線,告知主庫進行資料同步,待主庫確認回覆後,主從庫之間可以開始同步。主要的操作是從庫給主庫傳送 psync 命令,主庫根據這個命令引數來啟動複製。而 psync 命令包含了主庫的 runID 和 複製進度 offset兩個引數:

    • runID: 是每個 Redis 例項啟動時隨機生成的 ID,它是用來唯一標記這個例項。而當主從庫第一次複製時,從庫不知道主庫的 runID 。因此將 runID 設定為 “?”。
    • offset:第一次設定為 -1,表示第一次複製

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

  2. 主庫將所有資料同步給從庫:資料同步給從庫後,從庫接收到資料後,在本地完成資料載入。這個過程主要依賴於記憶體快照生成的 RDB 檔案。

    內部具體的流程是,主庫先執行 bgsave 命令,執行持久化生成 RDB 檔案,並將該檔案傳送給從庫。從庫在接收到 RDB 檔案後,會先清空當前資料庫,然後再載入 RDB 檔案。

    在主庫同步從庫過程中,我們知道 bgsave命令系統會 fork 一個子執行緒來建立 RDB 檔案,另外的主執行緒可以繼續處理命令請求。而在同步過程中的新增的寫操作,主庫會在記憶體中用專門的 replication buffer 來記錄這些寫操作。

  3. 主庫將新接收的寫操作再傳送給從庫:當主庫完成 RDB 檔案傳送後,就會將 replication buffer 中的寫操作傳送給從庫,從庫再執行這些寫操作。到此完成所有的運算元據同步。

結合一個例項來展示三個階段:

現有兩個例項,例項1(ip: 172.16.19.3)和例項 2(ip: 172.16.19.5)。在從例項2上執行以下命令後,例項2 就變成了例項 1 的從例項。執行以下命令後,例項2 就變成了例項1 的從庫,並開始複製資料:

replicaof 172.16.19.3 6379

2.1.2 主從從模式複製

在一次全量複製中,對於主庫而言需要完成兩個消耗資源的操作:傳送 RDB 檔案和 repl buffer 檔案。所以如果請求的從庫數量較多時,雖然主庫會 fork 子執行緒進行生成 RDB 檔案,但是從庫請求數量過多,也會導致主執行緒 fork 操作過多,最終也會阻塞主執行緒的其他正常請求。所以,為了解決這個問題,我們可以通過 “主-從-從”模式來將主庫生成 RDB 和傳輸 RDB 的壓力,以級聯的方式分散到從庫上。如下圖:

具體操作是: 在部署主從叢集的時候,手動選擇一個從庫(比如選擇記憶體資源配置較高的從庫)用於級聯其他的從庫。然後可以再選擇一些從庫(如三分之一的從庫),在這些從庫上執行下面命令,讓它們和剛才所選的從庫建立起主從關係:

replicaof 所選從庫的IP 6379

這樣,這些級聯的從庫不用和主庫進行互動,而只需要和連線的從庫進行寫操作同步即可。這樣就可以減輕主庫的壓力了。

2.1.3 增量複製

在主從庫完成第一次的全量複製後,它們會形成一個網路連線,主庫會通過這個連線將後續收到的命令操作再同步給從庫。這個過程也稱作為基於長連線的命令傳播。這個長連線可以避免頻繁的建立連線開銷,後續我們會再提命令傳播。

那麼為什麼需要增量複製呢,因為連線過程中存在著網路連線和阻塞,如果網路連線中斷,主從庫之間就無法實現命令傳播。那如果再次進行全量複製,其開銷就有點得不償失。所以新設計出了增量複製,而與全量複製不相同,增量複製只會把主從庫網路斷聯期間主庫收到的命令同步給從庫。其中重點就是利用repl_backlog_buffer 緩衝區,上面我們知道,在全量複製時,主庫會把寫操作命令寫入 replication buffer ,與此同時,也會把這些操作命令寫入 repl_backlog_buffer 緩衝區中(repl_backlog_buffer 是一個環形緩衝區,主庫會記錄自己寫到的位置,從庫則會記錄自己已經讀到的位置)。如下圖:

主庫在緩衝區的寫位置偏移量就是 master_repl_offset,從庫的讀位置偏移量是 slave_repl_offset。正常情況下兩個偏移量基本相等。

接下來我們看看網路連線斷開時,主庫有可能會收到新的寫操作命令,一般而言,master_repl_offset會大於 slave_repl_offset。所以當主從庫網路連線後,主庫只需要將 master_repl_offsetslave_repl_offset 中間的命令操作同步給從庫。

如上圖中的 repl_backlog_buffer示意圖,主庫和從庫之間相差了put d eput d f 兩個操作,在增量複製時,主庫只需要將它們同步給從庫即可。

我們再來看一下增量複製的流程:

repl_backlog_buffer 是一個環形緩衝區,所以存在這樣的情況:在緩衝區寫滿後,主庫會繼續寫入,這樣就會覆蓋之前的寫操作,那麼這有可能就會導致主從庫之間的資料不一致。此時,我們可以調整 repl_backlog_size 這個引數,實際情況下可以根據應用調整 repl_backlog_size 的大小。

2.2 命令傳播

在上述初始化全量複製結束後,主從庫兩者的資料庫狀態達到一致完成了同步,後面兩者則處於長連線狀態。此時主庫只需要一直將自己執行的寫命令傳送給從庫,從庫一直接收並執行主庫發來的寫命令即可保證主從庫的資料一致性了。這個時候主從庫會互相成為對方的客戶端。

2.2.1 主庫的命令傳播

當完成了同步之後,主從庫就會進入命令傳播階段,這時主庫只要一直將自己執行的寫命令傳送給從庫,而從庫只要一 直接收並執行主庫發來的寫命令,就可以保證主從庫保持資料操作一致性。

2.2.2 從庫的心跳檢測

在該階段,從庫預設會以每秒一次的頻率向主庫傳送命令:

REPLCONF ACK <replication_offset>
  • replication_offset 是從庫當前的複製偏移量

  • REPLCONF ACK 命令的作用有

    • 檢測主從庫的網路連線狀態:當主庫超過一秒鐘未收到從庫發來的 REPLCONF ACK 命令,那麼主庫就會知道主從庫之間連線出現問題。

    • 輔助實現 min-slaves 選項:Redis的min-slaves-to-writemin-slaves-max-lag兩個選項可以防止主庫在不安全的情況下執行寫命令。

      • 舉個例子,如果我們向主庫提供以下設定:

        min-slaves-to-write 3 
        min-slaves-max-lag 10
        

        那麼在從庫的數量少於3個,或者三個從庫的延遲(lag) 值都大於或等於10秒時,主庫將拒絕執行寫命令,這裡的延遲值就是上面提到的INFO replication命令的lag值。

    • 檢測命令丟失: 如果因為網路故障,主庫傳播給從庫的寫命令在半路丟 失,那麼當從庫向主庫傳送REPLCONF ACK命令時,主庫將發覺從庫當前的複製偏移量少於自己的複製偏移量,然後主庫就會根據從庫提交的複製偏移量,在複製積壓緩衝區(repl_backlog)裡面找到從庫缺少的資料,並將這些資料重新傳送給從庫。

總結就是在傳播命令階段,主庫通過向從庫傳播命令來更新從庫的狀態,保持主從庫一致。而從庫則通過向主庫傳送命令來進行心跳檢測,以及命令丟失檢測。

三、主從複製的面試題

3.1 AOF 記錄的操作命令更全,相比 RDB 丟失的資料更少,那麼為什麼主從庫間的複製不使用 AOF ?

  • RDB 檔案是以壓縮二進位制的方式儲存,檔案小,所以在從庫載入 RDB 檔案時,速度會很快。而 AOF 需要依次重放每個寫命令,過程相對 RDB 檔案的方式要慢的多。
  • 如果使用AOF 做全量複製,開啟 AOF 功能需要選擇檔案刷盤的方式,選擇不當會嚴重影響 Redis 的效能。而 RDB 持久化方式只需要定時備份和主從全量複製資料時才會觸發生成一次快照。在大多數丟失資料不敏感的業務場景,其實是不需要開啟 AOF 的。

3.2 如何處理讀寫分離中的問題

Redis 的讀寫分離可以實現 Redis 的讀負載均衡,能夠提高 Redis 伺服器的併發量,但是在使用 Redis 讀寫分離時,也需要注意延遲不一致、資料過期問題。

3.2.1 延遲與不一致問題

對於命令傳播階段:因為命令傳播階段是非同步操作,所以延遲與資料的不一致無法避免。有以下解決方式:

  • 若應用對資料不一致的接受程度較低,可以優化中從節點之間的網路環境、使用叢集同時擴充套件寫負載和讀負載、監控主從節點延遲(offset)判斷,若從節點延遲過大,則通知應用不再通過該從節點讀取資料

對於非命令傳播的其他階段,可以對 slave-serve-stale-date 設定為 no 。則從節點只能響應 info、slaveof 等少數命令,可以保證對資料的一致性。

3.2.2 資料過期問題

資料過期問題已經在Redis 的鍵管理 中提到過,在單機 Redis 中存在惰性刪除和定期刪除兩種刪除策略。而在主從複製場景下,從庫不會主動刪除資料,主要通過主庫控制從庫中過期資料的刪除。而主庫的刪除策略都不能保證主庫及時對過期資料執行刪除操作,所以當客戶端通過 Redis 從庫讀取資料時很容易讀取到已經過期的資料。

3.2.3 故障切換問題

在沒有使用哨兵的讀寫分離場景下,建議寫監控程式進行切換讀寫分別連線的 Redis 節點。針對於手動進行切換的方式更復雜但是不容易出錯。

總結

在使用讀寫分離前,可以考慮其他方法增加Redis的讀負載能力:如儘量優化主節點(減少慢查詢、減少持久化等其他情況帶來的阻塞等)提高負載能力;使用Redis叢集同時提高讀負載能力和寫負載能力等。如果使用讀寫分離,可以使用哨兵,使主從節點的故障切換儘可能自動化,並減少對應用程式的侵入。

參考資料

《Redis 設計與實現》

《Redis 開發與運維》

https://pdai.tech/md/db/nosql-redis/db-redis-x-copy.html

https://time.geekbang.org/column/article/272852

https://kaiwu.lagou.com/course/courseInfo.htm?courseId=59#/detail/pc?id=1782

相關文章