Redis複製實現原理

摘要

個人前一篇文章《淺析Redis複製》已經介紹了Redis複製相關特性,這篇文章主要在理解Redis複製相關源碼的基礎之上介紹Redis複製的實現原理。git

Redis複製實現原理

應用場景化

爲了更好地表達與理解,咱們先舉個實際應用場景例子來看看Redis複製是怎麼工做的,咱們先啓動一臺master:github

$ ./redis-server --port 8000

而後啓動一個redis客戶端和上面那臺監聽8000端口的Redis實例鏈接:redis

$ ./redis-cli -p 8000

咱們向redis寫一個數據:緩存

127.0.0.1:8000> set msg doni
OK
127.0.0.1:8000> get msg
"doni"

因而咱們能夠假設如下場景:網絡

咱們有一臺master實例master,master已經處於正常工做的狀態,接受讀寫請求,這個時候因爲單臺機器的壓力過大,咱們想再啓動一個Redis實例來分擔master的讀壓力,假設咱們新啓動的這個實例叫slave。

已知M1的IP爲127.0.0.1,端口爲:8000

首先咱們先啓動redis實例,同時啓動一個客戶端鏈接這個實例:架構

$ ./redis-server --port 8001 

$ ./redis-cli -p 8001

這個時候slave是沒有數據的:框架

127.0.0.1:8001> get msg
(nil)

咱們能夠用下面命令來讓slave和master進行復制:less

127.0.0.1:8001> slaveof 127.0.0.1 8000

因而,slave就得到了master上寫的數據了:異步

127.0.0.1:8001> get msg
"doni"

上面的例子和很直觀也很簡單,下面咱們就在腦海中緩存這個應用場景,來看看redis是如何實現複製的。socket

處理slaveof

咱們首先須要看看slave接收到客戶端的slaveof命令是如何處理的,下面是slave接收到客戶端的slaveof命令的處理流程圖:

slaveof命令處理流程圖

解釋下上圖,redis實例接收到客戶端的slaveof命令後的處理流程大體以下:

  1. 判斷當前模式是否爲cluster,若是是則不支持複製。
  2. 判斷命令是否爲slave'of no one,若是是,這代表客戶端把當前實例設置爲master。
  3. 若是客戶端指定了host和port,則將host和port設置爲當前的master信息。
  4. 將當前實例的複製狀態設置爲REPL_STATE_CONNECT。

除了上面的幾個大步驟以外,在第二步和第三步之間還作了下面一些事情:

  1. 釋放以前被阻塞的客戶端,這些一般是使用Redis阻塞列表而被阻塞的客戶端。
  2. 斷開當前實例的全部slave。
  3. 清除緩存的master信息。
  4. 釋放backlog,backlog是堆積環形緩衝區。
  5. 取消正在進行的握手過程。

上面就是Redis處理slaveof命令的大體流程,誒,好像並無作關於複製的事情誒。別急,若是看過個人另外一篇《Redis網絡架構及單線程模型》文章的同窗都應該知道redis的單線線程模型,這裏slaveof命令處理關鍵的一步已經將當前redis實例的複製狀態設置爲了REPL_STATE_CONNECT狀態,在redis的eventloop裏面天然會對處於這個狀態的redis實例進行處理。

鏈接master

複製異步處理的觸發邏輯一方面是I/O事件驅動的一部分,另外一方面就是eventloop對時間事件處理的一部分,其實也是定時任務,redis定時任務最外面一層是serverCron方法,serverCron方法囊括了其餘幾乎全部定時處理邏輯的入口,能夠列個不徹底列表以下:

  1. 過時key處理。
  2. 軟件watchdog。
  3. 更新統計信息。
  4. rehash。
  5. 觸發備份RDB文件或者AOF重寫邏輯。
  6. 客戶端超時處理。
  7. 複製邏輯。
  8. ……

咱們這裏只關心複製邏輯,調用代碼以下:

run_with_period(1000) replicationCron();

run_with_period方法是redis封裝的一個幫助方法,最然serverCron的調用頻率很高,是1毫秒一次:

if (aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
        serverPanic("Can't create event loop timers.");
        exit(1);
}

可是redis經過run_with_period實現了能夠並非每隔1毫秒必需要執行全部邏輯,run_with_period方法指定了具體的執行時間間隔。上面能夠看出,redis主進程大概是1000毫秒也就是1秒鐘執行一次replicationCron邏輯,replicationCron作什麼事情呢,它作的事情不少,咱們只關心本文的主線邏輯:

if (server.repl_state == REPL_STATE_CONNECT) {
   if (connectWithMaster() == C_OK) {
       serverLog(LL_NOTICE,"MASTER <-> SLAVE sync started");
   }
}

若是當前實例的複製狀態爲REPL_STATE_CONNECT,咱們就會嘗試着鏈接剛纔slaveof指定的master,鏈接master的主要實如今connectWithMaster裏面,connectWithMaster的邏輯相對簡單一些,大體作了下面三件事情:

  1. 和指定的master創建鏈接,獲取master的socket句柄,即fd。
  2. 註冊fd的讀寫事件,事件處理器爲syncWithMaster。
  3. 設置當前實例的複製狀態爲REPL_STATE_CONNECTING。

