Redis的同步機制
前言
在Redis中,使用者可以通過執行SLAVEOF命令或者設定slaveof選項,讓一個伺服器去複製另一個伺服器,我們稱被複制的伺服器為主伺服器(master),而對主伺服器進行復制的伺服器則被稱為從伺服器(slave)。
1 舊版複製功能的實現
Redis的複製功能分為同步(sync )和命令傳播( command propagate )兩個操作:
- 同步操作用於將從伺服器的資料庫狀態更新至主伺服器當前所處的資料庫狀態。
- 命令傳播操作則用於在主伺服器的資料庫狀態被修改,導致主從伺服器的資料庫狀態出現不一致時,讓主從伺服器的資料庫重新回到一致狀態。本節接下來將對同步和命令傳播兩個操作進行詳細的介紹。
1.1 同步
當客戶端向從伺服器傳送SLAVEOF命令,要求從伺服器複製主伺服器時,從伺服器首先需要執行同步操作,也即是,將從伺服器的資料庫狀態更新至主伺服器當前所處的資料庫狀態。
從伺服器對主伺服器的同步操作需要通過向主伺服器傳送SYNC命令來完成,以下是SYNC命令的執行步驟:
- 從伺服器向主伺服器傳送SYNC命令。
- 收到SYNC命令的主伺服器執行BGSAVE命令,在後臺生成一個RDB檔案,並使用一個緩衝區記錄從現在開始執行的所有寫命令。
- 當主伺服器的BGSAVE命令執行完畢時,主伺服器會將BGSAVE命令生成的RDB檔案傳送給從伺服器,從伺服器接收並載人這個RDB檔案,將自己的資料庫狀態更新至主伺服器執行BGSAVE命令時的資料庫狀態。
- 主伺服器將記錄在緩衝區裡面的所有寫命令傳送給從伺服器,從伺服器執行這些寫命令,將自己的資料庫狀態更新至主伺服器資料庫當前所處的狀態。
1.2 命令傳播
在同步操作執行完畢之後,主從伺服器兩者的資料庫將達到一致狀態,但這種一致並不是一層不變的,每當主伺服器執行客服端傳送的寫命令時,主伺服器的資料庫就有可能會被修改,並導致主從伺服器狀態不再一致。
為了讓主從伺服器再次回到一致狀態,主伺服器需要對從伺服器執行命令傳播操作:主伺服器會將自己執行的寫命令,也即是造成主從伺服器不一致的那條寫命令,傳送給從伺服器執,行,當從伺服器執行了相同的寫命令之後,主從伺服器將再次回到一致狀態。
1.3 舊版複製功能的缺陷
在Redis 2.8以前,從伺服器對主伺服器的複製可以分為以下兩種情況:
- 初次複製:從伺服器以前沒有複製過任何主伺服器,或者從伺服器當前要複製的主伺服器和上一次複製的主伺服器不同。
- 斷線後重複製:處於命令傳播階段的主從伺服器因為網路原因而中斷了複製,但從伺服器通過自動重連線重新連上了主伺服器,並繼續複製主伺服器。
對於初次複製來說,舊版複製功能能夠很好地完成任務,但對於斷線後重複製來說,舊版複製功能雖然也能讓主從伺服器重新回到一致狀態,但效率卻非常低,從伺服器需要讓主伺服器將所有執行的寫命令的RDB檔案,從新發送給從伺服器。但主從伺服器斷開的時間可能很短,主伺服器在斷線期間執行的寫命令可能很少,而執行少量寫命令所產生的資料量通常比整個資料庫的資料量要少得多,在這種情況下,為了讓從伺服器補足一小部分缺失的資料,卻要讓主從伺服器重新執行一次SYNC命令,這種做法無疑是非常低效的。
SYNC命令是一個非常耗費資源的操作
每次執行SYNC命令,主從伺服器需要執行以下動作:
- 主伺服器需要執行BGSAVE命令來生成RDB檔案,這個生成操作會耗費主伺服器大量的CPU、記憶體和磁碟I/O資源。
- 主伺服器需要將自己生成的RDB檔案傳送給從伺服器,這個傳送操作會耗費主從伺服器大量的網路資源(頻寬和流量),並對主伺服器響應命令請求的時間產生影響。
- 接收到RDB檔案的從伺服器需要載入主伺服器發來的RDB檔案,並且在載入期間,從伺服器會因為阻塞而沒辦法處理命令請求。
因為SYNC命令是一個如此耗費資源的操作,所以Redis有必要保證在真正有需要時才執行SYNC命令。
2 新版複製功能的實現
為了解決舊版複製功能在處理斷線重複制情況時的低效問題,,Redis從2.8版本開始,使用PSYNC命令代替SYNC命令來執行復制時的同步操作。
PSYNC命令具有完整重同步( full resynchronization)和部分重同步( partial resynchronization)兩種模式,
- 其中完整重同步用於處理初次複製情況:完整重同步的執行步驟和SYNC命令的執行步驟基本一樣,它們都是通過讓主伺服器建立併發送RDB檔案,以及向從伺服器傳送儲存在緩衝區裡面的寫命令來進行同步。
- 而部分重同步則用於處理斷線後重複製情況:當從伺服器在斷線後重新連線主伺服器時,如果條件允許,主伺服器可以將主從伺服器連線斷開期間執行的寫命令傳送給從伺服器,從伺服器只要接收並執行這些寫命令,就可以將資料庫更新至主伺服器當前所處的狀態。
PSYNC命令的部分重同步模式解決了舊版複製功能在處理斷線後重複製時出現的低效情況。
2.1 部分重同步的實現
在瞭解了PSYNC命令的由來,以及部分重同步的工作方式之後,是時候來介紹一下部分重同步的實現細節了。
部分重同步功能由以下三個部分構成:
- 主伺服器的複製偏移量( replication offset )和從伺服器的複製偏移量。
- 主伺服器的複製積壓緩衝區(replication backlog)。
- 伺服器的執行ID (run ID )。
以下三個小節將分別介紹這三個部分。
2.2 複製偏移量
執行復制的雙方——主伺服器和從伺服器會分別維護一個複製偏移量:
- 主伺服器每次向從伺服器傳播N個位元組的資料時,就將自己的複製偏移量的值加上N
- 從伺服器每次收到主伺服器傳播來的N個位元組的資料時,就將自己的複製偏移量的值加上N。
比如主伺服器和從伺服器(A,B,C)的複製偏移量的值都為10086。
如果這時主伺服器向三個從伺服器傳播長度為33位元組的資料,那麼主伺服器的複製偏移量將更新為10086+33-10119,而三個從伺服器在接收到主伺服器傳播的資料之後,也會將複製偏移量更新為10119 。
通過對比主從伺服器的複製偏移量,程式可以很容易地知道主從伺服器是否處於一致狀態:
- 如果主從伺服器處於一致狀態,那麼主從伺服器兩者的偏移量總是相同的。
- 相反,如果主從伺服器兩者的偏移量並不相同,那麼說明主從伺服器並未處於一致狀態。
考慮以下這個例子:假設主從伺服器當前的複製偏移量都為10086,但是就在主伺服器要向從伺服器傳播長度為33位元組的資料之前,從伺服器A斷線了,那麼主伺服器傳播的資料將只有從伺服器B和從伺服器C能收到,在這之後,主伺服器、從伺服器B和從伺服器C三個伺服器的複製偏移量都將更新為10119,而斷線的從伺服器A的複製偏移量仍然停留在10086,這說明從伺服器A與主伺服器並不一致 。
假設從伺服器A在斷線之後就立即重新連線主伺服器,並且成功,那麼接下來,從伺服器將向主伺服器傳送PSYNC命令,報告從伺服器A當前的複製偏移量為10086,那麼這時,主伺服器應該對從伺服器執行完整重同步還是部分重同步呢?如果執行部分重同步的話,主伺服器又如何補償從伺服器A在斷線期間丟失的那部分資料呢?以上問題的答案都和複製積壓緩衝區有關。
2.3 複製積壓緩衝區
複製積壓緩衝區是由主伺服器維護的一個固定長度(fixed-size )先進先出( FIFO )佇列,預設大小為1MB。
當主伺服器進行命令傳播時,它不僅會將寫命令傳送給所有從伺服器 還會將寫命令人 隊到複製積壓緩衝區裡面 。
因此,主伺服器的複製積壓緩衝區裡面會儲存著一部分最近傳播的寫命令,並且複製積壓緩衝區會為佇列中的每個位元組記錄相應的複製偏移量 。
當從伺服器重新連上主伺服器時,從伺服器會通過PSYNC命令將自己的複製偏移量offset傳送給主伺服器,主伺服器會根據這個複製偏移量來決定對從伺服器執行何種同步操作:
- 如果offset偏移量之後的資料(也即是偏移量offset+1開始的資料)仍然存在於複製積壓緩衝區裡面、那麼主伺服器將對從伺服器執行部分重同步操作。
- 相反,如果offset偏移量之後的資料已經不存在於複製積壓緩衝區,那麼主伺服器將對從伺服器執行完整重同步操作。
2.4 伺服器執行ID
除了複製偏移量和複製積壓穿衝區之外,實現部分重同步還需要用到伺服器執行ID:
- 每個Redis伺服器,不論主伺服器還是從服務,都會有自己的執行ID。
- 執行ID在伺服器啟動時自動生成,由40個隨機的十六進位制字元組成。
當從伺服器對主伺服器進行初次複製時,主伺服器會將自己的執行ID傳送給從伺服器,而從伺服器則會將這個執行ID儲存起來。
當從伺服器斷線並重新連上一個主伺服器時,從伺服器將向當前連線的主伺服器傳送之1前儲存的執行ID:
- 如果從伺服器儲存的執行ID和當前連線的主伺服器的執行ID相同,那麼說明從伺服器斷線之前複製的就是當前連線的這個主伺服器,主伺服器可以繼續嘗試執行部分重同步操作。
- 相反地,如果從伺服器儲存的執行ID和當前連線的主伺服器的執行ID並不相同,那麼說明從伺服器斷線之前複製的主伺服器並不是當前連線的這個主伺服器,主伺服器將對從伺服器執行完整重同步操作。
3 參考資料
黃健巨集著《Redis設計與實現》第15章