Redis宕機后,Redis如何實現(xiàn)快速恢復?
當前,我們已經(jīng)深入了解了Redis中的AOF(Append-Only File)持久化方法,它的優(yōu)勢在于記錄操作命令,不會顯著增加持久化數(shù)據(jù)量。通常情況下,只要你沒有選擇always的持久化策略,AOF方法對性能的影響是相對較小的。
然而,由于AOF方法記錄的是操作命令而不是實際的數(shù)據(jù),所以在使用AOF進行故障恢復時,需要逐一執(zhí)行所有的操作日志。當操作日志非常龐大時,這個恢復過程會變得非常緩慢,從而影響了正常的使用體驗。顯然,這并不是我們理想的情況。那么,是否有其他方法既可以保障數(shù)據(jù)可靠性,又能在宕機后實現(xiàn)快速恢復呢?
當然有,這就是我們今天要一同探討的另一種持久化方法:內存快照。內存快照的概念很像拍照,它記錄了某一時刻的內存中數(shù)據(jù)狀態(tài),就像照片一樣。當你給朋友拍照時,一張照片能完美地捕捉朋友的瞬間。對于Redis,它通過將某一時刻的數(shù)據(jù)狀態(tài)以文件形式寫入磁盤來實現(xiàn)這種效果,這個文件就是快照,通常稱為RDB文件,其中RDB代表Redis數(shù)據(jù)庫。
RDB文件的優(yōu)勢在于,即使發(fā)生宕機,快照文件也不會丟失,因此可靠性得到了保證。接下來,我們將深入了解RDB持久化方法,包括它的工作原理和配置等方面的細節(jié)。這將有助于你更好地理解如何選擇適當?shù)某志没椒ǎ詽M足你的特定需求。
與AOF持久化相比,RDB持久化記錄的是某一時刻的數(shù)據(jù)狀態(tài),而不是每個操作命令。這意味著在數(shù)據(jù)恢復過程中,我們可以直接將RDB文件加載到內存中,迅速完成恢復。聽起來似乎非常理想,但內存快照也并不是毫無缺點的最佳選項。為什么會這樣呢?
我們需要考慮兩個關鍵問題:
- 哪些數(shù)據(jù)進行快照?這關系到快照執(zhí)行的效率問題。
- 在進行快照時,數(shù)據(jù)是否能夠被修改、新增或刪除?這關系到Redis是否會被阻塞,以及是否能夠同時處理其他請求。
這或許還不夠清晰,我來用拍照的例子進行解釋。當我們拍照時,通常要思考兩個問題:
- 如何構圖?也就是我們打算在照片中捕捉哪些人或物體。
- 在按下快門之前,需要確保拍攝對象不亂動,以避免照片模糊。
你可以看到,這兩個問題非常重要,接下來,我們將詳細討論這兩個問題,首先是“構圖”問題,即我們應該選擇哪些數(shù)據(jù)進行快照。
給哪些內存數(shù)據(jù)做快照?
Redis 的數(shù)據(jù)都在內存中,為了提供所有數(shù)據(jù)的可靠性保證,它執(zhí)行的是全量快照,也就是說,把內存中的所有數(shù)據(jù)都記錄到磁盤中,這就類似于給 100 個人拍合影,把每一個人都拍進照片里。這樣做的好處是,一次性記錄了所有數(shù)據(jù),一個都不少。
當你給一個人拍照時,只用協(xié)調一個人就夠了,但是,拍 100 人的大合影,卻需要協(xié)調 100 個人的位置、狀態(tài),等等,這當然會更費時費力。同樣,給內存的全量數(shù)據(jù)做快照,把它們全部寫入磁盤也會花費很多時間。而且,全量數(shù)據(jù)越多,RDB 文件就越大,往磁盤上寫數(shù)據(jù)的時間開銷就越大。
對于 Redis 而言,它的單線程模型就決定了,我們要盡量避免所有會阻塞主線程的操作,所以,針對任何操作,我們都會提一個靈魂之問:“它會阻塞主線程嗎?”RDB 文件的生成是否會阻塞主線程,這就關系到是否會降低 Redis 的性能。
Redis 提供了兩個命令來生成 RDB 文件,分別是 save 和 bgsave。
save:在主線程中執(zhí)行,會導致阻塞;
bgsave:創(chuàng)建一個子進程,專門用于寫入 RDB 文件,避免了主線程的阻塞,這也是 Redis RDB 文件生成的默認配置。
好了,這個時候,我們就可以通過 bgsave 命令來執(zhí)行全量快照,這既提供了數(shù)據(jù)的可靠性保證,也避免了對 Redis 的性能影響。
接下來,我們要關注的問題就是,在對內存數(shù)據(jù)做快照時,這些數(shù)據(jù)還能“動”嗎? 也就是說,這些數(shù)據(jù)還能被修改嗎?這個問題非常重要,這是因為,如果數(shù)據(jù)能被修改,那就意味著 Redis 還能正常處理寫操作。否則,所有寫操作都得等到快照完了才能執(zhí)行,性能一下子就降低了。
快照時數(shù)據(jù)能修改嗎?
在給別人拍照時,一旦對方動了,那么這張照片就拍糊了,我們就需要重拍,所以我們當然希望對方保持不動。對于內存快照而言,我們也不希望數(shù)據(jù)“動”。
舉個例子。我們在時刻 t 給內存做快照,假設內存數(shù)據(jù)量是 4GB,磁盤的寫入帶寬是 0.2GB/s,簡單來說,至少需要 20s(4/0.2 = 20)才能做完。如果在時刻 t+5s 時,一個還沒有被寫入磁盤的內存數(shù)據(jù) A,被修改成了 A’,那么就會破壞快照的完整性,因為 A’不是時刻 t 時的狀態(tài)。因此,和拍照類似,我們在做快照時也不希望數(shù)據(jù)“動”,也就是不能被修改。
但是,如果快照執(zhí)行期間數(shù)據(jù)不能被修改,是會有潛在問題的。對于剛剛的例子來說,在做快照的 20s 時間里,如果這 4GB 的數(shù)據(jù)都不能被修改,Redis 就不能處理對這些數(shù)據(jù)的寫操作,那無疑就會給業(yè)務服務造成巨大的影響。
你可能會想到,可以用 bgsave 避免阻塞啊。這里我就要說到一個常見的誤區(qū)了,避免阻塞和正常處理寫操作并不是一回事。此時,主線程的確沒有阻塞,可以正常接收請求,但是,為了保證快照完整性,它只能處理讀操作,因為不能修改正在執(zhí)行快照的數(shù)據(jù)。
為了快照而暫停寫操作,肯定是不能接受的。所以這個時候,Redis 就會借助操作系統(tǒng)提供的寫時復制技術(Copy-On-Write, COW),在執(zhí)行快照的同時,正常處理寫操作。
簡單來說,bgsave 子進程是由主線程 fork 生成的,可以共享主線程的所有內存數(shù)據(jù)。bgsave 子進程運行后,開始讀取主線程的內存數(shù)據(jù),并把它們寫入 RDB 文件。
此時,如果主線程對這些數(shù)據(jù)也都是讀操作(例如圖中的鍵值對 A),那么,主線程和 bgsave 子進程相互不影響。但是,如果主線程要修改一塊數(shù)據(jù)(例如圖中的鍵值對 C),那么,這塊數(shù)據(jù)就會被復制一份,生成該數(shù)據(jù)的副本。然后,bgsave 子進程會把這個副本數(shù)據(jù)寫入 RDB 文件,而在這個過程中,主線程仍然可以直接修改原來的數(shù)據(jù)。
寫時復制機制保證快照期間數(shù)據(jù)可修改
這既保證了快照的完整性,也允許主線程同時對數(shù)據(jù)進行修改,避免了對正常業(yè)務的影響。
到這里,我們就解決了對“哪些數(shù)據(jù)做快照”以及“做快照時數(shù)據(jù)能否修改”這兩大問題:Redis 會使用 bgsave 對當前內存中的所有數(shù)據(jù)做快照,這個操作是子進程在后臺完成的,這就允許主線程同時可以修改數(shù)據(jù)。
現(xiàn)在,我們再來看另一個問題:多久做一次快照?我們在拍照的時候,還有項技術叫“連拍”,可以記錄人或物連續(xù)多個瞬間的狀態(tài)。那么,快照也適合“連拍”嗎?
可以每秒做一次快照嗎?
對于內存快照,所謂的“連拍”即連續(xù)執(zhí)行多次快照。這將大大減小快照之間的時間間隔,即使在某一刻發(fā)生宕機,由于上一刻的快照剛剛執(zhí)行,所丟失的數(shù)據(jù)也會降至最低。然而,快照間隔時間的選擇成為關鍵。
如下圖所示,我們在T0時刻首次執(zhí)行了一次快照,然后在T0+t時刻再次執(zhí)行了快照。在這段時間內,數(shù)據(jù)塊5和9發(fā)生了修改。如果在t時間內出現(xiàn)宕機,只能按照T0時刻的快照進行恢復。這時,由于數(shù)據(jù)塊5和9的修改沒有被記錄在快照中,它們的值將無法完全恢復。
快照機制下的數(shù)據(jù)丟失
所以,要想盡可能恢復數(shù)據(jù),t 值就要盡可能小,t 越小,就越像“連拍”。那么,t 值可以小到什么程度呢,比如說是不是可以每秒做一次快照?畢竟,每次快照都是由 bgsave 子進程在后臺執(zhí)行,也不會阻塞主線程。
這種想法其實是錯誤的。雖然 bgsave 執(zhí)行時不阻塞主線程,但是,如果頻繁地執(zhí)行全量快照,也會帶來兩方面的開銷。
首先,頻繁將完整的數(shù)據(jù)寫入磁盤會對磁盤造成巨大的壓力。多個快照爭相使用有限的磁盤帶寬,這可能導致前一個快照尚未完成,后一個快照已經(jīng)開始,從而產(chǎn)生惡性循環(huán)。
另一方面,bgsave子進程需要通過fork操作從主線程中創(chuàng)建。雖然子進程在創(chuàng)建后不會再次阻塞主線程,但是fork這個創(chuàng)建過程本身會阻塞主線程。而且主線程的內存越大,阻塞時間越長。如果頻繁進行fork以創(chuàng)建bgsave子進程,這將頻繁地阻塞主線程。那么,有沒有更好的方法呢?
在這種情況下,可以考慮使用增量快照。增量快照指的是在生成完整快照后,后續(xù)的快照只記錄已更改的數(shù)據(jù),從而避免每次生成完整快照的開銷。
在第一次做完全量快照后,T1 和 T2 時刻如果再做快照,我們只需要將被修改的數(shù)據(jù)寫入快照文件就行。但是,這么做的前提是,我們需要記住哪些數(shù)據(jù)被修改了。你可不要小瞧這個“記住”功能,它需要我們使用額外的元數(shù)據(jù)信息去記錄哪些數(shù)據(jù)被修改了,這會帶來額外的空間開銷問題。如下圖所示:
增量快照示意圖
如果我們?yōu)槊總€鍵值對的修改都記錄一條記錄,那么當有1萬個鍵值對被修改時,我們就需要額外記錄1萬條記錄。有時,鍵值對可能非常小,例如只有32字節(jié),而為了記錄它們的修改,我們可能需要8字節(jié)的元數(shù)據(jù)信息。在某些情況下,為了記錄修改所引入的額外空間開銷會相當大。對于內存資源寶貴的Redis來說,這可能不是一個劃算的選擇。
從這里可以看出,雖然與AOF相比,快照的恢復速度更快,但快照的頻率很難確定。如果頻率太低,一旦在兩次快照之間發(fā)生宕機,可能會有大量數(shù)據(jù)丟失。如果頻率太高,將導致額外的開銷。那么,有沒有一種方法既能利用RDB的快速恢復,又能以較小的開銷盡量減少數(shù)據(jù)丟失呢?
Redis 4.0 中提出了一個混合使用 AOF 日志和內存快照的方法。簡單來說,內存快照以一定的頻率執(zhí)行,在兩次快照之間,使用 AOF 日志記錄這期間的所有命令操作。
這樣一來,快照不用很頻繁地執(zhí)行,這就避免了頻繁 fork 對主線程的影響。而且,AOF 日志也只用記錄兩次快照間的操作,也就是說,不需要記錄所有操作了,因此,就不會出現(xiàn)文件過大的情況了,也可以避免重寫開銷。
如下圖所示,T1 和 T2 時刻的修改,用 AOF 日志記錄,等到第二次做全量快照時,就可以清空 AOF 日志,因為此時的修改都已經(jīng)記錄到快照中了,恢復時就不再用日志了。
內存快照和AOF混合使用
這個方法既能享受到 RDB 文件快速恢復的好處,又能享受到 AOF 只記錄操作命令的簡單優(yōu)勢,頗有點“魚和熊掌可以兼得”的感覺,建議你在實踐中用起來。
小結
在本篇文章中,我們深入學習了Redis用于避免數(shù)據(jù)丟失的內存快照方法。這種持久化方式具有快速恢復數(shù)據(jù)庫的顯著優(yōu)勢,只需將RDB文件直接加載到內存中,避免了AOF方式需要逐一重新執(zhí)行操作命令所帶來的性能低效問題。
然而,內存快照方法也存在一些限制。它類似于拍攝內存的“大合影”,這不可避免地會占用較多的時間和計算資源。雖然Redis采用了bgsave和寫時復制等方式來最小化內存快照對正常讀寫操作的影響,但頻繁的快照仍然可能對性能產(chǎn)生不可接受的壓力。因此,將RDB和AOF方式混合使用,可以充分利用它們各自的優(yōu)勢,規(guī)避它們的弱點,以較小的性能開銷來同時保證數(shù)據(jù)的可靠性和性能。
最后,關于選擇AOF和RDB的問題,我愿意提供三點建議:
- 當數(shù)據(jù)絕不能丟失時,混合使用內存快照和AOF方式是一個明智的選擇。
- 如果可以容忍分鐘級別的數(shù)據(jù)丟失,可以只使用RDB方式。
- 如果決定僅采用AOF方式,首選使用everysec的配置選項,因為它在可靠性和性能之間取得了較好的平衡。