1. 程式人生 > 其它 >redis_06 _ 資料同步:主從庫如何實現資料一致

redis_06 _ 資料同步:主從庫如何實現資料一致

前兩節課,我們學習了AOF和RDB,如果Redis發生了宕機,它們可以分別通過回放日誌和重新讀入RDB檔案的方式恢復資料,從而保證儘量少丟失資料,提升可靠性。

不過,即使用了這兩種方法,也依然存在服務不可用的問題。比如說,我們在實際使用時只運行了一個Redis例項,那麼,如果這個例項宕機了,它在恢復期間,是無法服務新來的資料存取請求的。

那我們總說的Redis具有高可靠性,又是什麼意思呢?其實,這裡有兩層含義:一是資料儘量少丟失,二是服務儘量少中斷。AOF和RDB保證了前者,而對於後者,Redis的做法就是增加副本冗餘量,將一份資料同時儲存在多個例項上。即使有一個例項出現了故障,需要過一段時間才能恢復,其他例項也可以對外提供服務,不會影響業務使用。

多例項儲存同一份資料,聽起來好像很不錯,但是,我們必須要考慮一個問題:這麼多副本,它們之間的資料如何保持一致呢?資料讀寫操作可以發給所有的例項嗎?

實際上,Redis提供了主從庫模式,以保證資料副本的一致,主從庫之間採用的是讀寫分離的方式。

  • 讀操作:主庫、從庫都可以接收;
  • 寫操作:首先到主庫執行,然後,主庫將寫操作同步給從庫。

那麼,為什麼要採用讀寫分離的方式呢?

你可以設想一下,如果在上圖中,不管是主庫還是從庫,都能接收客戶端的寫操作,那麼,一個直接的問題就是:如果客戶端對同一個資料(例如k1)前後修改了三次,每一次的修改請求都發送到不同的例項上,在不同的例項上執行,那麼,這個資料在這三個例項上的副本就不一致了(分別是v1、v2和v3)。在讀取這個資料的時候,就可能讀取到舊的值。

如果我們非要保持這個資料在三個例項上一致,就要涉及到加鎖、例項間協商是否完成修改等一系列操作,但這會帶來鉅額的開銷,當然是不太能接受的。

而主從庫模式一旦採用了讀寫分離,所有資料的修改只會在主庫上進行,不用協調三個例項。主庫有了最新的資料後,會同步給從庫,這樣,主從庫的資料就是一致的。

那麼,主從庫同步是如何完成的呢?主庫資料是一次性傳給從庫,還是分批同步?要是主從庫間的網路斷連了,資料還能保持一致嗎?這節課,我就和你聊聊主從庫同步的原理,以及應對網路斷連風險的方案。

好了,我們先來看看主從庫間的第一次同步是如何進行的,這也是Redis例項建立主從庫模式後的規定動作。

主從庫間如何進行第一次同步?

當我們啟動多個Redis例項的時候,它們相互之間就可以通過replicaof(Redis 5.0之前使用slaveof)命令形成主庫和從庫的關係,之後會按照三個階段完成資料的第一次同步。

例如,現在有例項1(ip:172.16.19.3)和例項2(ip:172.16.19.5),我們在例項2上執行以下這個命令後,例項2就變成了例項1的從庫,並從例項1上覆制資料:

replicaof  172.16.19.3  6379

接下來,我們就要學習主從庫間資料第一次同步的三個階段了。你可以先看一下下面這張圖,有個整體感知,接下來我再具體介紹。

第一階段是主從庫間建立連線、協商同步的過程,主要是為全量複製做準備。在這一步,從庫和主庫建立起連線,並告訴主庫即將進行同步,主庫確認回覆後,主從庫間就可以開始同步了

具體來說,從庫給主庫傳送psync命令,表示要進行資料同步,主庫根據這個命令的引數來啟動複製。psync命令包含了主庫的runID複製進度offset兩個引數。

  • runID,是每個Redis例項啟動時都會自動生成的一個隨機ID,用來唯一標記這個例項。當從庫和主庫第一次複製時,因為不知道主庫的runID,所以將runID設為“?”。
  • offset,此時設為-1,表示第一次複製。

主庫收到psync命令後,會用FULLRESYNC響應命令帶上兩個引數:主庫runID和主庫目前的複製進度offset,返回給從庫。從庫收到響應後,會記錄下這兩個引數。

這裡有個地方需要注意,FULLRESYNC響應表示第一次複製採用的全量複製,也就是說,主庫會把當前所有的資料都複製給從庫

在第二階段,主庫將所有資料同步給從庫。從庫收到資料後,在本地完成資料載入。這個過程依賴於記憶體快照生成的RDB檔案。

