Redis(7)——持久化【一文了解】

一、持久化簡介

Redis 的數據 全部存儲 在 內存 中,如果 突然宕機,數據就會全部丟失,因此必須有一套機制來保證 Redis 的數據不會因爲故障而丟失,這種機制就是 Redis 的 持久化機制,它會將內存中的數據庫狀態 保存到磁盤 中。

持久化發生了什麼 | 從內存到磁盤

我們來稍微考慮一下 Redis 作爲一個 "內存數據庫" 要做的關於持久化的事情。通常來說,從客戶端發起請求開始,到服務器真實地寫入磁盤,需要發生如下幾件事情:

Redis(7)——持久化【一文了解】

詳細版 的文字描述大概就是下面這樣:

  1. 客戶端向數據庫 發送寫命令 (數據在客戶端的內存中)

  2. 數據庫 接收 到客戶端的 寫請求 (數據在服務器的內存中)

  3. 數據庫 調用系統 API 將數據寫入磁盤 (數據在內核緩衝區中)

  4. 操作系統將 寫緩衝區 傳輸到 磁盤控控制器 (數據在磁盤緩存中)

  5. 操作系統的磁盤控制器將數據 寫入實際的物理媒介 中 (數據在磁盤中)

注意: 上面的過程其實是 極度精簡 的,在實際的操作系統中,緩存 和 緩衝區 會比這 多得多...

如何儘可能保證持久化的安全

如果我們故障僅僅涉及到 軟件層面 (該進程被管理員終止或程序崩潰) 並且沒有接觸到內核,那麼在 上述步驟 3 成功返回之後,我們就認爲成功了。即使進程崩潰,操作系統仍然會幫助我們把數據正確地寫入磁盤。

如果我們考慮 停電/ 火災 等 更具災難性 的事情,那麼只有在完成了第 5 步之後,纔是安全的。

Redis(7)——持久化【一文了解】

機房”火了“

所以我們可以總結得出數據安全最重要的階段是:步驟三、四、五,即:

  • 數據庫軟件調用寫操作將用戶空間的緩衝區轉移到內核緩衝區的頻率是多少?

  • 內核多久從緩衝區取數據刷新到磁盤控制器?

  • 磁盤控制器多久把數據寫入物理媒介一次?

  • 注意: 如果真的發生災難性的事件,我們可以從上圖的過程中看到,任何一步都可能被意外打斷丟失,所以只能 儘可能地保證 數據的安全,這對於所有數據庫來說都是一樣的。

我們從 第三步 開始。Linux 系統提供了清晰、易用的用於操作文件的 POSIX file API20 多年過去,仍然還有很多人對於這一套 API 的設計津津樂道,我想其中一個原因就是因爲你光從 API 的命名就能夠很清晰地知道這一套 API 的用途:

int open(const char *path, int oflag, .../*,mode_t mode */);
int close (int filedes);int remove( const char *fname );
ssize_t write(int fildes, const void *buf, size_t nbyte);
ssize_t read(int fildes, void *buf, size_t nbyte);
  • 參考自:API 設計最佳實踐的思考 - https://www.cnblogs.com/yuanjiangw/p/10846560.html

所以,我們有很好的可用的 API 來完成 第三步,但是對於成功返回之前,我們對系統調用花費的時間沒有太多的控制權。

然後我們來說說 第四步。我們知道,除了早期對電腦特別瞭解那幫人 (操作系統就這幫人搞的),實際的物理硬件都不是我們能夠 直接操作 的,都是通過 操作系統調用 來達到目的的。爲了防止過慢的 I/O 操作拖慢整個系統的運行,操作系統層面做了很多的努力,譬如說 上述第四步 提到的 寫緩衝區,並不是所有的寫操作都會被立即寫入磁盤,而是要先經過一個緩衝區,默認情況下,Linux 將在 30 秒後實際提交寫入。

Redis(7)——持久化【一文了解】

但是很明顯,30 秒 並不是 Redis 能夠承受的,這意味着,如果發生故障,那麼最近 30 秒內寫入的所有數據都可能會丟失。幸好 PROSIX API 提供了另一個解決方案:fsync,該命令會 強制 內核將 緩衝區 寫入 磁盤,但這是一個非常消耗性能的操作,每次調用都會 阻塞等待 直到設備報告 IO 完成,所以一般在生產環境的服務器中,Redis 通常是每隔 1s 左右執行一次 fsync 操作。

