一、概述
在現有企業中80%公司大部分使用的是redis單機服務,在實際的場景當中單一節點的redis容易面臨風險。
面臨問題
-
機器故障。我們部署到一臺 Redis 服務器,當發生機器故障時,需要遷移到另外一臺服務器並且要保證數據是同步的。而數據是最重要的,如果你不在乎,基本上也就不會使用 Redis 了。
-
容量瓶頸。當我們有需求需要擴容 Redis 內存時,從 16G 的內存升到 64G,單機肯定是滿足不了。當然,你可以重新買個 128G 的新機器。
解決辦法
要實現分佈式數據庫的更大的存儲容量和承受高併發訪問量,我們會將原來集中式數據庫的數據分別存儲到其他多個網絡節點上。
Redis 爲了解決這個單一節點的問題,會把數據複製多個副本部署到其他節點,實現 Redis的高可用和對數據的備份,從而保證數據和服務的高可用。
二、什麼是主從複製
主從複製,是指將一臺Redis服務器的數據,複製到其他的Redis服務器。前者稱爲主節點(master),後者稱爲從節點(slave)。數據的複製是單向的,只能由主節點到從節點。
默認情況下,每臺Redis服務器都是主節點;且一個主節點可以有多個從節點(或沒有從節點),但一個從節點只能有一個主節點。
三、主從複製的作用
-
數據備份:主從複製實現了數據的熱備份,是持久化之外的另一種數據備份方式。
-
故障恢復:當主節點出現問題時,可以由從節點提供服務,實現快速的故障恢復。
-
讀寫分離:可以用於實現讀寫分離,主庫寫、從庫讀,讀寫分離不僅可以提高服務器的負載能力,同時可根據需求的變化,改變從庫的數量;
-
負載均衡:在主從複製的基礎上,配合讀寫分離,可以由主節點提供寫服務,由從節點提供讀服務(即寫Redis數據時應用連接主節點,讀Redis數據時應用連接從節點),分擔服務器負載;尤其是在寫少讀多的場景下,通過多個從節點分擔讀負載,可以大大提高Redis服務器的併發量。
-
高可用基石:除了上述作用以外,主從複製還是哨兵和集羣能夠實施的基礎,因此說主從複製是Redis高可用的基礎。
四、如何實現主從同步
有兩種方式可以用來完成主從Redis服務器的同步設置,都需要針對slave服務器上進行設置,指定slave需要連接的Redis服務器(可能是master,也可能是slave)。
在配置文件中設置
在作爲slave的Redis服務器的配置文件(/etc/redis/6379.conf)中設置。
slaveof 127.0.0.1 6379 #指定master的ip和端口
很明顯,這種設置方式非常簡單,但是需要修改配置文件,並且配置文件是在服務器啓動時加載的。所以服務器不啓動無法修改,操作不靈活。這種配置方式適合於作爲部署時的初始配置。
通過客戶端命令設置
Redis服務器啓動後,直接通過客戶端執行命令:slaveof,則該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的主服務器。
五、主從同步的原理
主從同步分爲 2 個步驟:同步(全量同步)和命令傳播(增量同步)
- 同步:將從服務器的數據庫狀態更新成主服務器當前的數據庫狀態。
- 命令傳播:當主服務器數據庫狀態被修改後,導致主從服務器數據庫狀態不一致,需要讓主從數據同步到一致狀態。
上面就是主從同步 2 個步驟的作用,下面我打算稍微細說這兩個步驟的實現過程。
這裏需要提前說明一下:在 Redis 2.8 版本之前,進行主從複製時一定會順序執行上述兩個步驟,而從 2.8 開始則可能只需要執行命令傳播即可。在下文也會解釋爲什麼會這樣?
同步(全量同步)
當客戶端向從服務器發送SLAVEOF命令,要求從服務器複製主服務器時,從服務器首先需要執行同步操作,將從服務器的數據庫狀態更新至主服務器當前所處的數據庫狀態。
從服務器對主服務器的同步操作需要通過向主服務器發送SYNC命令來完成,具體步驟如下:
1)從服務器連接主服務器,發送SYNC命令;
2)主服務器接收到SYNC命名後,開始執行BGSAVE命令生成RDB文件,並使用緩衝區記錄此後執行的所有寫命令;
3)主服務器BGSAVE執行完後,向所有從服務器發送快照文件,並在發送期間繼續記錄被執行的寫命令;
4)從服務器收到快照文件後丟棄所有舊數據,載入收到的快照;
5)主服務器快照發送完畢後開始向從服務器發送緩衝區中的寫命令;
6)從服務器完成對快照的載入,開始接收命令請求,並執行來自主服務器緩衝區的寫命令;
命令傳播(增量同步)
在執行完同步操作之後,主從服務器之間數據庫狀態已經相同了。但這個狀態並非一成不變,如果主服務器執行了寫操作,那麼主服務器的數據庫狀態就會修改,並導致主從服務器狀態不再一致。
所以爲了讓主從服務器再次回到一致狀態,主服務器需要對從服務器執行命令傳播操作:主服務器會將自己執行的寫命令,也就是造成主從服務器不一致的那條寫命令,發送給從服務器執行,當從服務器執行了相同的寫命令之後,主從服務器將再次回到一致狀態。注:Redis 同步的是指令流。
六、優化版同步操作
舊版複製功能的缺陷
在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文件,以及向從服務器發送保存在緩衝區裏面的寫命令來進行同步;
- 部分重同步:用於處理斷線後重複製情況,當從服務器在斷線後重新連接主服務器時,如果條件允許,主服務器可以將主從服務器連接斷開期間執行的寫命令發送給從服務器,從服務器只要接收並執行這些寫命令,就可以將數據庫更新至主服務器當前所處的狀態。
上面的介紹中,出現了「如果條件允許」,那又是鬼什麼條件呢?—— 其實就是一個偏移量的比較,具體可以繼續往下看。
部分重同步的實現
部分重同步功能由以下三個部分構成:
- 主服務器的複製偏移量(replication offset)和從服務器的複製偏移量;
- 主服務器的複製積壓緩衝區(replication backlog);
- 服務器的運行ID(run ID)。
複製偏移量
執行復制的雙方——主服務器和從服務器會分別維護一個複製偏移量:
- 主服務器每次向從服務器傳播N個字節的數據時,就將自己的複製偏移量的值加上N;
- 從服務器每次收到主服務器傳播來的N個字節的數據時,就將自己的複製偏移量的值加上N;
通過對比主從服務器的複製偏移量,程序可以很容易地知道主從服務器是否處於一致狀態:
- 如果主從服務器處於一致狀態,那麼主從服務器兩者的偏移量總是相同的;
- 相反,如果主從服務器兩者的偏移量並不相同,那麼說明主從服務器並未處於一致狀態。
如下面的情況:
假設從服務器A在斷線之後就立即重新連接主服務器,並且成功,那麼接下來,從服務器將向主服務器發送PSYNC命令,報告從服務器A當前的複製偏移量爲10086,那麼這時,主服務器應該對從服務器執行完整重同步還是部分重同步呢?如果執行部分重同步的話,主服務器又如何補償從服務器A在斷線期間丟失的那部分數據呢?以上問題的答案都和複製積壓緩衝區有關。
複製積壓緩衝區
複製積壓緩衝區是由主服務器維護的一個固定長度(fixed-size)先進先出(FIFO)隊列,默認大小爲1MB。
和普通先進先出隊列隨着元素的增加和減少而動態調整長度不同,固定長度先進先出隊列的長度是固定的,當入隊元素的數量大於隊列長度時,最先入隊的元素會被彈出,而新元素會被放入隊列。
當主服務器進行命令傳播時,它不僅會將寫命令發送給所有從服務器,還會將寫命令入隊到複製積壓緩衝區裏面,如圖所示。
因此,主服務器的複製積壓緩衝區裏面會保存着一部分最近傳播的寫命令,並且複製積壓緩衝區會爲隊列中的每個字節記錄相應的複製偏移量,就像下表所示的那樣。
當從服務器重新連上主服務器時,從服務器會通過PSYNC命令將自己的複製偏移量offset發送給主服務器,主服務器會根據這個複製偏移量來決定對從服務器執行何種同步操作:
- 如果offset偏移量之後的數據(也即是偏移量offset+1開始的數據)仍然存在於複製積壓緩衝區裏面,那麼主服務器將對從服務器執行部分重同步操作;
- 相反,如果offset偏移量之後的數據已經不存在於複製積壓緩衝區,那麼主服務器將對從服務器執行完整重同步操作。
服務器運行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命令,並與主服務器執行完整同步操作。
七、心跳檢測
剛纔提到,主從同步有同步和命令傳播 2 個步驟,當完成了同步之後,主從服務器就會進入命令傳播階段,在命令傳播階段,從服務器默認會以每秒一次的頻率,向主服務器發送命令: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丟失了。也無所謂,主服務器馬上就知道了。
八、主從同步具體過程
步驟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:命令傳播
當完成了同步之後,主從服務器就會進入命令傳播階段,這時主服務器只要一直將自己執行的寫命令發送給從服務器,而從服務器只要一直接收並執行主服務器發來的寫命令,就可以保證主從服務器一直保持一致了。
參考:
https://www.cnblogs.com/lukexwang/p/4711977.html
https://blog.csdn.net/weixin_42711549/article/details/83061052