1. 程式人生 > 其它 >redis_05 _ 記憶體快照:宕機後,Redis如何實現快速恢復

redis_05 _ 記憶體快照:宕機後,Redis如何實現快速恢復

上節課,我們學習了Redis避免資料丟失的AOF方法。這個方法的好處,是每次執行只需要記錄操作命令,需要持久化的資料量不大。一般而言,只要你採用的不是always的持久化策略,就不會對效能造成太大影響。

但是,也正因為記錄的是操作命令,而不是實際的資料,所以,用AOF方法進行故障恢復的時候,需要逐一把操作日誌都執行一遍。如果操作日誌非常多,Redis就會恢復得很緩慢,影響到正常使用。這當然不是理想的結果。那麼,還有沒有既可以保證可靠性,還能在宕機時實現快速恢復的其他方法呢?

當然有了,這就是我們今天要一起學習的另一種持久化方法:記憶體快照。所謂記憶體快照,就是指記憶體中的資料在某一個時刻的狀態記錄。這就類似於照片,當你給朋友拍照時,一張照片就能把朋友一瞬間的形象完全記下來。

對Redis來說,它實現類似照片記錄效果的方式,就是把某一時刻的狀態以檔案的形式寫到磁碟上,也就是快照。這樣一來,即使宕機,快照檔案也不會丟失,資料的可靠性也就得到了保證。這個快照檔案就稱為RDB檔案,其中,RDB就是Redis DataBase的縮寫。

和AOF相比,RDB記錄的是某一時刻的資料,並不是操作,所以,在做資料恢復時,我們可以直接把RDB檔案讀入記憶體,很快地完成恢復。聽起來好像很不錯,但記憶體快照也並不是最優選項。為什麼這麼說呢?

我們還要考慮兩個關鍵問題:

  • 對哪些資料做快照?這關係到快照的執行效率問題;
  • 做快照時,資料還能被增刪改嗎?這關係到Redis是否被阻塞,能否同時正常處理請求。

這麼說可能你還不太好理解,我還是拿拍照片來舉例子。我們在拍照時,通常要關注兩個問題:

  • 如何取景?也就是說,我們打算把哪些人、哪些物拍到照片中;
  • 在按快門前,要記著提醒朋友不要亂動,否則拍出來的照片就模糊了。

你看,這兩個問題是不是非常重要呢?那麼,接下來,我們就來具體地聊一聊。先說“取景”問題,也就是我們對哪些資料做快照。

給哪些記憶體資料做快照?

Redis的資料都在記憶體中,為了提供所有資料的可靠性保證,它執行的是全量快照,也就是說,把記憶體中的所有資料都記錄到磁碟中,這就類似於給100個人拍合影,把每一個人都拍進照片裡。這樣做的好處是,一次性記錄了所有資料,一個都不少。

當你給一個人拍照時,只用協調一個人就夠了,但是,拍100人的大合影,卻需要協調100個人的位置、狀態,等等,這當然會更費時費力。同樣,給記憶體的全量資料做快照,把它們全部寫入磁碟也會花費很多時間。而且,全量資料越多,RDB檔案就越大,往磁碟上寫資料的時間開銷就越大。

對於Redis而言,它的單執行緒模型就決定了,我們要儘量避免所有會阻塞主執行緒的操作,所以,針對任何操作,我們都會提一個靈魂之問:“它會阻塞主執行緒嗎?”RDB檔案的生成是否會阻塞主執行緒,這就關係到是否會降低Redis的效能。

Redis提供了兩個命令來生成RDB檔案,分別是save和bgsave。

  • save:在主執行緒中執行,會導致阻塞;
  • bgsave:建立一個子程序,專門用於寫入RDB檔案,避免了主執行緒的阻塞,這也是Redis RDB檔案生成的預設配置。

好了,這個時候,我們就可以通過bgsave命令來執行全量快照,這既提供了資料的可靠性保證,也避免了對Redis的效能影響。

接下來,我們要關注的問題就是,在對記憶體資料做快照時,這些資料還能“動”嗎? 也就是說,這些資料還能被修改嗎? 這個問題非常重要,這是因為,如果資料能被修改,那就意味著Redis還能正常處理寫操作。否則,所有寫操作都得等到快照完了才能執行,效能一下子就降低了。

快照時資料能修改嗎?

