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

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

與之相關的,在 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。這也是爲了避免不必要的內存浪費。

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 函數實現的。

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。

在一個空的 repl_backlog 中插入 abcdef 時,各變量做如下更新:

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 爲 3

接着,插入數據 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 在全局範圍內的偏離量爲 5

接着,插入數據 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.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章