6. 主從同步
Redis支持主從同步,其官方文檔 http://www.redis.io/topics/
replication 簡要介紹了它的主從同步機制。翻譯官方文檔的文章, Redis的主從同步
有以下特點:
•一個Master可以有多個Slave
•Slave可以接受其它Slave的連接,因此Masters和Slaves之間可以行程一個網絡
•同步時,作爲Master的一端(可能其狀態是Slave)的同步是非阻塞的,它可以繼
續處理命令;作爲Slave一端的第一次同步是阻塞的。
•主從機制可以用於提高擴展性,比如可以用多個Slaves處理複雜的讀請求。
•可以通過Slave備份數據,以降低Master的負載。
6.1. 建立連接
接下來我們看Redis中Slave和Master是如何建立連接的。 Redis通過
server.replstate表明Master和Slave的狀態,初始時,狀態均爲
REDIS_REPL_NONE。 server.masterhost和server.masterport爲Master的地址,
Master結點的這兩個變量爲NULL。
Slave的server.replstate狀態除了REDIS_REPL_NONE外,還有
/* Slave replication state - slave side */
#define REDIS_REPL_NONE 0 /* No active replication */
#define REDIS_REPL_CONNECT 1 /* Must connect to master */
#define REDIS_REPL_TRANSFER 2 /* Receiving .rdb from master */
#define REDIS_REPL_CONNECTED 3 /* Connected to master */
Master對於每個Slave都維護一個狀態,在結構體redisClient.replstate中,
因爲Slave對於Master,其本質也是一個客戶端。 Master對每個Slave標記狀態,有四
種:
/* Slave replication state - from the point of view of master
* Note that in SEND_BULK and ONLINE state the slave receives new updates
* in its output queue. In the WAIT_BGSAVE state instead the server is waiting
* to start the next background saving in order to send updates to it. */
#define REDIS_REPL_WAIT_BGSAVE_START 3 /* master waits bgsave to start feeding
it */
#define REDIS_REPL_WAIT_BGSAVE_END 4 /* master waits bgsave to start bulk DB
transmission */
#define REDIS_REPL_SEND_BULK 5 /* master is sending the bulk DB */
#define REDIS_REPL_ONLINE 6 /* bulk DB already transmitted, receive updates */
REDIS_REPL_WAIT_BGSAVE_START表示Master準備dump數據到磁盤,任何新建
的Slave連接均處於該狀態; Master開始數據dump後,將Slave的狀態修改爲
REDIS_REPL_WAIT_BGSAVE_END,當dump完成後,通知Slave,狀態變爲
REDIS_REPL_SEND_BULK,準備向其發送數據;當發送完成, Slave就緒後,狀態變爲
REDIS_REPL_ONLINE,至此Master可以向狀態爲REDIS_REPL_ONLINE的Slave發送更
新命令。
對於Slave,如果server.replstate!=REDIS_REPL_CONNECTED,則無法處理命令(獲取Slave狀態的命令除外);對於Master,如果c.replstate!=REDIS_REPL_ONLINE,也不會向它發送命令。serverCron()中每隔10秒會調用replicationCron(),處理主從同步相關的功能。 replicationCron()會檢測同數據傳輸超時, Slave與Master連接斷開等問題。在分析可能的故障前,先分析建立連接的過程。
6.1.1. Master
Redis啓動時可以通過配置文件指定爲Master狀態或Slave狀態, Slave也可以通過Sync命令與另一個Master(可能是Master,也可能是與Master有連接的Slave)建立主從連接。下面分析syncCommand()的處理。首先,如果發送sync命令的結點已經是Slave狀態,則無需再次同步。如果接受sync指令的結點,本身也是Slave,且沒有與任何Master建立連接,則無法響應sync命令。如果Slave與Master之間還有其它交互命令沒有處理完,也無法繼續響應sync命令。然後Master開始檢測bgsave狀態,如果已經有bgsave進程,再判斷是否正在響應其它Slave的sync命令,且正在dump數據(Slave的replstate爲REDIS_REPL_BGSAVE_END),則直接將新Slave的replstate狀態置爲REDIS_REPL_BGSAVE_END,並準備將dump好的數據傳給新Slave。如果沒有dump好的數據,則將新Slave的狀態置爲REDIS_REPL_BGSAVE_START,並等待bgsave進程完成,之後再啓動bgsave,爲Slave dump數據。如果沒有bgsave進程,則啓動新的bgsave進程,並將Slave的狀態置爲REDIS_REPL_BGSAVE_END。最後,將新Slave加入Master的server.slaves列表。
void syncCommand(redisClient *c) {
/* ignore SYNC if aleady slave or in monitor mode */
if (c->flags & REDIS_SLAVE) return;
/* Refuse SYNC requests if we are a slave but the link with our master
* is not ok... */
if (server.masterhost && server.replstate != REDIS_REPL_CONNECTED) {
addReplyError(c,"Can't SYNC while not connected with my master");
return;
}
/* SYNC can't be issued when the server has pending data to send to
* the client about already issued commands. We need a fresh reply
* buffer registering the differences between the BGSAVE and the current
* dataset, so that we can copy to other slaves if needed. */
if (listLength(c->reply) != 0) {
addReplyError(c,"SYNC is invalid with pending input");
return;
}
redisLog(REDIS_NOTICE,"Slave ask for synchronization");
/* Here we need to check if there is a background saving operation
* in progress, or if it is required to start one */
if (server.bgsavechildpid != -1) {
/* Ok a background save is in progress. Let's check if it is a good
* one for replication, i.e. if there is another slave that is
* registering differences since the server forked to save */
redisClient *slave;
listNode *ln;
listIter li;
listRewind(server.slaves,&li);
while((ln = listNext(&li))) {
slave = ln->value;
if (slave->replstate == REDIS_REPL_WAIT_BGSAVE_END) break;
}
if (ln) {
/* Perfect, the server is already registering differences for
* another slave. Set the right state, and copy the buffer. */
listRelease(c->reply);
c->reply = listDup(slave->reply);
c->replstate = REDIS_REPL_WAIT_BGSAVE_END;
redisLog(REDIS_NOTICE,"Waiting for end of BGSAVE for SYNC");
} else {
/* No way, we need to wait for the next BGSAVE in order to
* register differences */
c->replstate = REDIS_REPL_WAIT_BGSAVE_START;
redisLog(REDIS_NOTICE,"Waiting for next BGSAVE for SYNC");
}
} else {
/* Ok we don't have a BGSAVE in progress, let's start one */
redisLog(REDIS_NOTICE,"Starting BGSAVE for SYNC");
if (rdbSaveBackground(server.dbfilename) != REDIS_OK) {
redisLog(REDIS_NOTICE,"Replication failed, can't BGSAVE");
addReplyError(c,"Unable to perform background save");
return;
}
c->replstate = REDIS_REPL_WAIT_BGSAVE_END;
}
c->repldbfd = -1;
c->flags |= REDIS_SLAVE;
c->slaveseldb = 0;
listAddNodeTail(server.slaves,c);
return;
}
如前文Snapshot快照分析,當bgsave進程備份完成時,在serverCron()中會調用backgroundSaveDoneHandler()處理,該函數中調用updateSlavesWaitingBgsave()處理等待bgsave的Slave結點。
updateSlavesWaitingBgsave()檢查server.slaves列表,當發現狀態爲REDIS_REPL_WAIT_BGSAVE_START的結點時,會再次啓動bgsave進程進行dump數據。當發現狀態爲REDIS_REPL_WAIT_BGSAVE_START的結點時,會準備將數據發送給Slave,並將Slave的狀態修改爲REDIS_REPL_SEND_BULK。 Master在Slave上監聽WRITEABLE事件,當Slave可寫時,調用sendBulkToSlave()將數據發送到Slave。在Master中, sendBulkToSlave()將數據從磁盤讀入,通過socket發送到Slave,完成後刪除WRITEABLE事件,並將Slave狀態改爲REDIS_REPL_ONLINE。然後再次監聽Slave的WRITEABLE事件,當Slave可寫時,表明Slave已經準備好開始同步指令。
6.1.2. Slave
Redis可以在配置文件中指定爲Slave模式,並指定Master的地址,這樣的Slave結點啓動時即爲REDIS_REPL_ONLINE狀態,然後在replicationCron()中調用syncWithMaster()與Master同步。syncWithMaster()中,向Master發送sync命令,並準備接收數據的目錄。然後通過readSyncBulkPayload()處理Master發送的dump的數據。 Slave通過syncRead()和syncWrite()與Master傳輸數據,這兩個方法底層是異步IO的,但封裝成阻塞型,因此Slave第一次同步是阻塞的。replicationCron()會定時Master的dump數據是否發送完成,如果長時間沒有收到dump數據的數據包, Slave會通過replicationAbortSyncTransfer()取消數據同步。
6.2. 指令同步
在call()中, Master會將改變了數據的命令通過replicationFeedSlaves()同步到Slaves中。 Slave以處理普通命令的流程處理這些Master發來的命令。特別地在serverCron()中, Master會刪除過期的數據, Slave則等待Master同步DEL指令將過期數據刪除。replicationCron()中, Master會定時向所有Slave發送心跳指令,同時,Slave的replicationCron()會通過Ping指令檢查Slave與Master的連接狀態,如果Slave長時間沒有收到主機的Ping,則會斷開與主機的連接。
6.3. 主從轉換
slaveof可以轉換Redis中結點的狀態。對於Slave結點, slaveof no one可以將Slave與Master斷開,並將Slave轉爲主機。對於Master, slaveof ip port可以講主機轉爲Slave。
Redis的主從轉換非常簡單,如果同步未完成,則取消同步;否則,直接改變server.masterhost等狀態。