一文深度揭祕Redis的磁碟持久化機制

零壹技術棧發表於2019-10-12

前言

Redis 是記憶體資料庫,資料都是儲存在記憶體中,為了避免程式退出導致資料的永久丟失,需要定期將 Redis 中的資料以資料或命令的形式從記憶體儲存到本地磁碟。當下次 Redis 重啟時,利用持久化檔案進行資料恢復。Redis 提供了 RDB 和 AOF 兩種持久化機制,前者將當前的資料儲存到磁碟,後者則是將每次執行的寫命令儲存到磁碟(類似於 MySQL 的 Binlog)。本文將詳細介紹 RDB 和 AOF 兩種持久化方案,包括操作方法和持久化的實現原理。

一文深度揭祕Redis的磁碟持久化機制

正文

Redis 是一個基於鍵值對(K-V)儲存的資料庫伺服器,下面先介紹 Redis 資料庫的內部構造以及 K-V 的儲存形式,有助於我們更容易理解 Redis 的持久化機制。

1. Redis 資料庫結構

一個單機的 Redis 伺服器預設情況下有 16 個資料庫(0-15 號),資料庫的個數是可配置的。Redis 預設使用的是 0 號資料庫,可以使用 SELECT 命令切換資料庫。

一文深度揭祕Redis的磁碟持久化機制

Redis 中的每個資料庫都由一個 redis.h/redisDb 結構表示,它記錄了單個 Redis 資料庫的鍵空間、所有鍵的過期時間、處於阻塞狀態和就緒狀態的鍵、資料庫編號等等。

typedef struct redisDb {
    // 資料庫鍵空間,儲存著資料庫中的所有鍵值對
    dict *dict;
    // 鍵的過期時間,字典的鍵為鍵,字典的值為過期事件 UNIX 時間戳
    dict *expires;
    // 正處於阻塞狀態的鍵
    dict *blocking_keys;
    // 可以解除阻塞的鍵
    dict *ready_keys;
    // 正在被 WATCH 命令監視的鍵
    dict *watched_keys;
    struct evictionPoolEntry *eviction_pool;
    // 資料庫編號
    int id;
    // 資料庫的鍵的平均 TTL,統計資訊
    long long avg_ttl;
} redisDb;

複製程式碼

由於 Redis 是一個鍵值對資料庫(key-value pairs database), 所以它的資料庫本身也是一個字典,對應的結構正是 redisDb。其中,dict 指向的是一個記錄鍵值對資料的字典,它的鍵是一個字串物件,它的值則可以是字串、列表、雜湊表、集合和有序集合在內的任意一種 Redis 型別物件。 expires 指向的是一個用於記錄鍵的過期時間的字典,它的鍵為 dict 中的資料庫鍵,它的值為這個資料庫鍵的過期時間戳,這個值以 long long 型別表示。

一文深度揭祕Redis的磁碟持久化機制

2. RDB 持久化

RDB 持久化(也稱作快照持久化)是指將記憶體中的資料生成快照儲存到磁碟裡面,儲存的檔案字尾是 .rdb。rdb 檔案是一個經過壓縮的二進位制檔案,當 Redis 重新啟動時,可以讀取 rdb 快照檔案恢復資料。RDB 功能最核心的是 rdbSave 和 rdbLoad 兩個函式, 前者用於生成 RDB 檔案並儲存到磁碟,而後者則用於將 RDB 檔案中的資料重新載入到記憶體中:

一文深度揭祕Redis的磁碟持久化機制

RDB 檔案是一個單檔案的全量資料,很適合資料的容災備份與恢復,通過 RDB 檔案恢復資料庫耗時較短,通常 1G 的快照檔案載入記憶體只需 20s 左右。Redis 提供了手動觸發儲存、自動儲存間隔兩種 RDB 檔案的生成方式,下面先介紹 RDB 的建立和載入過程。

2.1. RDB 的建立和載入

Redis 伺服器預設是通過 RDB 方式完成持久化的,對應 redis.conf 檔案的配置項如下:

# RDB檔案的名稱
dbfilename dump.rdb
# 備份RDB和AOF檔案存放路徑
dir /usr/local/var/db/redis/
複製程式碼

2.1.1. 手動觸發儲存

Redis 提供了兩個用於生成 RDB 檔案的命令,一個是 SAVE,另一個是 BGSAVE。而觸發 Redis 進行 RDB 備份的方式有兩種,一種是通過 SAVE 命令、BGSAVE 命令手動觸發快照生成的方式,另一種是配置儲存時間和寫入次數,由 Redis 根據條件自動觸發儲存操作。

1. SAVE 命令

SAVE 是一個同步式的命令,它會阻塞 Redis 伺服器程式,直到 RDB 檔案建立完成為止。在伺服器程式阻塞期間,伺服器不能處理任何其他命令請求。

  • 客戶端命令
127.0.0.1:6379> SAVE
OK
複製程式碼
  • 服務端日誌
6266:M 15 Sep 2019 08:31:01.258 * DB saved on disk
複製程式碼

執行 SAVE 命令後,Redis 在服務端程式(PID 為 6266)執行了 SAVE 操作,這個操作發生期間會一直阻塞 Redis 客戶端的請求處理。

