Redis 主从复制的原理及演进
本文作者:百度基礎架構部工程師,王鈺
Redis 的主從復制經歷了多次演進,本文將從最基本的原理和實現講起,并層層遞進,逐步呈現 Redis 主從復制的演進歷史。大家將了解到 Redis 主從復制的原理,以及各個改進版本解決了什么問題,并最終看清 Redis 7.0 主從復制原理的全貌。
什么是主從復制?
在數據庫語境下,復制(replication)就是將數據從一個數據庫復制到另一個數據庫中。主從復制,是將數據庫分為主節點和從節點,主節點源源不斷地將數據復制給從節點,保證主從節點中存有相同的數據。
有了主從復制,數據可以有多份副本,這帶來了多種好處:
第一,提升數據庫系統的請求處理能力。單個節點能夠支撐的讀流量有限,部署多個節點,并構成主從關系,用主從復制保持主從節點數據一致,如此主從節點可以一起提供服務。
第二,提升整個系統的可用性。因為從節點中有主節點數據的副本,當主節點宕機后,可以立刻提升其中一個從節點為主節點,繼續提供服務。
Redis 主從復制原理
實現主從復制,直觀的思路是產生一份主節點數據的快照發送給從節點,并以此做為基準,隨后將快照時刻之后的增量數據發送給從節點,如此就能保證主從數據的一致。總體來看,主從復制一般包含全量數據同步、增量同步兩個階段。
在 Redis 的主從復制實現中,包含兩個類似階段:全量數據同步和命令傳播。
-
全量數據同步:主節點產生一份全量數據的快照,即 RDB 文件,并將此快照發送給從節點。且從產生快照時刻起,記錄新接收到的寫命令。當快照發送完成后,將累積的寫命令發送給從節點,從節點執行這些寫命令。此時基準已經建立完成,主從節點間數據已經大體一致。
-
命令傳播:全量數據同步完成后,主節點將執行過的寫命令源源不斷地發送給從節點,從節點執行這些命令,保證主從節點中數據有相同的變更,如此保證主從數據持續一致。
下圖中給出了 Redis 主從復制的整個過程:
主從關系建立后,從節點向主節點發送一個 SYNC 命令請求進行主從同步。
主節點收到 SYNC 命令后,執行 fork 創建一個子進程,子進程中將所有的數據按特定編碼存儲到 RDB(Redis Database) 文件中,這就產生了數據庫的快照。
主節點將此快照發送給從節點,從節點接收并載入快照。
主節點接著將生成快照、發送快照期間積壓的寫命令發送給從節點,從節點接收這些命令并執行,命令執行后,從節點中的數據也就有了同樣的變更。
此后,主節點源源不斷地新執行的寫命令同步到從節點,從節點執行傳播來的命令。如此,主從數據保持一致。需要說明的是,命令傳播存在時延的,所以任意時刻,不能保證主從節點間數據完全一致。
以上就是 Redis 主從復制的基本原理,很簡單很容易理解,Redis 最初就采用這種方案,但這種方案存在一些問題:
fork 耗時過長,阻塞主進程
執行fork?時,需要拷貝大量的內存頁表,這是一個耗時較多的操作,尤其當內存使用量較大的時候。組內同學曾做過測試,內存占用 10GB 時,fork 需要消耗 100 多毫秒。fork?的時候主進程阻塞 100 多毫秒,這對 Redis 而言,實在太長了。另外fork?之后,如果主庫中有不少的寫入,那么由于寫時復制機制,會額外消耗不少的內存,還會增大響應時間。
主從間網絡閃斷會觸發全量同步
假如主從之間的網絡出現了故障,連接意外斷開,主節點無法繼續傳播命令至該從節點。之后網絡恢復,從節點重新連接上主節點后,主節點不能再繼續傳播新接收到的命令了,因為從節點已經漏掉了一些命令。此時,從節點需要從頭再來,再次執行全部的同步過程,而這要付出很高的代價。
網絡閃斷是常發生的事情,閃斷期間主節點中可能只寫入了比較少的數據,但就因為這很少的一部分數據,需要讓從節點進行一次代價高昂的全量同步。這種做法是非常低效的,該如何解決這問題呢?下一節 Redis 部分重同步給你答案。
Redis 部分重同步
網絡短暫斷開后,從節點需要重新同步,這很浪費資源,很不環保。從節點為什么需要重新同步呢?因為主從斷開期間有部分命令沒有同步到從節點上去。如果忽略這些命令繼續傳播后續的命令,則會導致數據的錯亂,因為丟失掉的命令是不能忽略的。為什么不將那些命令保存下來呢?這樣當從節點重新連接后,就可以將斷連期間的命令補充給它了,這樣就不需要重新全量同步了。
Redis 2.8 版本后,引入了部分同步。它在主節點中維護了一個復制積壓緩沖區,命令一方面會傳播到從節點,另外還會記錄在這個緩沖區中。保存所有的命令是不必要的,Redis 中使用了一個環形的緩沖區,這樣就可以只保留最近的一些命令了。
命令是保存下來了,但從節點重新連接后,主節點該從什么地方開始給從節點發送命令呢?如果能給所有命令編一個號,則從節點只需要告訴主節點自己最后收到的命令的編號,主節點就知道該從什么位置發送命令了。Redis 的實現中是對字節進行編號,這個編號在 Redis 的語境中叫做復制偏移量。
有了部分同步后,主從復制的流程變成了下面這樣:
主從復制的時候不再使用SYNC命令,而是使用PSYNC,意思的Partial SYNC,部分同步。PSYNC的語法如下:
命令中的兩個參數,一個是主節點的編號,一個是復制偏移量。每個Redis節點都有一個 40 字節的編號,PSYNC 命令中攜帶的編號是期望進行同步的主節點的編號。復制偏移量則表示當前從節點想要從什么地方開始部分同步。
如果是第一次進行主從復制,自然是不知道主節點的編號,復制偏移量也無意義,此時使用 PSYNC ? -1 來進行全量同步。另外,如果從節點指定的復制偏移量不在主節點的復制積壓緩沖區的范圍內,部分同步會失敗,會轉向全量同步。
有了部分同步,網絡閃斷后就可以避免全量同步了。但是因為主節點只能保留最近的部分命令,保存多少取決于復制積壓緩沖區的大小。如果從節點斷開時間過長,或者斷開期間主節點新執行的寫命令足夠多,漏掉的命令就無法全部保存到復制積壓緩沖區中了。加大復制積壓緩沖區可以盡可能多地避免全量同步,但這同時會造成額外的內存消耗。
部分同步消耗了部分內存來保存最近執行的寫命令,避免閃斷后的全同步,這是很直觀、很容易想象的解決方案。這種方案很好,是否還存在其他問題呢?考慮以下問題:
假如從節點重啟了怎么辦?
部分同步依賴主節點的編號和復制偏移量,從節點在初次同步的時候會獲取到主節點的編號,并在之后的同步中不斷調整復制偏移量,這些信息都存儲在內存中。當從節點意外重啟后,盡管本地存有 RDB 或 AOF 文件,還是需要進行一次全量同步。但實際上完全可以載入本地數據,并執行部分同步即可。
假如主從切換了怎么辦?
假如主節點意外宕機,外圍監控組件執行了主從切換。此時其他從節點對應的主節點就變化了,從節點中記錄的主節點編號就匹配不上新的主節點了,此時會進行一次全量同步。但實際上所有的從節點在主從切換之前同步進度應該是差不多的,而且新提升的從節點包含的數據應該最全,切主后所有從節點都執行一次全量同步,這實在不合理。
以上問題如何解決,請繼續往后看。
同源增量同步
從節點重啟后丟失了原主節點編號和復制偏移量,這導致重啟后需要全量同步,這很好辦,把這些信息存下來就可以了。
主從切換后,主節點信息變化了,導致從節點需要全量同步,這也容易解決,只需能確認新主節點上的數據是從原主節點復制來的,那就可以繼續從新的主節點上進行復制。
Redis 4.0 以后,對 PSYNC 進行了改進,提出了同源增量復制的解決方案,該方案解決了前面提到的兩個問題。
從節點重啟后,需要跟主節點全量同步,這本質上是因為從節點丟失了主節點的編號信息,在 Redis 4.0 后,主節點的編號信息被寫入到 RDB 中持久化保存。
切主后,從節點需要和新主節點全量同步,本質原因是新的主節點不認原主節點的編號。從節點發送 PSYNC <原主節點編號> <復制偏移量> 給新的主節點,如果新的主節點能夠認識 <原主節點編號>,并明白自己的數據就是從該節點復制來的。那么新的主節點就應該清楚它和該從節點師出同門,應該接受部分同步。如何才能識別呢,只需要讓從節點在切換為主節點時,將自己之前的主節點的編號記錄下來即可。
Redis 4.0 以后,主從切換后,新的主節點會將先前的主節點記錄下來,觀察 info replication 的結果,可以可以看到 master_replid 和 master_replid2 兩個編號,前者是當前主節點的編號,后者為先前主節點的編號:
127.0.0.1:6379> slaveof no oneOK127.0.0.1:6379> info replication# Replicationrole:master...master_replid:b34aff08d983991b3feb4567a2ac0308984a892amaster_replid2:a3f2428d31e096a99d87affa6cc787cceb6128a2master_repl_offset:38599second_repl_offset:38600...repl_backlog_histlen:5180Redis 中目前值保留了兩個主節點編號,但完全可以實現一個鏈表,將過往的主節點的編號信息都記錄下來,這樣就可以追溯的更遠了。這樣以來,如果一個從節點斷開后,執行了多次主從切換,該從節重新連接后,依然可以識別出它們的數據是同源的。但 Redis 沒有這么做,這是因為沒有必要,因為就算數據是同源的,但復制積壓緩沖區中保存的數據是有限的,多次主從切換后,復制積壓緩沖區中保存的命令已經無法滿足部分同步了。
有了同源增量復制后,主節點切換后,其他從節點可以基于新的主節點繼續增量同步。
此時,主從復制看起來已經不存在太大的問題了。但做 Redis 的那幫家伙,總挖空心思想著能不能再做些優化。下面我將描述 Redis 主從復制的一些優化策略。
無盤全量同步和無盤加載
Redis 執行全量復制,需要生成當前數據庫的一份快照,具體做法是執行 fork 創建子進程,子進程遍歷所有數據并編碼后寫入 RDB 文件中。RDB 生成后,在主進程中,會讀取此文件并發送給從節點。
讀寫磁盤上的 RDB 文件是比較耗資源的,在主進程中執行勢必會導致 Redis 的響應時間變長。因此一個優化方案是 dump 后直接將數據直接發送數據給從節點,不需要將數據先寫入到 RDB 。Redis 6.0 中實現了這種無盤全量同步和無盤加載的策略。
采用無盤全量同步,避免了對磁盤的操作,但也有缺點。一般情況下,在子進程中直接使用網絡發送數據,這比在子進程中生成 RDB 要慢,這意味著子進程需要存活的時間相對較長。子進程存在的時間越長,寫時復制造成的影響就越大,進而導致消耗的內存會更多。
在全量復制時候,從節點一般是先接收 RDB 將其存在本地,接收完成后再載入 RDB。同樣地,從節點也可以直接載入主節點發來的數據,避免將其存入本地的 RDB 文件中,而后再從磁盤加載。
共享主從復制緩沖區
在主節點的視角中,從節點就是一個客戶端,從節點發送了 PSYNC 命令后,主節點就要與它們完成全量同步,并不斷地把寫命令同步給從節點。Redis 的每個客戶端連接上存在一個發送緩沖區。
主節點執行了寫命令后,就會將命令內容寫入到各個連接的發送緩沖區中。發送緩沖區存儲的是待傳播的命令,這意味著多個發送緩沖區中的內容其實是相同的。而且,這些命令還在復制積壓緩沖區中存了一份呢。這就造成了大量的內存浪費,尤其是存在很多從節點的時候。
在 Redis 7.0 中,我們團隊的同學提出并實現了共享主從復制緩沖區的方案解決了這個問題。該方案讓發送緩沖區與復制積壓緩沖區共享,避免了數據的重復,可有效節省內存。
總結
本文回顧并總結了 Redis 主從復制的演化過程,并解釋了各次演化所解決的問題。最后,描述了對 Redis 主從復制進行優化的一些策略。
下面是對全文的總結:
宏觀來看 Redis 的主從復制分為全量同步和命令傳播兩個階段。主節點先發送快照給從節點,然后源源不斷地將命令傳播給從節點,以此保證主從數據的一致。
Redis 2.8 之前的主從復制存在閃斷后需要重新全量同步的問題,Redis 2.8 引入了復制積壓緩沖區解決了這一問題。
在 Redis 4.0 中,同源增量復制的策略被提出,解決了主從切換后從節點需要全量同步的問題。至此,Redis 的主從復制整體上已經比較完善了。
Redis 6.0 中,為進一步優化主從復制的性能,無盤同步和加載被提出,避免全量同步時讀寫磁盤,提高主從同步的速度。
在?Redis 7.0 rc1 中,采用了共享主從復制緩沖區的策略,降低了主從復制帶來的內存開銷。
希望本文能幫助大家回顧 Redis 主從復制的原理,并對其建立更加深刻的印象。
總結
以上是生活随笔為你收集整理的Redis 主从复制的原理及演进的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 保证业务高效运营 专有云虚拟网络是关键
- 下一篇: 软件工程能力漫谈:比编码更重要的,是项目