就這?Redis持久化策略——AOF


我是蟬沐風,一個讓你沉迷於技術的講述者
公眾號【蟬沐風】,歡迎大家關注留言

上一篇文章給大家介紹了Redis是如何通過RDB的方式進行數據持久化的

就這?Redis持久化策略——RDB

這篇文章繼續為大家介紹Redis的另一種持久化策略——AOF

什么是AOF

男孩“一覺醒來”忘記了對女孩子的承諾,這時候女孩子把曾經海誓山盟的錄音逐條播放給男孩子聽,幫助他“恢復記憶”。

“男孩一覺醒來”像極了Redis宕機重啟的樣子,而女孩子的錄音就是Redis的AOF日志

AOF(Append Only File)以文本的形式(文本格式由Redis自定義,后文會講到),通過將所有對數據庫的寫入命令記錄到AOF文件中,達到記錄數據庫狀態的目的。

注意:AOF文件只會記錄Redis的寫操作命令,因為讀命令對數據的恢復沒有任何意義

image

Redis默認並未開啟AOF功能,redis.conf配置文件中,關於AOF的相關配置如下

# 是否開啟AOF功能(開啟:yes 關閉:no)
appendonly yes
# 生成的AOF文件名稱
appendfilename 6379.aof
# AOF寫回策略
appendfsync everysec
# 當前AOF文件大小和最后一次重寫后的大小之間的比率>=指定的增長百分比則進行重寫
# 如100代表當前AOF文件大小是上次重寫的兩倍時候才重寫
auto-aof-rewrite-percentage 100
# AOF文件最小重寫大小,只有當AOF文件大小大於該值時候才可能重寫,4.0默認配置64mb。
auto-aof-rewrite-min-size 64mb

AOF日志格式

下面我們通過一個例子,看一下AOF機制是如何保存我們的操作日志的,我們對Redis進行如下操作

127.0.0.1:6379[3]> RPUSH list 1 2 3 4 5
(integer) 5
127.0.0.1:6379[3]> LRANGE list 0 -1
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
127.0.0.1:6379[3]> RPOP list
"5"
127.0.0.1:6379[3]> LPUSH list 0
(integer) 5
127.0.0.1:6379[3]> KEYS *
1) "list"
127.0.0.1:6379[3]> LRANGE list 0 -1
1) "0"
2) "1"
3) "2"
4) "3"
5) "4"

Redis會將上述所有的寫指令保存到AOF文件中,如下所示

RPUSH list 1 2 3 4 5
RPOP list
LPUSH list 0

當然,AOF文件不是直接以指令的格式進行存儲的,我們以第一條指令RPUSH list 1 2 3 4 5為例,看一下AOF文件實際保存該條指令的格式。

*2
$6
SELECT
$1
3
*7
$5
RPUSH
$4
list
$1
1
$1
2
$1
3
$1
4
$1
5

除了 SELECT命令是AOF程序自己加上去的之外, 其他命令都是之前我們在終端里執行的命令。自動添加這條指令是因為Redis恢復數據的時候需要知道待恢復的數據屬於哪一個數據庫。

其中,*2表示當前命令有2個部分,每部分都是由$+數字開頭,后面緊跟着具體的命令、鍵或值。數字表示這部分中的命令、鍵或值一共有多少字節。例如,$6 SELECT表示這部分有 6 個字節,也就是SELECT命令。

AOF日志的生成過程

從我們發送寫指令開始到指令保存在AOF文件中,需要經歷4步,分別為命令傳播命令追加文件寫入文件同步

命令傳播

命令傳播:Redis 將執行完的命令、命令的參數、命令的參數個數等信息發送到 AOF程序中

image

大家有沒有關注到其中的一個細節,AOF日志寫入是在Redis成功執行命令之后才進行的,為什么要在執行之后而不是之前呢?原因有兩點:

首先試想一下,如果我們不小心輸錯了Redis指令,然后Redis緊接着將該指令保存到了AOF文件中,等到Redis進行數據恢復的時候就可能導致錯誤。因此這種寫后日志的形式可以避免對指令進行語法檢查,避免出現記錄錯誤指令的情況