2. BGSAVE 命令

BGSAVE 是一個非同步式的命令,和 SAVE 命令直接阻塞伺服器程式的做法不同,BGSAVE 命令會派生出一個子程式,由子程式負責建立 RDB 檔案,伺服器程式(父程式)繼續處理客戶的命令。

  • 客戶端命令
127.0.0.1:6379> BGSAVE
Background saving started
複製程式碼
  • 服務端日誌
6266:M 15 Sep 2019 08:31:22.914 * Background saving started by pid 6283
6283:C 15 Sep 2019 08:31:22.915 * DB saved on disk
6266:M 15 Sep 2019 08:31:22.934 * Background saving terminated with success
複製程式碼

通過服務端輸出的日誌,可以發現 Redis 在服務端程式(PID 為 6266)會為 BGSAVE 命令單獨建立(fork)一個子程式(PID 為 6283),並由子程式在後臺完成 RDB 的儲存過程,在操作完成之後通知父程式然後退出。在整個過程中,伺服器程式只會消耗少量時間在建立子程式和處理子程式訊號量上面,其餘時間都是待命狀態。

BGSAVE 是觸發 RDB 持久化的主流方式,下面給出 BGSAVE 命令生成快照的流程:

一文深度揭祕Redis的磁碟持久化機制

  1. 客戶端發起 BGSAVE 命令,Redis 主程式判斷當前是否存在正在執行備份的子程式,如果存在則直接返回
  2. 父程式 fork 一個子程式 (fork 的過程中會造成阻塞的情況),這個過程可以使用 info stats 命令檢視 latest_fork_usec 選項,檢視最近一次 fork 操作消耗的時間,單位是微秒
  3. 父程式 fork 完成之後,則會返回 Background saving started 的資訊提示,此時 fork 阻塞解除
  4. fork 建立的子程式開始根據父程式的記憶體資料生成臨時的快照檔案,然後替換原檔案
  5. 子程式備份完畢後向父程式傳送完成資訊,父程式更新統計資訊
3. SAVE 和 BGSAVE 的比較
命令 SAVE BGSAVE
IO 型別 同步 非同步
是否阻塞 全程阻塞 fork 時發生阻塞
複雜度 O(n) O(n)
優點 不會消耗額外的記憶體 不阻塞客戶端
缺點 阻塞客戶端 fork 子程式消耗記憶體

2.1.2. 自動觸發儲存

因為 BGSAVE 命令可以在不阻塞伺服器程式的情況下執行,所以 Redis 的配置檔案 redis.conf 提供了一個 save 選項,讓伺服器每隔一段時間自動執行一次 BGSAVE 命令。使用者可以通過 save 選項設定多個儲存條件,只要其中任意一個條件被滿足,伺服器就會執行 BGSAVE 命令。 Redis 配置檔案 redis.conf 預設配置了以下 3 個儲存條件:

save 900 1
save 300 10
save 60 10000
複製程式碼

那麼只要滿足以下 3 個條件中的任意一個,BGSAVE 命令就會被自動執行:

  • 伺服器在 900 秒之內,對資料庫進行了至少 1 次修改。
  • 伺服器在 300 秒之內,對資料庫進行了至少 10 次修改。
  • 伺服器在 60 秒之內,對資料庫進行了至少 10000 次修改。

比如通過命令 SET msg "hello" 插入一條鍵值對,等待 900 秒後 Reids 伺服器程式自動觸發儲存,輸出如下:

6266:M 15 Sep 2019 08:46:22.981 * 1 changes in 900 seconds. Saving...
6266:M 15 Sep 2019 08:46:22.986 * Background saving started by pid 6266
6476:C 15 Sep 2019 08:46:23.015 * DB saved on disk
6266:M 15 Sep 2019 08:46:23.096 * Background saving terminated with success
複製程式碼

Redis 伺服器會週期性地操作 serverCron 函式,這個函式每隔 100 毫秒就會執行一次,它的一項任務就是檢查 save 選項所設定的儲存條件是否滿足,如果滿足的話,就自動執行 BGSAVE 命令。

2.1.3. 啟動自動載入

和使用 SAVE 和 BGSAVE 命令建立 RDB 檔案不同,Redis 沒有專門提供用於載入 RDB 檔案的命令,RDB 檔案的載入過程是在 Redis 伺服器啟動時自動完成的。啟動時只要在指定目錄檢測到 RDB 檔案的存在,Redis 就會通過 rdbLoad 函式自動載入 RDB 檔案。

下面是 Redis 伺服器啟動時列印的日誌,倒數第 2 條日誌是在成功載入 RDB 檔案後列印的。

$ redis-server /usr/local/etc/redis.conf
6266:M 15 Sep 2019 08:30:41.832 # Server initialized
6266:M 15 Sep 2019 08:30:41.833 * DB loaded from disk: 0.001 seconds
6266:M 15 Sep 2019 08:30:41.833 * Ready to accept connections

複製程式碼

由於 AOF 檔案屬於增量的寫入命令備份,RDB 檔案屬於全量的資料備份,所以更新頻率比 RDB 檔案的更新頻率高。所以如果 Redis 伺服器開啟了 AOF 持久化功能,那麼伺服器會優先使用 AOF 檔案來還原資料庫狀態;只有在 AOF 的持久化功能處於關閉狀態時,伺服器才會使用使用 RDB 檔案還原資料庫狀態。

一文深度揭祕Redis的磁碟持久化機制

2.2. RDB 的檔案結構

RDB 檔案是經過壓縮的二進位制檔案,下面介紹關於 RDB 檔案內部構造的一些細節。

2.2.1. 儲存路徑

SAVE 命令和 BGSAVE 命令都只會備份當前資料庫,備份檔名預設為 dump.rdb,可通過配置檔案修改備份檔名 dbfilename xxx.rdb。可以通過以下命令檢視備份檔案目錄和 RDB 檔名稱:

$ redis-cli -h 127.0.0.1 -p 6379
127.0.0.1:6379> CONFIG GET dir
1) "dir"
2) "/usr/local/var/db/redis"
127.0.0.1:6379> CONFIG GET dbfilename
1) "dbfilename"
2) "dump.rdb"

複製程式碼

RDB 檔案的儲存路徑既可以在啟動前配置,也可以通過命令動態設定。

  • 配置項:通過 dir 配置指定目錄,dbfilename 指定檔名
  • 動態指定:Redis 啟動後也可以動態修改 RDB 儲存路徑,在磁碟損害或空間不足時非常有用,執行命令為:
CONFIG SET dir $newdir
CONFIG SET dbfilename $newFileName

複製程式碼

2.2.2. 檔案格式

RDB 檔案有固定的格式要求,它儲存的是二進位制資料,大體可以分為以下 5 部分:

  • REDIS:檔案頭儲存的是長為 5 個位元組的 REDIS 字元,用於標識當前檔案為 RDB 型別
  • db_version:一個 4 個位元組長的整數字符串,用於記錄 RDB 檔案的版本號
  • aux:記錄著 RDB 檔案中後設資料資訊,包含 8 個附加
    • redis-ver:Redis 例項的版本號
    • redis-bits:執行 Redis 例項的主機架構,64 位或 32 位
    • ctime:RDB 建立時的 Unix 時間戳
    • used_mem:儲存快照時使用的記憶體大小
    • repl-stream-db:Redis 伺服器的 db 的索引
    • repl-id:Redis 主例項的 ID(replication id)
    • repl-offset:Redis 主例項的偏稱量(replication offset)
    • aof-preamble:是否在 AOF 檔案頭部放置 RDB 快照(即開啟混合持久化)
  • databases:部分包含著零個或者任意多個資料庫,以及各個資料庫的鍵值對資料
  • EOF:是 1 個位元組的常量,用於標誌 RDB 檔案的正文內容結束
  • check_sum:一個 8 位元組長的整數,儲存著由前面四個部分計算得到的校驗和,用於檢測 RDB 檔案的完整性

一文深度揭祕Redis的磁碟持久化機制

1. database

一個 RDB 檔案的 databases 部分包含著零個或者任意多個資料庫(database),而每個非空的 database 都包含 SELECTDB、db_number 以及 key_value_pairs 三個部分:

  • SELECTDB:長度為一個位元組的常量,告訴使用者程式接下來要讀取的是一個 db_number
  • db_number:儲存著一個資料庫編號。當程式讀到 db_number 時,伺服器會立即呼叫 SELECT 命令切換到對應編號的資料庫
  • key_value_pairs:儲存了資料庫中的所有鍵值對資料,包括帶過期時間和不帶過期時間兩種型別的鍵值對
2. key_value_pairs

RDB 的 key_value_pairs 部分儲存了一個或者多個鍵值對,如果鍵值對有過期時間,過期時間會被儲存在鍵值對的前面。下面是這兩種鍵值對的內部結構:

一文深度揭祕Redis的磁碟持久化機制

  • EXPIREMENT_MS:長度為一個位元組的常量,告訴使用者程式接下來要讀取的是一個以毫秒為單位的過期時間
  • ms:一個長度為 8 個位元組的整數,記錄著鍵值對的過期時間,是一個以毫秒為單位的時間戳
  • TYPE:記錄了 value 的型別,長度為 1 個位元組。每個 TYPE 常量都代表了一種物件型別或者底層編碼, 當伺服器讀入 RDB 檔案中的鍵值對資料時, 程式會根據 TYPE 的值來決定如何讀入和解釋 value 的資料。它的值定義通常為以下常量之一:
    • REDIS_RDB_TYPE_STRING:字串
    • REDIS_RDB_TYPE_LIST:列表型別
    • REDIS_RDB_TYPE_SET:集合型別
    • REDIS_RDB_TYPE_ZSET:有序集合
    • REDIS_RDB_TYPE_HASH:雜湊型別
    • REDIS_RDB_TYPE_LIST_ZIPLIST:列表型別
    • REDIS_RDB_TYPE_SET_INT_SET:集合型別
    • REDIS_RDB_TYPE_ZSET_ZIPLIST:有序集合
    • REDIS_RDB_TYPE_HASH_ZIPLIST:雜湊型別
  • key:一個字串物件,編碼格式和 REDIS_RDB_TYPE_STRING 型別的 value 一樣
  • value:取決於 TYPE 的型別,物件型別可以是 string、list、set、zset 和 hash

為了檢視 RDB 檔案內部的結構,執行以下命令往 Redis 伺服器插入 3 條鍵值對資料:

127.0.0.1:6379> SADD fruits "apple" "banana" "orange"
(integer) 3
127.0.0.1:6379> LPUSH numbers 128 256 512
(integer) 3
127.0.0.1:6379> SET msg "hello"
OK

複製程式碼

執行 SAVE 操作,將 Redis 程式中的資料強制持久化到 dump.rdb 檔案中

127.0.0.1:6379> SAVE
OK

複製程式碼

通過 Linux 的 od 命令將二進位制檔案 dump.rdb 中的資料轉換為 ASCII 格式輸出,跟前面提到的儲存格式大致是一樣的:

$ od -c dump.rdb
0000000    R   E   D   I   S   0   0   0   9 372  \t   r   e   d   i   s
0000020    -   v   e   r 005   5   .   0   .   5 372  \n   r   e   d   i
0000040    s   -   b   i   t   s 300   @ 372 005   c   t   i   m   e 200
0000060  200 200 231   ] 372  \b   u   s   e   d   -   m   e   m 302 200
0000100   \v 020  \0 372  \f   a   o   f   -   p   r   e   a   m   b   l
0000120    e 300  \0 376  \0 373 003  \0  \0 003   m   s   g 005   h   e
0000140    l   l   o 016  \a   n   u   m   b   e   r   s 001 027 027  \0
0000160   \0  \0 022  \0  \0  \0 003  \0  \0 300  \0 002 004 300  \0 001
0000200  004 300 200  \0 377 002 006   f   r   u   i   t   s 003 006   o
0000220    r   a   n   g   e 005   a   p   p   l   e 006   b   a   n   a
0000240    n   a 377 214   ک  **   3 366   <   r   X
0000253

複製程式碼

2.3. RDB 常用的配置項

下面是 redis.conf 檔案中和 RDB 檔案相關的常用配置項(以及預設值):

  • save m n:bgsave 自動觸發的條件;如果沒有 save m n 配置,相當於自動的 RDB 持久化關閉,不過此時仍可以通過其他方式觸發。
  • stop-writes-on-bgsave-error yes:當 bgsave 出現錯誤時,Redis 是否停止執行寫命令。如果設定為 yes,則當硬碟出現問題時,可以及時發現,避免資料的大量丟失;如果設定為 no,則 Redis 忽略 bgsave 的錯誤繼續執行寫命令,當對 Redis 伺服器的系統(尤其是硬碟)使用了監控時,該選項考慮設定為 no。
  • rdbcompression yes:是否開啟 RDB 檔案壓縮。
  • rdbchecksum yes:是否開啟 RDB 檔案的校驗,在寫入檔案和讀取檔案時都起作用。關閉 checksum 在寫入檔案和啟動檔案時大約能帶來 10% 的效能提升,但是資料損壞時無法發現。
  • dbfilename dump.rdb:設定 RDB 的檔名。
  • dir ./:設定 RDB 檔案和 AOF 檔案所在目錄。

3. AOF 持久化

RDB 持久化是定期把記憶體中的資料全量寫入到檔案中,除此之外,RDB 還提供了基於 AOF(Append Only File)的持久化功能。AOF 會把 Redis 伺服器每次執行的寫命令記錄到一個日誌檔案中,當伺服器重啟時再次執行 AOF 檔案中的命令來恢復資料。

一文深度揭祕Redis的磁碟持久化機制

AOF 的主要作用是解決了資料持久化的實時性,目前已經成為了 Redis 持久化的主流方式。

3.1. AOF 的建立和載入

預設情況下 AOF 功能是關閉的,Redis 只會通過 RDB 完成資料持久化的。開啟 AOF 功能需要 redis.conf 檔案中將 appendonly 配置項修改為 yes,這樣在開啟 AOF 持久化功能的同時,將基於 RDB 的快照持久化置於低優先順序。修改 redis.conf 如下:

# 此選項為AOF功能的開關,預設為no,通過yes來開啟aof功能
appendonly yes
# 指定AOF檔名稱
appendfilename appendonly.aof
# 備份RDB和AOF檔案存放路徑
dir /usr/local/var/db/redis/

複製程式碼

3.1.1. AOF 的建立

重啟 Redis 伺服器程式以後,dir 目錄下會生成一個 appendonly.aof 檔案,由於此時伺服器未執行任何寫指令,因此 AOF 檔案是空的。執行以下命令寫入幾條測試資料:

127.0.0.1:6379> SADD fruits "apple" "banana" "orange"
(integer) 3
127.0.0.1:6379> LPUSH numbers 128 256 512
(integer) 3
127.0.0.1:6379> SET msg "hello"
OK

複製程式碼

AOF 檔案是純文字格式的,上述寫命令按順序被寫入了 appendonly.aof 檔案(省掉換行符 '\r\n'):

/usr/local/var/db/redis$ cat appendonly.aof
*2 $6 SELECT $1 0
*5 $4 SADD $6 fruits $5 apple $6 banana $6 orange
*5 $5 LPUSH $7 numbers $3 128 $3 256 $3 512
*3 $3 SET $3 msg $5 hello

複製程式碼

RDB 持久化的方式是將 apple、banana、orange 的鍵值對資料儲存為 RDB 的二進位制檔案,而 AOF 是通過把 Redis 伺服器執行的 SADD、LPUSH、SET 等命令儲存到 AOF 的文字檔案中。下圖是 AOF 檔案內部的構造圖:

一文深度揭祕Redis的磁碟持久化機制

3.1.2. AOF 的載入

再次重啟 Redis 伺服器程式,觀察啟動日誌會發現 Redis 會通過 AOF 檔案載入資料:

52580:M 15 Sep 2019 16:09:47.015 # Server initialized
52580:M 15 Sep 2019 16:09:47.015 * DB loaded from append only file: 0.001 seconds
52580:M 15 Sep 2019 16:09:47.015 * Ready to accept connections

複製程式碼

通過命令讀取 AOF 檔案還原的鍵值對資料:

127.0.0.1:6379> SMEMBERS fruits
1) "apple"
2) "orange"
3) "banana"
127.0.0.1:6379> LRANGE numbers 0 -1
1) "512"
2) "256"
3) "128"
127.0.0.1:6379> GET msg
"hello"

複製程式碼

3.2. AOF 的執行流程

AOF 不需要設定任何觸發條件,對 Redis 伺服器的所有寫命令都會自動記錄到 AOF 檔案中,下面介紹 AOF 持久化的執行流程。

一文深度揭祕Redis的磁碟持久化機制

AOF 檔案的寫入流程可以分為以下 3 個步驟:

  1. 命令追加(append):將 Redis 執行的寫命令追加到 AOF 的緩衝區 aof_buf
  2. 檔案寫入(write)和檔案同步(fsync):AOF 根據對應的策略將 aof_buf 的資料同步到硬碟
  3. 檔案重寫(rewrite):定期對 AOF 進行重寫,從而實現對寫命令的壓縮。

3.2.1. 命令追加

Redis 使用單執行緒處理客戶端命令,為了避免每次有寫命令就直接寫入磁碟,導致磁碟 IO 成為 Redis 的效能瓶頸,Redis 會先把執行的寫命令追加(append)到一個 aof_buf 緩衝區,而不是直接寫入檔案。

命令追加的格式是 Redis 命令請求的協議格式,它是一種純文字格式,具有相容性好、可讀性強、容易處理、操作簡單避免二次開銷等優點。在 AOF 檔案中,除了用於指定資料庫的 select 命令(比如:select 0 為選中 0 號資料庫)是由 Redis 新增的,其他都是客戶端傳送來的寫命令。

3.2.2. 檔案寫入和檔案同步

Redis 提供了多種 AOF 快取區的檔案同步策略,相關策略涉及到作業系統的 write() 函式和 fsync() 函式,說明如下:

1. write()

為了提高檔案的寫入效率,當使用者呼叫 write 函式將資料寫入檔案時,作業系統會先把資料寫入到一個記憶體緩衝區裡,當緩衝區被填滿或超過了指定時限後,才真正將緩衝區的資料寫入到磁碟裡。

2. fsync()

雖然作業系統底層對 write() 函式進行了優化 ,但也帶來了安全問題。如果當機記憶體緩衝區中的資料會丟失,因此係統同時提供了同步函式 fsync() ,強制作業系統立刻將緩衝區中的資料寫入到磁碟中,從而保證了資料持久化。

Redis 提供了 appendfsync 配置項來控制 AOF 快取區的檔案同步策略,appendfsync 可配置以下三種策略:

  • appendfsync always:每執行一次命令儲存一次

命令寫入 aof_buf 緩衝區後立即呼叫系統 fsync 函式同步到 AOF 檔案,fsync 操作完成後執行緒返回,整個過程是阻塞的。這種情況下,每次有寫命令都要同步到 AOF 檔案,硬碟 IO 成為效能瓶頸,Redis 只能支援大約幾百 TPS 寫入,嚴重降低了 Redis 的效能。

  • appendfsync no:不儲存

命令寫入 aof_buf 緩衝區後呼叫系統 write 操作,不對 AOF 檔案做 fsync 同步;同步由作業系統負責,通常同步週期為 30 秒。這種情況下,檔案同步的時間不可控,且緩衝區中堆積的資料會很多,資料安全性無法保證。

  • appendfsync everysec:每秒鐘儲存一次

命令寫入 aof_buf 緩衝區後呼叫系統 write 操作,write 完成後執行緒立刻返回,fsync 同步檔案操作由單獨的程式每秒呼叫一次。everysec 是前述兩種策略的折中,是效能和資料安全性的平衡,因此也是 Redis 的預設配置,也是比較推崇的配置選項。

檔案同步策略 write 阻塞 fsync 阻塞 當機時的資料丟失量
always 阻塞 阻塞 最多隻丟失一個命令的資料
no 阻塞 不阻塞 作業系統最後一次對 AOF 檔案 fsync 後的資料
everysec 阻塞 不阻塞 一般不超過 1 秒鐘的資料

3.2.3. 檔案重寫

隨著命令不斷寫入 AOF,檔案會越來越大,導致檔案佔用空間變大,資料恢復時間變長。為了解決這個問題,Redis 引入了重寫機制來對 AOF 檔案中的寫命令進行合併,進一步壓縮檔案體積。

AOF 檔案重寫指的是把 Redis 程式內的資料轉化為寫命令,同步到新的 AOF 檔案中,然後使用新的 AOF 檔案覆蓋舊的 AOF 檔案,這個過程不對舊的 AOF 檔案的進行任何讀寫操作。

1. 觸發機制

AOF 重寫過程提供了手動觸發和自動觸發兩種機制:

  • 手動觸發:直接呼叫 bgrewriteaof 命令,該命令的執行與 bgsave 有些類似,都是 fork 子程式進行具體的工作,且都只有在 fork 時會阻塞
  • 自動觸發:根據 auto-aof-rewrite-min-size 和 auto-aof-rewrite-percentage 配置項,以及 aof_current_size 和 aof_base_size 的狀態確定觸發時機
    • auto-aof-rewrite-min-size:執行 AOF 重寫時,檔案的最小體積,預設值為 64MB
    • auto-aof-rewrite-percentage:執行 AOF 重寫時,當前 AOF 大小(aof_current_size)和上一次重寫時 AOF 大小(aof_base_size)的比值
2. 重寫流程

下面以手動觸發 AOF 重寫為例,當 bgrewriteaof 命令被執行時,AOF 檔案重寫的流程如下:

一文深度揭祕Redis的磁碟持久化機制

  1. 客戶端通過 bgrewriteaof 命令對 Redis 主程式發起 AOF 重寫請求
  2. 當前不存在正在執行 bgsave/bgrewriteaof 的子程式時,Redis 主程式通過 fork 操作建立子程式,這個過程主程式是阻塞的。如果發現 bgrewriteaof 子程式直接返回;如果發現 bgsave 子程式則等 bgsave 執行完成後再執行 fork 操作
  3. 主程式的 fork 操作完成後,繼續處理其他命令,把新的寫命令同時追加到 aof_buf 和 aof_rewrite_buf 緩衝區中
    • 在檔案重寫完成之前,主程式會繼續把寫命令追加到 aof_buf 緩衝區,根據 appendfsync 策略同步到舊的 AOF 檔案,這樣可以避免 AOF 重寫失敗造成資料丟失,保證原有的 AOF 檔案的正確性
    • 由於 fork 操作運用寫時複製技術,子程式只能共享 fork 操作時的記憶體資料,主程式會把新命令追加到一個 aof_rewrite_buf 緩衝區中,避免 AOF 重寫時丟失這部分資料
  4. 子程式讀取 Redis 程式中的資料快照,生成寫入命令並按照命令合併規則批量寫入到新的 AOF 檔案
  5. 子程式寫完新的 AOF 檔案後,向主程式發訊號,主程式更新統計資訊,具體可以通過 info persistence 檢視
  6. 主程式接受到子程式的訊號以後,將 aof_rewrite_buf 緩衝區中的寫命令追加到新的 AOF 檔案
  7. 主程式使用新的 AOF 檔案替換舊的 AOF 檔案,AOF 重寫過程完成
3. 壓縮機制

檔案重寫之所以能夠壓縮 AOF 檔案的大小,原因在於以下幾方面:

  • 過期的資料不再寫入 AOF 檔案
  • 無效的命令不再寫入 AOF 檔案。比如:重複為資料設值(set mykey v1, set mykey v2)、刪除鍵值對資料(sadd myset v1, del myset)等等
  • 多條命令可以合併為單個。比如:sadd myset v1, sadd myset v2, sadd myset v3 可以合併為 sadd myset v1 v2 v3。不過為了防止單條命令過大造成客戶端緩衝區溢位,對於 list、set、hash、zset 型別的 key,並不一定只使用單條命令,而是以某個 Redis 定義的一個常量為界,將命令拆分為多條

3.3. AOF 常用的配置項

下面是 redis.conf 檔案中和 AOF 檔案相關的常用配置項(以及預設值):

  • appendonly no:是否開啟 AOF 持久化功能
  • appendfilename "appendonly.aof":AOF 檔案的名稱
  • dir ./:RDB 檔案和 AOF 檔案所在目錄
  • appendfsync everysec:fsync 持久化策略
  • no-appendfsync-on-rewrite no:重寫 AOF 檔案期間是否禁止 fsync 操作。如果開啟該選項,可以減輕檔案重寫時 CPU 和磁碟的負載(尤其是磁碟),但是可能會丟失 AOF 重寫期間的資料,需要在負載和安全性之間進行平衡
  • auto-aof-rewrite-percentage 100:AOF 檔案重寫觸發條件之一
  • auto-aof-rewrite-min-size 64mb:AOF 檔案重寫觸發條件之一
  • aof-load-truncated yes:如果 AOF 檔案結尾損壞,Redis 伺服器在啟動時是否仍載入 AOF 檔案

4. 資料恢復機制

前面提到當 AOF 持久化功能開啟時,Redis 伺服器啟動時優先執行 AOF 檔案的命令恢復資料,只有當 AOF 功能關閉時,才會優先載入 RDB 快照的檔案資料。

  • 當 AOF 功能關閉,且 RDB 持久化開啟時,Redis 伺服器啟動日誌:
6266:M 15 Sep 2019 08:30:41.832 # Server initialized
6266:M 15 Sep 2019 08:30:41.833 * DB loaded from disk: 0.001 seconds
6266:M 15 Sep 2019 08:30:41.833 * Ready to accept connections

複製程式碼
  • 當 AOF 功能開啟,且 AOF 檔案存在時,Redis 伺服器啟動日誌:
9447:M 15 Sep 2019 23:01:46.601 # Server initialized
9447:M 15 Sep 2019 23:01:46.602 * DB loaded from append only file: 0.001 seconds
9447:M 15 Sep 2019 23:01:46.602 * Ready to accept connections

複製程式碼
  • 當 AOF 功能開啟,且 AOF 檔案不存在時,即使 RDB 檔案存在也不會載入,Redis 伺服器啟動日誌:
9326:M 15 Sep 2019 22:49:24.203 # Server initialized
9326:M 15 Sep 2019 22:49:24.203 * Ready to accept connections

複製程式碼

5. RDB 和 AOF 對比

持久化機制 RDB AOF
啟動優先順序
磁碟檔案體積
資料還原速度
資料安全性 容易丟失資料 根據策略決定
操作輕重級別

5.1. RDB 的優缺點

5.1.1. 優點

  • RDB 是一個壓縮過的非常緊湊的檔案,儲存著某個時間點的資料集,適合做資料的備份、災難恢復
  • 可以最大化 Redis 的效能,在儲存 RDB 檔案,伺服器程式只需 fork 一個子程式來完成 RDB 檔案的建立,父程式不需要做 IO 操作
  • 與 AOF 持久化方式相比,恢復大資料集的時候會更快

5.1.2. 缺點

  • RDB 的資料安全性是不如 AOF 的,儲存整個資料集是個重量級的過程,根據配置可能要幾分鐘才進行一次持久化,如果伺服器當機,那麼就可能丟失幾分鐘的資料
  • Redis 資料集較大時,fork 的子程式要完成快照會比較耗費 CPU 和時間

5.2. AOF 的優缺點

5.2.1. 優點

  • 資料更完整,安全性更高,秒級資料丟失(取決於 fsync 策略,如果是 everysec,最多丟失 1 秒的資料)
  • AOF 檔案是一個只進行追加的命令檔案,且寫入操作是以 Redis 協議的格式儲存的,內容是可讀的,適合誤刪緊急恢復

5.2.2. 缺點

  • 對於相同的資料集,AOF 檔案的體積要遠遠大於 RDB 檔案,資料恢復也會比較慢
  • 根據所使用的 fsync 策略,AOF 的速度可能會慢於 RDB。不過在一般情況下, 每秒 fsync 的效能依然非常高

6. RDB-AOF 混合持久化

在重啟 Redis 伺服器時,一般很少使用 RDB 快照檔案來恢復記憶體狀態,因為會丟失大量資料。更多的是使用 AOF 檔案進行命令重放,但是執行 AOF 命令效能相對 RDB 來說要慢很多。這樣在 Redis 資料很大的情況下,啟動需要消耗大量的時間。

鑑於 RDB 快照可能會造成資料丟失,AOF 指令恢復資料慢,Redis 4.0 版本提供了一套基於 AOF-RDB 的混合持久化機制,保留了兩種持久化機制的優點。這樣重寫的 AOF 檔案由兩部份組成,一部分是 RDB 格式的頭部資料,另一部分是 AOF 格式的尾部指令。

Redis 4.0 版本的混合持久化功能預設是關閉的,通過配置 aof-use-rdb-preamble 為 yes 開啟此功能:

# 開啟AOF-RDB混合持久化機制
aof-use-rdb-preamble yes

複製程式碼

檢視 Redis 伺服器是否開啟混合持久化功能:

127.0.0.1:6379> CONFIG GET aof-use-rdb-preamble
1) "aof-use-rdb-preamble"
2) "yes"

複製程式碼

如圖所示,將 RDB 資料檔案的內容和增量的 AOF 命令檔案存在一起。這裡的 AOF 命令不再是全量的命令,而是自持久化開始到持久化結束的這段時間伺服器程式執行的增量 AOF 命令,通常這部分 AOF 命令很小。

一文深度揭祕Redis的磁碟持久化機制

在 Redis 伺服器重啟的時候,可以預先載入 AOF 檔案頭部全量的 RDB 資料,然後再重放 AOF 檔案尾部增量的 AOF 命令,從而大大減少了重啟過程中資料還原的時間。

7. 持久化策略選擇

7.1. RDB 和 AOF 效能開銷

在介紹持久化策略之前,首先要明白無論是 RDB 還是 AOF 方式,開啟持久化都是會造成效能開銷的。

  • RDB 持久化:
    • BGSAVE 命令在進行 fork 操作時,Redis 伺服器主程式會發生阻塞
    • Redis 子程式向磁碟寫入資料也會帶來 IO 壓力
  • AOF 持久化:
    • 向磁碟寫入資料的頻率大大提高,IO 壓力更大,甚至可能造成 AOF 追加阻塞問題
    • AOF 檔案重寫與 RDB 的 BGSAVE 過程類似,存在父程式 fork 時的阻塞和子程式的 IO 壓力問題

相對來說,由於 AOF 向磁碟中寫入資料的頻率更高,因此對 Redis 伺服器主程式效能的影響會更大。

7.2. 持久化策略

在實際生產環境中,根據資料量、應用對資料的安全要求、預算限制等不同情況,會有各種各樣的持久化策略。

  1. 完全不使用任何持久化功能
  2. 使用 RDB 或 AOF 其中一種
  3. 同時開啟 RDB 和 AOF 持久化

對於分散式環境,持久化的選擇必須與 Redis 的主從策略一起考慮,因為主從複製與持久化同樣具有資料備份的功能,而且主節點(Master Node)和從節點(Slave Node)可以獨立選擇持久化方案。

下面分場景來討論持久化策略的選擇,下面的討論也只是作為參考,實際方案可能更復雜更具多樣性。

7.2.1. 資料庫快取

如果 Redis 中的資料完全丟棄也沒有關係(如 Redis 完全用作 DB 層資料的快取),那麼無論是單機,還是主從架構,都可以不進行任何持久化。

7.2.2. 單機環境

在單機環境下,如果可以接受十幾分鍾或更多的資料丟失,RDB 方案對 Redis 的效能更加有利;如果只能接受秒級別的資料丟失,選擇 AOF 方案更合適。

7.2.3. 主從部署

在多數情況下,Redis 都會配置主從部署機制。從節點(slave)既可以實現資料的熱備,也可以進行讀寫分擔 Redis 讀請求,以及在主節點(master)當機後的頂替作用。

在這種情況下,一種可行的做法如下:

  • master:完全關閉持久化(包括 RDB 和 AOF 功能),這樣可以讓主節點的效能達到最好
  • slave:關閉 RDB 功能,開啟 AOF 功能(如果對資料安全要求不高,開啟 RDB 關閉 AOF 也可以)。定時對持久化檔案進行備份(如備份到其他資料夾,並標記好備份的時間)。然後關閉 AOF 的自動重寫功能,然後新增定時任務,在每天 Redis 伺服器閒時(如凌晨 12 點)呼叫 bgrewriteaof 手動重寫。

為什麼開啟了主從複製,可以實現資料的熱備份,還需要設定持久化呢?因為在一些特殊情況下,主從複製仍然不足以保證資料的安全,例如:

  • master 和 slave 同時停止:如果 master 節點和 slave 節點位於同一個機房,則一次停電事故就可能導致 master 和 slave 機器同時關機,Redis 伺服器程式停止。如果沒有持久化,則面臨的是資料的完全丟失。
  • master 重啟:如果 master 節點因為故障當機,並且系統中有自動拉起機制(即檢測到服務停止後重啟該服務)將 master 節點自動重啟。
    • 由於沒有持久化檔案,那麼 master 重啟後資料是空的,slave 同步資料也變成了空的
    • 如果 master 和 slave 節點都沒有開啟持久化,同樣會引發資料的完全丟失

7.2.4. 異地備災

前面的幾種持久化策略,針對的都是一般的系統故障,如程式異常退出、當機、斷電等,這些故障不會損壞硬碟。但是對於一些可能導致硬碟損壞的災難情況,如火災地震,就需要進行異地災備。

  • 單機環境:可以定時將 RDB 檔案或重寫後的 AOF 檔案,通過 scp 命令拷貝到遠端機器,如阿里雲、AWS 等
  • 主從部署,可以定時在 master 節點上執行 BGSAVE 操作,然後將 RDB 檔案拷貝到遠端機器,或者在 slave 節點上執行 bgrewriteaof 命令重寫 AOF 檔案後,將 AOF 檔案拷貝到遠端機器上。

由於 RDB 檔案檔案小、恢復速度快,災難恢復一般採用 RDB 方式;異地備份的頻率根據資料安全性的需要及其它條件來確定,但最好不要低於一天一次。

小結

本文主要開篇介紹了 Redis 伺服器的資料庫結構,進一步介紹了 Redis 提供的幾種持久化機制,包括基於資料快照的 RDB 全量持久化、基於命令追加的 AOF 增量持久化以及 Redis 4.0 支援的混合持久化。對於 RDB 的持久化方式,給出了 RDB 快照的建立和還原過程,RDB 的檔案結構以及相關配置項。對於 AOF 的持久化方式,給出了 AOF 日誌的建立和還原過程,AOF 的執行流程,AOF 檔案內部的格式以及相關配置項。在文章結尾分析了 RDB 和 AOF 方式各自的優缺點,效能開銷,以及在單機環境、主從部署、異地備災場景下的持久化策略。

關於公眾號

本帳號持續分享後端技術乾貨,包括虛擬機器基礎,多執行緒程式設計,高效能框架,非同步、快取和訊息中介軟體,分散式和微服務,架構學習和進階等學習資料和文章。

一文深度揭祕Redis的磁碟持久化機制

相關文章