Redis 源碼分析之主從複製(2)

repl backlog 是一個由 master 維護的固定長度的環形 buffer,默認大小 1M,在配置文件中能夠經過 repl-backlog-size 項進行配置。能夠把它當作一個 FIFO 的隊列,當隊列中元素過多時,最先進入隊列的元素被彈出(數據被覆蓋)。它爲了解決上一篇博客中提到的舊版本主從複製存在的問題而存在的。redis

與之相關的,在 redisServer 中涉及到不少以 repl 爲前綴的變量,這個只列舉幾個,函數

// 全部 slave 共享一份 backlog, 只針對部分複製
char *repl_backlog; 

// backlog 環形 buffer 的長度
long long repl_backlog_size;

// backlog 中有效數據大小, 開始時 <repl_backlog_size,但 buffer 滿後一直 =repl_backlog_size
long long repl_backlog_histlen;

// backlog 中的最新數據末尾位置(從這裏寫數據到 backlog)
long long repl_backlog_idx;

// 最老數據首字節位置,全局範圍內(而非積壓隊列內)的偏移量(從這裏讀 backlog 數據)
long long repl_backlog_off;

建立 backlog

void syncCommand(client *c) {
      // ...
      if (listLength(server.slaves) == 1 && server.repl_backlog == NULL)
        createReplicationBacklog();
    return;
}

能夠看到,在 SYNCPSYNC 命令的實現函數 syncCommand 末尾,只有當實例只有一個 slave,且 repl_backlog 爲空時,會調用 createReplicationBacklog 函數去建立 backlog。這也是爲了不沒必要要的內存浪費。this

void createReplicationBacklog(void) {
    serverAssert(server.repl_backlog == NULL);
    // 默認大小爲 1M
    server.repl_backlog = zmalloc(server.repl_backlog_size);
    server.repl_backlog_histlen = 0;
    server.repl_backlog_idx = 0;
  
    // 確保以前使用過 backlog 的 slave 引起錯誤的 PSYNC 操做
    server.master_repl_offset++;
    
    // 儘管沒有數據
    // 但事實上,第一個字節的邏輯位置是 master_repl_offset 的下一個字節
    server.repl_backlog_off = server.master_repl_offset+1;
}

寫數據到 backlog

將數據放入 repl backlog 是經過 feedReplicationBacklog 函數實現的。spa

void feedReplicationBacklog(void *ptr, size_t len) {
    unsigned char *p = ptr;

    // 全局複製偏移量更新
    server.master_repl_offset += len;

    // 環形 buffer ,每次寫儘量多的數據,並在到達尾部時將 idx 重置到頭部
    while(len) {
        // repl_backlog 剩餘長度
        size_t thislen = server.repl_backlog_size - server.repl_backlog_idx;
        if (thislen > len) thislen = len;

        // 從 repl_backlog_idx 開始,copy thislen 的數據
        memcpy(server.repl_backlog+server.repl_backlog_idx,p,thislen);

        // 更新 idx ,指向新寫入的數據以後
        server.repl_backlog_idx += thislen;

        // 若是 repl_backlog 寫滿了,則環繞回去從 0 開始
        if (server.repl_backlog_idx == server.repl_backlog_size)
            server.repl_backlog_idx = 0;
        len -= thislen;
        p += thislen;
      
        // 更新 repl_backlog_histlen
        server.repl_backlog_histlen += thislen;
    }
    // repl_backlog_histlen 不可能超過 repl_backlog_size,由於以後環形寫入時會覆蓋開頭位置的數據
    if (server.repl_backlog_histlen > server.repl_backlog_size)
        server.repl_backlog_histlen = server.repl_backlog_size;

    server.repl_backlog_off = server.master_repl_offset -
                              server.repl_backlog_histlen + 1;
}

以上函數中許多關鍵變量的更新邏輯比較抽象,下面畫個圖以輔助理解。n8Qbyd.jpg日誌

master_repl_offset 爲全局複製偏移量,它的初始值是隨機的,假設等於 2。code

在一個空的 repl_backlog 中插入 abcdef 時,各變量作以下更新:server

