從上一篇的 《深刻理解高效能Redis的本質》 中可以知道, 我們經常在資料庫層上加一層快取(如Redis),來保證資料的存取效率。
這樣效能確實也有了大幅度的提升,但是本身Redis也是一層服務,也存在宕機、故障的可能性。
一旦服務掛起,可能生產的後果包括如下幾方面:
1、Redis的資料是存在記憶體中的,所以一旦掛起,記憶體中的資料會全部丟失。
2、I/O從記憶體層級遷移到磁碟層級,效能極速下降。
3、原本存取快取的請求會透過快取層直接投向資料庫,給資料庫帶來極大的壓力,甚至導致雪崩。
所以,快取層崩潰產生的後果是災難的。為了避免宕機和宕機後的資料丟失, 為了保證資料的快速恢復,Redis提供了兩個持久化資料的能力, AOF(Append Only FIle)紀錄檔 和 RDB 快照。
大規模高並行的分散式場景,經常會遇到問題就是Redis掛起,導致存取失敗,而所有的請求透過快取層投向資料庫,給資料庫造成極大的壓力。
而Redis的資料是儲存在快取記憶體中,即使我們重啟並且恢復使用,快取池依舊是空的,因為記憶體被釋放了。
重新建立快取的過程,對資料庫也是一個暴擊的過程,很可能會導致整個系統呼叫鏈的雪崩。參考我的這篇《架構與思維:一次快取雪崩的災難覆盤》
我們知道,Redis 資料都是儲存在記憶體中,能不能將記憶體中的資料進一步寫到磁碟上,Redis 重啟的時候就可以把磁碟上的資料快速恢復到記憶體中。這樣,即使Redis宕機重啟之後,依然能夠正常的提供服務。
但是不能忽略一個問題,Redis和MySQL最大的區別之一就是一個儲存在記憶體,一個持久化在磁碟。但是如果每次資料的變化(新增、修改、刪除快取)都要寫記憶體並同時寫磁碟,這樣成本太高,記憶體+磁碟,會讓 Redis 效能大大降低。而且還要保證原子性操作,避免記憶體和磁碟的資料不一致。
為了避免實時寫入高頻操作磁碟帶來的負面效應。Redis提供了記憶體快照策略。
我們知道,Redis 在 執行寫(增、刪、改)指令過程中,記憶體中資料會持續的在變化。而記憶體快照,指的是 Redis 記憶體中的資料在某一刻的狀態。就好比如是拍照一樣,你把那一刻的資料都定格下來,持久化到磁碟上。打遊戲的同學可以想象存檔。
快照檔案我們稱之為 RDB 檔案,即 Redis DataBase 的縮寫。
Redis 通過定時執行 RDB 記憶體快照,這樣就不必每次執行寫指令都存檔,只需要在執行記憶體快照的時候寫磁碟。這樣既保證Redis的高效讀寫,還實現了定時持久化,宕機後可快速恢復資料。
如上圖,在做資料恢復時,直接將 RDB 檔案讀入記憶體完成恢復。
Redis 提供了兩種模式來生成 RDB 檔案:
save模式是主程序執行,非常不建議使用主程序執行的方式,在 《深刻理解高效能Redis的本質》 中,
我們知道他的主操作都是在單執行緒模型上完成的。所以儘量避免 RDB 檔案生成影響主執行緒的網路I/O和鍵值對讀寫。
上面提到的另外一種方式,fork一個子程序來寫RDB檔案。
Redis 使用作業系統的多程序寫時複製技術 COW(Copy On Write) 來實現快照持久化,這個很重要,具體可以瞭解下這篇《Copy On Write機制》,寫的不錯。
Redis 在持久化時會呼叫 glibc 的函數fork產生一個子程序,由這個子程序來處理快照持久化的動作,子程序可以共用主程序的所有記憶體資料,所以它讀取到主程序的資料之後寫入到 RDB 檔案。而父程序繼續處理使用者端的寫操作,不受影響。
在建立 RDB 檔案時,程式會對資料庫中的鍵進行檢查,僅僅將未過期的鍵儲存到新建立的 RDB 檔案中。
當主程序執行寫指令修改資料的時候,這個資料就會複製一份副本, bgsave 子程序讀取這個副本資料寫到 RDB 檔案,所以主程序就可以直接修改原來的資料。
這既保證了快照的完整性,也允許主程序同時對資料進行修改,避免了對正常業務的影響。
雖然說Redis 使用 bgsave 函數 fork 子程序在後臺完成 記憶體中的資料做快照,沒有影響父程序繼續處理使用者端的各種操作。
但是需注意一點,過於頻繁的執行全量的資料快照,必然會導致嚴重的效能開銷:
AOF 紀錄檔儲存了 Redis 伺服器的順序指令序列,AOF 紀錄檔只記錄對記憶體進行修改的指令記錄。
假設 AOF 紀錄檔記錄了自 Redis 範例建立以來所有的修改性指令序列,那麼就可以通過對一個空的 Redis 範例順序執行所有的指令。
也就是說,可以通過重放(replay),來建立 Redis 當前範例的記憶體資料結構。這種模式有沒有很熟悉,有沒有想到MySQL主從同步時候的relay log。
AOF記錄紀錄檔有兩種模式,一種是預寫式紀錄檔,也稱寫前紀錄檔(Write Ahead Log, WAL): 在實際寫資料之前,將修改的資料寫到紀錄檔檔案中。
另外一種是寫後紀錄檔: 先執行寫操作,當資料存入記憶體後,再記錄紀錄檔。
預寫式紀錄檔類似 MySQL Innodb 引擎 中的 redo log,修改資料前先記錄紀錄檔,再修改。
Redis 接收到 set keyName someValue
命令的時候,會先將資料寫到記憶體,Redis 會按照如下格式寫入 AOF 檔案。
*3
:表示當前指令分為三個部分,每個部分都是 $ + 數位
開頭,後面是3部分的具體內容:指令、鍵、值。
數位
:表示這部分的命令、鍵、值多佔用的位元組大小。比如 $3
表示這部分包含 3 個字元,也就是 set
的長度。
推薦使用寫後紀錄檔的模式,避免了額外的檢查開銷,不需要對執行的命令進行語法檢查。如果使用寫前紀錄檔的話,就需要先檢查語法是否有誤,否則紀錄檔記錄了錯誤的命令,在使用紀錄檔恢復的時候就會出錯。另外,寫後才記錄紀錄檔,不會阻塞當前的 寫 指令執行。
# set keyName someValue
*3
$3
set
$7 #長度為7
keyName
$9 #長度為9
someValue
# 執行 mset key1 1 ,key2 2 ,key33 3
# aof紀錄檔如下:
*7 # 本批命令需要往下讀7行非 $ 開始的命令
$4 #接著讀取4個位元組寬度,‘mset’長度為4,記為 $4
mset
$4 #接著讀取4個位元組寬度,‘key1’長度為4,記為 $4
key1
$1 #接著讀取1個位元組寬度,‘1’長度為1,記為 $1
1
$4
key2
$1
2
$5 #接著讀取的位元組寬度,‘$key33’長度為5,記為 $5
key33
$1
3
上面的問題,在Redis高頻讀寫的時候是必然存在的,想要解決,在寫入的時候做一層緩衝就可以了,避免直塞。這時候Redis提供了一種執行策略叫寫回策略。
為了提高紀錄檔檔案的寫入效率,寫回策略會做如下變化:
fsync
和fdatasync
兩個同步函數,它們可以強制讓作業系統立即將緩衝區中的資料寫入到硬碟裡面,從而確保寫入資料的安全性。appendfsync
寫回策略直接決定 AOF 持久化功能的效率和安全性,以下是 appendfsync
的3個列舉:寫磁碟會帶來效能上的損耗,所以寫回的策略要根據實際情況做一個取捨,比如你是偏向效能還是可靠性。
always 同步寫回可以做到資料不丟失,但是每次執行寫指令都需要寫入磁碟,效能最差。
everysec 每秒寫回,避免了同步寫回的效能開銷,但是如果服務發生宕機,會有大約1s時間週期的資料丟失,這種模式是在效能和可靠性之間做了妥協。
no 作業系統控制,執行寫指令後就寫入 AOF 檔案緩衝,再執行後續的寫磁碟指令,效能最好,但有可能丟失更多的資料。
我們可以根據服務的實際情況來抉擇策略,看是偏向高效能還是高可靠。
現實情況下,無論使用RDB或者AOF都差點意思。使用 rdb 來恢復記憶體狀態,勢必會丟失一部分資料。 使用 AOF 紀錄檔重放,重放對效能有一定的影響,而且在 Redis 範例很大的情況下,需要花費很長的時間。
Redis 4.0 解決了這個問題,才用了一個新的持久化模式——混合持久化,該 混合模式 預設是關閉狀態的。
將 RDB 檔案的內容和 rdb快照時間點之後的增量的 AOF 紀錄檔檔案存在一起。這時候 AOF 紀錄檔不需要再是全量的紀錄檔,而是最近一次快照時間點之後到當下發生的增量 AOF 紀錄檔,通常這部分 AOF 紀錄檔很小。
所以執行有如下順序: