redis主從超時檢測

晴天哥發表於2018-06-17

 redis的主從超時檢測主要從以下三個方面進行判斷,分別是主監測從、從監測主、正常關閉。

  • 主監測從:slave定期傳送replconf ack offset命令到master來報告自己的存活狀況
  • 從監測主:master定期傳送ping命令或者
    命令到slave來報告自己的存貨狀況
  • 正常關閉:eventLoop監測埠關閉的nio事件

週期性傳送心跳命令

 定期執行函式serverCron內部會週期性的執行replicationCron方法,該方法內部執行的動作包括重連線主伺服器、向主伺服器傳送 ACK 、判斷資料傳送失敗情況、斷開本伺服器超時的從伺服器,向從伺服器傳送PING或者
命令

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
    clientsCron();

    // 對資料庫執行各種操作
    databasesCron();

    // 重連線主伺服器、向主伺服器傳送 ACK 、判斷資料傳送失敗情況、斷開本伺服器超時的從伺服器,等等
    run_with_period(1000) replicationCron();

    // 增加 loop 計數器
    server.cronloops++;

    return 1000/server.hz;
}

 replicationCron內部做的事情就是週期性的傳送心跳命令包括:

  • slave發往master的replconf ack offset,用於主檢測從的存活性
  • master發往slave的PING命令,用於從檢測主的存活性
  • master發往slave的
    命令,用於從檢測主的存活性
