redis學習筆記——主從同步(複製)

在Redis中,用戶能夠經過執行SLAVEOF命令或者設置slaveof選項,讓一個服務器去複製(replicate)另外一個服務器,咱們稱呼被複制的服務器爲主服務器(master),而對主服務器進行復制的服務器則被稱爲從服務器(slave),如圖所示。html

假設如今有兩個Redis服務器,地址分別爲127.0.0.1:6379和127.0.0.1:12345,若是咱們向服務器127.0.0.1:12345發送如下命令:redis

127.0.0.1:12345> SLAVEOF 127.0.0.1 6379
OK數據庫

那麼服務器127.0.0.1:12345將成爲127.0.0.1:6379的從服務器,而服務器127.0.0.1:6379則會成爲127.0.0.1:12345的主服務器。安全

(記得去http://redisdoc.com/topic/replication.html上將一些操做進行補充)服務器

本文是按照《Redis設計與實現》一書所整理的,感受原書講的很是棒,因此下面的這部分的知識將按照原書的邏輯進行介紹:網絡

先介紹舊版複製功能在處理斷線後從新鏈接的從服務器時,會趕上怎樣的低效狀況。新版複製功能是如何經過部分重同步來解決舊版複製功能的低效問題的,並說明部分重同步的實現原理。併發

舊版複製功能的實現

Redis的複製功能分爲同步(sync)和命令傳播(command propagate)兩個操做:異步

  • 同步操做用於將從服務器的數據庫狀態更新至主服務器當前所處的數據庫狀態;
  • 命令傳播操做則用於在主服務器的數據庫狀態被修改,致使主從服務器的數據庫狀態出現不一致時,讓主從服務器的數據庫從新回到一致狀態。

同步

當客戶端向從服務器發送SLAVEOF命令,要求從服務器複製主服務器時,從服務器首先須要執行同步操做,也便是,將從服務器的數據庫狀態更新至主服務器當前所處的數據庫狀態。ui

從服務器對主服務器的同步操做須要經過向主服務器發送SYNC命令來完成,如下是SYNC命令的執行步驟:spa

  1. 從服務器向主服務器發送SYNC命令;
  2. 收到SYNC命令的主服務器執行BGSAVE命令,在後臺生成一個RDB文件,並使用一個緩衝區記錄從如今開始執行的全部寫命令;
  3. 當主服務器的BGSAVE命令執行完畢時,主服務器會將BGSAVE命令生成的RDB文件發送給從服務器,從服務器接收並載入這個RDB文件,將本身的數據庫狀態更新至主服務器執行BGSAVE命令時的數據庫狀態
  4. 主服務器將記錄在緩衝區裏面的全部寫命令發送給從服務器,從服務器執行這些寫命令,將本身的數據庫狀態更新至主服務器數據庫當前所處的狀態。

命令傳播

在執行完同步操做以後,主從服務器之間數據庫狀態已經相同了。但這個狀態並不是一成不變,若是主服務器執行了寫操做,那麼主服務器的數據庫狀態就會修改,並致使主從服務器狀態再也不一致。

因此爲了讓主從服務器再次回到一致狀態,主服務器須要對從服務器執行命令傳播操做:主服務器會將本身執行的寫命令,也便是形成主從服務器不一致的那條寫命令,發送給從服務器執行,當從服務器執行了相同的寫命令以後,主從服務器將再次回到一致狀態。

舊版複製功能的缺陷

在Redis中,從服務器對主服務器的複製能夠分爲如下兩種狀況:

  • 初次複製:從服務器之前沒有複製過任何主服務器,或者從服務器當前要複製的主服務器和上一次複製的主服務器不一樣;
  • 斷線後重複製:處於命令傳播階段的主從服務器由於網絡緣由而中斷了複製,但從服務器經過自動重鏈接從新連上了主服務器,並繼續複製主服務器。

對於初次複製來講,舊版複製功能可以很好地完成任務,但對於斷線後重複製來講,舊版複製功能雖然也能讓主從服務器從新回到一致狀態,但效率卻很是低。

咱們給出一個例子進行說明:

從服務器終於從新鏈接上主服務器,由於這時主從服務器的狀態已經再也不一致,因此從服務器將向主服務器發送SYNC命令,而主服務器會將包含鍵k1至鍵k10089的RDB文件發送給從服務器,從服務器經過接收和載入這個RDB文件來將本身的數據庫更新至主服務器數據庫當前所處的狀態。

上面給出的例子可能有一點理想化,由於在主從服務器斷線期間,主服務器執行的寫命令可能會有成百上千個之多,而不只僅是兩三個寫命令。但總的來講,主從服務器斷開的時間越短,主服務器在斷線期間執行的寫命令就越少,而執行少許寫命令所產生的數據量一般比整個數據庫的數據量要少得多,在這種狀況下,爲了讓從服務器補足一小部分缺失的數據,卻要讓主從服務器從新執行一次SYNC命令,這種作法無疑是很是低效的。

SYNC命令是一個很是耗費資源的操做

SYNC命令是很是消耗資源的,由於每次執行SYNC命令,主從服務器須要執行一下操做:

  1. 主服務器須要執行BGSAVE命令來生成RDB文件,這個生成操做會耗費主服務器大量的CPU、內存和磁盤I/O資源;
  2. 主服務器須要將本身生成的RDB文件發送給從服務器,這個發送操做會耗費主從服務器大量的網絡資源(帶寬和流量),並對主服務器響應命令請求的時間產生影響;
  3. 接收到RDB文件的從服務器須要載入主服務器發來的RDB文件,而且在載入期間,從服務器會由於阻塞而沒辦法處理命令請求

SYNC是一個如此消耗資源的命令,因此Redis最好在真須要的時候才須要執行SYNC命令。

新版複製功能的實現

爲了解決舊版複製功能在處理斷線重複制狀況時的低效問題,Redis從2.8版本開始,使用PSYNC命令代替SYNC命令來執行復制時的同步操做。

PSYNC命令具備完整重同步(full resynchronization)和部分重同步(partial resynchronization)兩種模式:

  • 其中完整重同步用於處理初次複製狀況:完整重同步的執行步驟和SYNC命令的執行步驟基本同樣,它們都是經過讓主服務器建立併發送RDB文件,以及向從服務器發送保存在緩衝區裏面的寫命令來進行同步;
  • 而部分重同步則用於處理斷線後重複製狀況:當從服務器在斷線後從新鏈接主服務器時,若是條件容許,主服務器能夠將主從服務器鏈接斷開期間執行的寫命令發送給從服務器,從服務器只要接收並執行這些寫命令,就能夠將數據庫更新至主服務器當前所處的狀態。

咱們如今試舉一例來看看使用PSYNC處理斷線後狀況:

下圖展現了主從服務器在執行部分重同步時的通訊過程。

其實看到這裏的時候內心仍是有一個疑問的:若是上面的例子是T3時候從服務器掉線,而後在T10093的時候才鏈接上或者更長的時間呢!!!你這樣一條指令一條指令地傳輸過去還不如直接來一個SYNC命令快一些。因此在我看來使用PSYNC進行操做時,何時部分重同步,何時所有重同步是一個策略問題。固然Redis會解決這個問題,因此你們繼續看0_0

部分重同步的實現

部分重同步功能由如下三個部分構成:

  • 主服務器的複製偏移量(replication offset)和從服務器的複製偏移量;
  • 主服務器的複製積壓緩衝區(replication backlog)
  • 服務器的運行ID(run ID)。

複製偏移量

執行復制的雙方——主服務器和從服務器會分別維護一個複製偏移量:

  • 主服務器每次向從服務器傳播N個字節的數據時,就將本身的複製偏移量的值加上N;
  • 從服務器每次收到主服務器傳播來的N個字節的數據時,就將本身的複製偏移量的值加上N;

(我靠!!難道從服務器沒有反饋嗎?丟包了怎麼辦?難道是用TCP?你們繼續看,我只是想穿插一些個人思路)

經過對比主從服務器的複製偏移量,程序能夠很容易地知道主從服務器是否處於一致狀態:

  • 若是主從服務器處於一致狀態,那麼主從服務器二者的偏移量老是相同的;
  • 相反,若是主從服務器二者的偏移量並不相同,那麼說明主從服務器並未處於一致狀態。

以下面的狀況:

假設從服務器A在斷線以後就當即從新鏈接主服務器,而且成功,那麼接下來,從服務器將向主服務器發送PSYNC命令,報告從服務器A當前的複製偏移量爲10086,那麼這時,主服務器應該對從服務器執行完整重同步仍是部分重同步呢?若是執行部分重同步的話,主服務器又如何補償從服務器A在斷線期間丟失的那部分數據呢?以上問題的答案都和複製積壓緩衝區有關。

複製積壓緩衝區

複製積壓緩衝區是由主服務器維護的一個固定長度(fixed-size)先進先出(FIFO)隊列,默認大小爲1MB。

和普通先進先出隊列隨着元素的增長和減小而動態調整長度不一樣,固定長度先進先出隊列的長度是固定的,當入隊元素的數量大於隊列長度時,最早入隊的元素會被彈出,而新元素會被放入隊列。

當主服務器進行命令傳播時,它不只會將寫命令發送給全部從服務器,還會將寫命令入隊到複製積壓緩衝區裏面,如圖所示。

所以,主服務器的複製積壓緩衝區裏面會保存着一部分最近傳播的寫命令,而且複製積壓緩衝區會爲隊列中的每一個字節記錄相應的複製偏移量,就像下表所示的那樣。

當從服務器從新連上主服務器時,從服務器會經過PSYNC命令將本身的複製偏移量offset發送給主服務器,主服務器會根據這個複製偏移量來決定對從服務器執行何種同步操做:

  • 若是offset偏移量以後的數據(也便是偏移量offset+1開始的數據)仍然存在於複製積壓緩衝區裏面,那麼主服務器將對從服務器執行部分重同步操做;
  • 相反,若是offset偏移量以後的數據已經不存在於複製積壓緩衝區,那麼主服務器將對從服務器執行完整重同步操做。

根據須要調整複製積壓緩衝區的大小

Redis爲複製積壓緩衝區設置的默認大小爲1MB,若是主服務器須要執行大量寫命令,又或者主從服務器斷線後重鏈接所需的時間比較長,那麼這個大小也許並不合適。若是複製積壓緩衝區的大小設置得不恰當,那麼PSYNC命令的複製重同步模式就不能正常發揮做用,所以,正確估算和設置複製積壓緩衝區的大小很是重要。
複製積壓緩衝區的最小大小能夠根據公式second*write_size_per_second來估算:

  • 其中second爲從服務器斷線後從新鏈接上主服務器所需的平均時間(以秒計算);
  • 而write_size_per_second則是主服務器平均每秒產生的寫命令數據量(協議格式的寫命令的長度總和);

例如,若是主服務器平均每秒產生1 MB的寫數據,而從服務器斷線以後平均要5秒才能從新鏈接上主服務器,那麼複製積壓緩衝區的大小就不能低於5MB。
爲了安全起見,能夠將復制積壓緩衝區的大小設爲2*second*write_size_per_second,這樣能夠保證絕大部分斷線狀況都能用部分重同步來處理。
至於複製積壓緩衝區大小的修改方法,能夠參考配置文件中關於repl-backlog-size選項的說明。

服務器運行ID

除了複製偏移量和複製積壓緩衝區以外,實現部分重同步還須要用到服務器運行ID(run ID):

  • 每一個Redis服務器,不論主服務器仍是從服務,都會有本身的運行ID
  • 運行ID在服務器啓動時自動生成,由40個隨機的十六進制字符組成,例如53b9b28df8042fdc9ab5e3fcbbbabff1d5dce2b3;

當從服務器對主服務器進行初次複製時,主服務器會將本身的運行ID傳送給從服務器,而從服務器則會將這個運行ID保存起來(注意哦,是從服務器保存了主服務器的ID)。

當從服務器斷線並從新連上一個主服務器時,從服務器將向當前鏈接的主服務器發送以前保存的運行ID:

  • 若是從服務器保存的運行ID和當前鏈接的主服務器的運行ID相同,那麼說明從服務器斷線以前複製的就是當前鏈接的這個主服務器,主服務器能夠繼續嘗試執行部分重同步操做;
  • 相反地,若是從服務器保存的運行ID和當前鏈接的主服務器的運行ID並不相同,那麼說明從服務器斷線以前複製的主服務器並非當前鏈接的這個主服務器,主服務器將對從服務器執行完整重同步操做。

PSYNC命令的實現

PSYNC命令的調用方法有兩種:

  • 若是從服務器之前沒有複製過任何主服務器,或者以前執行過SLAVEOF no one命令,那麼從服務器在開始一次新的複製時將向主服務器發送PSYNC ? -1命令,主動請求主服務器進行完整重同步(由於這時不可能執行部分重同步);
  • 相反地,若是從服務器已經複製過某個主服務器,那麼從服務器在開始一次新的複製時將向主服務器發送PSYNC <runid> <offset>命令:其中runid是上一次複製的主服務器的運行ID,而offset則是從服務器當前的複製偏移量,接收到這個命令的主服務器會經過這兩個參數來判斷應該對從服務器執行哪一種同步操做。

根據狀況,接收到PSYNC命令的主服務器會向從服務器返回如下三種回覆的其中一種:

  • 若是主服務器返回+FULLRESYNC <runid> <offset>回覆,那麼表示主服務器將與從服務器執行完整重同步操做:其中runid是這個主服務器的運行ID,從服務器會將這個ID保存起來,在下一次發送PSYNC命令時使用;而offset則是主服務器當前的複製偏移量,從服務器會將這個值做爲本身的初始化偏移量;
  • 若是主服務器返回+CONTINUE回覆,那麼表示主服務器將與從服務器執行部分重同步操做,從服務器只要等着主服務器將本身缺乏的那部分數據發送過來就能夠了;
  • 若是主服務器返回-ERR回覆,那麼表示主服務器的版本低於Redis 2.8,它識別不了PSYNC命令,從服務器將向主服務器發送SYNC命令,並與主服務器執行完整同步操做。

 複製的實現

步驟1:設置主服務器的地址和端口

當客戶端向從服務器發送如下命令時:

127.0.0.1:12345> SLAVEOF 127.0.0.1 6379
OK

從服務器首先要作的就是將客戶端給定的主服務器IP地址127.0.0.1以及端口6379保存到服務器狀態的masterhost屬性和masterport屬性裏面:

struct redisServer {
    // ...
    // 主服務器的地址
    char *masterhost;
    // 主服務器的端口
    int masterport;
    // ...
};

SLAVEOF命令是一個異步命令,在完成masterhost屬性和masterport屬性的設置工做以後,從服務器將向發送SLAVEOF命令的客戶端返回OK,表示複製指令已經被接收,而實際的複製工做將在OK返回以後才真正開始執行。

步驟2:創建套接字鏈接

在SLAVEOF命令執行以後,從服務器將根據命令所設置的IP地址和端口,建立連向主服務器的套接字鏈接,如圖15-14所示。

若是從服務器建立的套接字能成功鏈接(connect)到主服務器,那麼從服務器將爲這個套接字關聯一個專門用於處理複製工做的文件事件處理器,這個處理器將負責執行後續的複製工做,好比接收RDB文件,以及接收主服務器傳播來的寫命令,諸如此類。

而主服務器在接受(accept)從服務器的套接字鏈接以後,將爲該套接字建立相應的客戶端狀態,並將從服務器看做是一個鏈接到主服務器的客戶端來對待,這時從服務器將同時具備服務器(server)和客戶端(client)兩個身份:從服務器能夠向主服務器發送命令請求,而主服務器則會向從服務器返回命令回覆。

步驟3:發送PING命令

從服務器成爲主服務器的客戶端以後,作的第一件事就是向主服務器發送一個PING命令。

這個PING命令主要是爲了:

  • 經過發送PING命令檢查套接字的讀寫狀態;
  • 經過PING命令能夠檢查主服務器可否正常處理命令。

從服務器在發送PING命令以後可能遇到如下三種狀況:

  • 主服務器向從服務器返回了一個命令回覆,但從服務器卻不能在規定的時限內讀取命令回覆的內容(timeout),說明網絡鏈接狀態不佳,從服務器將斷開並從新建立連向主服務器的套接字;
  • 若是主服務器返回一個錯誤,那麼表示主服務器暫時沒有辦法處理從服務器的命令請求,,從服務器也將斷開並從新建立連向主服務器的套接字;
  • 若是從服務器讀取到"PONG"回覆,那麼表示主從服務器之間的網絡鏈接狀態正常,那就繼續執行下面的複製步驟。

步驟4:身份驗證

從服務器在收到主服務器返回的"PONG"回覆以後,下一步要作的就是決定是否進行身份驗證:

  • 若是從服務器設置了masterauth選項,那麼進行身份驗證。不然不進行身份認證;

在須要進行身份驗證的狀況下,從服務器將向主服務器發送一條AUTH命令,命令的參數爲從服務器masterauth選項的值

從服務器在身份驗證階段可能遇到的狀況有如下幾種:

  • 主服務器沒有設置requirepass選項,從服務器沒有設置masterauth,那麼就繼續後面的複製工做;
  • 若是從服務器的經過AUTH命令發送的密碼和主服務器requirepass選項所設置的密碼相同,那麼也繼續後面的工做,不然返回錯誤invaild password;
  • 若是主服務器設置了requireoass選項,但從服務器沒有設置masterauth選項,那麼服務器將返回NOAUTH錯誤。反過來若是主服務器沒有設置requirepass選項,可是從服務器卻設置了materauth選項,那麼主服務器返回no password is set錯誤;

全部錯誤到只有一個結果:停止目前的複製工做,並從建立套接字開始從新執行復制,直到身份驗證經過,或者從服務器放棄執行復製爲止。

步驟5:發送端口信息

身份驗證步驟以後,從服務器將執行命令REPLCONF listening-port <port-number>,向主服務器發送從服務器的監聽端口號。

主服務器在接收到這個命令以後,會將端口號記錄在從服務器所對應的客戶端狀態的slave_listening_port屬性中:

typedef struct redisClient {
    // ...
    // 從服務器的監聽端口號
    int slave_listening_port;
    // ...

}redisClient;

slave_listening_port屬性目前惟一的做用就是在主服務器執行INFO replication命令時打印出從服務器的端口號。

步驟6:同步

在這一步,從服務器將向主服務器發送PSYNC命令,執行同步操做,並將本身的數據庫更新至主服務器數據庫當前所處的狀態。

須要注意的是在執行同步操做前,只有從服務器是主服務器的客戶端。可是執行從不操做以後,主服務器也會稱爲從服務器的客戶端:

  • 若是PSYNC命令執行的是完整同步操做,那麼主服務器只有成爲了從服務器的客戶端才能將保存在緩衝區中的寫命令發送給從服務器執行;
  • 若是PSYNC命令執行的是部分同步操做,那麼主服務器只有成爲了從服務器的客戶端才能將保存在複製積壓緩衝區中的寫命令發送給從服務器執行;

步驟7:命令傳播

當完成了同步以後,主從服務器就會進入命令傳播階段,這時主服務器只要一直將本身執行的寫命令發送給從服務器,而從服務器只要一直接收並執行主服務器發來的寫命令,就能夠保證主從服務器一直保持一致了。

心跳檢測

在命令傳播階段,從服務器默認會以每秒一次的頻率,向主服務器發送命令:REPLCONF ACK <replication_offset>

其中replication_offset是從服務器當前的複製偏移量。

發送REPLCONF ACK命令對於主從服務器有三個做用:

  • 檢測主從服務器的網絡鏈接狀態;
  • 輔助實現min-slaves選項;
  • 檢測命令丟失。

檢測主從服務器的網絡鏈接狀態

若是主服務器超過一秒鐘沒有收到從服務器發來的REPLCONF ACK命令,那麼主服務器就知道主從服務器之間的鏈接出現問題了。

經過向主服務器發送INFO replication命令,在列出的從服務器列表的lag一欄中,咱們能夠看到相應從服務器最後一次向主服務器發送REPLCONF ACK命令距離如今過了多少秒:

127.0.0.1:6379> INFO replication
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=12345,state=online,offset=211,lag=0  

#剛剛發送過 REPLCONF ACK命令
slave1:ip=127.0.0.1,port=56789,state=online,offset=197,lag=15   

#15秒以前發送過REPLCONF ACK命令

master_repl_offset:211
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:2
repl_backlog_histlen:210

在通常狀況下,lag的值應該在0秒或者1秒之間跳動,若是超過1秒的話,那麼說明主從服務器之間的鏈接出現了故障。

輔助實現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 <replication_offset>就能夠知道,每發送一次這個命令從服務器都會向主服務器報告一次本身的複製偏移量。那此時儘管主服務器發送給從服務器的SET key value丟失了。也無所謂,主服務器立刻就知道了。

相關文章
相關標籤/搜索