redis主從複製機制講解

轉自:https://www.cnblogs.com/lukexwang/p/4711977.html

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

假設現在有兩個Redis服務器,地址分別爲127.0.0.1:6379和127.0.0.1:12345,如果我們向服務器127.0.0.1:12345發送以下命令:

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命令,要求從服務器複製主服務器時,從服務器首先需要執行同步操作,也即是,將從服務器的數據庫狀態更新至主服務器當前所處的數據庫狀態。

從服務器對主服務器的同步操作需要通過向主服務器發送SYNC命令來完成,以下是SYNC命令的執行步驟:

從服務器向主服務器發送SYNC命令;
收到SYNC命令的主服務器執行BGSAVE命令,在後臺生成一個RDB文件,並使用一個緩衝區記錄從現在開始執行的所有寫命令;
當主服務器的BGSAVE命令執行完畢時,主服務器會將BGSAVE命令生成的RDB文件發送給從服務器,從服務器接收並載入這個RDB文件,將自己的數據庫狀態更新至主服務器執行BGSAVE命令時的數據庫狀態。
主服務器將記錄在緩衝區裏面的所有寫命令發送給從服務器,從服務器執行這些寫命令,將自己的數據庫狀態更新至主服務器數據庫當前所處的狀態。
命令傳播
在執行完同步操作之後,主從服務器之間數據庫狀態已經相同了。但這個狀態並非一成不變,如果主服務器執行了寫操作,那麼主服務器的數據庫狀態就會修改,並導致主從服務器狀態不再一致。

所以爲了讓主從服務器再次回到一致狀態,主服務器需要對從服務器執行命令傳播操作:主服務器會將自己執行的寫命令,也即是造成主從服務器不一致的那條寫命令,發送給從服務器執行,當從服務器執行了相同的寫命令之後,主從服務器將再次回到一致狀態。

舊版複製功能的缺陷
在Redis中,從服務器對主服務器的複製可以分爲以下兩種情況:

初次複製:從服務器以前沒有複製過任何主服務器,或者從服務器當前要複製的主服務器和上一次複製的主服務器不同;
斷線後重複製:處於命令傳播階段的主從服務器因爲網絡原因而中斷了複製,但從服務器通過自動重連接重新連上了主服務器,並繼續複製主服務器。
對於初次複製來說,舊版複製功能能夠很好地完成任務,但對於斷線後重複製來說,舊版複製功能雖然也能讓主從服務器重新回到一致狀態,但效率卻非常低。

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

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

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

SYNC命令是一個非常耗費資源的操作
SYNC命令是非常消耗資源的,因爲每次執行SYNC命令,主從服務器需要執行一下操作:

主服務器需要執行BGSAVE命令來生成RDB文件,這個生成操作會耗費主服務器大量的CPU、內存和磁盤I/O資源;
主服務器需要將自己生成的RDB文件發送給從服務器,這個發送操作會耗費主從服務器大量的網絡資源(帶寬和流量),並對主服務器響應命令請求的時間產生影響;
接收到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。
爲了安全起見,可以將複製積壓緩衝區的大小設爲2secondwrite_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-write和min-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丟失了。也無所謂,主服務器馬上就知道了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章