Redis 持久化,超詳細吐血總結(7)

我們熟知redis是內存數據庫,它將自己的數據存儲在內存裏面,如果如圖redis進程退出或突然宕機,數據就會全部丟失,因此必須有一種機制來保證 Redis 的數據不會因爲故障而丟失,這種機制就是 Redis 的持久化機制。

Redis 的持久化機制有兩種,第一種是快照(RDB),第二種是 AOF 日誌。快照是一次全量備份,AOF 日誌是連續的增量備份。快照是內存數據的二進制序列化形式,在存儲上非常緊湊,而 AOF 日誌記錄的是內存數據修改的指令記錄文本

RDB原理

RDB持久化是把當前進程數據生成快照保存到硬盤的過程,觸發RDB持久化過程分爲手動觸發和自動觸發

觸發機制

  1. save命令:阻塞當前redis服務器,直RDB過程完成爲止。(如果內存比較大會造成redis長時間阻塞,這樣顯然不是我們想要的。線上禁止使用)
  2. bgsave命令:redis進程執行fork操作創建子進程,RDB持久化過程由子進程負責,完成後自動結束。阻塞只發生在fork階段,一般很短(和實例數據大小有關係)

除了執行命令手動觸發之外,redis內部還存在自動觸發RDB的持久化機制,如下

  1. 使用save相關配置,如“save m n” 表示m秒內數據集存在n次修改時(可以配置多組條件,其中一個達標就觸發),自動觸發bgsave
  2. 如果從節點執行全量複製操作,主節點自動執行bgsave生成RDB文件併發送給從節點(具體複製細節見下一篇)
  3. 執行debug reload命令重寫加載redis時,
  4. 默認情況下執行shutdown命令時,如果沒有開啓aof持久化則自動執行bgsave

bgsvae運作流程如下

  1. 執行bgsave命令,redis父進程判斷是否存在正在執行的子進程,如RDB/AOF子進程,如果存在bgsave命令直接返回(生成RDB、AOF要浪費大量磁盤io資源,如果開啓aof,磁盤io有可能成爲redis的瓶頸)
  2. 父進程執行fork操作創建子進程,fork操作過程中父進程會阻塞,通過info stats 命令查看latest_fork_usec選項,可以獲取最近一個fork操作的耗時(單位ms)(具體阻塞時間和內存大小有關)
  3. 父進程fork完成後,bgsave命令返回“Background saving started”信息並不在阻塞父進程,子進程創建RDB文件,根據父進程內存生成的臨時快照文件,完成後對原有的文件進行院子替換。執行lastsave命令可以獲取最後一次生成RDB的時間
  4. 子進程發送信號給父進程表示完成,父進程更新統計信息

至此RDB的生成過程大概講完了,我們知道Redis爲了不阻塞主線程,調用 glibc 的函數fork產生一個子進程來生成RDB備份文件,試想一個問題,如果一個大型的 hash 字典正在持久化,結果一個請求過來把它給刪掉了,還沒持久化完呢,這尼瑪要怎麼搞??

Copy On Write

Redis 使用操作系統的多進程 COW(Copy On Write) 機制來實現快照持久化。子進程剛剛產生時,它和父進程共享內存裏面的代碼段和數據段.這是 Linux 操作系統的機制,爲了節約內存資源,所以儘可能讓它們共享起來。在進程分離的一瞬間,內存的增長幾乎沒有明顯變化。子進程做數據持久化,它不會修改現有的內存數據結構,它只是對數據結構進行遍歷讀取,然後序列化寫到磁盤中。但是父進程不一樣,它必須持續服務客戶端請求,然後對內存數據結構進行不間斷的修改。

這個時候就會使用操作系統的 COW 機制來進行數據段頁面的分離。數據段是由很多操作系統的頁面組合而成(一頁4k),當父進程對其中一個頁面的數據進行修改時,會將被共享的頁面複製一份分離出來,然後對這個複製的頁面進行修改。這時子進程相應的頁面是沒有變化的,還是進程產生時那一瞬間的數據。

