在分佈式系統中,爲了解決單點問題,通常會把數據集複製多個副本部署到其他機器,以滿足故障恢復和負載均衡等需求。redis 爲我們提供的複製功能,實現了相同數據的多個副本,這也是其實現 HA 的基礎。
參與 redis 複製功能的節點被分成兩個角色,主節點(master)和從節點(slave),複製的數據流是單向的,即 master → slave。
默認情況下,每個 redis 實例都是 master,mater 與 slave 的關係爲 1:n(也可以沒有 slave),但一個 slave 只能有一個 master。
redis 的複製功能涉及同步(sync)和命令傳播(command propagate)兩個階段。
同步階段用於將 slave 的數據庫狀態更新至 master 當前所處的數據庫狀態,即追數據階段;
命令傳播階段則用於當 master 數據庫狀態改變,導致主從節點數據庫狀態不一致時,使之重新回到一致狀態。
同步階段
在 2.8 版本以前,slave 對 master 的同步,是通過 slave 向 master 發送 SYNC 命令完成的。
1)slave 向 master 發送 SYNC;
2)master 收到 SYNC 後,執行 BGSAVE 命令,生成包含當前數據庫狀態的 RDB 文件,同時自身使用一個 buffer 記錄從現在開始執行的所有改變其數據庫狀態的命令,RDB 生成完畢後將其發送給 slave;
3)slave 收到 RDB 文件後,載入數據,將自己的數據庫狀態更新至 master 執行 BGSAYE 時的狀態;
4)master 將 buffer 累積的命令發給 slave;
5)slave 解析 master 發來的命令並執行,將數據追至與 master 當前所處的狀態一致。
如果以上任一一步因爲網絡或者其他原因而中斷,當 slave 再次連上 master 時,master 仍然需要重新做一個 BGSAVE,而這個命令是通過 fork
子進程來做的,頻繁執行會影響性能,且複製效率低下。
爲解決以上問題,redis 從 2.8 版本開始,引入新的同步命令 PSYNC 以支持斷點續傳。
要支持斷點續傳,就需要記錄上次同步的位置,藉助了以下三個變量:
1)master/slave 的複製偏移量(replication offset);
2)master 的複製積壓緩衝區(replication backlog);
3)服務器的運行 ID(run ID)。
具體細節可以參考《redis 設計與實現》這本書的第 15 章。
命令傳播階段
在 redisServer
結構體中有一個 dirty
變量記錄了自上一次成功執行 save 或者 bgsave 之後,數據庫狀態改變的次數。通過比較執行命令前後 的 dirty
值,就可以知道當前命令執行後數據庫狀態是否發生了改變,只有改變了才需要做 command propagate。
void call(client *c, int flags) {
...
dirty = server.dirty;
start = ustime();
c->cmd->proc(c);
duration = ustime()-start;
dirty = server.dirty-dirty;
if (dirty < 0) dirty = 0;
...
if (dirty) propagate_flags |= (PROPAGATE_AOF|PROPAGATE_REPL);
...
if (propagate_flags != PROPAGATE_NONE)
propagate(c->cmd,c->db->id,c->argv,c->argc,propagate_flags);
...
}
void propagate(struct redisCommand *cmd, int dbid, robj **argv, int argc,
int flags)
{
if (server.aof_state != AOF_OFF && flags & PROPAGATE_AOF)
feedAppendOnlyFile(cmd,dbid,argv,argc);
if (flags & PROPAGATE_REPL)
replicationFeedSlaves(server.slaves,dbid,argv,argc);
}
對於主從複製的命令傳播,在 replicationFeedSlaves
函數中實現。
void replicationFeedSlaves(list *slaves, int dictid, robj **argv, int argc) {
listNode *ln;
listIter li;
int j, len;
char llstr[LONG_STR_SIZE];
// 如果 backlog buffer 爲空,且沒有 slave,直接返回
if (server.repl_backlog == NULL && listLength(slaves) == 0) return;
serverAssert(!(listLength(slaves) != 0 && server.repl_backlog == NULL));
// 如果 dictid 與上一次 repl 選擇的不一致,需要插入一條 select 命令
if (server.slaveseldb != dictid) {
robj *selectcmd;
......
}
server.slaveseldb = dictid;
// 將命令以 redis 協議的格式寫入 replication backlog
if (server.repl_backlog) {
char aux[LONG_STR_SIZE+3];
/* Add the multi bulk reply length. */
aux[0] = '*';
len = ll2string(aux+1,sizeof(aux)-1,argc);
aux[len+1] = '\r';
aux[len+2] = '\n';
feedReplicationBacklog(aux,len+3);
for (j = 0; j < argc; j++) {
// $..CRLF
long objlen = stringObjectLen(argv[j]);
aux[0] = '$';
len = ll2string(aux+1,sizeof(aux)-1,objlen);
aux[len+1] = '\r';
aux[len+2] = '\n';
feedReplicationBacklog(aux,len+3);
feedReplicationBacklogWithObject(argv[j]);
feedReplicationBacklog(aux+len+1,2); // CRLF
}
}
/* 將命令發送給所有的 slave. */
listRewind(server.slaves,&li);
while((ln = listNext(&li))) {
client *slave = ln->value;
/* Don't feed slaves that are still waiting for BGSAVE to start */
if (slave->replstate == SLAVE_STATE_WAIT_BGSAVE_START) continue;
// 以 redis 協議的格式發送給 slave
addReplyMultiBulkLen(slave,argc);
for (j = 0; j < argc; j++)
addReplyBulk(slave,argv[j]);
}
}
以上便是主從同步的兩個階段,更多相關代碼詳解請看後面的博客分析。