1. 程式人生 > 其它 >Redis學習記錄 04 AOF(Append Only File)日誌

Redis學習記錄 04 AOF(Append Only File)日誌

前言

Redis 的業務場景下,一般是當作快取使用,因為它把後端資料庫中的資料儲存在記憶體中,然後直接從記憶體中讀取資料,響應速度會非常快。

但是,這裡也有一個絕對不能忽略的問題:一旦伺服器宕機,記憶體中的資料將全部丟失。

很容易想到的一個解決方案是,從後端資料庫恢復這些資料,但這種方式存在兩個問題:一是,需要頻繁訪問資料庫,會給資料庫帶來巨大的壓力;二是,這些資料是從慢速資料庫中讀取出來的,效能肯定比不上從 Redis 中讀取,導致使用這些資料的應用程式響應變慢。

所以,對 Redis 來說,實現資料的持久化,避免從後端資料庫中進行恢復,是至關重要的。

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

。首先學習下 AOF 日誌。

1、AOF日誌是如何實現的

說到日誌,我們比較熟悉的是資料庫的寫前日誌(Write Ahead Log, WAL),也就是說,在實際寫資料前,先把修改的資料記到日誌檔案中,以便故障時進行恢復

不過,AOF 日誌正好相反,它是寫後日志,“寫後”的意思是 Redis 是先執行命令,把資料寫入記憶體,然後才記錄日誌,如下圖所示:

圖1 Redis AOF操作過程

那 AOF 為什麼要先執行命令再記日誌呢?要回答這個問題,我們要先知道 AOF 裡記錄了什麼內容。

傳統資料庫的日誌,例如 redo log(重做日誌),記錄的是修改後的資料,而 AOF 裡記錄的是 Redis 收到的每一條命令,這些命令是以文字形式儲存的。

我們以 Redis 收到“set testkey testvalue”命令後記錄的日誌為例,看看 AOF 日誌的內容。

其中,“*3”表示當前命令有三個部分,每部分都是由“$+數字”開頭,後面緊跟著具體的命令、鍵或值。這裡,“數字”表示這部分中的命令、鍵或值一共有多少位元組。例如,“$3 set”表示這部分有 3 個位元組,也就是“set”命令。

圖2 Redis AOF日誌內容

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

而寫後日志這種方式,就是先讓系統執行命令,只有命令能執行成功,才會被記錄到日誌中,否則,系統就會直接向客戶端報錯。所以,Redis 使用寫後日志這一方式的一大好處是,可以避免出現記錄錯誤命令的情況。

除此之外,AOF 還有一個好處:它是在命令執行後才記錄日誌,所以不會阻塞當前的寫操作。

不過,AOF 也有兩個潛在的風險。

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

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

仔細分析的話,你就會發現,這兩個風險都是和 AOF 寫回磁碟的時機相關的。這也就意味著,如果我們能夠控制一個寫命令執行完後 AOF 日誌寫回磁碟的時機,這兩個風險就解除了。

2、三種寫回策略

其實,對於這個問題,AOF 機制給我們提供了三個選擇,也就是 AOF 配置項 appendfsync 的三個可選值。

Always,同步寫回:每個寫命令執行完,立馬同步地將日誌寫回磁碟;

Everysec,每秒寫回:每個寫命令執行完,只是先把日誌寫到 AOF 檔案的記憶體緩衝區,每隔一秒把緩衝區中的內容寫入磁碟;

No,作業系統控制的寫回:每個寫命令執行完,只是先把日誌寫到 AOF 檔案的記憶體緩衝區,由作業系統決定何時將緩衝區內容寫回磁碟。

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

  • “同步寫回”可以做到基本不丟資料,但是它在每一個寫命令後都有一個慢速的落盤操作,不可避免地會影響主執行緒效能;
  • 雖然“作業系統控制的寫回”在寫完緩衝區後,就可以繼續執行後續的命令,但是落盤的時機已經不在 Redis 手中了,只要 AOF 記錄沒有寫回磁碟,一旦宕機對應的資料就丟失了;
  • “每秒寫回”採用一秒寫回一次的頻率,避免了“同步寫回”的效能開銷,雖然減少了對系統性能的影響,但是如果發生宕機,上一秒內未落盤的命令操作仍然會丟失。所以,這隻能算是,在避免影響主執行緒效能和避免資料丟失兩者間取了個折中。

圖3 AOF寫回策略

到這裡,我們就可以根據系統對高效能和高可靠性的要求,來選擇使用哪種寫回策略了。總結一下就是:

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

但是,按照系統的效能需求選定了寫回策略,並不是“高枕無憂”了。畢竟,AOF 是以檔案的形式在記錄接收到的所有寫命令。隨著接收的寫命令越來越多,AOF 檔案會越來越大。

這也就意味著,我們一定要小心 AOF 檔案過大帶來的效能問題。

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

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

所以,我們就要採取一定的控制手段,這個時候,AOF 重寫機制就登場了。

3、日誌檔案太大了怎麼辦?

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

比如說,當讀取了鍵值對“testkey”: “testvalue”之後,重寫機制會記錄 set testkey testvalue 這條命令。這樣,當需要恢復時,可以重新執行該命令,實現“testkey”: “testvalue”的寫入。

為什麼重寫機制可以把日誌檔案變小呢? 實際上,重寫機制具有“多變一”功能。所謂的“多變一”,也就是說,舊日誌檔案中的多條命令,在重寫後的新日誌中變成了一條命令。

我們知道,AOF 檔案是以追加的方式,逐一記錄接收到的寫命令的。當一個鍵值對被多條寫命令反覆修改時,AOF 檔案會記錄相應的多條命令。但是,在重寫的時候,是根據這個鍵值對當前的最新狀態,為它生成對應的寫入命令。

這樣一來,一個鍵值對在重寫日誌中只用一條命令就行了,而且,在日誌恢復時,只用執行這條命令,就可以直接完成這個鍵值對的寫入了。

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

圖4AOF重寫減少日誌大小

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

不過,雖然 AOF 重寫後,日誌檔案會縮小,但是,要把整個資料庫的最新資料的操作日誌都寫回磁碟,仍然是一個非常耗時的過程。

這時,我們就要繼續關注另一個問題了:重寫會不會阻塞主執行緒?

4、AOF 重寫會阻塞嗎?

和 AOF 日誌由主執行緒寫回不同,重寫過程是由後臺子程序 bgrewriteaof 來完成的,這也是為了避免阻塞主執行緒,導致資料庫效能下降。

重寫的過程可以總結為“一個拷貝,兩處日誌”

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

注意,fork子程序時,子程序是會拷貝父程序的頁表,即虛實對映關係,而不會拷貝實體記憶體。子程序複製了父程序頁表,也能共享訪問父程序的記憶體資料了,此時,類似於有了父程序的所有記憶體資料。

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

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

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

圖5AOF非阻塞的重寫過程

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

小結

Redis 通過AOF 方法避免資料丟失。這個方法通過逐一記錄操作命令,在恢復時再逐一執行命令的方式,保證了資料的可靠性。

這個方法看似“簡單”,但也是充分考慮了對 Redis 效能的影響。

總結來說,它提供了 AOF 日誌的三種寫回策略,分別是 Always、Everysec 和 No,這三種策略在可靠性上是從高到低,而在效能上則是從低到高。

此外,為了避免日誌檔案過大,Redis 還提供了 AOF 重寫機制,直接根據資料庫裡資料的最新狀態,生成這些資料的插入命令,作為新日誌。這個過程通過後臺執行緒完成,避免了對主執行緒的阻塞。

其中,三種寫回策略體現了系統設計中的一個重要原則 ,即 trade-off,或者稱為“取捨”,指的就是在效能和可靠性保證之間做取捨。

注意到,落盤時機和重寫機制都是在“記日誌”這一過程中發揮作用的。例如,落盤時機的選擇可以避免記日誌時阻塞主執行緒,重寫可以避免日誌檔案過大。但是,在“用日誌”的過程中,也就是使用 AOF 進行故障恢復時,我們仍然需要把所有的操作記錄都執行一遍。再加上 Redis 的單執行緒設計,這些命令操作只能一條一條按順序執行,這個“重放”的過程就會很慢了。

那麼,有沒有既能避免資料丟失,又能更快地恢復的方法呢?當然有,那就是 RDB 快照了。

問答

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

答:Redis採用fork子程序重寫AOF檔案時,潛在的阻塞風險包括:fork子程序 和 AOF重寫過程中父程序產生寫入的場景,下面依次介紹。

a、fork子程序,fork這個瞬間一定是會阻塞主執行緒的

fork時並不會一次性拷貝所有記憶體資料給子程序,fork採用作業系統提供的寫實複製(Copy On Write)機制,就是為了避免一次性拷貝大量記憶體資料給子程序造成的長時間阻塞問題,但fork子程序需要拷貝程序必要的資料結構,其中有一項就是拷貝記憶體頁表(虛擬記憶體和實體記憶體的對映索引表),這個拷貝過程會消耗大量CPU資源,拷貝完成之前整個程序是會阻塞的,阻塞時間取決於整個例項的記憶體大小,例項越大,記憶體頁表越大,fork阻塞時間越久。拷貝記憶體頁表完成後,子程序與父程序指向相同的記憶體地址空間,也就是說此時雖然產生了子程序,但是並沒有申請與父程序相同的記憶體大小。那什麼時候父子程序才會真正記憶體分離呢?“寫實複製”顧名思義,就是在寫發生時,才真正拷貝記憶體真正的資料,這個過程中,父程序也可能會產生阻塞的風險,就是下面介紹的場景。

b、fork出的子程序指向與父程序相同的記憶體地址空間,此時子程序就可以執行AOF重寫,把記憶體中的所有資料寫入到AOF檔案中。但是此時父程序依舊是會有流量寫入的。

如果父程序操作的是一個已經存在的key,那麼這個時候父程序就會真正拷貝這個key對應的記憶體資料,申請新的記憶體空間,這樣逐漸地,父子程序記憶體資料開始分離,父子程序逐漸擁有各自獨立的記憶體空間。因為記憶體分配是以頁為單位進行分配的,預設4k,如果父程序此時操作的是一個bigkey,重新申請大塊記憶體耗時會變長,可能會產阻塞風險。

另外,如果作業系統開啟了記憶體大頁機制(Huge Page,頁面大小2M),那麼父程序申請記憶體時阻塞的概率將會大大提高,所以在Redis機器上需要關閉Huge Page機制。Redis每次fork生成RDB或AOF重寫完成後,都可以在Redis log中看到父程序重新申請了多大的記憶體空間。

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

答:AOF重寫不復用AOF本身的日誌,一個原因是父子程序寫同一個檔案必然會產生競爭問題,控制競爭就意味著會影響父程序的效能。

二是如果AOF重寫過程中失敗了,那麼原本的AOF檔案相當於被汙染了,無法做恢復使用。所以Redis AOF重寫一個新檔案,重寫失敗的話,直接刪除這個檔案就好了,不會對原先的AOF檔案產生影響。等重寫完成之後,直接替換舊檔案即可