在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
在執行完同步操做以後,主從服務器之間數據庫狀態已經相同了。但這個狀態並不是一成不變,若是主服務器執行了寫操做,那麼主服務器的數據庫狀態就會修改,並致使主從服務器狀態再也不一致。
因此爲了讓主從服務器再次回到一致狀態,主服務器須要對從服務器執行命令傳播操做:主服務器會將本身執行的寫命令,也便是形成主從服務器不一致的那條寫命令,發送給從服務器執行,當從服務器執行了相同的寫命令以後,主從服務器將再次回到一致狀態。
在Redis中,從服務器對主服務器的複製能夠分爲如下兩種狀況:
對於初次複製來講,舊版複製功能可以很好地完成任務,但對於斷線後重複製來講,舊版複製功能雖然也能讓主從服務器從新回到一致狀態,但效率卻很是低。
咱們給出一個例子進行說明:
從服務器終於從新鏈接上主服務器,由於這時主從服務器的狀態已經再也不一致,因此從服務器將向主服務器發送SYNC命令,而主服務器會將包含鍵k1至鍵k10089的RDB文件發送給從服務器,從服務器經過接收和載入這個RDB文件來將本身的數據庫更新至主服務器數據庫當前所處的狀態。
上面給出的例子可能有一點理想化,由於在主從服務器斷線期間,主服務器執行的寫命令可能會有成百上千個之多,而不只僅是兩三個寫命令。但總的來講,主從服務器斷開的時間越短,主服務器在斷線期間執行的寫命令就越少,而執行少許寫命令所產生的數據量一般比整個數據庫的數據量要少得多,在這種狀況下,爲了讓從服務器補足一小部分缺失的數據,卻要讓主從服務器從新執行一次SYNC命令,這種作法無疑是很是低效的。
SYNC命令是很是消耗資源的,由於每次執行SYNC命令,主從服務器須要執行一下操做:
SYNC是一個如此消耗資源的命令,因此Redis最好在真須要的時候才須要執行SYNC命令。
爲了解決舊版複製功能在處理斷線重複制狀況時的低效問題,Redis從2.8版本開始,使用PSYNC命令代替SYNC命令來執行復制時的同步操做。
PSYNC命令具備完整重同步(full resynchronization)和部分重同步(partial resynchronization)兩種模式:
咱們如今試舉一例來看看使用PSYNC處理斷線後狀況:
下圖展現了主從服務器在執行部分重同步時的通訊過程。
其實看到這裏的時候內心仍是有一個疑問的:若是上面的例子是T3時候從服務器掉線,而後在T10093的時候才鏈接上或者更長的時間呢!!!你這樣一條指令一條指令地傳輸過去還不如直接來一個SYNC命令快一些。因此在我看來使用PSYNC進行操做時,何時部分重同步,何時所有重同步是一個策略問題。固然Redis會解決這個問題,因此你們繼續看0_0
部分重同步功能由如下三個部分構成:
執行復制的雙方——主服務器和從服務器會分別維護一個複製偏移量:
(我靠!!難道從服務器沒有反饋嗎?丟包了怎麼辦?難道是用TCP?你們繼續看,我只是想穿插一些個人思路)
經過對比主從服務器的複製偏移量,程序能夠很容易地知道主從服務器是否處於一致狀態:
以下面的狀況:
假設從服務器A在斷線以後就當即從新鏈接主服務器,而且成功,那麼接下來,從服務器將向主服務器發送PSYNC命令,報告從服務器A當前的複製偏移量爲10086,那麼這時,主服務器應該對從服務器執行完整重同步仍是部分重同步呢?若是執行部分重同步的話,主服務器又如何補償從服務器A在斷線期間丟失的那部分數據呢?以上問題的答案都和複製積壓緩衝區有關。
複製積壓緩衝區是由主服務器維護的一個固定長度(fixed-size)先進先出(FIFO)隊列,默認大小爲1MB。
和普通先進先出隊列隨着元素的增長和減小而動態調整長度不一樣,固定長度先進先出隊列的長度是固定的,當入隊元素的數量大於隊列長度時,最早入隊的元素會被彈出,而新元素會被放入隊列。
當主服務器進行命令傳播時,它不只會將寫命令發送給全部從服務器,還會將寫命令入隊到複製積壓緩衝區裏面,如圖所示。
所以,主服務器的複製積壓緩衝區裏面會保存着一部分最近傳播的寫命令,而且複製積壓緩衝區會爲隊列中的每一個字節記錄相應的複製偏移量,就像下表所示的那樣。
當從服務器從新連上主服務器時,從服務器會經過PSYNC命令將本身的複製偏移量offset發送給主服務器,主服務器會根據這個複製偏移量來決定對從服務器執行何種同步操做:
Redis爲複製積壓緩衝區設置的默認大小爲1MB,若是主服務器須要執行大量寫命令,又或者主從服務器斷線後重鏈接所需的時間比較長,那麼這個大小也許並不合適。若是複製積壓緩衝區的大小設置得不恰當,那麼PSYNC命令的複製重同步模式就不能正常發揮做用,所以,正確估算和設置複製積壓緩衝區的大小很是重要。
複製積壓緩衝區的最小大小能夠根據公式second*write_size_per_second來估算:
例如,若是主服務器平均每秒產生1 MB的寫數據,而從服務器斷線以後平均要5秒才能從新鏈接上主服務器,那麼複製積壓緩衝區的大小就不能低於5MB。
爲了安全起見,能夠將復制積壓緩衝區的大小設爲2*second*write_size_per_second,這樣能夠保證絕大部分斷線狀況都能用部分重同步來處理。
至於複製積壓緩衝區大小的修改方法,能夠參考配置文件中關於repl-backlog-size選項的說明。
除了複製偏移量和複製積壓緩衝區以外,實現部分重同步還須要用到服務器運行ID(run ID):
當從服務器對主服務器進行初次複製時,主服務器會將本身的運行ID傳送給從服務器,而從服務器則會將這個運行ID保存起來(注意哦,是從服務器保存了主服務器的ID)。
當從服務器斷線並從新連上一個主服務器時,從服務器將向當前鏈接的主服務器發送以前保存的運行ID:
PSYNC命令的調用方法有兩種:
根據狀況,接收到PSYNC命令的主服務器會向從服務器返回如下三種回覆的其中一種:
步驟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命令以後可能遇到如下三種狀況:
步驟4:身份驗證
從服務器在收到主服務器返回的"PONG"回覆以後,下一步要作的就是決定是否進行身份驗證:
在須要進行身份驗證的狀況下,從服務器將向主服務器發送一條AUTH命令,命令的參數爲從服務器masterauth選項的值。
從服務器在身份驗證階段可能遇到的狀況有如下幾種:
全部錯誤到只有一個結果:停止目前的複製工做,並從建立套接字開始從新執行復制,直到身份驗證經過,或者從服務器放棄執行復製爲止。
步驟5:發送端口信息
身份驗證步驟以後,從服務器將執行命令REPLCONF listening-port <port-number>,向主服務器發送從服務器的監聽端口號。
主服務器在接收到這個命令以後,會將端口號記錄在從服務器所對應的客戶端狀態的slave_listening_port屬性中:
typedef struct redisClient {
// ...
// 從服務器的監聽端口號
int slave_listening_port;
// ...
}redisClient;
slave_listening_port屬性目前惟一的做用就是在主服務器執行INFO replication命令時打印出從服務器的端口號。
步驟6:同步
在這一步,從服務器將向主服務器發送PSYNC命令,執行同步操做,並將本身的數據庫更新至主服務器數據庫當前所處的狀態。
須要注意的是在執行同步操做前,只有從服務器是主服務器的客戶端。可是執行從不操做以後,主服務器也會稱爲從服務器的客戶端:
步驟7:命令傳播
當完成了同步以後,主從服務器就會進入命令傳播階段,這時主服務器只要一直將本身執行的寫命令發送給從服務器,而從服務器只要一直接收並執行主服務器發來的寫命令,就能夠保證主從服務器一直保持一致了。
在命令傳播階段,從服務器默認會以每秒一次的頻率,向主服務器發送命令:REPLCONF ACK <replication_offset>
其中replication_offset是從服務器當前的複製偏移量。
發送REPLCONF ACK命令對於主從服務器有三個做用:
檢測主從服務器的網絡鏈接狀態
若是主服務器超過一秒鐘沒有收到從服務器發來的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丟失了。也無所謂,主服務器立刻就知道了。