在給別人拍照時,一旦對方動了,那麼這張照片就拍糊了,我們就需要重拍,所以我們當然希望對方保持不動。對於記憶體快照而言,我們也不希望資料“動”。

舉個例子。我們在時刻t給記憶體做快照,假設記憶體資料量是4GB,磁碟的寫入頻寬是0.2GB/s,簡單來說,至少需要20s(4/0.2 = 20)才能做完。如果在時刻t+5s時,一個還沒有被寫入磁碟的記憶體資料A,被修改成了A’,那麼就會破壞快照的完整性,因為A’不是時刻t時的狀態。因此,和拍照類似,我們在做快照時也不希望資料“動”,也就是不能被修改。

但是,如果快照執行期間資料不能被修改,是會有潛在問題的。對於剛剛的例子來說,在做快照的20s時間裡,如果這4GB的資料都不能被修改,Redis就不能處理對這些資料的寫操作,那無疑就會給業務服務造成巨大的影響。

你可能會想到,可以用bgsave避免阻塞啊。這裡我就要說到一個常見的誤區了,避免阻塞和正常處理寫操作並不是一回事。此時,主執行緒的確沒有阻塞,可以正常接收請求,但是,為了保證快照完整性,它只能處理讀操作,因為不能修改正在執行快照的資料。

為了快照而暫停寫操作,肯定是不能接受的。所以這個時候,Redis就會藉助作業系統提供的寫時複製技術(Copy-On-Write, COW),在執行快照的同時,正常處理寫操作。

簡單來說,bgsave子程序是由主執行緒fork生成的,可以共享主執行緒的所有記憶體資料。bgsave子程序執行後,開始讀取主執行緒的記憶體資料,並把它們寫入RDB檔案。

此時,如果主執行緒對這些資料也都是讀操作(例如圖中的鍵值對A),那麼,主執行緒和bgsave子程序相互不影響。但是,如果主執行緒要修改一塊資料(例如圖中的鍵值對C),那麼,這塊資料就會被複制一份,生成該資料的副本(鍵值對C’)。然後,主執行緒在這個資料副本上進行修改。同時,bgsave子程序可以繼續把原來的資料(鍵值對C)寫入RDB檔案。

這既保證了快照的完整性,也允許主執行緒同時對資料進行修改,避免了對正常業務的影響。

到這裡,我們就解決了對“哪些資料做快照”以及“做快照時資料能否修改”這兩大問題:Redis會使用bgsave對當前記憶體中的所有資料做快照,這個操作是子程序在後臺完成的,這就允許主執行緒同時可以修改資料。

現在,我們再來看另一個問題:多久做一次快照?我們在拍照的時候,還有項技術叫“連拍”,可以記錄人或物連續多個瞬間的狀態。那麼,快照也適合“連拍”嗎?

可以每秒做一次快照嗎?

對於快照來說,所謂“連拍”就是指連續地做快照。這樣一來,快照的間隔時間變得很短,即使某一時刻發生宕機了,因為上一時刻快照剛執行,丟失的資料也不會太多。但是,這其中的快照間隔時間就很關鍵了。

如下圖所示,我們先在T0時刻做了一次快照,然後又在T0+t時刻做了一次快照,在這期間,資料塊5和9被修改了。如果在t這段時間內,機器宕機了,那麼,只能按照T0時刻的快照進行恢復。此時,資料塊5和9的修改值因為沒有快照記錄,就無法恢復了。

所以,要想盡可能恢復資料,t值就要儘可能小,t越小,就越像“連拍”。那麼,t值可以小到什麼程度呢,比如說是不是可以每秒做一次快照?畢竟,每次快照都是由bgsave子程序在後臺執行,也不會阻塞主執行緒。

這種想法其實是錯誤的。雖然bgsave執行時不阻塞主執行緒,但是,如果頻繁地執行全量快照,也會帶來兩方面的開銷

一方面,頻繁將全量資料寫入磁碟,會給磁碟帶來很大壓力,多個快照競爭有限的磁碟頻寬,前一個快照還沒有做完,後一個又開始做了,容易造成惡性迴圈。

另一方面,bgsave子程序需要通過fork操作從主執行緒創建出來。雖然,子程序在建立後不會再阻塞主執行緒,但是,fork這個建立過程本身會阻塞主執行緒,而且主執行緒的記憶體越大,阻塞時間越長。如果頻繁fork出bgsave子程序,這就會頻繁阻塞主執行緒了(所以,在Redis中如果有一個bgsave在執行,就不會再啟動第二個bgsave子程序)。那麼,有什麼其他好方法嗎?