握手機制

上面已經註冊了當前實例和master的讀寫I/O事件即事件處理器,因爲I/O事件分離相關邏輯都由系統框架完成,也就是eventloop,所以咱們能夠直接看當前實例針對master鏈接的I/O處理實現部分,也就是syncWithMaster處理器。

syncWithMaster主要實現了當前實例和master之間的握手協議,核心是賦值狀態遷移,咱們能夠用下面一張圖表示:

slave和msater的握手機制

上圖爲slave在syncWithMaster階段作的事情,主要是和master進行握手,握手成功以後最後肯定複製方案,中間涉及到遷移的狀態集合以下:

#define REPL_STATE_CONNECTING 2 /* 等待和master鏈接 */
/* --- 握手狀態開始 --- */
#define REPL_STATE_RECEIVE_PONG 3 /* 等待PING返回 */
#define REPL_STATE_SEND_AUTH 4 /* 發送認證消息 */
#define REPL_STATE_RECEIVE_AUTH 5 /* 等待認證回覆 */
#define REPL_STATE_SEND_PORT 6 /* 發送REPLCONF信息,主要是當前實例監聽端口 */
#define REPL_STATE_RECEIVE_PORT 7 /* 等待REPLCONF返回 */
#define REPL_STATE_SEND_CAPA 8 /* 發送REPLCONF capa */
#define REPL_STATE_RECEIVE_CAPA 9 /* 等待REPLCONF返回 */
#define REPL_STATE_SEND_PSYNC 10 /* 發送PSYNC */
#define REPL_STATE_RECEIVE_PSYNC 11 /* 等待PSYNC返回 */
/* --- 握手狀態結束 --- */
#define REPL_STATE_TRANSFER 12 /* 正在從master接收RDB文件 */

當slave向master發送PSYNC命令以後,通常會獲得三種回覆,他們分別是:

  • +FULLRESYNC:很差意思,須要全量複製哦。
  • +CONTINUE:嘿嘿,能夠進行增量同步。
  • -ERR:很差意思,目前master還不支持PSYNC。

當slave和master肯定好複製方案以後,slave註冊一個讀取RDB文件的I/O事件處理器,事件處理器爲readSyncBulkPayload,而後將狀態設置爲REPL_STATE_TRANSFER,這基本就是syncWithMaster的實現。

處理PSYNC

全量仍是增量

咱們已經知道slave是怎麼同master創建鏈接,怎麼和master進行握手的了,那麼master那邊是什麼狀況呢,master在與slave握手以後,對於psync命令處理的祕密都在syncCommand方法裏面,syncCommand方法實際包括兩個命令處理的實現,一個是sync,一個是psync。咱們繼續看看,master對slave的psync的請求處理,若是當前請求不知足psync的條件,則須要進行全量複製,知足psync的條件有兩個,一個是slave帶來的runid是否爲當前master的runid:

if (strcasecmp(master_runid, server.runid)) {
        //若是slave帶來的runid「?」,說明slave想要強制走全量複製
        if (master_runid[0] != '?') {
            serverLog(LL_NOTICE,"Partial resynchronization not accepted: "
                "Runid mismatch (Client asked for runid '%s', my runid is '%s')",
                master_runid, server.runid);
        } else {
            serverLog(LL_NOTICE,"Full resync requested by slave %s",
                replicationGetSlaveName(c));
        }
        goto need_full_resync;
    }

若是不是,則須要全量同步。第二個條件即當前slave帶來的複製offset,master在backlog中是否還能找到:

if (getLongLongFromObjectOrReply(c,c->argv[2],&psync_offset,NULL) !=
       C_OK) goto need_full_resync;
    if (!server.repl_backlog ||
        psync_offset < server.repl_backlog_off ||
        psync_offset > (server.repl_backlog_off + server.repl_backlog_histlen))
    {
        if (psync_offset > server.master_repl_offset) {
         //警告:slave帶過來的offset不知足增量複製的條件
        }
        goto need_full_resync;
    }

若是找不到,很差意思,仍是須要全量複製的,若是兩個條件都知足,master會告訴slave能夠增量複製,回覆+CONTINUE消息。

複製是否正在進行

若是在當前slave執行復制請求以前,剛好已經有其餘的slave已經請求過了,且master這個時候正在進行子進程傳輸(包括RDB文件備份和socket傳輸),那麼分下面兩種狀況處理:

  1. 若是複製方式是RDB disk方式,則找到當前master狀態爲SLAVE_STATE_WAIT_BGSAVE_END的slave,複製這個slave的offset到當前slave的offset,這是爲了當子進程完成RDB文件備份以後, 當前請求複製的slave能夠和以前的slave一塊兒進行master的複製操做。
  2. 若是複製方式是Diskless方式,則當前進來的slave並不會向上面那個slave這麼幸運了,由於基於socket的複製已經正在進行了,當前slave只能參與下一輪的子進程複製,且狀態爲SLAVE_STATE_WAIT_BGSAVE_START。