子進程因爲數據沒有變化,它能看到的內存裏的數據在進程產生的一瞬間就凝固了,再也不會改變,這也是爲什麼 Redis 的持久化叫「快照」的原因。接下來子進程就可以非常安心的遍歷數據了進行序列化寫磁盤了。

RDB的載入

和使用save或者bgsave命令不同,RDB的載入是在服務器啓動的時候自動執行的,所以Redis並沒有專門用於載入RDB的文件命令。值得一提的是:

  1. 如果服務器開啓了AOF持久化功能,那麼服務器會優先使用AOF文件來還原數據庫
  2. 數據庫在主從複製時候會觸發RDB加載(下一篇細聊)

RDB文件的處理

保存:RDB文件保存在dir配置的指定目錄下,文件名通過dbfilename配置指定。可以通過執行 config set dir{new Dir} 和 config set dbfilename{newFileName} 運行期動態執行。

壓縮:redis 默認採用LZF算法對生成的RDB文件做壓縮處理,壓縮後的文件遠小於內存大小,默認開啓,可以通過參數config set rdbcompression{yes|no}動態修改

RDB的優缺點

優點:

  1. RDB是一個緊湊壓縮的二進制文件,某個時間點的上的快照。適合全量複製
  2. redis加載RDB恢復數據遠快於AOF的方式
  3. 如果Redis加載損壞的RDB文件時拒絕啓動,並打印如下日誌:# Short read or OOM loading DB. Unrecoverable error, aborting now.    這時可以使用Redis提供的redis-check-dump工具檢測RDB文件並獲取對應的錯誤報告。

缺點:

  1. RDB方式數據沒辦法做到實時持久化/秒級持久化。因爲bgsave每次運行都要執行fork操作創建子進程,屬於重量級操作,頻繁執行成本過高。
  2. RDB文件使用特定二進制格式保存,Redis版本演進過程中有多個格式的RDB版本,存在老版本Redis服務無法兼容新版RDB格式的問題。

針對RDB不適合實時持久化的問題,Redis提供了AOF持久化方式來解決。


AOF原理

AOF的主要作用是解決了數據持久化的實時性,AOF 日誌存儲的是 Redis 服務器的順序指令序列,AOF 日誌只記錄對內存進行修改的指令記錄。

Redis 會在收到客戶端修改指令後,進行參數校驗進行邏輯處理後,如果沒問題,就立即將該指令文本存儲到 AOF 日誌中,也就是先執行指令纔將日誌存盤。這點不同於mysql、hbase等存儲引擎,它們都是先存儲日誌再做邏輯處理。(所以單機redis不能做到一條數據也不丟失)

使用AOF

開啓AOF功能需要設置配置:appendonly yes,默認不開啓。AOF文件名通過appendfilename配置設置,默認文件名是appendonly.aof。保存路徑同RDB持久化方式一致,通過dir配置指定。AOF的工作流程操作:命令寫入(append)、文件同步(sync)、文件重寫(rewrite)、重啓加載(load)。

命令寫入and文件同步

服務器在執行完一個寫命令後(如 set k v,lpush k v 等),會把寫入命令會追加到aof_buf(緩衝區)中。後續防止丟失aof_buf中的數據,在調用linux的glibc提供的fsync函數將aof_buf中的數據強制刷新到磁盤。

AOF爲什麼把命令追加到aof_buf中?Redis使用單線程響應命令,如果每次寫AOF文件命令都直接追加到硬盤(調用fsync),那麼性能完全取決於當前硬盤負載。先寫入緩衝區aof_buf中,還有另一個好處,Redis可以提供多種緩衝區同步硬盤的策略,在性能和安全性方面做出平衡。

Redis提供了多種AOF緩衝區同步文件策略,由參數appendfsync控制,不同值的含義如下

  1. always: 命令寫入aof_buf後調用系統fsync操作同步到aof文件,fsync完成後線程返回(性能最差,完全取決於磁盤速度。即便如此redis也不能保證一條數據也不丟)
  2. everysec: 命令寫入aof_buf後調用系統write操作,write完成後線程返回。fsync同步文件操作由專門的線程每秒調用一次(默認配置。理論上會丟失1s的數據。(嚴格來說丟棄1s的數據不準確,下文有講))
  3. no:命令寫入aof_bug後調用系統的write操作,不對AOF文件做fsync同步,同步硬盤操作由操作系統負責,通常同步週期30s

系統調用write和fsync說明:

  1. write :Linux在內核提供頁緩衝區用來提高硬盤IO性能。write操作在寫入系統緩衝區後直接返回。同步文件之前,如果此時系統故障宕機,緩衝區內數據將丟失。
  2. fsync : 將指定文件的內容強制從內核緩存刷到磁盤

AOF重寫機制

我們知道AOF持久化是通過保存被執行的寫命令來實現的,隨着命令不斷寫入AOF,文件會越來越大。爲了解決這個問題,Redis引入AOF重寫機制壓縮文件體積。AOF文件重寫是把Redis進程內的數據轉化爲寫命令同步到新AOF

重寫後爲什麼變小?

  1. 已經過期的數據不再寫入文件。
  2. 舊的AOF文件含有無效命令,如set key1、del key1 。重寫使用進程內數據直接生成,這樣新的AOF文件只保留最終數據的寫入命令。
  3. 多條寫命令可以合併爲一個,如:lpush list a、lpush list b、lpush list c可以轉化爲:lpush list a b c。爲了防止單條命令過大造成客戶端緩衝區溢出,對於list、set、hash、zset等類型操作,以64個元素爲界拆分爲多條其實這裏直接遍歷的是當前內存數據,如直接把一個大個的list的生成一個個lpush list a1 a2 ..a60 ,步子大了容易扯着蛋,所以默認以64個元素爲一組
  4. 更小的AOF文件可以被Redis更快的加載

AOF重寫觸發條件

手動觸發:直接調用bgrewriteaof命令。

自動觸發:根據auto-aof-rewrite-min-size和auto-aof-rewrite-percentage參數確定自動觸發時機。

  1. auto-aof-rewrite-min-size:表示運行AOF重寫時文件最小體積,默認爲64MB。
  2. auto-aof-rewrite-percentage(設爲x):代表當前AOF文件空間(aof_current_size)和上一次重寫後AOF文件空間(aof_base_size)的百分比如auto-aof-rewrite-percentage 100 當爲兩倍大小是重寫

自動觸發條件:aof_current_size > auto-aof-rewrite-min-size && (x+1)*aof_base_size

其中aof_current_size和aof_base_size可以在info Persistence統計信息中查看。

AOF重寫Redis做了哪些事情呢?

AOF重寫功能作爲一個輔助功能,redis肯定不希望阻塞主進程的執行,所以redis把AOF重寫放到一個子進程去執行。上文講到frok運用cow(寫時複製技術)技術,所以子進程只能看到fork那一瞬間產生的鏡像數據。爲了解決這一個問題redis設置了一個 AOF重寫緩存區(aof_rewrite_bug) 用來存儲AOF重寫期間產生的命令,等子進程重寫完成後通知父進程,父進程把重寫緩存區的數據追加到新的AOF文件(注:這裏值得注意,AOF重寫期間如果有大量的寫入,父進程在把aof_rewrite_buf寫到新的aof文件時會造成大量的寫盤操作,會造成性能的下降,redis 4.0以後增加管道機制來優化這裏(把aof_rewrite_buf追加工作交給子進程去做),感興趣的同學可以自行查閱)

所以說在AOF重寫期間主進程做了下面幾件事

  1. 執行客戶端發來的命令
  2. 將執行後的寫命令追加到AOF緩衝區
  3. 將執行後的寫明瞭追加到AOF重寫緩衝區

值得注意的是,redis每次重寫寫入磁盤的大小由aof-rewrite-incremental-fsync控制,默認爲32MB,防止單次刷盤數據過多造成硬盤阻塞

過程如圖:

我們知道AOF重寫期間會消耗大量磁盤IO,可以開啓配置no-appendfsync-on-rewrite yes,表示在AOF重寫期間不做fsync操作,默認爲no。但是如此一來某些情況下會丟掉重寫期間的所有數據。慎重啊,鐵子

重啓加載

講RDB時提到,如果開啓AOF優先加載AOF文件,否則執行RDB文件


AOF追加阻塞

當開啓AOF持久化時,默認以及常用的同步硬盤策略是everysec(每s一刷),對於這種方式,Redis使用另一個線程每s執行fsync同步磁盤。試想一個問題,假設硬盤資源繁忙,fsync刷盤緩慢,主線程該如何做?

主線程寫入AOF緩衝區後會對比上次AOF同步時間

  1. 如果距上次同步成功時間在2S內,主線程直接返回
  2. 如果距上次同步成功時間超過2s,主線程阻塞,直到同步操作完成

發現兩個問題:

  1. everysec配置最多可能丟失2s數據,不是1s
  2. 如果系統fsync慢會阻塞主線程

RDB-AOF混合持久化(redis 4.0+提供)

細細想來aofrewrite時也是先寫一份全量數據到新AOF文件中再追加增量只不過全量數據是以redis命令的格式寫入。那麼是否可以先以RDB格式寫入全量數據再追加增量日誌呢這樣既可以提高aofrewrite和恢復速度也可以減少文件大小還可以保證數據的完畢性整合RDB和AOF的優點那麼現在4.0實現了這一特性——RDB-AOF混合持久化。

綜上所述RDB-AOF混合持久化體現在aofrewrite時,即在AOF重寫時把frok的那個鏡像寫成RDB,後續AOF重寫緩衝裏的數據繼續追加到該文件中。

 配置爲 aof-use-rdb-preamble no  #默認關閉,yes 打開


寫在最後:

小結

  1. redis提供兩種持久化方式:RDB和aof
  2. RDB使用一次性生成內存快照,產生的文件爲二進制序列,非常緊湊,因此加載更快。但是由於其爲快照,所以不能做到實時持久化,一般用於冷備和複製傳輸
  3. save命令會阻塞主線程不建議使用,bgsave通過fork創建子進程生成RDB避免阻塞
  4. AOF通過追加寫命令到文件實現持久化,因爲需要不斷追加寫命令,所以AOF需要定期執行重寫來降低文件體積
  5. 如果寫命令直接寫入磁盤勢必會造成性能過低,所以redis提供了一個AOF緩衝區。寫命令寫入到AOF緩衝區後續在調用fsync異步刷盤
  6. AOF子進程執行期間使用copy-on-write機制和父進程共享內存,但是該技術只能看到fork那一瞬間內存的快照,所以需要一個AOF重寫緩衝區,保存新的寫入命令避免數據丟失
  7. 持久化阻塞主線程的場景有:fork阻塞和AOF追加阻塞,fork阻塞時間跟內存和系統有關,AOF追加阻塞說明磁盤資源緊張

文章涉及到的命令及配置

  1. 命令 :手動備份RDB:save(阻塞) 和 bgsave(fork子進程)
  2. 配置: save m n 表示m秒內數據集存在n次修改時(可以配置多組條件,其中一個達標就觸發),自動觸發bgsave
  3. 命令 :debug reload  重新加載redis
  4. 日誌文件名配置 : 文件保存在dir配置的指定目錄下,文件名通過dbfilename配置指定。可以通過執行 config set dir{new Dir} 和 config set dbfilename{newFileName} 運行期動態執行。
  5. 開啓aof: appendonly yes
  6.  appendfsync  always|everysec|no : AOF緩衝區同步磁盤,always:總是同步,everysec:1s刷新,no:交給系統(大概30s)
  7. 命令 : bgrewriteaof :手動重寫AOF
  8. 配置:auto-aof-rewrite-min-size:表示運行AOF重寫時文件最小體積,默認爲64MB。
  9. 配置:auto-aof-rewrite-percentage:代表當前AOF文件空間(aof_current_size)和上一次重寫後AOF文件空間(aof_base_size)的百分比(如auto-aof-rewrite-percentage 100 當爲兩倍大小是重寫)。
  10. 配置:aof-rewrite-incremental-fsync:AOF每次重寫一次刷盤大小,默認爲32MB
  11. 配置:no-appendfsync-on-rewrite :AOF重寫期間是否刷新AOF緩衝區,默認爲no(刷新)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章