Redis4.0版本相比原來3.x版本,增加了很多新特性,如模塊化、PSYN2.0、非阻塞DEL和FLUSHALL/FLUSHDB、RDB-AOF混合持久化等功能。尤其是模塊化功能,作者從七年前的redis1.0版本就開始謀划,終於在4.0版本發布了,所以版本號也就從3.x直接迭代到了4.0以表示版本變化之大。簡單看了一下新版的PSYN2.0,雖然很多細節沒搞清楚,但是大概流程倒是搞明白了。
一、主要流程
在新版的PSYN2.0中,相比原來的PSYN功能,最大的變化支持兩種場景下的部分重同步,一個場景是slave提升為master后,其他slave可以從新提升的master進行部分重同步。另外一個場景就是slave重啟后,可以進行部分重同步。
在具體實現上redis服務器在生成RDB文件的時候會把當前的復制ID和復制偏移量一起保存到RDB文件中,后面服務器重啟的時候,就可以從RDB文件中讀取復制ID和復制偏移量,從而可以進行部分重同步功能。另外當服務器從slave提升為master后,會保存兩個復制ID,其他slave復制的時候可以根據第二個復制ID來進行部分重同步。
假設兩台Redis服務器A和B啟動后,對B執行slaveof命令,使得B變為A的從服務器,整體流程如下
1、B在啟動的時候,會嘗試從RDB文件中加載復制ID和復制偏移量(loadDataFromDisk),如果沒有配置RDB文件或者RDB文件中不包含主從復制相關信息,那么使用隨機生成的復制ID
2、B接收到slaveof命令時候,設置復制狀態為REPL_STATE_CONNECT(replicationSetmaster)
3、周期調度的時間事件中,如果檢測到REPL_STATE_CONNECT狀態,則會初始化連向master的套接字(serverCron->replicationCron->connectWithmaster),套接字關聯事件對應的事件處理程序(syncWithmaster)
4、連接建立后,對應的事件處理程序,則會依據相關設置等,依次發送PING、AUTH、PORT、IP、CAPA、PSYN等信息(syncWithmaster)
其中CAPA表示發送端在主從復制方面支持的能力,目前Redis4.0版本支持兩種能力,一個是EOF、另外一個是PSYNC2。
EOF表示slave可以接收直接由套接字發送過來的RDB流。傳統的全量復制方式是master首先生成一個RDB文件,然后以$<count> bulk format發送到slave。而EOF格式下,master並不會在硬盤上生成RDB文件,而是先通過$EOF:<40 bytes delimiter>通知slave一個隨機生成的40byte結束符,master邊生成RDB文件,邊發往slave,在slave以接收到的delimiter來判斷接收過程結束。
PSYNC2則表示支持Redis4.0最新的PSYN復制操作。
PSYN信息中,slave會把自己的復制ID和復制偏移量發給master
5、master根接收到PSYN消息后,根據復制ID如果復制ID與自己的復制ID相同且復制偏移量仍然存在於復制緩存區中(server.repl_backlog),那么執行部分重同步,回復CONTINUE消息,並從復制緩存區中復制相關的數據到slave。否則執行全量重同步,回復FULLRESYNC消息,生成RDB傳輸到slave。(實際上master會維護兩個復制id,如果PSYN復制ID與第二個復制ID相同且PSYN的復制偏移量沒有超過第二個復制ID的偏移量,也會執行部分重同步,參考后面的slave提升為master場景)
7、CONTINUE消息和FULLRESYNC消息中都會帶有master的復制ID,slave需要將自己的復制ID更新為master的復制ID(如果兩者不同)。
6、同步成功以后,后續master接收到修改數據庫的新命令,則會把新命令傳輸到slave執行。
二、網絡閃斷超時
master在與slave同步后,slave每隔1s發送一個REPLCONF ACK消息給master,master每隔10s給slave發送一個PING消息。如果master在一定的時間內(server.repl_timeout)沒有收到消息,則會釋放slave連接。同樣如果slave在一定的時間(server.repl_timeout)內沒有收到master的PING消息,也會釋放套接字連接(replicationCron)。但是slave會設置復制狀態為REPL_STATE_CONNECT(replicationHandlemasterDisconnection),后續slave端定時事件調度的時候,則會根據復制狀態嘗試重新連接。
測試腳本如下
#!/bin/sh
TIMEOUT=15
M_IP=127.0.0.1
M_PORT=6379
M_OUT=master.out
S1_IP=127.0.0.2
S1_PORT=6380
S1_OUT=slave_1.out
S1_RDB_NAME=slave_1.rdb
nohup ../src/redis-server --port $M_PORT --bind $M_IP --repl-timeout $TIMEOUT > $M_OUT &
nohup ../src/redis-server --port $S1_PORT --bind $S1_IP --dbfilename $S1_RDB_NAME --repl-timeout $TIMEOUT > $S1_OUT&
echo set redis hello | nc $M_IP $M_PORT
echo lpush num 1 2 3 | nc $M_IP $M_PORT
sleep 1
echo slaveof 127.0.0.1 6379 | nc $S1_IP $S1_PORT
sleep 1
echo set redis world | nc $M_IP $M_PORT
echo lpush num 4 | nc $M_IP $M_PORT
echo save | nc $S1_IP $S1_PORT
sleep 5
echo "modify iptables"
sudo iptables -I INPUT -s 127.0.0.1 -d 127.0.0.2 -j DROP
sudo iptables -I OUTPUT -s 127.0.0.2 -d 127.0.0.1 -j DROP
echo set redis helloworld | nc $M_IP $M_PORT
echo lpush num 5 | nc $M_IP $M_PORT
sleep 25
echo "restore iptables"
sudo iptables -D INPUT -s 127.0.0.1 -d 127.0.0.2 -j DROP
sudo iptables -D OUTPUT -s 127.0.0.2 -d 127.0.0.1 -j DROP
最終運行如上腳本,得到如下結果,可以看到在slave初始啟動的時候,NO20處slave通過REPLCONF CAPA信息通知master,自己支持EOF格式的RDB傳輸同時支持PSYNC2.0協議,接着通過PSYNC發送了一個隨機生成的復制ID,master收到這個ID后發現與自己的復制ID不相符,需要全量復制,因此回復FULLRESYNC消息,slave收到FULLRESYNC消息后,會把自己的復制ID更新為f8e28****,后續slave生成RDB文件的時候,就會把f8e28****這個復制ID和復制偏移量保存進去。
網絡閃斷超時后,master和slave都會釋放連接,當網絡恢復后,slave會重新建立連接,同時通過PSYNC消息把對應的復制ID和復制偏移量發給master,master收到PSYNC消息后,發現復制ID與自己相同,說明這個slave之前是自己的slave,同時復制偏移量位於緩存區范圍內,因此滿足進行部分重同步的條件,回復CONTINUE消息,指示slave進行部分重同步
三、slave提升為master以及slave重啟場景
這兩種場景是PSYN2.0新增支持的場景,原理也很容易理解,就是通過復制ID來進行匹配的。直接看示例吧,通過一個綜合示例來看一下這兩種場景
示例場景:首先有一個master和兩個從服務器slave1和slave2,同步過程中首先把slave2進程kill掉,然后在master上執行兩個命令同步到slave1后,把slave1手動提升為master,並把slave2重啟指向之前的slave1
測試腳本
#!/bin/sh
TIMEOUT=15
M_IP=127.0.0.1
M_PORT=6379
M_OUT=master.out
S1_IP=127.0.0.2
S1_PORT=6380
S1_OUT=slave_1.out
S1_RDB_NAME=slave_1.rdb
S2_IP=127.0.0.3
S2_PORT=6381
S2_OUT=slave_2.out
S2_RDB_NAME=slave_2.rdb
nohup ../src/redis-server --port $M_PORT --bind $M_IP --repl-timeout $TIMEOUT > $M_OUT &
nohup ../src/redis-server --port $S1_PORT --bind $S1_IP --dbfilename $S1_RDB_NAME --repl-timeout $TIMEOUT > $S1_OUT&
nohup ../src/redis-server --port $S2_PORT --bind $S2_IP --dbfilename $S2_RDB_NAME --repl-timeout $TIMEOUT > $S2_OUT&
sleep 1
s1_pid=`cat $S1_OUT|grep PID`
s1_pid=`echo ${s1_pid#*PID:}`
echo s1_pid:$s1_pid
s2_pid=`cat $S2_OUT|grep PID`
s2_pid=`echo ${s2_pid#*PID:}`
echo s1_pid:$s2_pid
echo set redis hello | nc $M_IP $M_PORT
echo lpush num 1 2 3 | nc $M_IP $M_PORT
sleep 1
echo slaveof $M_IP $M_PORT | nc $S1_IP $S1_PORT
echo slaveof $M_IP $M_PORT | nc $S2_IP $S2_PORT
sleep 1
echo set redis world | nc $M_IP $M_PORT
echo lpush num 4 | nc $M_IP $M_PORT
sleep 1
echo save | nc $S2_IP $S2_PORT
kill -9 $s2_pid
sleep 3
echo set redis helloworld | nc $M_IP $M_PORT
echo lpush num 5 | nc $M_IP $M_PORT
echo "slave1提升為master"
echo slaveof no one | nc $S1_IP $S1_PORT
echo "重啟slave2 並設置 slaveof $S1_IP $S1_PORT"
nohup ../src/redis-server --port $S2_PORT --bind $S2_IP --dbfilename $S2_RDB_NAME --repl-timeout $TIMEOUT > $S2_OUT&
sleep 1
echo slaveof $S1_IP $S1_PORT | nc $S2_IP $S2_PORT
需要注意的是在No68和No69處,slave2重啟並設置slaveof 127.0.0.2 6380的時候,127.0.0.2:6380雖然回復了CONTINUE消息,但是復制ID卻發生了變化。原因是在127.0.0.2 6380執行slaveof no one的時候,Redis服務器會把當前的復制ID和復制偏移量保存起來,並重新生成一個新的復制ID(shiftReplicationId)。如果后續PSYN收到的復制ID與先前保存的復制ID相同,且復制偏移量小於先前保存的復制偏移量那么就可以進行部分重同步(當前如果與新生成的復制ID相同,且偏移量在緩存區內,同樣可以進行部分重同步)。這里需要重新生成一個復制ID的原因就是舊有的復制ID的復制偏移量在執行完slaveof no one后不能在繼續增加,否則會導致數據混淆。(masterTryPartialResynchronization)
補充說明
1、Redis的RESP協議https://redis.io/topics/protocol
2、Redis4.0 http://antirez.com/news/110