此時,我們可以做增量快照,所謂增量快照,就是指,做了一次全量快照後,後續的快照只對修改的資料進行快照記錄,這樣可以避免每次全量快照的開銷。

在第一次做完全量快照後,T1和T2時刻如果再做快照,我們只需要將被修改的資料寫入快照檔案就行。但是,這麼做的前提是,我們需要記住哪些資料被修改了。你可不要小瞧這個“記住”功能,它需要我們使用額外的元資料資訊去記錄哪些資料被修改了,這會帶來額外的空間開銷問題。如下圖所示:

如果我們對每一個鍵值對的修改,都做個記錄,那麼,如果有1萬個被修改的鍵值對,我們就需要有1萬條額外的記錄。而且,有的時候,鍵值對非常小,比如只有32位元組,而記錄它被修改的元資料資訊,可能就需要8位元組,這樣的畫,為了“記住”修改,引入的額外空間開銷比較大。這對於記憶體資源寶貴的Redis來說,有些得不償失。

到這裡,你可以發現,雖然跟AOF相比,快照的恢復速度快,但是,快照的頻率不好把握,如果頻率太低,兩次快照間一旦宕機,就可能有比較多的資料丟失。如果頻率太高,又會產生額外開銷,那麼,還有什麼方法既能利用RDB的快速恢復,又能以較小的開銷做到儘量少丟資料呢?

Redis 4.0中提出了一個混合使用AOF日誌和記憶體快照的方法。簡單來說,記憶體快照以一定的頻率執行,在兩次快照之間,使用AOF日誌記錄這期間的所有命令操作。

這樣一來,快照不用很頻繁地執行,這就避免了頻繁fork對主執行緒的影響。而且,AOF日誌也只用記錄兩次快照間的操作,也就是說,不需要記錄所有操作了,因此,就不會出現檔案過大的情況了,也可以避免重寫開銷。

如下圖所示,T1和T2時刻的修改,用AOF日誌記錄,等到第二次做全量快照時,就可以清空AOF日誌,因為此時的修改都已經記錄到快照中了,恢復時就不再用日誌了。

這個方法既能享受到RDB檔案快速恢復的好處,又能享受到AOF只記錄操作命令的簡單優勢,頗有點“魚和熊掌可以兼得”的感覺,建議你在實踐中用起來。

小結

這節課,我們學習了Redis用於避免資料丟失的記憶體快照方法。這個方法的優勢在於,可以快速恢復資料庫,也就是隻需要把RDB檔案直接讀入記憶體,這就避免了AOF需要順序、逐一重新執行操作命令帶來的低效效能問題。

不過,記憶體快照也有它的侷限性。它拍的是一張記憶體的“大合影”,不可避免地會耗時耗力。雖然,Redis設計了bgsave和寫時複製方式,儘可能減少了記憶體快照對正常讀寫的影響,但是,頻繁快照仍然是不太能接受的。而混合使用RDB和AOF,正好可以取兩者之長,避兩者之短,以較小的效能開銷保證資料可靠性和效能。

最後,關於AOF和RDB的選擇問題,我想再給你提三點建議:

  • 資料不能丟失時,記憶體快照和AOF的混合使用是一個很好的選擇;
  • 如果允許分鐘級別的資料丟失,可以只使用RDB;
  • 如果只用AOF,優先使用everysec的配置選項,因為它在可靠性和效能之間取了一個平衡。

每課一問

我曾碰到過這麼一個場景:我們使用一個2核CPU、4GB記憶體、500GB磁碟的雲主機執行Redis,Redis資料庫的資料量大小差不多是2GB,我們使用了RDB做持久化保證。當時Redis的執行負載以修改操作為主,寫讀比例差不多在8:2左右,也就是說,如果有100個請求,80個請求執行的是修改操作。你覺得,在這個場景下,用RDB做持久化有什麼風險嗎?你能幫著一起分析分析嗎?

到這裡,關於持久化我們就講完了,這塊兒內容是熟練掌握Redis的基礎,建議你一定好好學習下這兩節課。如果你覺得有收穫,希望你能幫我分享給更多的人,幫助更多人解決持久化的問題。