Redis 複製實現原理

Float_Lu發表於2016-06-08

摘要

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

Redis複製實現原理

應用場景化

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

然後啟動一個redis客戶端和上面那臺監聽8000埠的Redis例項連線:

我們向redis寫一個資料:

於是我們可以假設以下場景:

首先我們先啟動redis例項,同時啟動一個客戶端連線這個例項:

這個時候slave是沒有資料的:

我們可以用下面命令來讓slave和master進行復制:

於是,slave就獲得了master上寫的資料了:

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

處理slaveof

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

1

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方法是redis封裝的一個幫助方法,最然serverCron的呼叫頻率很高,是1毫秒一次:

但是redis通過run_with_period實現了可以並不是每隔1毫秒必須要執行所有邏輯,run_with_period方法指定了具體的執行時間間隔。上面可以看出,redis主程式大概是1000毫秒也就是1秒鐘執行一次replicationCron邏輯,replicationCron做什麼事情呢,它做的事情很多,我們只關心本文的主線邏輯:

如果當前例項的複製狀態為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之間的握手協議,核心是賦值狀態遷移,我們可以用下面一張圖表示:

2

slave和msater的握手機制

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

當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:

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

如果找不到,不好意思,還是需要全量複製的,如果兩個條件都滿足,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:

然後RDB的檔案傳送由sendBulkToSlave處理器來完成,master對於RDB檔案傳送完畢之後會把slave的狀態設定為:online。這裡需要注意的是,在把slave設定為online狀態之後會註冊寫處理器,將堆積在reply的資料傳送給slave:

這部分的內容即為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之後才會將累積的資料傳送出去,這點有些不同。

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

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

RDB檔案傳輸協議

上面握手機制部分提到,當slave和master握手完畢之後註冊了個readSyncBulkPayload處理器,用於讀取master傳送過來的RDB檔案,RDB檔案通過TCP連線傳輸,本質上是一個資料流,slave端是如何區分當前傳輸方式是RDB disk方式還是diskless方式的呢?實際上對於不同的複製方式,資料傳輸協議也是不同的,假設我們把這個長長的RDB檔案流稱為RDB檔案報文,我們來看看兩種方式的不同協議格式:

3

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

打賞支援我寫出更多好文章,謝謝!

打賞作者

打賞支援我寫出更多好文章,謝謝!

任選一種支付方式

Redis 複製實現原理 Redis 複製實現原理

相關文章