Redis 雙主實現
redis雙主設計
背景
目前redis僅支持主從復制模式,可以支持在線備份、讀寫分離等功能,實際應用中通常通過sentinel服務做主從切換的管理,這增加了管理的復雜度和維護成本,基于此360基礎架構組聯合DBA從redis內部實現了雙主功能。
主從復制介紹
redis支持樹形的主從異步復制,并具有非阻塞、部分同步等特性,下面簡單介紹下其實現原理以及目前redis主從復制模式在故障發生時的數據丟失情況。
實現原理
數據同步
slave開始連接到master時需要同步master已有的數據,具體過程如下圖所示,主要有以下幾個步驟:
-
slave向master發送PSYNC命令,將緩存的master的runid和reploff發送給master。
-
master根據slave發送的reploff判斷需要開始同步的數據是否在當前 緩沖區(積壓空間)中,如果在的話完成和slave的連接建立并向slave發送CONTINUE回復標志,開始發送積壓空間中的數據;否則,向 slave發送FULLSYNC標志并開啟子進程dump當前時刻的快照數據到本地rdb文件。
-
slave接收psync命令的返回值,并判斷是CONTINUE還是FULLSYNC,如果是CONTINUE則完成連接過程準備接收master數據;否則,觸發接收master dump的rdb文件事件,等待master發送rdb文件。
-
master將快照數據dump到本地文件后,向slave發送rdb文件
-
slave接收完master發送的rdb文件后,清空本地庫并重新加載接收的rdb文件,由于這時的數據變化較大,如果開啟了aof選項則還需進行aof rewrite操作
-
開始傳播master積壓空間中的新數據。
數據傳播
master實例會維護其slave實例列表,當有更改操作發生時,其會通過連接建立時創建的socket向所有slave實例發送操作命令進行 數據傳播,同時為了防止故障恢復slave重新連接master時每次都進行全量同步,master實例會內部維護一個緩沖區(積壓空間)來緩存部分 slave命令,master數據傳播的具體過程為:
-
寫入本地庫
-
寫入積壓空間
-
觸發寫入slave實例事件,進行異步傳播;如果此刻正在進行數據同步操作則將命令寫入緩沖區,待同步操作完成后再觸發向slave實例寫入事件。
數據丟失
redis主從模式并不能保證數據的100%完整,在網絡故障、主庫宕機等情況下提升slave為master時可能會丟失部分數據,如下圖所 示,假如在某個時刻master的數據還未完全傳播給slave時,master宕機等情況發生并將slave提升為master,這時原來master 未傳播的一部分數據將丟失。
雙主復制設計
前提假設
由于時間、精力等因素,目前我們在進行雙主設計時結合如下實際項目需求進行了一些設計折衷,后續我們會繼續完善相關設計。
-
上層保證某個時間點只有一個master在寫
-
故障時允許丟失少量數據
總體設計
為避免額外維護成本,雙主模塊完全在redis內部實現,雙主兩個實例各自創建一個socket進行彼此通信;加入雙主復制后不會影響原有的主從 復制模式,但如下圖所示,主從復制實例可以通過我們新增的doublemasterof命令轉化為雙主復制,雙主復制實例也可以通過原有的slaveof 命令轉換為主從復制實例。
同步策略
redis雙主實例網絡故障恢復或重啟等情況下會進行重新連接以同步彼此數據,主從模式下同步策略很簡單,只需要從庫同步主庫數據即可,而雙主模 式下我們必須根據一定的策略來選出一個實例作為數據同步的對象,我們考慮到兩種具體同步策略:基于數據量和基于時間戳的同步策略。
數據量同步策略
基于數據量的同步策略可以理解為數據量少的實例去同步數據量多的實例,這種同步策略在故障發生時數據已經全部傳播到另外一個實例的情況下,故障恢 復后可以保證數據完整性,但如下圖所示,假如原來操作在雙主A上執行,某一時刻雙主A上的操作還未同步到雙主B上發生了網絡故障,上層會切到B上繼續寫 入,當寫入了圖中紅色所示大小的數據后網絡故障恢復,這時進行數據同步時就有可能丟失A或B上的部分數據。
時間戳同步策略
對于基于時間戳的同步策略,我們會在redis內部維護雙主實例的最近更新時間戳,故障恢復進行數據同步時時間戳較舊的實例會同步時間戳較新的實 例;和基于數據量同步策略一樣當故障發生時如果數據已經全部傳播到另外實例則故障恢復后可以保證數據完整性,否則,如下圖所示故障恢復后將丟失雙主A實例 的部分數據。
我們的選擇
根據業務需求,我們需要保證最新寫入的數據不會丟失,所以具體實現上我們選擇了基于時間戳的同步策略。
同步實現
我們在redis原有的全同步,部分同步的基礎上增加了ignore resync策略以實現雙主同步,具體實現如下圖所示,有以下幾個步驟:
-
雙主實例A鏈接到另外一個實例B時會通過發送2mastersync命令,該命令會將A實例最后的更新時間戳發送給B
-
B實例判斷A的最后更新時間戳是否比自己新,如果比自己新則直接將A標示為 ONLINE并向A發送IGNORE runid reploff信息,A接收到IGNORE信息后記錄下runid reploff,即可完成與B的同步;否則,則按照主從同步方式進行全同步或部分同步。
數據傳播
對一個雙主實例的更改操作,redis內部會通過雙主實例建立連接時創建的socket異步傳播給另一個雙主實例,這里要解決的問題是要避免數據 再次傳播回來,具體實現上我們通過雙主實例的runid進行判斷,每個雙主實例內部會維護其另外一個實例的runid信息,當有更改命令要執行時,我們會 通過runid來判斷該命令是否是其雙主實例傳播過來的,如果是將不再回傳。
復制偏移量維護
redis主從實現機制上,當通信模塊接收到主庫的更改命令時會直接在從庫上增加其復制偏移量來記錄數據同步的位置,而對于雙主實例我們知道為避免數據循環傳播,雙主實例A傳播給雙主實例B的命令不會回傳過來,那么該如何維護其復制偏移量呢?設計上我們考慮了兩種策略:
策略一
如下圖所示,雙主A向雙主B傳播一條數據后,B會回復A一個ACK length確認,A接收到確認信息后將自己的復制偏移量增加length。
策略二
如下圖所示,雙主A再向B傳播數據之前自己主動增加復制偏移量,雙主B不會向雙主A回復確認信息
策略一對比策略二進一步保證了數據的完整性,但同時帶來了一定的網絡開銷,兩種策略都不能完全保證復制偏移量再網路故障下的正確性(策略一在ACK丟失的情況下無法保證復制偏移量正確),結合目前的需求為了不影響性能我們選擇了策略二。
小結
結合目前項目的需求我們在redis內部實現了雙主功能,但是也有一些需要改進的地方,歡迎大家提出意見,后面我們會不斷完善,敬請關注!