就這?Redis持久化策略——AOF
今天為大家介紹Redis的另一種持久化策略——AOF。
什么是AOF
男孩“一覺醒來”忘記了對女孩子的承諾,這時候女孩子把曾經(jīng)海誓山盟的錄音逐條播放給男孩子聽,幫助他“恢復(fù)記憶”。
“男孩一覺醒來”像極了Redis宕機重啟的樣子,而女孩子的錄音就是Redis的AOF日志。
AOF(Append Only File)以文本的形式(文本格式由Redis自定義,后文會講到),通過將所有對數(shù)據(jù)庫的寫入命令記錄到AOF文件中,達到記錄數(shù)據(jù)庫狀態(tài)的目的。
注意:AOF文件只會記錄Redis的寫操作命令,因為讀命令對數(shù)據(jù)的恢復(fù)沒有任何意義。
Redis默認并未開啟AOF功能,redis.conf配置文件中,關(guān)于AOF的相關(guān)配置如下:
- # 是否開啟AOF功能(開啟:yes 關(guān)閉: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程序自己加上去的之外, 其他命令都是之前我們在終端里執(zhí)行的命令。自動添加這條指令是因為Redis恢復(fù)數(shù)據(jù)的時候需要知道待恢復(fù)的數(shù)據(jù)屬于哪一個數(shù)據(jù)庫。
其中,*2表示當前命令有2個部分,每部分都是由$+數(shù)字開頭,后面緊跟著具體的命令、鍵或值。數(shù)字表示這部分中的命令、鍵或值一共有多少字節(jié)。例如,$6 SELECT表示這部分有 6 個字節(jié),也就是SELECT命令。
AOF日志的生成過程
從我們發(fā)送寫指令開始到指令保存在AOF文件中,需要經(jīng)歷4步,分別為命令傳播、命令追加、文件寫入和文件同步。
命令傳播
命令傳播:Redis 將執(zhí)行完的命令、命令的參數(shù)、命令的參數(shù)個數(shù)等信息發(fā)送到 AOF程序中。
大家有沒有關(guān)注到其中的一個細節(jié),AOF日志寫入是在Redis成功執(zhí)行命令之后才進行的,為什么要在執(zhí)行之后而不是之前呢?原因有兩點:
首先試想一下,如果我們不小心輸錯了Redis指令,然后Redis緊接著將該指令保存到了AOF文件中,等到Redis進行數(shù)據(jù)恢復(fù)的時候就可能導(dǎo)致錯誤。因此這種寫后日志的形式可以避免對指令進行語法檢查,避免出現(xiàn)記錄錯誤指令的情況。
其次,先執(zhí)行命令后保存日志,不會阻塞當前的寫操作。
但是,AOF寫后日志也有兩個風(fēng)險。
第一個風(fēng)險,假如Redis寫操作成功之后突然宕機,此時AOF日志還未來得及寫入,則該條指令和相關(guān)參數(shù)就有丟失的風(fēng)險。
第二個風(fēng)險,AOF雖然避免了對當前操作的阻塞,但是有可能阻塞下一個操作。因為保存AOF日志的部分工作也是由主線程完成的(下文有詳細介紹),Redis的內(nèi)存操作速度和文件寫入速度簡直是云泥之別,如果主線程在文件保存的過程中花費太長的時間必然會阻塞后續(xù)的操作。
分析就會發(fā)現(xiàn),第一個風(fēng)險與AOF寫回磁盤的時機有關(guān),寫回磁盤的頻率越高,發(fā)生數(shù)據(jù)丟失的可能性就越小。第二個風(fēng)險和文件寫入方式以及時機有關(guān),如果Redis每次成功執(zhí)行指令之后都力圖將當前指令同步到AOF文件,開銷必然很大。
因此Redis引入了緩沖區(qū)的概念,緩沖區(qū)對應(yīng)了文件的寫入方式(不求一步到位,允許循序漸進地寫入),而何時將緩沖區(qū)的內(nèi)容徹底同步到文件就涉及到了AOF的同步策略(寫回磁盤的時機)。
命令追加
在AOF開啟的情況下,Redis會將成功執(zhí)行的寫指令以上文我們講過的協(xié)議格式追加到Redis的aof_buf緩沖區(qū)。
- struct redisServer {
- // ...
- // AOF緩沖區(qū)
- sds aof_buf;
- // ...
- };
aof_buf 緩沖區(qū)保存著所有等待寫入到AOF 文件的協(xié)議文本。
至此,將命令追加到緩存區(qū)的步驟完成。
文件寫入
文件的寫入和同步操作往往被放在一起介紹,這里之所以分開,是想向讀者強調(diào)文件的寫入和同步是兩步不同的操作。
為了提高文件的寫入效率,當用戶調(diào)用write函數(shù)將數(shù)據(jù)寫入到文件時,操作系統(tǒng)內(nèi)核會將數(shù)據(jù)首先保存在內(nèi)存緩沖區(qū)中,等到緩沖區(qū)的空間被填滿或者到達一定的時機之后,內(nèi)核會將數(shù)據(jù)同步到磁盤。
這種同步過于依賴于操作系統(tǒng)內(nèi)核,時機無法掌控。為此,操作系統(tǒng)提供了fsync和fdatasync兩個同步函數(shù),可以強制內(nèi)核立即將緩沖區(qū)內(nèi)的數(shù)據(jù)同步到磁盤。
Redis的主服務(wù)進程本質(zhì)上是一個死循環(huán),循環(huán)中有負責(zé)接受客戶端的請求,并向客戶端發(fā)送回執(zhí)的邏輯,我們稱之為文件事件。
在AOF功能開啟的情況下,文件事件會將成功執(zhí)行之后的寫命令追加到aof_buf緩沖區(qū),在主服務(wù)進程死循環(huán)的最后,會調(diào)用flushAppendOnlyFile函數(shù),該函數(shù)會將aof_buf中的數(shù)據(jù)寫入到內(nèi)核緩沖區(qū),然后判斷是否應(yīng)該進行同步。偽代碼如下:
- void eventLoop {
- while(true){
- // ...
- // 文件事件,接受命令請求,返回客戶端回執(zhí)
- // 根據(jù)aof功能是否開啟,決定是否將寫命令追加到aof_buf緩沖區(qū)
- handleFileEvents();
- // 將aof_buf數(shù)據(jù)寫入內(nèi)核緩沖區(qū)
- // 判斷是否需要將數(shù)據(jù)同步到磁盤
- flushAppendOnlyFile();
- // ...
- }
- };
而是否進行同步則是由Redis配置中的appendOnlyFile選項來決定的。
文件同步
redis.conf配置文件中appendOnlyFile的選項有三個值可選,對應(yīng)三種AOF同步策略,分別是:
- No :同步時機由內(nèi)核決定。
- Everysec :每一秒鐘同步一次。
- Always :每執(zhí)行一個命令同步一次。
No
由操作系統(tǒng)內(nèi)核決定同步時機,每個寫命令執(zhí)行完,只是先把日志寫入AOF文件的內(nèi)核緩沖區(qū),不立即進行同步。在這種模式下, 同步只會在以下任意一種情況下被執(zhí)行:
- Redis 被關(guān)閉
- AOF功能被關(guān)閉
- 系統(tǒng)的寫緩存被刷新(可能是緩存已經(jīng)被寫滿,或者定期保存操作被執(zhí)行)
這三種情況下的同步操作都會引起 Redis 主進程阻塞。
Everysec
如果用戶未指定appendOnlyFile的值,則默認值為Everysec。每秒同步,每個寫命令執(zhí)行完,只是先把日志寫到 AOF文件的內(nèi)核緩沖區(qū),理論上每隔1秒把緩沖區(qū)中的內(nèi)容同步到磁盤,且同步操作有單獨的子線程進行,因此不會阻塞主進程。
需要注意的是,我們用的是「理論上」這樣的措辭,實際運行中該模式對fsync或fdatasync的調(diào)用并不是每秒一次,而是和調(diào)用flushAppendOnlyFile函數(shù)時Redis所處的狀態(tài)有關(guān)。
每當 flushAppendOnlyFile 函數(shù)被調(diào)用時, 可能會出現(xiàn)以下四種情況:
- 子線程正在執(zhí)行同步,并且這個同步的執(zhí)行時間未超過 2 秒,那么程序直接返回 ;這個同步已經(jīng)執(zhí)行超過 2 秒,那么程序執(zhí)行寫入操作 ,但不執(zhí)行新的同步操作 。但是,這時的寫入操作必須等待子線程先完成原本的同步操作 ,因此這里的寫入操作會比平時阻塞更長時間。
- 子線程沒有在執(zhí)行同步 ,并且上次成功執(zhí)行同步距今不超過1秒,那么程序執(zhí)行寫入,但不執(zhí)行同步 ;上次成功執(zhí)行同步距今已經(jīng)超過1秒,那么程序執(zhí)行寫入和同步 。
可以用流程圖表示這四種情況:
在Everysec模式下
- 如果在情況1下宕機,那么我們最多損失小于2秒內(nèi)的所有數(shù)據(jù)。
- 如果在情況2下宕機,那么我們損失的數(shù)據(jù)可能會超過2秒。
因此AOF在Everysec模式下只會丟失 1 秒鐘數(shù)據(jù)的說法實際上并不準確。
Always
每個寫命令執(zhí)行完,立刻同步地將日志寫回磁盤。此模式下同步操作是由 Redis 主進程執(zhí)行的,所以在同步執(zhí)行期間,主進程會被阻塞,不能接受命令請求。
AOF同步策略小結(jié)
對于三種 AOF 同步模式, 它們對Redis主進程的阻塞情況如下:
- 不同步(No):寫入和同步都由主進程執(zhí)行,兩個操作都會阻塞主進程;
- 每一秒鐘同步一次(Everysec):寫入操作由主進程執(zhí)行,阻塞主進程。同步操作由子線程執(zhí)行,不直接阻塞主進程,但同步操作完成的快慢會影響寫入操作的阻塞時長;
- 每執(zhí)行一個命令同步一次(Always):同模式 1 。
因為阻塞操作會讓 Redis 主進程無法持續(xù)處理請求, 所以一般說來, 阻塞操作執(zhí)行得越少、完成得越快, Redis 的性能就越好。
No的同步操作只會在AOF關(guān)閉或Redis關(guān)閉時執(zhí)行, 或由操作系統(tǒng)內(nèi)核觸發(fā)。在一般情況下, 這種模式只需要為寫入阻塞,因此它的寫入性能要比后面兩種模式要高, 但是這種性能的提高是以降低安全性為代價的:在這種模式下,如果發(fā)生宕機,那么丟失的數(shù)據(jù)量由操作系統(tǒng)內(nèi)核的緩存沖洗策略決定。
Everysec在性能方面要優(yōu)于Always , 并且在通常情況下,這種模式最多丟失不多于2秒的數(shù)據(jù), 所以它的安全性要高于No ,這是一種兼顧性能和安全性的保存方案。
Always的安全性是最高的,但性能也是最差的,因為Redis必須阻塞直到命令信息被寫入并同步到磁盤之后才能繼續(xù)處理請求。
三種 AOF模式的特性可以總結(jié)為如下表格
AOF生成過程小結(jié)
最后總結(jié)一下AOF文件的生成過程。以下步驟都是在AOF開啟的前提下進行的
- Redis成功執(zhí)行寫操作指令,然后將寫的指令按照自定義格式追加到aof_buf緩沖區(qū),這是第一個緩沖區(qū);
- Redis主進程將aof_buf緩沖區(qū)的數(shù)據(jù)寫入到內(nèi)核緩沖區(qū),這是第二個緩沖區(qū);
- 根據(jù)AOF同步策略適時地將內(nèi)核緩沖區(qū)的數(shù)據(jù)同步到磁盤,過程結(jié)束。
AOF文件的載入和數(shù)據(jù)還原
AOF文件中包含了能夠重建數(shù)據(jù)庫的所有寫命令,因此將所有命令讀入并依次執(zhí)行即可還原Redis之前的數(shù)據(jù)狀態(tài)。
Redis 讀取AOF文件并還原數(shù)據(jù)庫的詳細步驟如下:
- 創(chuàng)建一個不帶網(wǎng)絡(luò)連接的偽客戶端(fake client),偽客戶端執(zhí)行命令的效果, 和帶網(wǎng)絡(luò)連接的客戶端執(zhí)行命令的效果完全相同;
- 讀取AOF所保存的文本,并根據(jù)內(nèi)容還原出命令、命令的參數(shù)以及命令的個數(shù);
- 根據(jù)指令、指令的參數(shù)等信息,使用偽客戶端執(zhí)行命令。
- 執(zhí)行 2 和 3 ,直到AOF文件中的所有命令執(zhí)行完畢。
注意:為了避免對數(shù)據(jù)的完整性產(chǎn)生影響, 在服務(wù)器載入數(shù)據(jù)的過程中, 只有和數(shù)據(jù)庫無關(guān)的發(fā)布訂閱功能可以正常使用, 其他命令一律返回錯誤。
AOF重寫
AOF的作用是幫我們還原Redis的數(shù)據(jù)狀態(tài),其中包含了所有的寫操作,但是正常情況下客戶端會對同一個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的數(shù)據(jù)進行寫操作就進行了5次,其實對我們而言僅需要最后一條指令而已,但是AOF會將這5條指令都記錄下來。更極端的情況是有些被頻繁操作的鍵, 對它們所調(diào)用的命令可能有成百上千、甚至上萬條, 如果這樣被頻繁操作的鍵有很多的話,AOF文件的體積就會急速膨脹。
- 首先,AOF文件的體積受操作系統(tǒng)大小的限制,本身就不能無限增長;
- 其次,體積過于龐大的AOF文件會影響指令的寫入速度,阻塞時間延長;
- 最后AOF文件的體積越大,Redis數(shù)據(jù)恢復(fù)所需的時間也就越長。
為了解決AOF文件體積龐大的問題,Redis提供了rewrite的AOF重寫功能來精簡AOF文件體積。
AOF重寫的實現(xiàn)原理
雖然叫AOF「重寫」,但是新AOF文件的生成并非是在原AOF文件的基礎(chǔ)上進行操作得到的,而是讀取Redis當前的數(shù)據(jù)狀態(tài)來重新生成的。不難理解,后者的處理方式遠比前者高效。
為了避免阻塞主線程,導(dǎo)致數(shù)據(jù)庫性能下降,和 AOF 日志由主進程寫回不同,重寫過程是由子進程執(zhí)行bgrewriteaof 來完成的。這樣處理的最大好處是:
- 子進程進行 AOF重寫期間,主進程可以繼續(xù)處理命令請求;
- 子進程帶有主進程的數(shù)據(jù)副本,操作效率更高。
這里有兩個問題值得我們來思考一下。
1.為什么使用子進程,而不是多線程來進行AOF重寫呢?
如果是使用線程,線程之間會共享內(nèi)存,在修改共享內(nèi)存數(shù)據(jù)的時候,需要通過加鎖來保證數(shù)據(jù)的安全,這樣就會降低性能。
如果使用子進程,操作系統(tǒng)會使用「寫時復(fù)制」的技術(shù):fork子進程時,子進程會拷貝父進程的頁表,即虛實映射關(guān)系,而不會拷貝物理內(nèi)存。子進程復(fù)制了父進程頁表,也能共享訪問父進程的內(nèi)存數(shù)據(jù),達到共享內(nèi)存的效果。
不過這個共享的內(nèi)存只能以只讀的方式,當父子進程任意一方修改了該共享內(nèi)存,就會發(fā)生「寫時復(fù)制」,于是父子進程就有了獨立的數(shù)據(jù)副本,就不用加鎖來保證數(shù)據(jù)安全。
這里我把在就這?Redis持久化策略——RDB畫過的一張圖拿過來幫助大家理解一下寫時復(fù)制。
因此,有兩個過程可能會導(dǎo)致主進程阻塞:
- fork子進程的過程中,由于要復(fù)制父進程的頁表等數(shù)據(jù),阻塞的時間跟頁表的大小有關(guān),頁表越大阻塞的時間也越長,不過通常而言該過程是非??斓?
- fork完子進程后,如果父子進程任意一方修改了共享數(shù)據(jù),就會發(fā)生「寫時復(fù)制」,這期間會拷貝物理內(nèi)存,如果內(nèi)存越大,自然阻塞的時間也越長。
針對第二個過程,有一個小細節(jié)在這里提一下。寫時復(fù)制,復(fù)制的粒度為一個內(nèi)存頁。如果只是修改一個256B的數(shù)據(jù),父進程需要讀原來的整個內(nèi)存頁,然后再映射到新的物理地址寫入。一讀一寫會造成讀寫放大。如果內(nèi)存頁越大(例如2MB的大頁),那么讀寫放大也就越嚴重,對Redis性能造成影響。因此使用Redis的AOF功能時,需要注意頁表的大小不要設(shè)置的太大。
2.子進程在進行 AOF 重寫期間,主進程還需要繼續(xù)處理命令,而新的命令可能對現(xiàn)有的數(shù)據(jù)進行修改, 會讓當前數(shù)據(jù)庫的數(shù)據(jù)和重寫后的 AOF 文件中的數(shù)據(jù)不一致,這該怎么辦?
為了解決這個問題,Redis引入了另一個緩沖區(qū)的概念(這也是本文中涉及到的第3個緩沖區(qū)的概念)——AOF重寫緩沖區(qū)。
換言之, 當子進程在執(zhí)行AOF重寫(bgrewriteaof)時, 主進程需要執(zhí)行以下三個工作:
- 處理客戶端的命令請求;
- 將寫命令追加到AOF緩沖區(qū)(aof_buf);
- 將寫命令追加到AOF重寫緩沖區(qū)。
這樣一來可以保證:
- 現(xiàn)有的 AOF功能會繼續(xù)執(zhí)行,即使在 AOF 重寫期間發(fā)生停機,也不會有任何數(shù)據(jù)丟失;
- 所有對數(shù)據(jù)庫進行修改的命令都會被記錄到AOF重寫緩沖區(qū)中。
當子進程完成 AOF重寫之后, 它會向父進程發(fā)送一個完成信號, 父進程在接到完成信號之后, 會調(diào)用一個信號處理函數(shù), 并完成以下工作:
- 將 AOF重寫緩沖區(qū)中的內(nèi)容全部寫入到新AOF 文件中;
- 對新的 AOF 文件進行改名,覆蓋原有的 AOF 文件。注意,這是一個原子操作,改名過程中不接受客戶端指令。
當步驟 1 執(zhí)行完畢之后, 現(xiàn)有 AOF 文件、新 AOF 文件和數(shù)據(jù)庫三者的狀態(tài)就完全一致了。
當步驟 2 執(zhí)行完畢之后, 程序就完成了新舊兩個 AOF 文件的交替。
這個信號處理函數(shù)執(zhí)行完畢之后, 主進程就可以繼續(xù)像往常一樣接受命令請求了。 在整個 AOF 后臺重寫過程中, 只有將AOF重寫緩沖區(qū)數(shù)據(jù)寫入新AOF文件和改名操作會造成主進程阻塞, 其他時候, AOF 后臺重寫都不會對主進程造成阻塞, 這將 AOF 重寫對性能造成的影響降到了最低。
AOF 后臺重寫的觸發(fā)條件
再看一下關(guān)于AOF的其他兩個配置:
- auto-aof-rewrite-percentage 100
- auto-aof-rewrite-min-size 64mb
AOF 重寫可以由用戶通過調(diào)用 bgrewriteaof手動觸發(fā)。
另外, 服務(wù)器在 AOF 功能開啟的情況下, 會維持以下三個變量:
- 記錄當前 AOF 文件大小的變量 aof_current_size ;
- 記錄最后一次 AOF 重寫之后, AOF 文件大小的變量 aof_rewrite_base_size ;
- 增長百分比變量 aof_rewrite_perc 。
每次當Redis中的定時函數(shù) serverCron 執(zhí)行時, 它都會檢查以下條件是否全部滿足, 如果是的話, 就會觸發(fā)自動的 AOF 重寫:
- 沒有 bgsave 命令在進行。
- 沒有 bgrewriteaof 在進行。
- 當前 AOF 文件大小大于 我們設(shè)置的auto-aof-rewrite-min-size。
- 當前 AOF 文件大小和最后一次 AOF 重寫后的大小之間的比率大于等于指定的增長百分比auto-aof-rewrite-percentage。
默認情況下, 增長百分比為 100% , 也即是說, 如果前面三個條件都已經(jīng)滿足, 并且當前 AOF 文件大小比最后一次 AOF 重寫時的大小要大一倍的話, 那么觸發(fā)自動 AOF 重寫。
小結(jié)
經(jīng)過多番改稿,終于!給大家梳理完成Redis的AOF持久化方法,最后我們簡單總結(jié)一下。
AOF是將Redis的所有寫日志同步到磁盤的一種持久化方法,通過執(zhí)行AOF中記錄的所有指令可以達到恢復(fù)Redis原始數(shù)據(jù)狀態(tài)的目的。
對于指令的同步時機,Redis提供了三種AOF同步策略,分別是No,Everysec,Always,三種策略對Redis性能的負面影響是由低到高的,在數(shù)據(jù)可靠性上也是由低到高的。
為了解決AOF日志太大的問題,Redis提供了AOF重寫的機制,利用「寫時復(fù)制」和「AOF重寫緩沖區(qū)」達到精簡AOF文件的目的。