阿里面試官問我Redis持久化策略,這一篇文章足以征服他 一、AOF日誌的實現 AOF的回寫策略 AOF 重寫會阻塞嗎? 二、RDB內存快照 對哪些數據做快照? 快照期間,是否可以對數據進行改動?

redis是一個內存數據庫,一旦服務器宕機,內存中的數據將全部丟失。所以,對 Redis 來說,實現數據的持久化,避免從後端數據庫中進行恢復,是至關重要的。

目前,Redis 的持久化主要有兩大機制,即 AOF(Append Only File)日誌和 RDB 快照。

一、AOF日誌的實現

我們知道數據庫的寫前日誌(Write Ahead Log, WAL)是在在實際寫數據前,先把修改的數據記到日誌文件中,以便故障時進行恢復。不過,AOF 日誌正好相反,它是寫後日志,“寫後”的意思是 Redis 是先執行命令,把數據寫入內存,然後才記錄日誌,如下圖所示:

那 AOF 爲什麼要先執行命令再記日誌呢?

傳統數據庫的日誌,例如 redo log(重做日誌),記錄的是修改後的數據,而 AOF 裏記錄的是 Redis 收到的每一條命令,這些命令是以文本形式保存的。我們以 Redis 收到“set testkey testvalue”命令後記錄的日誌爲例,看看 AOF 日誌的內容。其中,“*3”表示當前命令有三個部分,每部分都是由“+數字”開頭,後面緊跟着具體的命令、鍵或值。這裏,“數字”表示這部分中的命令、鍵或值一共有多少字節。例如,“3 set”表示這部分有 3 個字節,也就是“set”命令。

但是,爲了避免額外的檢查開銷,Redis 在向 AOF 裏面記錄日誌的時候,並不會先去對這些命令進行語法檢查。所以,如果先記日誌再執行命令的話,日誌中就有可能記錄了錯誤的命令,Redis 在使用日誌恢復數據時,就可能會出錯。

而寫後日志這種方式,就是先讓系統執行命令,只有命令能執行成功,纔會被記錄到日誌中,否則,系統就會直接向客戶端報錯。所以,Redis 使用寫後日志這一方式的一大好處是,可以避免出現記錄錯誤命令的情況。除此之外,AOF 還有一個好處:它是在命令執行後才記錄日誌,所以不會阻塞當前的寫操作。

AOF 也有兩個潛在的風險:

首先,如果剛執行完一個命令,還沒有來得及記日誌就宕機了,那麼這個命令和相應的數據就有丟失的風險。如果此時 Redis 是用作緩存,還可以從後端數據庫重新讀入數據進行恢復,但是,如果 Redis 是直接用作數據庫的話,此時,因爲命令沒有記入日誌,所以就無法用日誌進行恢復了。

其次,AOF 雖然避免了對當前命令的阻塞,但可能會給下一個操作帶來阻塞風險。這是因爲,AOF 日誌也是在主線程中執行的,如果在把日誌文件寫入磁盤時,磁盤寫壓力大,就會導致寫盤很慢,進而導致後續的操作也無法執行了。

這兩個風險都是和 AOF 寫回磁盤的時機相關的。這也就意味着,如果我們能夠控制一個寫命令執行完後 AOF 日誌寫回磁盤的時機,這兩個風險就解除了。

AOF的回寫策略

AOF 機制給我們提供了三個選擇,也就是 AOF 配置項 appendfsync 的三個可選值。

  • Always,同步寫回:每個寫命令執行完,立馬同步地將日誌寫回磁盤;
  • Everysec,每秒寫回:每個寫命令執行完,只是先把日誌寫到 AOF 文件的內存緩衝區,每隔一秒把緩衝區中的內容寫入磁盤;
  • No,操作系統控制的寫回:每個寫命令執行完,只是先把日誌寫到 AOF 文件的內存緩衝區,由操作系統決定何時將緩衝區內容寫回磁盤。