具體來說,主庫執行bgsave命令,生成RDB檔案,接著將檔案發給從庫。從庫接收到RDB檔案後,會先清空當前資料庫,然後載入RDB檔案。這是因為從庫在通過replicaof命令開始和主庫同步前,可能儲存了其他資料。為了避免之前資料的影響,從庫需要先把當前資料庫清空。

在主庫將資料同步給從庫的過程中,主庫不會被阻塞,仍然可以正常接收請求。否則,Redis的服務就被中斷了。但是,這些請求中的寫操作並沒有記錄到剛剛生成的RDB檔案中。為了保證主從庫的資料一致性,主庫會在記憶體中用專門的replication buffer,記錄RDB檔案生成後收到的所有寫操作。

最後,也就是第三個階段,主庫會把第二階段執行過程中新收到的寫命令,再發送給從庫。具體的操作是,當主庫完成RDB檔案傳送後,就會把此時replication buffer中的修改操作發給從庫,從庫再重新執行這些操作。這樣一來,主從庫就實現同步了。

主從級聯模式分擔全量複製時的主庫壓力

通過分析主從庫間第一次資料同步的過程,你可以看到,一次全量複製中,對於主庫來說,需要完成兩個耗時的操作:生成RDB檔案和傳輸RDB檔案。

如果從庫數量很多,而且都要和主庫進行全量複製的話,就會導致主庫忙於fork子程序生成RDB檔案,進行資料全量同步。fork這個操作會阻塞主執行緒處理正常請求,從而導致主庫響應應用程式的請求速度變慢。此外,傳輸RDB檔案也會佔用主庫的網路頻寬,同樣會給主庫的資源使用帶來壓力。那麼,有沒有好的解決方法可以分擔主庫壓力呢?

其實是有的,這就是“主-從-從”模式。

在剛才介紹的主從庫模式中,所有的從庫都是和主庫連線,所有的全量複製也都是和主庫進行的。現在,我們可以通過“主-從-從”模式將主庫生成RDB和傳輸RDB的壓力,以級聯的方式分散到從庫上

簡單來說,我們在部署主從叢集的時候,可以手動選擇一個從庫(比如選擇記憶體資源配置較高的從庫),用於級聯其他的從庫。然後,我們可以再選擇一些從庫(例如三分之一的從庫),在這些從庫上執行如下命令,讓它們和剛才所選的從庫,建立起主從關係。

replicaof  所選從庫的IP 6379

這樣一來,這些從庫就會知道,在進行同步時,不用再和主庫進行互動了,只要和級聯的從庫進行寫操作同步就行了,這就可以減輕主庫上的壓力,如下圖所示:

好了,到這裡,我們瞭解了主從庫間通過全量複製實現資料同步的過程,以及通過“主-從-從”模式分擔主庫壓力的方式。那麼,一旦主從庫完成了全量複製,它們之間就會一直維護一個網路連線,主庫會通過這個連線將後續陸續收到的命令操作再同步給從庫,這個過程也稱為基於長連線的命令傳播,可以避免頻繁建立連線的開銷。

聽上去好像很簡單,但不可忽視的是,這個過程中存在著風險點,最常見的就是網路斷連或阻塞。如果網路斷連,主從庫之間就無法進行命令傳播了,從庫的資料自然也就沒辦法和主庫保持一致了,客戶端就可能從從庫讀到舊資料。

接下來,我們就來聊聊網路斷連後的解決辦法。

主從庫間網路斷了怎麼辦?

在Redis 2.8之前,如果主從庫在命令傳播時出現了網路閃斷,那麼,從庫就會和主庫重新進行一次全量複製,開銷非常大。

從Redis 2.8開始,網路斷了之後,主從庫會採用增量複製的方式繼續同步。聽名字大概就可以猜到它和全量複製的不同:全量複製是同步所有資料,而增量複製只會把主從庫網路斷連期間主庫收到的命令,同步給從庫。

那麼,增量複製時,主從庫之間具體是怎麼保持同步的呢?這裡的奧妙就在於repl_backlog_buffer這個緩衝區。我們先來看下它是如何用於增量命令的同步的。

當主從庫斷連後,主庫會把斷連期間收到的寫操作命令,寫入replication buffer,同時也會把這些操作命令也寫入repl_backlog_buffer這個緩衝區。

repl_backlog_buffer是一個環形緩衝區,主庫會記錄自己寫到的位置,從庫則會記錄自己已經讀到的位置

剛開始的時候,主庫和從庫的寫讀位置在一起,這算是它們的起始位置。隨著主庫不斷接收新的寫操作,它在緩衝區中的寫位置會逐步偏離起始位置,我們通常用偏移量來衡量這個偏移距離的大小,對主庫來說,對應的偏移量就是master_repl_offset。主庫接收的新寫操作越多,這個值就會越大。

同樣,從庫在複製完寫操作命令後,它在緩衝區中的讀位置也開始逐步偏移剛才的起始位置,此時,從庫已複製的偏移量slave_repl_offset也在不斷增加。正常情況下,這兩個偏移量基本相等。

主從庫的連線恢復之後,從庫首先會給主庫傳送psync命令,並把自己當前的slave_repl_offset發給主庫,主庫會判斷自己的master_repl_offset和slave_repl_offset之間的差距。

在網路斷連階段,主庫可能會收到新的寫操作命令,所以,一般來說,master_repl_offset會大於slave_repl_offset。此時,主庫只用把master_repl_offset和slave_repl_offset之間的命令操作同步給從庫就行。

就像剛剛示意圖的中間部分,主庫和從庫之間相差了put d e和put d f兩個操作,在增量複製時,主庫只需要把它們同步給從庫,就行了。

說到這裡,我們再借助一張圖,回顧下增量複製的流程。

不過,有一個地方我要強調一下,因為repl_backlog_buffer是一個環形緩衝區,所以在緩衝區寫滿後,主庫會繼續寫入,此時,就會覆蓋掉之前寫入的操作。如果從庫的讀取速度比較慢,就有可能導致從庫還未讀取的操作被主庫新寫的操作覆蓋了,這會導致主從庫間的資料不一致

因此,我們要想辦法避免這一情況,一般而言,我們可以調整repl_backlog_size這個引數。這個引數和所需的緩衝空間大小有關。緩衝空間的計算公式是:緩衝空間大小 = 主庫寫入命令速度 * 操作大小 - 主從庫間網路傳輸命令速度 * 操作大小。在實際應用中,考慮到可能存在一些突發的請求壓力,我們通常需要把這個緩衝空間擴大一倍,即repl_backlog_size = 緩衝空間大小 * 2,這也就是repl_backlog_size的最終值。

舉個例子,如果主庫每秒寫入2000個操作,每個操作的大小為2KB,網路每秒能傳輸1000個操作,那麼,有1000個操作需要緩衝起來,這就至少需要2MB的緩衝空間。否則,新寫的命令就會覆蓋掉舊操作了。為了應對可能的突發壓力,我們最終把repl_backlog_size設為4MB。

這樣一來,增量複製時主從庫的資料不一致風險就降低了。不過,如果併發請求量非常大,連兩倍的緩衝空間都存不下新操作請求的話,此時,主從庫資料仍然可能不一致。

針對這種情況,一方面,你可以根據Redis所在伺服器的記憶體資源再適當增加repl_backlog_size值,比如說設定成緩衝空間大小的4倍,另一方面,你可以考慮使用切片叢集來分擔單個主庫的請求壓力。關於切片叢集,我會在第9講具體介紹。

小結

這節課,我們一起學習了Redis的主從庫同步的基本原理,總結來說,有三種模式:全量複製、基於長連線的命令傳播,以及增量複製。

全量複製雖然耗時,但是對於從庫來說,如果是第一次同步,全量複製是無法避免的,所以,我給你一個小建議:一個Redis例項的資料庫不要太大,一個例項大小在幾GB級別比較合適,這樣可以減少RDB檔案生成、傳輸和重新載入的開銷。另外,為了避免多個從庫同時和主庫進行全量複製,給主庫過大的同步壓力,我們也可以採用“主-從-從”這一級聯模式,來緩解主庫的壓力。

長連線複製是主從庫正常執行後的常規同步階段。在這個階段中,主從庫之間通過命令傳播實現同步。不過,這期間如果遇到了網路斷連,增量複製就派上用場了。我特別建議你留意一下repl_backlog_size這個配置引數。如果它配置得過小,在增量複製階段,可能會導致從庫的複製進度趕不上主庫,進而導致從庫重新進行全量複製。所以,通過調大這個引數,可以減少從庫在網路斷連時全量複製的風險。

不過,主從庫模式使用讀寫分離雖然避免了同時寫多個例項帶來的資料不一致問題,但是還面臨主庫故障的潛在風險。主庫故障了從庫該怎麼辦,資料還能保持一致嗎,Redis還能正常提供服務嗎?在接下來的兩節課裡,我會和你具體聊聊主庫故障後,保證服務可靠性的解決方案。

每課一問

按照慣例,我給你提一個小問題。這節課,我提到,主從庫間的資料複製同步使用的是RDB檔案,前面我們學習過,AOF記錄的操作命令更全,相比於RDB丟失的資料更少。那麼,為什麼主從庫間的複製不使用AOF呢?

好了,這節課就到這裡,如果你覺得有收穫,歡迎你幫我把今天的內容分享給你的朋友。