1 介紹
從上一篇的 《深刻理解高性能Redis的本質》 中可以知道, 我們經常在數據庫層上加一層緩存(如Redis),來保證數據的訪問效率。
這樣性能確實也有了大幅度的提升,但是本身Redis也是一層服務,也存在宕機、故障的可能性。
一旦服務掛起,可能生產的后果包括如下幾方面:
1、Redis的數據是存在內存中的,所以一旦掛起,內存中的數據會全部丟失。
2、I/O從內存層級遷移到磁盤層級,性能極速下降。
3、原本訪問緩存的請求會透過緩存層直接投向數據庫,給數據庫帶來極大的壓力,甚至導致雪崩。
所以,緩存層崩潰產生的后果是災難的。為了避免宕機和宕機后的數據丟失, 為了保證數據的快速恢復,Redis提供了兩個持久化數據的能力, AOF(Append Only FIle)日志 和 RDB 快照。
2 關於RDB 內存快照
大規模高並發的分布式場景,經常會遇到問題就是Redis掛起,導致訪問失敗,而所有的請求透過緩存層投向數據庫,給數據庫造成極大的壓力。
而Redis的數據是存儲在高速緩存中,即使我們重啟並且恢復使用,緩存池依舊是空的,因為內存被釋放了。
重新建立緩存的過程,對數據庫也是一個暴擊的過程,很可能會導致整個系統調用鏈的雪崩。參考我的這篇《架構與思維:一次緩存雪崩的災難復盤》
我們知道,Redis 數據都是保存在內存中,能不能將內存中的數據進一步寫到磁盤上,Redis 重啟的時候就可以把磁盤上的數據快速恢復到內存中。這樣,即使Redis宕機重啟之后,依然能夠正常的提供服務。
但是不能忽略一個問題,Redis和MySQL最大的區別之一就是一個存儲在內存,一個持久化在磁盤。但是如果每次數據的變化(新增、修改、刪除緩存)都要寫內存並同時寫磁盤,這樣成本太高,內存+磁盤,會讓 Redis 性能大大降低。而且還要保證原子性操作,避免內存和磁盤的數據不一致。
2.1 使用內存快照
為了避免實時寫入高頻操作磁盤帶來的負面效應。Redis提供了內存快照策略。
我們知道,Redis 在 執行寫(增、刪、改)指令過程中,內存中數據會持續的在變化。而內存快照,指的是 Redis 內存中的數據在某一刻的狀態。就好比如是拍照一樣,你把那一刻的數據都定格下來,持久化到磁盤上。打游戲的同學可以想象存盤。
快照文件我們稱之為 RDB 文件,即 Redis DataBase 的縮寫。
Redis 通過定時執行 RDB 內存快照,這樣就不必每次執行寫指令都存盤,只需要在執行內存快照的時候寫磁盤。這樣既保證Redis的高效讀寫,還實現了定時持久化,宕機后可快速恢復數據。
如上圖,在做數據恢復時,直接將 RDB 文件讀入內存完成恢復。
2.2 生成RDB策略
Redis 提供了兩種模式來生成 RDB 文件:
- save: 由主線程來執行,同步阻塞,只有等save完成后,才能進行新操作;
- bgsave:執行后,會立刻返回OK,同時調用 glibc 的函數fork產生一個子進程用於寫入 RDB 文件,快照持久化完全交給子進程來處理。主進程繼續執行他自己的工作,非阻塞。
2.2.1 save模式
save模式是主進程執行,非常不建議使用主進程執行的方式,在 《深刻理解高性能Redis的本質》 中,
我們知道他的主操作都是在單線程模型上完成的。所以盡量避免 RDB 文件生成影響主線程的網絡I/O和鍵值對讀寫。
2.2.2 bgsave模式
上面提到的另外一種方式,fork一個子進程來寫RDB文件。
Redis 使用操作系統的多進程寫時復制技術 COW(Copy On Write) 來實現快照持久化,這個很重要,具體可以了解下這篇《Copy On Write機制》,寫的不錯。
Redis 在持久化時會調用 glibc 的函數fork產生一個子進程,由這個子進程來處理快照持久化的動作,子進程可以共享主進程的所有內存數據,所以它讀取到主進程的數據之后寫入到 RDB 文件。而父進程繼續處理客戶端的寫操作,不受影響。
在創建 RDB 文件時,程序會對數據庫中的鍵進行檢查,僅僅將未過期的鍵保存到新創建的 RDB 文件中。
當主進程執行寫指令修改數據的時候,這個數據就會復制一份副本, bgsave 子進程讀取這個副本數據寫到 RDB 文件,所以主進程就可以直接修改原來的數據。
這既保證了快照的完整性,也允許主進程同時對數據進行修改,避免了對正常業務的影響。
2.2.3 避免過頻全量照片
雖然說Redis 使用 bgsave 函數 fork 子進程在后台完成 內存中的數據做快照,沒有影響父進程繼續處理客戶端的各種操作。
但是需注意一點,過於頻繁的執行全量的數據快照,必然會導致嚴重的性能開銷:
- 頻繁生成 RDB 文件寫入磁盤,磁盤壓力過大,效率降低。
- fork 出來的 bgsave 子進程因為共享主線程的數據,一定程度上會阻塞主線程的運行,主線程的內存越大,阻塞時間越長。
2.3 總結
- 快照的恢復速度快,但是生成 RDB 文件的頻率需要把握一個度,頻率過低快照間隔數據較大,丟失的數據就會比較多;頻率太快,又會消耗額外開銷,降低Redis性能。
- RDB 建議采用二進制 + 數據壓縮的方式寫磁盤,文件體積小,數據恢復速度快。
3 AOF 日志
AOF 日志存儲了 Redis 服務器的順序指令序列,AOF 日志只記錄對內存進行修改的指令記錄。
假設 AOF 日志記錄了自 Redis 實例創建以來所有的修改性指令序列,那么就可以通過對一個空的 Redis 實例順序執行所有的指令。
也就是說,可以通過重放(replay),來建立 Redis 當前實例的內存數據結構。這種模式有沒有很熟悉,有沒有想到MySQL主從同步時候的relay log。
3.1 日志變更前后對比
AOF記錄日志有兩種模式,一種是預寫式日志,也稱寫前日志(Write Ahead Log, WAL): 在實際寫數據之前,將修改的數據寫到日志文件中。
另外一種是寫后日志: 先執行寫操作,當數據存入內存后,再記錄日志。
預寫式日志類似 MySQL Innodb 引擎 中的 redo log,修改數據前先記錄日志,再修改。
3.2 日志格式
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
3.3 可能存在的問題
- 可能存在丟失:比如Redis 剛執行完指令,還沒記錄日志宕機了,命令數據就丟了。
- AOF 避免了當前命令的阻塞,但是AOF 日志是主線程執行,將日志寫入磁盤過程中,如果磁盤壓力大就會導致執行變慢,降低后續的操作。
3.4 寫回策略
上面的問題,在Redis高頻讀寫的時候是必然存在的,想要解決,在寫入的時候做一層緩沖就可以了,避免直塞。這時候Redis提供了一種執行策略叫寫回策略。
3.4.1 寫回策略說明
為了提高日志文件的寫入效率,寫回策略會做如下變化:
- 當你調用 write 函數將數據寫入到文件時,這時候不是真正的落盤,而是將寫入數據暫存在操作系統的內存緩沖區里。
- 待到緩沖區的空間被填滿、或者超過了指定的閾值時候,才真正地將緩沖區中的數據寫入到磁盤里面。
這種做法顯然提高了效率,但也為寫入數據帶來了安全性問題,如果服務器發生了單機,那么保存在內存緩沖區里面的寫入數據就會丟失。
為此,系統提供了fsync
和fdatasync
兩個同步函數,它們可以強制讓操作系統立即將緩沖區中的數據寫入到硬盤里面,從而確保寫入數據的安全性。
Redis 提供的 AOF 配置項appendfsync
寫回策略直接決定 AOF 持久化功能的效率和安全性,以下是appendfsync
的3個枚舉: - always:同步寫回,寫指令執行完 即將緩沖區內容回寫到 AOF 文件。
- everysec:每秒寫回,寫指令執行完,日志寫到 AOF 文件緩沖區,緩沖區每隔一秒再把內容同步到磁盤。
- no: 操作系統控制,寫執行執行完畢,把日志寫到 AOF 文件內存緩沖區,由操作系統決定何時回寫到磁盤。
寫磁盤會帶來性能上的損耗,所以寫回的策略要根據實際情況做一個取舍,比如你是偏向性能還是可靠性。
always 同步寫回可以做到數據不丟失,但是每次執行寫指令都需要寫入磁盤,性能最差。
everysec 每秒寫回,避免了同步寫回的性能開銷,但是如果服務發生宕機,會有大約1s時間周期的數據丟失,這種模式是在性能和可靠性之間做了妥協。
no 操作系統控制,執行寫指令后就寫入 AOF 文件緩沖,再執行后續的寫磁盤指令,性能最好,但有可能丟失更多的數據。
3.4.2 寫回策略的選擇
我們可以根據服務的實際情況來抉擇策略,看是偏向高性能還是高可靠。
- 高性能需求,選擇 No 策略
- 高可靠性保證,就選擇 Always 策略
- 如果能夠接受數據存在少量丟失,又希望性能較好的話,就選擇 Everysec 策略
4 混合RDF/AOF 方式模式
現實情況下,無論使用RDB或者AOF都差點意思。使用 rdb 來恢復內存狀態,勢必會丟失一部分數據。 使用 AOF 日志重放,重放對性能有一定的影響,而且在 Redis 實例很大的情況下,需要花費很長的時間。
Redis 4.0 解決了這個問題,才用了一個新的持久化模式——混合持久化,該 混合模式 默認是關閉狀態的。
將 RDB 文件的內容和 rdb快照時間點之后的增量的 AOF 日志文件存在一起。這時候 AOF 日志不需要再是全量的日志,而是最近一次快照時間點之后到當下發生的增量 AOF 日志,通常這部分 AOF 日志很小。
所以執行有如下順序:
- 查找rdb內容,如果存在先加載 rdb內容再 重放剩余的 aof。
- 沒有rdb內容,直接以aof格式重放整個文件。
這樣快照就不用頻繁的執行,同時由於 AOF 只需要記錄最近一次快照之后的數據,不需要記錄所有的操作,避免了出現單次重放文件過大的問題。
5 總結
- RDB提供了快照模式,記錄某個時間的Redis內存狀態。RDB設計了 bgsave 和寫時復制,盡可能避免執行快照期間對讀寫指令的影響,但是頻繁快照會給磁盤帶來壓力以及 fork 阻塞主線程。需把握頻率。
- AOF 日志存儲了 Redis 服務的順序指令序列,通過重放(replay)指令來寫入日志文件,並通過寫回策略來避免高頻讀寫給Redis帶來壓力。
- RDB快照的照片時間間隔,必然會帶來數據缺失,如果允許分鍾級別的數據丟失,可以只使用 RDB。
- 如果只用 AOF,寫回策略優先使用 everysec 的配置選項,因為它在可靠性和性能之間取了一個平衡。
- 數據不能丟失時,內存快照和 AOF 的混合使用是一個很好的選擇。