其次,先執行命令后保存日志,不會阻塞當前的寫操作

但是,AOF寫后日志也有兩個風險。

第一個風險,假如Redis寫操作成功之后突然宕機,此時AOF日志還未來得及寫入,則該條指令和相關參數就有丟失的風險。

第二個風險,AOF雖然避免了對當前操作的阻塞,但是有可能阻塞下一個操作。因為保存AOF日志的部分工作也是由主線程完成的(下文有詳細介紹),Redis的內存操作速度和文件寫入速度簡直是雲泥之別,如果主線程在文件保存的過程中花費太長的時間必然會阻塞后續的操作。

分析就會發現,第一個風險與AOF寫回磁盤的時機有關,寫回磁盤的頻率越高,發生數據丟失的可能性就越小。第二個風險和文件寫入方式以及時機有關,如果Redis每次成功執行指令之后都力圖將當前指令同步到AOF文件,開銷必然很大。

因此Redis引入了緩沖區的概念,緩沖區對應了文件的寫入方式(不求一步到位,允許循序漸進地寫入),而何時將緩沖區的內容徹底同步到文件就涉及到了AOF的同步策略(寫回磁盤的時機)。

命令追加

在AOF開啟的情況下,Redis會將成功執行的寫指令以上文我們講過的協議格式追加到Redis的aof_buf緩沖區。

struct redisServer {

    // ...

    // AOF緩沖區
    sds aof_buf;

    // ...
};

aof_buf 緩沖區保存着所有等待寫入到AOF 文件的協議文本。

至此,將命令追加到緩存區的步驟完成。

文件寫入

文件的寫入同步操作往往被放在一起介紹,這里之所以分開,是想向讀者強調文件的寫入和同步是兩步不同的操作

為了提高文件的寫入效率,當用戶調用write函數將數據寫入到文件時,操作系統內核會將數據首先保存在內存緩沖區中,等到緩沖區的空間被填滿或者到達一定的時機之后,內核會將數據同步到磁盤。

這種同步過於依賴於操作系統內核,時機無法掌控。為此,操作系統提供了fsyncfdatasync兩個同步函數,可以強制內核立即將緩沖區內的數據同步到磁盤。

Redis的主服務進程本質上是一個死循環,循環中有負責接受客戶端的請求,並向客戶端發送回執的邏輯,我們稱之為文件事件

在AOF功能開啟的情況下,文件事件會將成功執行之后的寫命令追加aof_buf緩沖區,在主服務進程死循環的最后,會調用flushAppendOnlyFile函數,該函數會將aof_buf中的數據寫入到內核緩沖區,然后判斷是否應該進行同步。偽代碼如下:

void eventLoop {
    
    while(true){
        
        // ...

        // 文件事件,接受命令請求,返回客戶端回執
        // 根據aof功能是否開啟,決定是否將寫命令追加到aof_buf緩沖區
        handleFileEvents();

        // 將aof_buf數據寫入內核緩沖區
        // 判斷是否需要將數據同步到磁盤
        flushAppendOnlyFile();
        
        // ...

    }
   
};

而是否進行同步則是由Redis配置中的appendOnlyFile選項來決定的。

文件同步

redis.conf配置文件中appendOnlyFile的選項有三個值可選,對應三種AOF同步策略,分別是

  1. No :同步時機由內核決定。
  2. Everysec :每一秒鍾同步一次。
  3. Always :每執行一個命令同步一次。

No

由操作系統內核決定同步時機,每個寫命令執行完,只是先把日志寫入AOF文件的內核緩沖區,不立即進行同步。在這種模式下, 同步只會在以下任意一種情況下被執行:

  • Redis 被關閉
  • AOF功能被關閉
  • 系統的寫緩存被刷新(可能是緩存已經被寫滿,或者定期保存操作被執行)

這三種情況下的同步操作都會引起 Redis 主進程阻塞。

Everysec

