Redis replication 中的探活

happen發表於2022-03-20

redis 在執行過程中需要一些探活機制來保證對另一端的感知能力。

slave 重建 replication 階段

當由於網路或其他原因導致主從 link 斷開後,slave 會嘗試重建 replication 。在這個過程中,slave 的複製狀態機 repl_state 變數會經過一系列流傳,最終為 REPL_STATE_CONNECTED 狀態。

repl_state 在很多狀態的停留時間都有超時設定,以便出錯後儘早是否資源。server.repl_transfer_lastio 變數起到了計時器的作用,它記錄了slave 上一次從 master 進行 io 互動(即讀寫事件)的時間。

REPL_STATE_CONNECTING 超時

REPL_STATE_CONNECTING 階段,slave 會主從 connet master,該過程使用了非阻塞 IO,在replicationCron 函式裡週期性檢查是否超時。

/* Non blocking connection timeout? */
if (server.masterhost &&
    (server.repl_state == REPL_STATE_CONNECTING ||
     slaveIsInHandshakeState()) &&
     (time(NULL)-server.repl_transfer_lastio) > server.repl_timeout)
{
    serverLog(LL_WARNING,"Timeout connecting to the MASTER...");
    cancelReplicationHandshake();
}

REPL_STATE_TRANSFER 超時

REPL_STATE_TRANSFER 階段,slave 會從 master 接收 rdb 檔案,通常不會在一次 read 裡完成,所以需要在 replicationCron 函式裡週期性檢查該過程是否超時。

/* Bulk transfer I/O timeout? */
if (server.masterhost && 
    server.repl_state == REPL_STATE_TRANSFER &&
    (time(NULL)-server.repl_transfer_lastio) > server.repl_timeout)
{
    cancelReplicationHandshake();
}

如果 rdb 資料量過大,可能需要做一些額外處理,下面進行說明。

1)如果 master dump rdb 時間過長,在 slave 側,master client 遲遲沒有發來資料,回撥函式 readSyncBulkPayload 不會觸發,那麼 repl_transfer_lastio 變數始終得不到重新整理,會在超時檢查中 cancelReplicationHandshake,導致此次主從同步的失敗。因此,master 對於正在等待接收 rdb 的 slave,會週期性地傳送 \n 做探活。

// master 處理邏輯
listRewind(server.slaves,&li);
while((ln = listNext(&li))) {
    client *slave = ln->value;

    int is_presync =
        (slave->replstate == SLAVE_STATE_WAIT_BGSAVE_START ||
        (slave->replstate == SLAVE_STATE_WAIT_BGSAVE_END &&
         server.rdb_child_type != RDB_CHILD_TYPE_SOCKET));

    if (is_presync) {
        if (write(slave->fd, "\n", 1) == -1) {
            /* Don't worry about socket errors, it's just a ping. */
        }
    }
}

2)在 slave 接收到 rdb 檔案後,如果 rdb 過大,載入過程會 hang 住,repl_ack_time 變數得不到重新整理,讓 master 以為 slave 掛掉了,因此,在rdbLoadRio 時定期傳送 \n 做探活。

// slave 處理邏輯
if (server.masterhost && server.repl_state == REPL_STATE_TRANSFER)
    replicationSendNewlineToMaster();
...

void replicationSendNewlineToMaster(void) {
    static time_t newline_sent;
    if (time(NULL) != newline_sent) {
        newline_sent = time(NULL);
        if (write(server.repl_transfer_s,"\n",1) == -1) {
            /* Pinging back in this stage is best-effort. */
        }
    }
}

master 收到 \n 後在 processInlineBuffer 函式中重新整理 client->repl_ack_time 時間,防止 client 檢測超時。

// master 處理邏輯
if (querylen == 0 && getClientType(c) == CLIENT_TYPE_SLAVE)
    c->repl_ack_time = server.unixtime;

master-slave 正常 replication 階段

當主從正常進行 replication 時,master 會向 slave 持續傳送 commands stream,以維持主從 dataset 狀態的一致。

master 與 slave 在此過程中會傳送一些探活包,以感知主從複製的狀態。

slave 探活

slave 的探活依賴於 client.lastinteraction 變數,它記錄了本例項上一次從該 client fd 讀到資料的時間,在 read 事件回撥函式 readQueryFromClient 中更新。

我們知道,在 master 與 slave 看來,對方都是一種攜帶特殊 flag 的 client。

replicationCron 函式裡會檢查 master 是否正常。若異常,則釋放 master client。

// slave 處理邏輯
if (server.masterhost && server.repl_state == REPL_STATE_CONNECTED &&
    (time(NULL)-server.master->lastinteraction) > server.repl_timeout)
{
    serverLog(LL_WARNING,"MASTER timeout: no data nor PING received...");
    freeClient(server.master);
}

client.lastinteraction 變數的更新需要依賴於 master 的處理邏輯。
對於自己所認識的 slave 節點,master 會週期性地傳送 ping 命令,這個週期由配置引數 repl-ping-replica-period 決定,單位為 s。

// master 處理邏輯
if ((replication_cron_loops % server.repl_ping_slave_period) == 0 &&
    listLength(server.slaves))
{
    int manual_failover_in_progress =
        server.cluster_enabled &&
        server.cluster->mf_end &&
        clientsArePaused();

    // 跳過處於 mf 過程中的 slave 
    if (!manual_failover_in_progress) {
        ping_argv[0] = createStringObject("PING",4);
        replicationFeedSlaves(server.slaves, server.slaveseldb,
            ping_argv, 1);
        decrRefCount(ping_argv[0]);
    }
}

slave 收到 pong 回覆後重新整理 lastinteraction 值,並每秒進行超時檢查。

master 探活

master 的探活依賴於 client.repl_ack_time 變數,它記錄了上一次收到 slave 的 REPLCONF 命令的時間。

replicationCron 函式裡會檢查 slave 是否正常。若異常,則釋放 slave client。

// master 處理邏輯
/* Disconnect timedout slaves. */
if (listLength(server.slaves)) {
    listIter li;
    listNode *ln;

    listRewind(server.slaves,&li);
    while((ln = listNext(&li))) {
        client *slave = ln->value;
        
        // 注意:這裡是 SLAVE_STATE_ONLINE 狀態的 slave,
        // 必然將 slave fd 掛上了寫事件回撥。
        if (slave->replstate != SLAVE_STATE_ONLINE) continue;
        if (slave->flags & CLIENT_PRE_PSYNC) continue;
        if ((server.unixtime - slave->repl_ack_time) > server.repl_timeout)
        {
            serverLog(LL_WARNING, "Disconnecting timedout replica: %s",
                replicationGetSlaveName(slave));
            freeClient(slave);
        }
    }
}

為什麼 master 的探活不像 slave 那樣通過 ping 完成呢?
這是因為 master 需要知道它所認識的每個 slave 的 repl offset,因此,slave 每秒傳送 REPLCONF ACK <reploff>(offset 值取自 client->reploff 變數,在 slave processCommand 後更新),這同樣達到了 ping 的目的。

// slave 處理邏輯
if (server.masterhost && server.master &&
    !(server.master->flags & CLIENT_PRE_PSYNC))
    replicationSendAck();

master 收到 REPLCONF 命令後,會重新整理 repl_ack_time 值,並每秒進行超時檢查。

cluster 模式下的探活

redis cluster 模式下的探活,通過 cluster gossip 訊息進行平等地探活,在之前的文章裡有進行詳細地說明。

相關文章