寫在前面
在分散式環境中,資料副本 (Replica)
和複製 (Replication)
作為提升系統可用性和讀寫效能的有效手段被大量應用系統設計中,Redis
也不例外。Redis
作為單機資料庫使用時,適用常見有限且存在單點當機問題,無法維持高可用。因此 Redis
允許通過 SLAVEOF
命令或者 slaveof
配置項來讓一個 Redis server
複製另一個 Redis server
的資料集和狀態,我們稱之為主從複製,主伺服器下文稱 master
,從伺服器下文稱 slave
,Redis
採用非同步的複製機制。
主從複製機制的演變
從 Redis 2.6
到 4.0
開發人員對複製流程進行逐步的優化,以下是演進過程:
2.8
版本之前 Redis 複製採用sync
命令,無論是第一次主從複製還是斷線重連後再進行復制都採用全量同步,成本高2.8
~4.0
之間複製採用psync
命令,這一特性主要新增了Redis
在斷線重連時候可通過offset
資訊使用部分同步4.0
版本之後也採用psync
,相比於2.8
版本的psync
優化了增量複製,這裡我們稱為psync2
,2.8
版本的psync
可以稱為psync1
我們先介紹 psync1
的實現,再介紹 psync2
的優化點,至於舊版 sync
的機制本文不再贅述。
psync1
為了解決舊版 SYNC
在處理斷線重連複製場景下的低效問題,Redis 2.8
採用 PSYNC
代替 SYNC
命令。PSYNC
命令具有全量同步和部分同步兩種模式。
全量重同步
前者和 SYNC
大致相同,都是讓 master
生成併傳送 RDB
檔案,然後再將儲存在緩衝區中的寫命令傳播給 slave
來進行同步,相當於只有同步和命令傳播兩個階段。
部分重同步
部分同步適用於斷線重連之後的同步,slave
只需要接收斷線期間丟失的寫命令就可以,不需要進行全量同步。為了實現部分同步,引入了複製偏移量(offset)
、複製積壓緩衝區(replication backlog buffer)
和執行 ID (run_id)
三個概念。
複製偏移量
執行主從複製的雙方都會分別維護一個複製偏移量,master
每次向 slave
傳播 N
個位元組,自己的複製偏移量就增加 N
;同理 slave
接收 N
個位元組,自身的複製偏移量也增加 N
。通過對比主從之間的複製偏移量就可以知道主從間的同步狀態。
複製積壓緩衝區
複製積壓緩衝區是 master
維護的一個固定長度的 FIFO
佇列,預設大小為 1MB
。當 master
進行命令傳播時,不僅將寫命令發給 slave
還會同時寫進複製積壓緩衝區,因此 master
的複製積壓緩衝區會儲存一部分最近傳播的寫命令。當 slave
重連上 master
時會將自己的複製偏移量通過 PSYNC 命令發給 master
,master
檢查自己的複製積壓緩衝區,如果發現這部分未同步的命令還在自己的複製積壓緩衝區中的話就可以利用這些儲存的命令進行部分同步,反之如果斷線太久這部分命令已經不在複製緩衝區中了,那沒辦法只能進行全量同步。
執行 ID
令人疑惑的是上述邏輯看似已經很圓滿了,這個 run_id
是做什麼用呢?其實這是因為 master
可能會在 slave
斷線期間發生變更,例如可能超時失去聯絡或者當機導致斷線重連的是一個嶄新的 master
,不再是斷線前複製的那個了。自然嶄新的 master
沒有之前維護的複製積壓緩衝區,只能進行全量同步。因此每個 Redis server
都會有自己的執行 ID
,由 40
個隨機的十六進位制字元組成。當 slave
初次複製 master
時,master 會將自己的執行 ID 發給 slave
進行儲存,這樣 slave
重連時再將這個執行 ID
傳送給重連上的 master
,master
會接受這個 ID
並於自身的執行 ID
比較進而判斷是否是同一個 master
。
psync1 流程
如果 slave
以前沒有複製過任何 master,或者之前執行過 SLAVEOF NO ONE
命令,那麼 slave
在開始一次新的複製時將向主伺服器傳送 PSYNC ? -1
命令,主動請求 master
進行完整重同步(因為這時不可能執行部分重同步)。相反地,如果 slave
已經複製過某個 master,那麼 slave
在開始一次新的複製時將向 master
傳送 PSYNC runid offset
命令:其中 runid
是上一次複製的 master
的執行 ID
,而 offset
則是 slave
當前的複製偏移量,接收到這個命令的 master
會通過這兩個引數來判斷應該對 slave
執行哪種同步操作。
根據情況,接收到 PSYNC
命令的 master
會向 slave
返回以下三種回覆的其中一種:
如果 master
返回 +FULLRESYNC runid offset
回覆,那麼表示 master
將與 slave
執行完整重同步操作:其中 runid 是這個 master
的執行 ID
,slave
會將這個 ID
儲存起來,在下一次傳送 PSYNC
命令時使用;而 offset 則是 master
當前的複製偏移量,slave
會將這個值作為自己的初始化偏移量
如果 master
返回 +CONTINUE
回覆,那麼表示 master
將與 slave
執行部分同步操作,slave
只要等著 master
將自己缺少的那部分資料傳送過來就可以了
如果 master
返回 -ERR
回覆,那麼表示 master
的版本低於 Redis 2.8
,它識別不了 psync
命令,slave
將向 master
傳送 SYNC
命令,並與 master
執行完整同步操作
由此可見 psync
也有不足之處,當 slave
重啟以後 master
runid
發生變化,也就意味者 slave
還是會進行全量複製,而在實際的生產中進行 slave
的維護很多時候會進行重啟,而正是有由於全量同步需要 master
執行快照,以及資料傳輸會帶不小的影響。因此在 4.0
版本,psync
命令做了改進,我們稱之為 psync2
。
psync2
Redis 4.0
版本新增 混合持久化,還優化了 psync
(以下稱 psync2
),psync2
最大的變化支援兩種場景下的部分重同步,一個場景是 slave
提升為 master
後,其他 slave
可以從新提升的 master
進行部分重同步,這裡需要 slave
預設開啟複製積壓緩衝區;另外一個場景就是 slave
重啟後,可以進行部分重同步。這裡要注意和 psync1
的執行 ID
相比,這裡的複製 ID
有不一樣的意義。
優化細節
Redis 4.0
引入另外一個變數 master_replid 2
來存放同步過的 master
的複製 ID,同時複製 ID 在 slave
上的意義不同於之前的執行 ID,複製 ID 在 master
的意義和之前執行 ID
仍然是一樣的,但對於 slave
來說,它儲存的複製 ID
(即 replid
) 表示當前正在同步的 master
的複製 ID
。master_replid 2
則表示前一個 master
的複製 ID
(如果它之前沒複製過其他的 master
,那這個欄位沒用),這個在主從角色發生改變的時候會用到。
struct redisServer {
...
/* Replication (`master`) */
char replid[CONFIG_RUN_ID_SIZE+1]; /* My current replication ID. */
char replid2[CONFIG_RUN_ID_SIZE+1]; /* replid inherited from `master`*/
}
複製程式碼
slave
在意外關閉前會呼叫 rdbSaveInfoAuxFields
函式把當前的複製 ID
(即關閉前正在複製的 master
的 replid
,因為 slave
中的 replid
欄位儲存的是 master
的複製 ID
) 和複製偏移量一起儲存到 RDB
檔案中,後面該 slave
重啟的時候,就可以從 RDB
檔案中讀取複製 ID
和複製偏移量,然後重連上 master
後 slave
將這兩個值傳送給 master
,master
會如下判斷是否允許 psync
:
// 如果 `slave` 傳送過來的複製 ID 是當前 `master` 的複製 ID, 說明 `master` 沒變過
if (strcasecmp(master_replid, server.replid) &&
// 或者和現在的新 `master` 曾經屬於同一 `master`
(strcasecmp(master_replid, server.replid2) ||
// 但同步進度不能比當前 `master` 還快
psync_offset > server.second_replid_offset)) {
... ...
}
// 判斷同步進度是否已經超過範圍
if (!server.repl_backlog ||
psync_offset < server.repl_backlog_off ||
psync_offset > (server.repl_backlog_off + server.repl_backlog_histlen)) {
... ...
}
複製程式碼
另外當節點從 slave
提升為 master
後,會儲存兩個複製 ID(之前角色是 slave
的時候 replid2 沒用,現在要派上用場了),分別是 replid
和 replid 2
,其他 slave
複製的時候可以根據第二個複製 ID
來進行部分重同步。對應上述程式碼中第二行判斷的情況。
參考資料
寫在最後
這是一個不定時更新的、披著程式設計師外衣的文青小號,歡迎關注。