如果用戶未指定appendOnlyFile的值,則默認值為Everysec。每秒同步,每個寫命令執行完,只是先把日志寫到 AOF文件的內核緩沖區,理論上每隔1秒把緩沖區中的內容同步到磁盤,且同步操作有單獨的子線程進行,因此不會阻塞主進程。

需要注意的是,我們用的是「理論上」這樣的措辭,實際運行中該模式對fsyncfdatasync的調用並不是每秒一次,而是和調用flushAppendOnlyFile函數時Redis所處的狀態有關。

每當 flushAppendOnlyFile 函數被調用時, 可能會出現以下四種情況:

  • 子線程正在執行同步,並且

    • 這個同步的執行時間未超過 2 秒,那么程序直接返回 ;

    • 這個同步已經執行超過 2 秒,那么程序執行寫入操作 ,但不執行新的同步操作 。但是,這時的寫入操作必須等待子線程先完成原本的同步操作 ,因此這里的寫入操作會比平時阻塞更長時間。

  • 子線程沒有在執行同步 ,並且

    • 上次成功執行同步距今不超過1秒,那么程序執行寫入,但不執行同步 ;

    • 上次成功執行同步距今已經超過1秒,那么程序執行寫入和同步 。

可以用流程圖表示這四種情況:

image

Everysec模式下

  • 如果在情況1下宕機,那么我們最多損失小於2秒內的所有數據。

  • 如果在情況2下宕機,那么我們損失的數據可能會超過2秒。

因此AOFEverysec模式下只會丟失 1 秒鍾數據的說法實際上並不准確。

Always

每個寫命令執行完,立刻同步地將日志寫回磁盤。此模式下同步操作是由 Redis 主進程執行的,所以在同步執行期間,主進程會被阻塞,不能接受命令請求。

AOF同步策略小結

對於三種 AOF 同步模式, 它們對Redis主進程的阻塞情況如下:

  1. 不同步(No):寫入和同步都由主進程執行,兩個操作都會阻塞主進程;
  2. 每一秒鍾同步一次(Everysec):寫入操作由主進程執行,阻塞主進程。同步操作由子線程執行,不直接阻塞主進程,但同步操作完成的快慢會影響寫入操作的阻塞時長;
  3. 每執行一個命令同步一次(Always):同模式 1 。

因為阻塞操作會讓 Redis 主進程無法持續處理請求, 所以一般說來, 阻塞操作執行得越少、完成得越快, Redis 的性能就越好。

No的同步操作只會在AOF關閉或Redis關閉時執行, 或由操作系統內核觸發。在一般情況下, 這種模式只需要為寫入阻塞,因此它的寫入性能要比后面兩種模式要高, 但是這種性能的提高是以降低安全性為代價的:在這種模式下,如果發生宕機,那么丟失的數據量由操作系統內核的緩存沖洗策略決定。

Everysec在性能方面要優於Always , 並且在通常情況下,這種模式最多丟失不多於2秒的數據, 所以它的安全性要高於No ,這是一種兼顧性能和安全性的保存方案。

Always的安全性是最高的,但性能也是最差的,因為Redis必須阻塞直到命令信息被寫入並同步到磁盤之后才能繼續處理請求。

三種 AOF模式的特性可以總結為如下表格

image

AOF生成過程小結

image

最后總結一下AOF文件的生成過程。以下步驟都是在AOF開啟的前提下進行的

  1. Redis成功執行寫操作指令,然后將寫的指令按照自定義格式追加到aof_buf緩沖區,這是第一個緩沖區;
  2. Redis主進程將aof_buf緩沖區的數據寫入到內核緩沖區,這是第二個緩沖區;
  3. 根據AOF同步策略適時地將內核緩沖區的數據同步到磁盤,過程結束。

AOF文件的載入和數據還原

AOF文件中包含了能夠重建數據庫的所有寫命令,因此將所有命令讀入並依次執行即可還原Redis之前的數據狀態。