針對避免主線程阻塞和減少數據丟失問題,這三種寫回策略都無法做到兩全其美。我們來分析下其中的原因。

“同步寫回”可以做到基本不丟數據,但是它在每一個寫命令後都有一個慢速的落盤操作,不可避免地會影響主線程性能;

雖然“操作系統控制的寫回”在寫完緩衝區後,就可以繼續執行後續的命令,但是落盤的時機已經不在 Redis 手中了,只要 AOF 記錄沒有寫回磁盤,一旦宕機對應的數據就丟失了;

“每秒寫回”採用一秒寫回一次的頻率,避免了“同步寫回”的性能開銷,雖然減少了對系統性能的影響,但是如果發生宕機,上一秒內未落盤的命令操作仍然會丟失。所以,這隻能算是,在避免影響主線程性能和避免數據丟失兩者間取了個折中。

總結一下就是:想要獲得高性能,就選擇 No 策略;如果想要得到高可靠性保證,就選擇 Always 策略;如果允許數據有一點丟失,又希望性能別受太大影響的話,那麼就選擇 Everysec 策略。

按照系統的性能需求選定了寫回策略,並不是“高枕無憂”了。畢竟,AOF 是以文件的形式在記錄接收到的所有寫命令。隨着接收的寫命令越來越多,AOF 文件會越來越大。這也就意味着,我們一定要小心 AOF 文件過大帶來的性能問題。

這裏的“性能問題”,主要在於以下三個方面:

  • 一是,文件系統本身對文件大小有限制,無法保存過大的文件;
  • 二是,如果文件太大,之後再往裏面追加命令記錄的話,效率也會變低;
  • 三是,如果發生宕機,AOF 中記錄的命令要一個個被重新執行,用於故障恢復,如果日誌文件太大,整個恢復過程就會非常緩慢,這就會影響到 Redis 的正常使用。

因此我們需要AOF重寫機制

簡單來說,AOF 重寫機制就是在重寫時,Redis 根據數據庫的現狀創建一個新的 AOF 文件,也就是說,讀取數據庫中的所有鍵值對,然後對每一個鍵值對用一條命令記錄它的寫入。

爲什麼重寫機制可以把日誌文件變小呢?

實際上,重寫機制具有“多變一”功能。所謂的“多變一”,也就是說,舊日誌文件中的多條命令,在重寫後的新日誌中變成了一條命令。

AOF 文件是以追加的方式,逐一記錄接收到的寫命令的。當一個鍵值對被多條寫命令反覆修改時,AOF 文件會記錄相應的多條命令。但是,在重寫的時候,是根據這個鍵值對當前的最新狀態,爲它生成對應的寫入命令。這樣一來,一個鍵值對在重寫日誌中只用一條命令就行了,而且,在日誌恢復時,只用執行這條命令,就可以直接完成這個鍵值對的寫入了。

下面這張圖就是一個例子:

當我們對一個列表先後做了 6 次修改操作後,列表的最後狀態是[“D”, “C”, “N”],此時,只用 LPUSH u:list “N”, “C”, "D"這一條命令就能實現該數據的恢復,這就節省了五條命令的空間。對於被修改過成百上千次的鍵值對來說,重寫能節省的空間當然就更大了。

不過,雖然 AOF 重寫後,日誌文件會縮小,但是,要把整個數據庫的最新數據的操作日誌都寫回磁盤,仍然是一個非常耗時的過程。這時,我們就要繼續關注另一個問題了:重寫會不會阻塞主線程?

AOF 重寫會阻塞嗎?

和 AOF 日誌由主線程寫回不同,重寫過程是由後臺子進程 bgrewriteaof 來完成的,這也是爲了避免阻塞主線程,導致數據庫性能下降。

我把重寫的過程總結爲“一個拷貝,兩處日誌”。

“一個拷貝”就是指,每次執行重寫時,主線程 fork 出後臺的 bgrewriteaof 子進程。此時,fork 會把主線程的內存拷貝一份給 bgrewriteaof 子進程,這裏面就包含了數據庫的最新數據。然後,bgrewriteaof 子進程就可以在不影響主線程的情況下,逐一把拷貝的數據寫成操作,記入重寫日誌。

“兩處日誌”又是什麼呢?

因爲主線程未阻塞,仍然可以處理新來的操作。此時,如果有寫操作,第一處日誌就是指正在使用的 AOF 日誌,Redis 會把這個操作寫到它的緩衝區。這樣一來,即使宕機了,這個 AOF 日誌的操作仍然是齊全的,可以用於恢復。

而第二處日誌,就是指新的 AOF 重寫日誌。這個操作也會被寫到重寫日誌的緩衝區。這樣,重寫日誌也不會丟失最新的操作。等到拷貝數據的所有操作記錄重寫完成後,重寫日誌記錄的這些最新操作也會寫入新的 AOF 文件,以保證數據庫最新狀態的記錄。此時,我們就可以用新的 AOF 文件替代舊文件了。

總結來說,每次 AOF 重寫時,Redis 會先執行一個內存拷貝,用於重寫;然後,使用兩個日誌保證在重寫過程中,新寫入的數據不會丟失。而且,因爲 Redis 採用額外的線程進行數據重寫,所以,這個過程並不會阻塞主線程。

AOF 日誌重寫的時候,是由 bgrewriteaof 子進程來完成的,不用主線程參與,我們今天說的非阻塞也是指子進程的執行不阻塞主線程。但是,你覺得,這個重寫過程有沒有其他潛在的阻塞風險呢?如果有的話,會在哪裏阻塞?

  1. fork子進程,fork這個瞬間一定是會阻塞主線程的(注意,fork時並不會一次性拷貝所有內存數據給子進程),fork採用操作系統提供的寫實複製(Copy On Write)機制,就是爲了避免一次性拷貝大量內存數據給子進程造成的長時間阻塞問題,但fork子進程需要拷貝進程必要的數據結構,其中有一項就是拷貝內存頁表(虛擬內存和物理內存的映射索引表),這個拷貝過程會消耗大量CPU資源,拷貝完成之前整個進程是會阻塞的,阻塞時間取決於整個實例的內存大小,實例越大,內存頁表越大,fork阻塞時間越久。拷貝內存頁表完成後,子進程與父進程指向相同的內存地址空間,也就是說此時雖然產生了子進程,但是並沒有申請與父進程相同的內存大小。那什麼時候父子進程纔會真正內存分離呢?“寫實複製”顧名思義,就是在寫發生時,才真正拷貝內存真正的數據,這個過程中,父進程也可能會產生阻塞的風險,就是下面介紹的場景。
  2. fork出的子進程指向與父進程相同的內存地址空間,此時子進程就可以執行AOF重寫,把內存中的所有數據寫入到AOF文件中。但是此時父進程依舊是會有流量寫入的,如果父進程操作的是一個已經存在的key,那麼這個時候父進程就會真正拷貝這個key對應的內存數據,申請新的內存空間,這樣逐漸地,父子進程內存數據開始分離,父子進程逐漸擁有各自獨立的內存空間。因爲內存分配是以頁爲單位進行分配的,默認4k,如果父進程此時操作的是一個bigkey,重新申請大塊內存耗時會變長,可能會產阻塞風險。另外,如果操作系統開啓了內存大頁機制(Huge Page,頁面大小2M),那麼父進程申請內存時阻塞的概率將會大大提高,所以在Redis機器上需要關閉Huge Page機制。Redis每次fork生成RDB或AOF重寫完成後,都可以在Redis log中看到父進程重新申請了多大的內存空間。

AOF 重寫也有一個重寫日誌,爲什麼它不共享使用 AOF 本身的日誌呢?

AOF重寫不復用AOF本身的日誌,一個原因是父子進程寫同一個文件必然會產生競爭問題,控制競爭就意味着會影響父進程的性能。二是如果AOF重寫過程中失敗了,那麼原本的AOF文件相當於被污染了,無法做恢復使用。所以Redis AOF重寫一個新文件,重寫失敗的話,直接刪除這個文件就好了,不會對原先的AOF文件產生影響。等重寫完成之後,直接替換舊文件即可。

二、RDB內存快照

所謂內存快照,就是指內存中的數據在某一個時刻的狀態記錄。這就類似於照片,當你給朋友拍照時,一張照片就能把朋友一瞬間的形象完全記下來。

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

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

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

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

對哪些數據做快照?

Redis 的數據都在內存中,爲了提供所有數據的可靠性保證,它執行的是全量快照,也就是說,把內存中的所有數據都記錄到磁盤中,這樣做的好處是,一次性記錄了所有數據,一個都不少。

給內存的全量數據做快照,把它們全部寫入磁盤也會花費很多時間。而且,全量數據越多,RDB 文件就越大,往磁盤上寫數據的時間開銷就越大。

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

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

  • save:在主線程中執行,會導致阻塞;
  • bgsave:創建一個子進程,專門用於寫入 RDB 文件,避免了主線程的阻塞,這也是 Redis RDB 文件生成的默認配置。

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

快照期間,是否可以對數據進行改動?

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

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

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

簡單來說,bgsave 子進程是由主線程 fork 生成的,可以共享主線程的所有內存數據。bgsave 子進程運行後,開始讀取主線程的內存數據,並把它們寫入 RDB 文件。

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

這既保證了快照的完整性,也允許主線程同時對數據進行修改,避免了對正常業務的影響。到這裏,我們就解決了對“哪些數據做快照”以及“做快照時數據能否修改”這兩大問題:Redis 會使用 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 只記錄操作命令的簡單優勢,頗有點“魚和熊掌可以兼得”的感覺,建議你在實踐中用起來。

場景:我們使用一個 2 核 CPU、4GB 內存、500GB 磁盤的雲主機運行 Redis,Redis 數據庫的數據量大小差不多是 2GB,我們使用了 RDB 做持久化保證。當時 Redis 的運行負載以修改操作爲主,寫讀比例差不多在 8:2 左右,也就是說,如果有 100 個請求,80 個請求執行的是修改操作。你覺得,在這個場景下,用 RDB 做持久化有什麼風險嗎?

內存資源風險:Redis fork子進程做RDB持久化,由於寫的比例爲80%,那麼在持久化過程中,“寫實複製”會重新分配整個實例80%的內存副本,大約需要重新分配1.6GB內存空間,這樣整個系統的內存使用接近飽和,如果此時父進程又有大量新key寫入,很快機器內存就會被喫光,如果機器開啓了Swap機制,那麼Redis會有一部分數據被換到磁盤上,當Redis訪問這部分在磁盤上的數據時,性能會急劇下降,已經達不到高性能的標準(可以理解爲武功被廢)。如果機器沒有開啓Swap,會直接觸發OOM,父子進程會面臨被系統kill掉的風險。

CPU資源風險:雖然子進程在做RDB持久化,但生成RDB快照過程會消耗大量的CPU資源,雖然Redis處理處理請求是單線程的,但Redis Server還有其他線程在後臺工作,例如AOF每秒刷盤、異步關閉文件描述符這些操作。由於機器只有2核CPU,這也就意味着父進程佔用了超過一半的CPU資源,此時子進程做RDB持久化,可能會產生CPU競爭,導致的結果就是父進程處理請求延遲增大,子進程生成RDB快照的時間也會變長,整個Redis Server性能下降。
另外,可以再延伸一下,Redis進程是否綁定了CPU,如果綁定了CPU,那麼子進程會繼承父進程的CPU親和性屬性,子進程必然會與父進程爭奪同一個CPU資源,整個Redis Server的性能必然會受到影響!所以如果Redis需要開啓定時RDB和AOF重寫,進程一定不要綁定CPU。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章