master_repl_offset = 2 + 6 = 8
repl_backlog_idx = 0 + 6 = 6 ≠ 10
repl_backlog_histlen = 0 + 6 = 6 < 10
repl_backlog_off = 8 - 6 + 1 = 3 (最老數據 a 在全局範圍內的 offset 爲 3blog

接着,插入數據 ghijkl,從上圖能夠看出, repl_backlog 滿了,所以前面有 2 個數據被覆蓋了。各變量作以下更新:隊列

master_repl_offset = 8 + 6 = 14
repl_backlog_idx = 6 + 4 = 10 → 0 + 2 = 2 (分兩步)
repl_backlog_histlen = 6 + 4 = 10 → 10 + 2 = 12 > 10 → 10
repl_backlog_off = 14 - 10 + 1 = 5 (最老的數據 c 在全局範圍內的偏離量爲 5ip

接着,插入數據 mno,各變量作以下更新,

master_repl_offset = 14 + 3 = 17
repl_backlog_idx = 2 + 3 = 5
repl_backlog_histlen = 10 + 3 = 13 > 10 → 10
repl_backlog_off = 17 - 10 + 1 = 8 (最老的數據 f 在全局範圍內的偏離量爲 8

從 backlog 讀數據

當 slave 連上 master 後,會經過 PSYNC 命令將本身的複製偏移量發送給 master,格式爲 PSYNC <psync_runid> <psync_offset>。當首次創建鏈接時,psync_runid 值爲 ?,psync_offset 值爲 -1。這部分的實現邏輯在 slaveTryPartialResynchronization 函數,下一篇博客會有詳解。

master 根據收到的 psync_offset 值來判斷是進行部分重同步仍是徹底重同步,如下只看部分重同步的邏輯,完整邏輯在後面的博客中分析。

int masterTryPartialResynchronization(client *c) {
        // ...
      if (getLongLongFromObjectOrReply(c,c->argv[2],&psync_offset,NULL) !=
       C_OK) goto need_full_resync;
    psync_len = addReplyReplicationBacklog(c,psync_offset);
      // ...
}

讀取 backlog 數據的邏輯在 addReplyReplicationBacklog 函數中實現。

long long addReplyReplicationBacklog(client *c, long long offset) {
      // ....
      if (server.repl_backlog_histlen == 0) {
        serverLog(LL_DEBUG, "[PSYNC] Backlog history len is zero");
        return 0;
    }
    // ...
      // 計算須要跳過的數據長度
    skip = offset - server.repl_backlog_off;
    
    //  將 j 指向 backlog 中最老的數據(在 backlog 中的位置)
    j = (server.repl_backlog_idx +
        (server.repl_backlog_size-server.repl_backlog_histlen)) %
        server.repl_backlog_size;
  
    // 加上要跳過的 offset
      j = (j + skip) % server.repl_backlog_size;
    // 要發送數據的總長度
      len = server.repl_backlog_histlen - skip;
    serverLog(LL_DEBUG, "[PSYNC] Reply total length: %lld", len);
    while(len) {
        long long thislen =
            ((server.repl_backlog_size - j) < len) ?
            (server.repl_backlog_size - j) : len;

        serverLog(LL_DEBUG, "[PSYNC] addReply() length: %lld", thislen);
        // 從 backlog 的 j 這個位置開始發送數據
        addReplySds(c,sdsnewlen(server.repl_backlog + j, thislen));
        len -= thislen;
        // j 切換到 0 (有可能數據還沒發送完)
        j = 0;
    }
    return server.repl_backlog_histlen - skip;
}

很差理解的是從 backlog 中的哪裏開始發送數據給 slave,上面代碼中有兩處計算邏輯,我認爲主要是第一處,能夠分狀況進行拆解。
1)當 backlog 中有效數據充滿了整個 backlog 時,即 backlog 被徹底利用,計算退化成
j = server.repl_backlog_idx % server.repl_backlog_size,因爲 repl_backlog_idx 不可能大於server.repl_backlog_size,因此計算結果就等於 server.repl_backlog_idx,它是讀寫數據的分割點。
2)當 backlog 中尚有未使用的空間時,repl_backlog_idx 等於 server.repl_backlog_histlen,計算退化成 server.repl_backlog_size % server.repl_backlog_size = 0
我以爲這部分邏輯徹底能夠簡化點,否則還真很差理解。而後,後面就是加上 skip offset 的計算。

另外,發送數據時須要注意,上面所說的第 1)種狀況下,idx 在 backlog 中間,分兩次發送,即

n8wK1I.jpg

這時,會在 master 上看到日誌以下日誌,
Partial resynchronization request from xxx accepted. Sending xxx bytes of backlog starting from offset xxx.

相關文章
相關標籤/搜索