印象很深刻,之前我去面試就被一位老面試官問了:Redis主從複製機制,今天咱們就來分析分析!
下面詳細分析Redis主從複製機制中主從握手的過程。
Redis主從複製機制中有兩個角色:主節點與從節點。
主節點處理用戶請求,並將數據複製給從節點。
主從複製機制主要有以下作用:
(1)數據冗餘,將數據熱備份到從節點,即使主節點由於磁盤損壞丟失數據,從節點依然保留數據副本。
(2)讀/寫分離,可以由主節點提供寫服務,從節點提供讀服務,提高Redis服務整體吞吐量。
(3)故障恢復,主節點故障下線後,可以手動將從節點切換爲主節點,繼續提供服務。
(4)高可用基礎,主從複製機制是Sentinel和Cluster機制的基礎,Sentinel和Cluster都實現了故障轉移,即主節點故障停止後,Redis負責選擇一個從節點切換爲主節點,繼續提供服務。
下面將主從複製流程分爲三個階段。
(1)握手階段:主從連接成功後,從節點需要將自身信息(如IP地址、端口等)發送給主節點,以便主節點能認識自己。
(2)同步階段:從節點連接主節點後,需要先同步數據,數據達到一致(或者只有最新的變更不一致)後才進入複製階段。
Redis支持兩種同步機制:
全量同步:從節點發送命令PSYNC ? -1,要求進行全量同步,主節點返回響應+FULLRESYNC,表明同意全量同步。隨後,主節點生成RDB數據併發送給從節點。這種方式常用於新的從節點首次同步數據。
部分同步:從節點發送命令PSYNC replid offset,要求進行部分同步,主節點響應+CONTINUE,表明同意部分同步。主節點只需要把複製積壓區中offset偏移量之後的命令發送給從節點即可(主節點會將執行的寫命令都寫入複製積壓區)。這種方式常用於主從連接斷開重連時同步數據。如果offset不在複製積壓區中,那麼主節點也會返回+FULLRESYNC,要求進行全量同步。
(3)複製階段:主節點在運行期間,將執行的寫命令傳播給從節點,從節點接收並執行這些命令,從而達到複製數據的效果。Redis使用的是異步複製,主節點傳播命令後,並不會等待從節點返回ACK確認。異步複製的優點是低延遲和高性能,缺點是可能在短期內主從節點數據不一致。
本文中指的命令,包含命令名及執行命令的參數。
PSYNC命令涉及以下屬性:
server.master_repl_offset:記錄當前服務器已執行命令的偏移量。
server.replid:40位十六進制的隨機字符串,在主節點中是自身ID,在從節點中記錄的是主節點ID。
server.replid2:用於主節點,存放上一個主節點ID。
server.repl_backlog:複製積壓區,主節點將最近執行的寫命令寫入複製積壓區,用於實現部分同步。
下面介紹一下Redis主從握手流程。
主從複製的機制是由從節點發起流程,我們可以發送REPLICAOF命令到某個服務器,要求它成爲指定服務器的從節點:
REPLICAOF <masterip> <masterport>
或者在配置文件中添加配置REPLICAOF <masterip> <masterport>,這樣Redis服務器啓動後將成爲指定服務器的從節點。
提示:從Redis 5開始爲SLAVEOF命令提供別名REPLICAOF,這兩個命令的作用一樣。
下面以從節點的視角,分析主從握手的過程。
從節點握手階段涉及以下屬性。
server.repl_state:用於從節點,標誌從節點當前複製狀態。有如下值:
REPL_STATE_NONE:無主從複製關係。
REPL_STATE_CONNECT:待連接。
REPL_STATE_CONNECTING:正在連接。
…(部分握手狀態並沒有列出)
REPL_STATE_TRANSFER:從節點正在接收RDB數據。
REPL_STATE_CONNECTED:已連接,主從同步完成。
從節點使用replicaofCommand函數處理REPLICAOF命令。
該函數執行如下邏輯:
(1)如果處理的命令是REPLICAOF NO ONE,則將當前服務器轉換爲主節點,取消原來的主從複製關係,退出函數。
(2)調用replicationSetMaster函數,與給定服務器建立主從複製關係。
另外,我們在配置文件中配置REPLICAOF <masterip> <masterport>,Redis加載該配置,也會將server.repl_state設置爲REPL_STATE_CONNECT狀態(config.c)。
從節點server.repl_state進入REPL_STATE_CONNECT狀態後,主從複製流程已經開始。
serverCron時間事件負責對REPL_STATE_CONNECT狀態進行處理:
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) { ... if (server.repl_state == REPL_STATE_CONNECT) { if (connectWithMaster() == C_OK) { serverLog(LL_NOTICE,"MASTER <-> REPLICA sync started"); } } }
調用connectWithMaster函數進行處理,該函數負責建立主從網絡連接:
int connectWithMaster(void) { // [1] server.repl_transfer_s = server.tls_replication ? connCreateTLS() : connCreateSocket(); // [2] if (connConnect(server.repl_transfer_s, server.masterhost, server.masterport, NET_FIRST_BIND_ADDR, syncWithMaster) == C_ERR) { ... return C_ERR; } // [3] server.repl_transfer_lastio = server.unixtime; server.repl_state = REPL_STATE_CONNECTING; return C_OK; }
【1】創建一個Socket套接字。connCreateTLS函數創建TLS連接,connCreateSocket函數創建TCP連接,它們都返回套接字文件描述符。該連接是主從節點網絡通信的連接,本書稱之爲主從連接。
【2】connConnect函數負責連接到主節點,並且在連接成功後調用syncWithMaster函數。
【3】從節點server.repl_state進入REPL_STATE_CONNECTING狀態。
網絡連接成功後,從節點調用syncWithMaster函數,進入握手階段:
void syncWithMaster(connection *conn) { char tmpfile[256], *err = NULL; int dfd = -1, maxtries = 5; int psync_result; ... // [1] if (server.repl_state == REPL_STATE_CONNECTING) { connSetReadHandler(conn, syncWithMaster); connSetWriteHandler(conn, NULL); server.repl_state = REPL_STATE_RECEIVE_PONG; err = sendSynchronousCommand(SYNC_CMD_WRITE,conn,"PING",NULL); if (err) goto write_error; return; } ... // [2] if (server.repl_state != REPL_STATE_RECEIVE_PSYNC) { goto error; } // more }
【1】根據server.repl_state狀態,執行對應操作。
從節點發送給主節點的信息,主節點會記錄在從節點客戶端,並在INFO命令中輸出這些信息。另外,Sentinel模塊需要從主節點INFO命令響應中獲取這些從節點信息。
【2】執行到這裏,主從握手階段已經完成。server.repl_state必須處於REPL_STATE_ RECEIVE_PSYNC狀態,否則報錯。
下面使用Linux tcpdump工具抓取主從連接報文,分析主從節點握手階段的通信內容(主節點端口爲6000):
tcpdump tcp -i lo -nn port 6000 -T RESP
tcpdump支持RESP協議,最後一個選項-T RESP要求tcpdump以RESP協議格式解析報文。
其中6000端口爲主節點端口,60374端口爲從節點通信端口。從tcpdump的輸出可以清晰地看到主從節點在握手階段的通信內容。
提示:tcpdump解析後的RESP內容並不會展示數據類型的標誌符,如主節點對從節點PING命令的響應實際上是“-NOAUTH Authentication required.”,請讀者閱讀源碼時注意。
以主節點視角分析握手階段,主節點不斷處理來自從節點的命令(包括PING、AUTH、REPLCONF),感興趣的讀者可自行閱讀代碼。
好了,Redis主從握手流程到此就分析完畢了,這也算是看過源碼了,哈哈。