若是沒有子進程正在複製,這裏針對RDB disk方式和diskless方式,又要分兩種狀況討論:

  1. 若是是RDB disk方式,則啓動子進程進行RDB文件備份。
  2. 若是是diskless方式,則等待一段時間,也是爲了儘量讓後面的具備複製請求的slave一塊兒進來,參與這一輪複製,複製開始由定時任務異步啓動複製。

子進程結束後處理

RDB disk方式,當子進程備份RDB文件完畢,何時開始發送給slave的呢?diskless方式當子進程傳輸完畢,接下來又作什麼呢?對於RDB disk的方式,這裏涉及到一個I/O事件註冊的過程,也是由serverCron驅動的,當子進程結束以後,主進程會得知,而後經過backgroundSaveDoneHandler處理器來進行處理,針對RDB disk類型和diskless類型的複製,處理邏輯是不同的,咱們分別來看看。

RDB disk方式後處理

對於RDB disk複製方式,後處理主要是註冊向slave發送RDB文件的處理器sendBulkToSlave:

if (aeCreateFileEvent(server.el, slave->fd, 
                    AE_WRITABLE, sendBulkToSlave, slave) == AE_ERR) {
                    freeClient(slave);
                    continue;
 }

而後RDB的文件發送由sendBulkToSlave處理器來完成,master對於RDB文件發送完畢以後會把slave的狀態設置爲:online。這裏須要注意的是,在把slave設置爲online狀態以後會註冊寫處理器,將堆積在reply的數據發送給slave:

if (aeCreateFileEvent(server.el, slave->fd, AE_WRITABLE,
        sendReplyToClient, slave) == AE_ERR) {
        freeClient(slave);
        return;
    }

這部分的內容即爲RDB文件開始備份到發送給slave結束這段時間的增量數據,所以須要註冊I/O事件處理器,將這段時間累積的內容發送給slave,最終保持數據一致。

diskless方式後處理

diskless方式的後處理不一樣的是當子進程結束的時候,其實RDB文件已經傳輸完成了,並且其中作了些事情:

  • 當slave經過接受完RDB文件以後發送一個REPLCONF ACK給master。
  • master接收到slave的REPLCONF ACK以後,開始將緩存的增量數據發送給slave。

所以這裏不會註冊sendBulkToSlave處理器,只須要將slave設置爲online便可。咱們還能夠發現不一樣的一點,對於累積部分的數據處理,RDB disk方式是由master主動發送給slave的,而對於diskless方式,master收到slave的REPLCONF ACK以後纔會將累積的數據發送出去,這點有些不一樣。

當子進程結束,後處理的過程當中還要考慮到一種狀況:

不管是RDB disk方式仍是diskless方式,若是複製已經開始了,後來的slave須要同master複製,這部分的slave怎麼辦呢

怎麼辦呢,對於這類slave,slave的複製狀態爲SLAVE_STATE_WAIT_BGSAVE_START,語義上表示當前slave等待複製的開始,對於這種狀況,Redis會直接啓動子進程開始預備下一輪複製。

RDB文件傳輸協議

上面握手機制部分提到,當slave和master握手完畢以後註冊了個readSyncBulkPayload處理器,用於讀取master發送過來的RDB文件,RDB文件經過TCP鏈接傳輸,本質上是一個數據流,slave端是如何區分當前傳輸方式是RDB disk方式仍是diskless方式的呢?實際上對於不一樣的複製方式,數據傳輸協議也是不一樣的,假設咱們把這個長長的RDB文件流稱爲RDB文件報文,咱們來看看兩種方式的不一樣協議格式:

RDB文件傳輸協議

上面有兩種報文協議,第一種爲RDB disk方式的RDB文件報文傳輸協議,TCP流以"$"開始,而後緊跟着報文的長度,以換行符結束,這樣slave客戶端讀取長度以後就知道要從TCP後續的流中讀取多少內容就算結束了。第二種爲diskless複製方式的RDB文件報文傳輸協議,以"$EOF:"開頭,緊跟着40字節長度的隨機16進制字符串,RDB文件結尾也緊跟着一樣的40字節長度的隨機16進制字符串。slave客戶端分別由TCP數據流的頭部來判斷複製類型,而後根據不一樣的協議去解析RDB文件,當RDB文件傳輸完成以後,slave會將RDB文件保存在本地,而後載入,這樣slave就基本和master保持同步了。

總結

本文主要在瞭解Redis複製源碼的基礎之上介紹Redis複製的實現原理及一些細節,但願對你們有幫助。

注:本文由做者原創,若有疑問請聯繫做者。

redis複製源碼註釋地址:

https://github.com/ericbbcc/redis/blob/comments/src/replication.c

相關文章
相關標籤/搜索