Redis系列1:深刻理解高性能Redis的本質
Redis系列2:數據持久化提高可用性
1 主從復制介紹
上一篇《Redis系列2:數據持久化提高可用性》中,我們介紹了Redis中的數據持久化技術,包括 RDB快照 和 AOF日志 。有了這兩個利器,我們再也不用擔心機器宕機,數據丟失了。
但是持久化技術只是解決了Redis服務故障之后,快速數據恢復的問題。並沒有從根本上提升Redis的可用性,我們需要的是保障Redis的高可用,減少甚至避免Redis服務發生宕機的可能。
目前實現Redis高可用的模式主要有三種: 主從模式、哨兵模式、集群模式。這些我們后面會分成三篇介紹,今天我們先來聊一下主從模式。
Redis 提供的主從模式,是通過復制的方式,將主服務器上的Redis的數據同步復制一份到從 Redis 服務器,這種做法很常見,MySQL的主從也是這么做的。
主節點的Redis我們稱之為master,從節點的Redis我們稱之為slave,主從復制為單向復制,只能由主到從,不能由從到主。可以有多個從節點,比如1主3從甚至n從,從節點的多少根據實際的業務需求來判斷。
2 主從數據一致性保證
為了保證主服務器Redis的數據和從服務器Redis的數據的一致性,也為了分擔訪問壓力,均衡負載,應用層面一般采取讀寫分離的模式。
讀操作:主、從庫都可以執行,一般是在從庫上讀數據,對實時性和准確性有100%高真要求的部分業務,可以謹慎評估之后讀主庫;
寫操作:只在主庫上寫數據,寫完之后將寫操作指令同步到從庫。
如下圖:
2.1 為何采用讀寫分離模式?
讀寫分離模式的使用跟MySQL做讀寫分離的初衷是一樣的。因為我們已經划分了主從庫,而且從庫的數據是由主庫單向復制的。如果主從庫都可以執行寫指令,那么在高頻並發場景下對不同的副本數據做修改,操作會具有無序性,極易導致各副本產生數據不一致,這是分布式模式的弊病。 如果非要保證數據的強一致性,Redis 需要加鎖處理,或者使用隊列順序執行,這樣勢必降低Redis的性能,降低服務的吞吐能力,這就不是高性能Redis所能接受的。
2.2 主從復制還有其他作用么?
- 故障隔離和恢復:無論主節點或者從節點宕機,其他節點依然可以保證服務的正常運行,並可以手動切換主從。
- 讀寫隔離:Master 節點提供寫服務,Slave 節點提供讀服務,分攤流量壓力,均衡流量的負載。
- 提供高可用保障:主從模式是高可用的最基礎版本,也是哨兵模式和 cluster模式實施的前置條件。
3 搭建主從復制
主從復制的開啟,完全是在從節點配置和發起的,不需要我們在主節點做任何事情。
可以通過 replicaof(Redis 5.0 之前使用 slaveof)命令形成主庫和從庫的關系。在從節點開啟主從復制,有 3 種方式:
說明:masterip:主機IP,masterport:主機端口號
3.1 配置文件方式
在從服務器的配置文件中加入
replicaof <masterip> <masterport>
3.2 啟動命令方式
redis-server 啟動命令后面加入
--replicaof <masterip> <masterport>
3.3 通過在客戶端使用命令
啟動多個 Redis 實例后,直接通過客戶端執行命令:
replicaof <masterip> <masterport>
則該 Redis 實例成為從節點。
假設現在有主實例 (192.168.0.1:6379)、從實例 A(192.168.0.2:6380)和 從實例 B (192.168.0.3:6381),在從實例上分別執行以下命令,就成為了Slave,主實例成為 Master。
# redis 5.0之前
slaveof 192.168.0.1 6379
# redis 5.0之后
replicaof 192.168.0.1 6379
4 主從復制原理
主從庫模式開啟之后,應用層面采用讀寫分離,所有數據的寫操作只會在主庫上進行,而讀操作基本會在從庫上面進行(特殊情況下部分讀業務允許走主庫)。
主從會保持最終一致性:主庫有了數據更新之后,會立即同步給從庫,來保證主從庫的數據的一致的。
4.1 主從庫的同步步驟
那主從庫同步是如何完成的呢?一次性傳輸么,那樣數據會不會太大?分批傳遞么,那樣時效性會不會有問題?故障時候數據會不會丟失?重新連接之后中間產生的差額數據怎么補充才能保證一致性?帶着這些疑問我們繼續來分析下。
綜合上面的問題來看同步,會有三種重要場景:
- 首次配置完成主從庫之后的全量復制
- 主從正常運行期間,准實時同步
- 主從庫間網絡斷開重連,Append增量數據 + 准實時同步
4.1.1 主從庫第一次全量復制
主從庫第一次復制過程大體可以分為 3 個階段:准備階段(即建立連接准備)、主庫同步數據到從庫階段、發送同步期間增量指令到從庫的階段。
我們來看這張完整的流轉圖,從整體上有個認識。
4.1.1.1 建立連接
這個階段的主要作用是建立主從之間的連接,連接成立之后,才能夠做數據全量同步。主要包含如下步驟:
- 從節點的配置文件中的 replicaof 配置項中配置了主節點的 IP 和 port ,配置完成之后,從節點就知道要跟哪個主節點進行連接。
- 當連接成功之后,從庫開啟replicaof 操作,同時發送psync指令告訴主庫,我准備開始同步了。命令包含了主庫的 runID 和 復制進度 offset 兩個參數。
- runID:每個 Redis 實例啟動都會自動生成一個 唯一標識 ID,第一次主從復制,還不知道主庫 runID,所以參數會默認設置為:?。
- offset:因為第一次復制,沒有偏移量,所以默認設置為 -1,這樣就默認從第1條指令開始復制。
- 主庫收到 psync 命令后根據參數啟動復制,使用 FULLRESYNC 響應命令,同時帶上兩個參數:主庫 runID 和主庫目前的復制進度 offset,返回給從庫。
- 從庫收到響應后,記錄下這兩個參數。
4.1.1.2 主庫同步數據給從庫
第二階段
master 執行 bgsave命令生成 RDB 文件,並將文件發送給從庫,從庫收到 RDB 文件后保存到磁盤,清空當前Redis庫中的數據,再將 RDB 文件數據加載到內存中。
同時主庫為每一個 slave 開辟一塊 replication buffer 緩沖區記錄,用於記錄主庫生成 RDB 文件后那段時間(那段時間的產生的寫命令沒有被記錄到RDB文件中,但是主庫又會源源不斷的接收到新的請求指令,記錄緩沖區是為了保證數據不丟失)產生的所有寫指令。
4.1.1.3 發送新寫命令到從庫
第三階段
從第二階段我們可以知道,生成 RDB 文件之后,后續的操作指令並沒有被記錄,為了保證Redis主從庫數據的一致性,主庫會在內存中創建 replication buffer ,記錄 RDB 文件生成后的所有操作指令。
而從庫在接收完RDB主數據,先清空當前從庫數據,然后完成數據初始化。整個初始化工作完成之后,繼續執行從replication buffer 緩沖區發送過來的數據,避免數據斷層。
★ 主數據同步到從庫的過程中,主庫不會被阻塞,可以正常處理其他任意操作,這也是Redis保證高性能的必備條件。
replication buffer
緩沖區創建在 master 主庫上,存放的數據是下面三個時間內 master 數據的所有寫操作。
- master 執行 bgsave 生產 rdb 的期間的寫操作;
- master 傳輸 rdb 文件到 slave 期間的寫操作;
- slave 加載 rdb 文件將數據初始化到內存期間的寫操作。
三個步驟完成了Redis主從的全量復制。這邊需要注意的是,Redis中的通信,無論是主庫跟從庫之間,還是與客戶端之間的數據交互。本質上都是通過分配內存buffer來進行的,Master 會先把數據寫到 buffer 中,再通過網絡發送出去,從而完成數據交互。
RDB 文件作為二進制文件,無論是網絡傳輸還是寫入時的磁盤IO,效率都要比 AOF 高很多。同樣的,從庫進行數據恢復的時候,效率也會高一些。所以我們會選擇RDB文件做同步而不是AOF模式。
4.1.2 增量復制
4.1.2.1 主從網絡斷開之后的同步方式
高版本的Redis,在網絡斷開之后或者從實例服務故障恢復之后,主從庫會采用增量復制的方式繼續同步,而不是全量同步的模式,這樣會大大降低開銷,提升效率。
增量復制: 就是指網絡中斷或者從庫重啟等情況后的復制,只將中斷期間主節點執行的寫命令發送給從節點,與全量復制相比更加高效。
repl_backlog_buffer
主從庫重新連接之后可以實現增量復制。關鍵就在 repl_backlog_buffer 緩沖區 上面。
因為 master 會將寫指令操作記錄在 repl_backlog_buffer 緩沖區中,並使用 master_repl_offset 記錄master寫入的位置偏移量,slave 則使用 slave_repl_offset 記錄讀的偏移量。master 新增寫操作的時候,偏移量則會增加。從庫持續執行同步的寫指令后,slave_repl_offset 也會不斷增加。一般情況下,這兩個偏移量會保持同步,如下圖左。
但是網絡斷開或者從庫故障期間,主實例Redis一般會收到新的寫操作命令,但從實例則暫停執行,所以 master_repl_offset 會大於 slave_repl_offset。如下圖右。
需要注意的是, repl_backlog_buffer 並不是如圖中顯示的貌似無限隊列的模式,而是一個類似環形數組,如果數組內容滿了,就會從頭開始覆蓋前面的內容,因為給到的內存空間是有限的。
在主從之間重新連接之后,slave 會先發送 psync 命令給 master,同時將自己的 {runID,slave_repl_offset} 兩個參數發送給 master。master 只需要把 master_repl_offset 與 slave_repl_offset 之間的命令同步給從庫即可。增量復制的流程類似如下:
在配置repl_backlog_buffer 的時候,需要綜合考慮各種因素,太大了會導致增量執行周期比較長,還不如RDB全量覆蓋。太小了,有可能從庫還沒讀取到就被 Master 的新寫操作覆蓋了,那樣也只能執行全量復制。
所以我們需要給出一個合理 緩沖區Size。一般有如下的計算公式共參考:
repl_backlog_buffer_size = seconds * write_size_per_second
seconds:正常情況下從庫斷開,到重連主庫所需的平均時間,秒為單位。
write_size_per_second:主庫平均每秒產生的寫命令數據量大小。
如主服務器大約每秒產生 0.5 MB 的寫指令數據,而斷開到重連一般需要30s,那么緩沖區的大小就是 0.5 * 30s = 15 MB。
但是我們一般會保留一點buffer,比如 預留 0.5 倍,那就是 : 1.5 * 15 MB = 22.5 MB 。
4.1.2.2 基於長連接的命令傳播
上面的工作都是為了完成完整復制,那在完成全量復制之后,主從開始進入正常有序的同步了,具體應該怎么做呢?
主從完成全量復制之后,他們之間需要保持連接。當主庫收到操作指令的時候,通過這個連接同步給從庫,這個過程稱之為 基於長連接的命令傳播。
為了保證傳播的有效性和穩定性,從節點采用心跳機制進行偵測,發送命令:PING 和 REPLCONF ACK。
- 主->從:PING
每隔指定的時間(比如 1 分鍾,可配置),主節點會向從節點發送 PING 命令,偵測從節點有無超時來判斷從節點的健康情況。
- 從->主:REPLCONF ACK
命令執行傳播的階段,從服務器默認會以每秒一次的頻率,向主服務器發送命令,將復制的偏移量發送過去。
REPLCONF ACK <replication_offset>
replication_offset 的屬性指的是當前從實例服務器的復制偏移量。
從實例發送 REPLCONF ACK 命令對於主要實例,主要有以下作用:
- 檢測主從服務器的網絡通路是否正常。
- 輔助實現 min-slaves 選項,使用Redis的 min-slaves-to-write(少於n個從實例時,拒絕執行寫命令) 和 min-slaves-max-lag(主從延遲大於等於n秒時,拒絕執行寫命令)兩個選項可以防止主服務器在不安全的情況下執行寫命令。
- 檢測命令丟失, 從節點發送了 slave_replication_offset,主節點會對比 master_replication_offset ,如果不一致,說明從節點數據缺失,主節點會從 repl_backlog_buffer緩沖區中找到並推送缺失的數據。
4.1.2.3 如何確定執行全量同步還是部分同步?
從節點可以發送 psync 命令給主節點請求同步數據,主節點判斷從節點的當前狀態,看看具體同步是采用全量復制還是部分復制。核心的地方就是psync的參數,這個我們前面也已經提到過了:
下面我們來拆解下步驟:
- 從節點根據自身狀態,發送 psync命令給 master:
- 如果從實例從未執行過 replicaof ,則從節點發送 psync ? -1,代表全量,從 -1 處開始復制。
- 如果從節點之前執行過 replicaof,則取當前實例中記錄下 runID和 offset,執行命令
psync <runID> <offset>
, runID 是主節點 runID,offset 復制偏移量。
- 主節點根據接收到的psync命令及當前服務器狀態,決定執行全量復制還是部分復制:
- 對比主、從節點的 runID 一致,且從節點發送 slave_repl_offset 之后的數據在 repl_backlog_buffer緩沖區中均存在(隊列是環形的,有可能被擦除重寫了),則回復 CONTINUE,代表以追加模式進行部分復制。
- runID 與從節點發送的 runID 不同,或者從節點發送的 slave_repl_offset 之后的數據已不在主節點的 repl_backlog_buffer緩沖區中 (因為隊列是環形的,所以等待時間太長或者有斷連的情況,有可能被擦除重寫了),則回復從節點
FULLRESYNC <runid> <offset>
,表示要進行全量復制,同時記下主節點的 runID 和offset。
4.2 1主n從同步的理解
從上面的內容,我們得到以下兩點:
- 多個從庫情況下,每個從庫都會記錄自己的
slave_repl_offset
,各自復制的進度也不相同。 - 重連主庫進行恢復時,從庫會通過 psync 命令將 slave_repl_offset 告知主庫,主庫判斷從庫的狀態,來決定進行增量復制,還是全量復制。
- replication buffer 和 repl_backlog 的說明
replication buffer 是主從庫在進行全量復制時,主庫上用於和從庫連接的客戶端的 buffer,而 repl_backlog_buffer 是為了支持從庫增量復制,主庫上用於持續保存寫操作的一塊專用 buffer,所有從庫共享的。
主庫和從庫會各自記錄自己的復制進度,所以,不同的從庫在進行恢復時,需要將自己的復制進度(slave_repl_offset)發給主庫,主庫才可以按照偏移量取數據跟它同步。
如圖所示:
5 總結
- 主從復制的作用一個是為分擔讀寫壓力,均衡負載,另一個是為了保證部分實例宕機之后服務的持續可用性,所以Redis演變出主從架構和讀寫分離。
- 主從復制的步驟包括:建立連接的階段、數據同步的階段、基於長連接的命令傳播階段。
- 數據同步可以分為全量復制和部分復制,全量復制一般為第一次全量或者長時間主從連接斷開。
- 命令傳播階段主從節點之間有 PING(主到從的的探測) 和 REPLCONF ACK(從到主的ack應答) 命令,這種互相確認心跳的模式保證數據同步的穩定性。
- 主從模式是比較低級的可用性優化,要做到故障自動轉移,異常預警,高保活,還需要更為復雜的哨兵或者集群模式,這個后面我們會有專門的文章進行介紹。