Redis 讀取AOF文件並還原數據庫的詳細步驟如下:

  1. 創建一個不帶網絡連接的偽客戶端(fake client),偽客戶端執行命令的效果, 和帶網絡連接的客戶端執行命令的效果完全相同;
  2. 讀取AOF所保存的文本,並根據內容還原出命令、命令的參數以及命令的個數;
  3. 根據指令、指令的參數等信息,使用偽客戶端執行命令。
  4. 執行 2 和 3 ,直到AOF文件中的所有命令執行完畢。

注意:為了避免對數據的完整性產生影響, 在服務器載入數據的過程中, 只有和數據庫無關的發布訂閱功能可以正常使用, 其他命令一律返回錯誤。

AOF重寫

AOF的作用是幫我們還原Redis的數據狀態,其中包含了所有的寫操作,但是正常情況下客戶端會對同一個KEY進行多次不同的寫操作,如下

127.0.0.1:6379[3]> SET name chanmufeng1
OK
127.0.0.1:6379[3]> SET name chanmufeng2
OK
127.0.0.1:6379[3]> SET name chanmufeng3
OK
127.0.0.1:6379[3]> SET name chanmufeng4
OK
127.0.0.1:6379[3]> SET name chanmufeng
OK

例子中對name的數據進行寫操作就進行了5次,其實對我們而言僅需要最后一條指令而已,但是AOF會將這5條指令都記錄下來。更極端的情況是有些被頻繁操作的鍵, 對它們所調用的命令可能有成百上千、甚至上萬條, 如果這樣被頻繁操作的鍵有很多的話,AOF文件的體積就會急速膨脹。

首先,AOF文件的體積受操作系統大小的限制,本身就不能無限增長;

其次,體積過於龐大的AOF文件會影響指令的寫入速度,阻塞時間延長;

最后AOF文件的體積越大,Redis數據恢復所需的時間也就越長。

為了解決AOF文件體積龐大的問題,Redis提供了rewriteAOF重寫功能來精簡AOF文件體積。

AOF重寫的實現原理

雖然叫AOF「重寫」,但是新AOF文件的生成並非是在原AOF文件的基礎上進行操作得到的,而是讀取Redis當前的數據狀態來重新生成的。不難理解,后者的處理方式遠比前者高效。

為了避免阻塞主線程,導致數據庫性能下降,和 AOF 日志由主進程寫回不同,重寫過程是由子進程執行bgrewriteaof 來完成的。這樣處理的最大好處是:

  1. 子進程進行 AOF重寫期間,主進程可以繼續處理命令請求;
  2. 子進程帶有主進程的數據副本,操作效率更高。

這里有兩個問題值得我們來思考一下

1.為什么使用子進程,而不是多線程來進行AOF重寫呢?

如果是使用線程,線程之間會共享內存,在修改共享內存數據的時候,需要通過加鎖來保證數據的安全,這樣就會降低性能。

如果使用子進程,操作系統會使用「寫時復制」的技術:fork子進程時,子進程會拷貝父進程的頁表,即虛實映射關系,而不會拷貝物理內存。子進程復制了父進程頁表,也能共享訪問父進程的內存數據,達到共享內存的效果。

不過這個共享的內存只能以只讀的方式,當父子進程任意一方修改了該共享內存,就會發生「寫時復制」,於是父子進程就有了獨立的數據副本,就不用加鎖來保證數據安全。

這里我把在就這?Redis持久化策略——RDB畫過的一張圖拿過來幫助大家理解一下寫時復制

image

因此,有兩個過程可能會導致主進程阻塞:

  • fork子進程的過程中,由於要復制父進程的頁表等數據,阻塞的時間跟頁表的大小有關,頁表越大阻塞的時間也越長,不過通常而言該過程是非常快的;
  • fork完子進程后,如果父子進程任意一方修改了共享數據,就會發生「寫時復制」,這期間會拷貝物理內存,如果內存越大,自然阻塞的時間也越長;

針對第二個過程,有一個小細節在這里提一下。寫時復制,復制的粒度為一個內存頁。如果只是修改一個256B的數據,父進程需要讀原來的整個內存頁,然后再映射到新的物理地址寫入。一讀一寫會造成讀寫放大。如果內存頁越大(例如2MB的大頁),那么讀寫放大也就越嚴重,對Redis性能造成影響。因此使用Redis的AOF功能時,需要注意頁表的大小不要設置的太大。