到目前爲止,我們瞭解到瞭如何控制 第三步 和 第四步,但是對於 第五步,我們 完全無法控制。也許一些內核實現將試圖告訴驅動實際提交物理介質上的數據,或者控制器可能會爲了提高速度而重新排序寫操作,不會盡快將數據真正寫到磁盤上,而是會等待幾個多毫秒。這完全是我們無法控制的。

二、Redis 中的兩種持久化方式

方式一:快照

Redis(7)——持久化【一文了解】

Redis 快照 是最簡單的 Redis 持久性模式。當滿足特定條件時,它將生成數據集的時間點快照,例如,如果先前的快照是在2分鐘前創建的,並且現在已經至少有 100 次新寫入,則將創建一個新的快照。此條件可以由用戶配置 Redis 實例來控制,也可以在運行時修改而無需重新啓動服務器。快照作爲包含整個數據集的單個 .rdb 文件生成。

但我們知道,Redis 是一個 單線程 的程序,這意味着,我們不僅僅要響應用戶的請求,還需要進行內存快照。而後者要求 Redis 必須進行 IO 操作,這會嚴重拖累服務器的性能。

還有一個重要的問題是,我們在 持久化的同時內存數據結構 還可能在 變化,比如一個大型的 hash 字典正在持久化,結果一個請求過來把它刪除了,可是這纔剛持久化結束,咋辦?

Redis(7)——持久化【一文了解】

使用系統多進程 COW(Copy On Write) 機制 | fork 函數

操作系統多進程 COW(Copy On Write) 機制 拯救了我們。Redis 在持久化時會調用 glibc 的函數 fork 產生一個子進程,簡單理解也就是基於當前進程 複製 了一個進程,主進程和子進程會共享內存裏面的代碼塊和數據段:

Redis(7)——持久化【一文了解】

這裏多說一點,爲什麼 fork 成功調用後會有兩個返回值呢? 因爲子進程在複製時複製了父進程的堆棧段,所以兩個進程都停留在了 fork 函數中 (都在同一個地方往下繼續"同時"執行),等待返回,所以 一次在父進程中返回子進程的 pid,另一次在子進程中返回零,系統資源不夠時返回負數(僞代碼如下)

pid = os.fork()
if pid > 0:
  handle_client_request()  # 父進程繼續處理客戶端請求
if pid == 0:
  handle_snapshot_write()  # 子進程處理快照寫磁盤
if pid < 0:
  # fork error

所以 快照持久化 可以完全交給 子進程 來處理,父進程 則繼續 處理客戶端請求子進程 做數據持久化,它 不會修改現有的內存數據結構,它只是對數據結構進行遍歷讀取,然後序列化寫到磁盤中。但是 父進程 不一樣,它必須持續服務客戶端請求,然後對 內存數據結構進行不間斷的修改

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

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

方式二:AOF

Redis(7)——持久化【一文了解】

快照不是很持久。如果運行 Redis 的計算機停止運行,電源線出現故障或者您 kill -9 的實例意外發生,則寫入 Redis 的最新數據將丟失。儘管這對於某些應用程序可能不是什麼大問題,但有些使用案例具有充分的耐用性,在這些情況下,快照並不是可行的選擇。

AOF(Append Only File - 僅追加文件) 它的工作方式非常簡單:每次執行 修改內存 中數據集的寫操作時,都會 記錄 該操作。假設 AOF 日誌記錄了自 Redis 實例創建以來 所有的修改性指令序列,那麼就可以通過對一個空的 Redis 實例 順序執行所有的指令,也就是 「重放」,來恢復 Redis 當前實例的內存數據結構的狀態。

爲了展示 AOF 在實際中的工作方式,我們來做一個簡單的實驗:

./redis-server --appendonly yes  # 設置一個新實例爲 AOF 模式

然後我們執行一些寫操作:

redis 127.0.0.1:6379> set key1 Hello
OK
redis 127.0.0.1:6379> append key1 " World!"
(integer) 12
redis 127.0.0.1:6379> del key1
(integer) 1
redis 127.0.0.1:6379> del non_existing_key
(integer) 0

前三個操作實際上修改了數據集,第四個操作沒有修改,因爲沒有指定名稱的鍵。這是 AOF 日誌保存的文本:

$ cat appendonly.aof
*2
$6
SELECT
$1
0
*3
$3
set
$4
key1
$5
Hello
*3
$6
append
$4
key1
$7
 World!
*2
$3
del
$4
key1

如您所見,最後的那一條 DEL 指令不見了,因爲它沒有對數據集進行任何修改。

就是這麼簡單。當 Redis 收到客戶端修改指令後,會先進行參數校驗、邏輯處理,如果沒問題,就 立即 將該指令文本 存儲 到 AOF 日誌中,也就是說,先執行指令再將日誌存盤。這一點不同於 MySQLLevelDBHBase 等存儲引擎,如果我們先存儲日誌再做邏輯處理,這樣就可以保證即使宕機了,我們仍然可以通過之前保存的日誌恢復到之前的數據狀態,但是 Redis 爲什麼沒有這麼做呢?

Emmm... 沒找到特別滿意的答案,引用一條來自知乎上的回答吧:

  • @緣於專注 - 我甚至覺得沒有什麼特別的原因。僅僅是因爲,由於AOF文件會比較大,爲了避免寫入無效指令(錯誤指令),必須先做指令檢查?如何檢查,只能先執行了。因爲語法級別檢查並不能保證指令的有效性,比如刪除一個不存在的key。而MySQL這種是因爲它本身就維護了所有的表的信息,所以可以語法檢查後過濾掉大部分無效指令直接記錄日誌,然後再執行。

  • 更多討論參見:爲什麼Redis先執行指令,再記錄AOF日誌,而不是像其它存儲引擎一樣反過來呢?- https://www.zhihu.com/question/342427472

AOF 重寫

Redis(7)——持久化【一文了解】

Redis 在長期運行的過程中,AOF 的日誌會越變越長。如果實例宕機重啓,重放整個 AOF 日誌會非常耗時,導致長時間 Redis 無法對外提供服務。所以需要對 AOF 日誌 "瘦身"

Redis 提供了 bgrewriteaof 指令用於對 AOF 日誌進行瘦身。其 原理 就是 開闢一個子進程 對內存進行 遍歷 轉換成一系列 Redis 的操作指令,序列化到一個新的 AOF 日誌文件 中。序列化完畢後再將操作期間發生的 增量 AOF 日誌 追加到這個新的 AOF 日誌文件中,追加完畢後就立即替代舊的 AOF 日誌文件了,瘦身工作就完成了。

fsync

Redis(7)——持久化【一文了解】

AOF 日誌是以文件的形式存在的,當程序對 AOF 日誌文件進行寫操作時,實際上是將內容寫到了內核爲文件描述符分配的一個內存緩存中,然後內核會異步將髒數據刷回到磁盤的。

就像我們 上方第四步 描述的那樣,我們需要藉助 glibc 提供的 fsync(int fd) 函數來講指定的文件內容 強制從內核緩存刷到磁盤。但 "強制開車" 仍然是一個很消耗資源的一個過程,需要 "節制"!通常來說,生產環境的服務器,Redis 每隔 1s 左右執行一次 fsync 操作就可以了。

Redis 同樣也提供了另外兩種策略,一個是 永不 fsync,來讓操作系統來決定合適同步磁盤,很不安全,另一個是 來一個指令就 fsync 一次,非常慢。但是在生產環境基本不會使用,瞭解一下即可。

Redis 4.0 混合持久化

Redis(7)——持久化【一文了解】

重啓 Redis 時,我們很少使用 rdb 來恢復內存狀態,因爲會丟失大量數據。我們通常使用 AOF 日誌重放,但是重放 AOF 日誌性能相對 rdb 來說要慢很多,這樣在 Redis 實例很大的情況下,啓動需要花費很長的時間。

Redis 4.0 爲了解決這個問題,帶來了一個新的持久化選項——混合持久化。將 rdb 文件的內容和增量的 AOF 日誌文件存在一起。這裏的 AOF 日誌不再是全量的日誌,而是 自持久化開始到持久化結束的這段時間發生的增量 AOF 日誌,通常這部分 AOF 日誌很小:

Redis(7)——持久化【一文了解】

於是在 Redis 重啓的時候,可以先加載 rdb 的內容,然後再重放增量 AOF 日誌就可以完全替代之前的 AOF 全量文件重放,重啓效率因此大幅得到提升。高質量編程視頻shangyepingtai.xin

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章