// 複製 cron 函式,每秒呼叫一次
void replicationCron(void) {
        
        // 定期向主伺服器傳送 ACK 命令
        if (server.masterhost && server.master &&
        !(server.master->flags & REDIS_PRE_PSYNC))
        replicationSendAck();
    
    /* 
     * 如果伺服器有從伺服器,定時向它們傳送 PING 。
     *
     * 這樣從伺服器就可以實現顯式的 master 超時判斷機制,
     * 即使 TCP 連線未斷開也是如此。
     */
    if (!(server.cronloops % (server.repl_ping_slave_period * server.hz))) {
        listIter li;
        listNode *ln;
        robj *ping_argv[1];

        /* First, send PING */
        // 向所有已連線 slave (狀態為 ONLINE)傳送 PING
        ping_argv[0] = createStringObject("PING",4);
        replicationFeedSlaves(server.slaves, server.slaveseldb, ping_argv, 1);
        decrRefCount(ping_argv[0]);

        /*
         * 向那些正在等待 RDB 檔案的從伺服器(狀態為 BGSAVE_START 或 BGSAVE_END)
         * 傳送 "
"
         *
         * 這個 "
" 會被從伺服器忽略,
         * 它的作用就是用來防止主伺服器因為長期不傳送資訊而被從伺服器誤判為超時
         */
        listRewind(server.slaves,&li);
        while((ln = listNext(&li))) {
            redisClient *slave = ln->value;

            if (slave->replstate == REDIS_REPL_WAIT_BGSAVE_START ||
                slave->replstate == REDIS_REPL_WAIT_BGSAVE_END) {
                if (write(slave->fd, "
", 1) == -1) {
                    /* Don`t worry, it`s just a ping. */
                }
            }
        }
    }
}

主監測從

 master收到slave的replconf命令的時候更新c->repl_ack_time,也就是代表收到slave傳送ack命令的時間。

/* REPLCONF <option> <value> <option> <value> ...
 * 由 slave 使用,在 SYNC 之前配置複製程式(process)
 * 目前這個函式的唯一作用就是,讓 slave 告訴 master 它正在監聽的埠號
 * 然後 master 就可以在 INFO 命令的輸出中列印這個號碼了。
 * 將來可能會用這個命令來實現增量式複製,取代 full resync 。
 */
void replconfCommand(redisClient *c) {
    for (j = 1; j < c->argc; j+=2) {
      else if (!strcasecmp(c->argv[j]->ptr,"ack")) {
            // 更新最後一次傳送 ack 的時間
            c->repl_ack_time = server.unixtime;
           
            return;
        } 
    }
    addReply(c,shared.ok);
}

 master從判斷當前時間和上一次ack時間來判斷slave的存,(server.unixtime – slave->repl_ack_time) > server.repl_timeout。如果超時就釋放和slave的連線。

// 複製 cron 函式,每秒呼叫一次
void replicationCron(void) {
    // 斷開超時從伺服器
    if (listLength(server.slaves)) {
        listIter li;
        listNode *ln;

        // 遍歷所有從伺服器
        listRewind(server.slaves,&li);
        while((ln = listNext(&li))) {
            redisClient *slave = ln->value;
            // 釋放超時從伺服器
            if ((server.unixtime - slave->repl_ack_time) > server.repl_timeout)
            {
                char ip[REDIS_IP_STR_LEN];
                int port;

                if (anetPeerToString(slave->fd,ip,sizeof(ip),&port) != -1) {
                    redisLog(REDIS_WARNING,
                        "Disconnecting timedout slave: %s:%d",
                        ip, slave->slave_listening_port);
                }
                
                // 釋放
                freeClient(slave);
            }
        }
    }

從監測主

 slave每次接收到master傳送過來的命令的時候都會更新client的上一次互動時間也就是c->lastinteraction,這裡的client c代表就是slave連線master的server.master的redis client物件。


/*
 * 讀取客戶端的查詢緩衝區內容
 */
void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
    
    // 讀入內容到查詢快取
    nread = read(fd, c->querybuf+qblen, readlen);

    if (nread) {
        // 記錄伺服器和客戶端最後一次互動的時間
        c->lastinteraction = server.unixtime;
    } 

    // 從查詢快取重讀取內容,建立引數,並執行命令
    // 函式會執行到快取中的所有內容都被處理完為止
    processInputBuffer(c);
}

 slave定期檢查當前時間和上一次互動時間的差值是否大於最大超時時間:(time(NULL)-server.master->lastinteraction) > server.repl_timeout,如果超時就斷開連線。

// 複製 cron 函式,每秒呼叫一次
void replicationCron(void) {

    // 嘗試連線到主伺服器,但超時
    if (server.masterhost &&
        (server.repl_state == REDIS_REPL_CONNECTING ||
         server.repl_state == REDIS_REPL_RECEIVE_PONG) &&
        (time(NULL)-server.repl_transfer_lastio) > server.repl_timeout)
    {
        redisLog(REDIS_WARNING,"Timeout connecting to the MASTER...");
        // 取消連線
        undoConnectWithMaster();
    }

    // RDB 檔案的傳送已超時?
    if (server.masterhost && server.repl_state == REDIS_REPL_TRANSFER &&
        (time(NULL)-server.repl_transfer_lastio) > server.repl_timeout)
    {
        redisLog(REDIS_WARNING,"Timeout receiving bulk data from MASTER... If the problem persists try to set the `repl-timeout` parameter in redis.conf to a larger value.");
        // 停止傳送,並刪除臨時檔案
        replicationAbortSyncTransfer();
    }

    // 從伺服器曾經連線上主伺服器,但現在超時
    if (server.masterhost && server.repl_state == REDIS_REPL_CONNECTED &&
        (time(NULL)-server.master->lastinteraction) > server.repl_timeout)
    {
        redisLog(REDIS_WARNING,"MASTER timeout: no data nor PING received...");
        // 釋放主伺服器
        freeClient(server.master);
    }

    // 嘗試連線主伺服器
    if (server.repl_state == REDIS_REPL_CONNECT) {
        redisLog(REDIS_NOTICE,"Connecting to MASTER %s:%d",
            server.masterhost, server.masterport);
        if (connectWithMaster() == REDIS_OK) {
            redisLog(REDIS_NOTICE,"MASTER <-> SLAVE sync started");
        }
    }
}

正常關閉

 判斷socket正常的關閉的途徑就是通過socket的read方法來判斷:

  • 讀取報文資料出錯
  • 讀取報文長度為0,這裡需要解釋下:
     1. TCP recv返回0, 說明對方關閉;
     2. 註冊EPOLLERR, 收到事件是關閉;
     3. recv/send 返回-1時, 如果錯誤不是EWOULDBLOCK或者EINTR, 也主動關閉連線。
void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
   
    // 讀入內容到查詢快取
    nread = read(fd, c->querybuf+qblen, readlen);

    // 讀入出錯
    if (nread == -1) {
        if (errno == EAGAIN) {
            nread = 0;
        } else {
            redisLog(REDIS_VERBOSE, "Reading from client: %s",strerror(errno));
            freeClient(c);
            return;
        }
    // 遇到 EOF
    } else if (nread == 0) {
        redisLog(REDIS_VERBOSE, "Client closed connection");
        freeClient(c);
        return;
    }
}


相關文章