Redis系列2:資料持久化提高可用性

Brand發表於2022-06-27

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的高效讀寫,還實現了定時持久化,當機後可快速恢復資料。
image
如上圖,在做資料恢復時,直接將 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 檔案,所以主程式就可以直接修改原來的資料。
image
這既保證了快照的完整性,也允許主程式同時對資料進行修改,避免了對正常業務的影響。

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,修改資料前先記錄日誌,再修改。
image

3.2 日誌格式

Redis 接收到 set keyName someValue 命令的時候,會先將資料寫到記憶體,Redis 會按照如下格式寫入 AOF 檔案。
*3:表示當前指令分為三個部分,每個部分都是 $ + 數字開頭,後面是3部分的具體內容:指令、鍵、值。
數字:表示這部分的命令、鍵、值多佔用的位元組大小。比如 $3表示這部分包含 3 個字元,也就是 set 的長度。
image

推薦使用寫後日志的模式,避免了額外的檢查開銷,不需要對執行的命令進行語法檢查。如果使用寫前日誌的話,就需要先檢查語法是否有誤,否則日誌記錄了錯誤的命令,在使用日誌恢復的時候就會出錯。另外,寫後才記錄日誌,不會阻塞當前的 指令執行。

# 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 函式將資料寫入到檔案時,這時候不是真正的落盤,而是將寫入資料暫存在作業系統的記憶體緩衝區裡。
  • 待到緩衝區的空間被填滿、或者超過了指定的閾值時候,才真正地將緩衝區中的資料寫入到磁碟裡面。
    這種做法顯然提高了效率,但也為寫入資料帶來了安全性問題,如果伺服器發生了單機,那麼儲存在記憶體緩衝區裡面的寫入資料就會丟失。
    為此,系統提供了fsyncfdatasync兩個同步函式,它們可以強制讓作業系統立即將緩衝區中的資料寫入到硬碟裡面,從而確保寫入資料的安全性。
    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 只需要記錄最近一次快照之後的資料,不需要記錄所有的操作,避免了出現單次重放檔案過大的問題。
    image

5 總結

  • RDB提供了快照模式,記錄某個時間的Redis記憶體狀態。RDB設計了 bgsave 和寫時複製,儘可能避免執行快照期間對讀寫指令的影響,但是頻繁快照會給磁碟帶來壓力以及 fork 阻塞主執行緒。需把握頻率。
  • AOF 日誌儲存了 Redis 服務的順序指令序列,通過重放(replay)指令來寫入日誌檔案,並通過寫回策略來避免高頻讀寫給Redis帶來壓力。
  • RDB快照的照片時間間隔,必然會帶來資料缺失,如果允許分鐘級別的資料丟失,可以只使用 RDB。
  • 如果只用 AOF,寫回策略優先使用 everysec 的配置選項,因為它在可靠性和效能之間取了一個平衡。
  • 資料不能丟失時,記憶體快照和 AOF 的混合使用是一個很好的選擇。

相關文章