2.子進程在進行 AOF 重寫期間,主進程還需要繼續處理命令,而新的命令可能對現有的數據進行修改, 會讓當前數據庫的數據和重寫后的 AOF 文件中的數據不一致,這該怎么辦?

為了解決這個問題,Redis引入了另一個緩沖區的概念(這也是本文中涉及到的第3個緩沖區的概念)——AOF重寫緩沖區

image

換言之, 當子進程在執行AOF重寫(bgrewriteaof)時, 主進程需要執行以下三個工作:

  1. 處理客戶端的命令請求;
  2. 將寫命令追加到AOF緩沖區aof_buf);
  3. 將寫命令追加到AOF重寫緩沖區

這樣一來可以保證:

  1. 現有的 AOF功能會繼續執行,即使在 AOF 重寫期間發生停機,也不會有任何數據丟失;
  2. 所有對數據庫進行修改的命令都會被記錄到AOF重寫緩沖區中

當子進程完成 AOF重寫之后, 它會向父進程發送一個完成信號, 父進程在接到完成信號之后, 會調用一個信號處理函數, 並完成以下工作:

  1. 將 AOF重寫緩沖區中的內容全部寫入到新AOF 文件中;
  2. 對新的 AOF 文件進行改名,覆蓋原有的 AOF 文件。注意,這是一個原子操作,改名過程中不接受客戶端指令。

當步驟 1 執行完畢之后, 現有 AOF 文件、新 AOF 文件和數據庫三者的狀態就完全一致了。

當步驟 2 執行完畢之后, 程序就完成了新舊兩個 AOF 文件的交替。

這個信號處理函數執行完畢之后, 主進程就可以繼續像往常一樣接受命令請求了。 在整個 AOF 后台重寫過程中, 只有將AOF重寫緩沖區數據寫入新AOF文件和改名操作會造成主進程阻塞, 其他時候, AOF 后台重寫都不會對主進程造成阻塞, 這將 AOF 重寫對性能造成的影響降到了最低。

AOF 后台重寫的觸發條件

再看一下關於AOF的其他兩個配置

auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

AOF 重寫可以由用戶通過調用 bgrewriteaof手動觸發。

另外, 服務器在 AOF 功能開啟的情況下, 會維持以下三個變量:

  • 記錄當前 AOF 文件大小的變量 aof_current_size
  • 記錄最后一次 AOF 重寫之后, AOF 文件大小的變量 aof_rewrite_base_size
  • 增長百分比變量 aof_rewrite_perc

每次當Redis中的定時函數 serverCron 執行時, 它都會檢查以下條件是否全部滿足, 如果是的話, 就會觸發自動的 AOF 重寫:

  1. 沒有 bgsave 命令在進行。
  2. 沒有 bgrewriteaof 在進行。
  3. 當前 AOF 文件大小大於 我們設置的auto-aof-rewrite-min-size
  4. 當前 AOF 文件大小和最后一次 AOF 重寫后的大小之間的比率大於等於指定的增長百分比auto-aof-rewrite-percentage

默認情況下, 增長百分比為 100% , 也即是說, 如果前面三個條件都已經滿足, 並且當前 AOF 文件大小比最后一次 AOF 重寫時的大小要大一倍的話, 那么觸發自動 AOF 重寫。

小結

經過多番改稿,終於!給大家梳理完成Redis的AOF持久化方法,最后我們簡單總結一下。

AOF是將Redis的所有寫日志同步到磁盤的一種持久化方法,通過執行AOF中記錄的所有指令可以達到恢復Redis原始數據狀態的目的。

對於指令的同步時機,Redis提供了三種AOF同步策略,分別是NoEverysecAlways,三種策略對Redis性能的負面影響是由低到高的,在數據可靠性上也是由低到高的。

為了解決AOF日志太大的問題,Redis提供了AOF重寫的機制,利用「寫時復制」和「AOF重寫緩沖區」達到精簡AOF文件的目的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM