Redis 分別提供了 RDB 和 AOF 兩種持久化機制:
server [label = "命令請求"]; server -> aof [ label = "網(wǎng)絡協(xié)議格式的\n命令內容"];}" />
本章首先介紹 AOF 功能的運作機制,了解命令是如何被保存到 AOF 文件里的,觀察不同的 AOF 保存模式對數(shù)據(jù)的安全性、以及 Redis 性能的影響。
之后會介紹從 AOF 文件中恢復數(shù)據(jù)庫狀態(tài)的方法,以及該方法背后的實現(xiàn)機制。
最后還會介紹對 AOF 進行重寫以調整文件體積的方法,并研究這種方法是如何在不改變數(shù)據(jù)庫狀態(tài)的前提下進行的。
因為本章涉及 AOF 運行的相關機制,如果還沒了解過 AOF 功能的話,請先閱讀 Redis 持久化手冊中關于 AOF 的部分 。
Redis 將所有對數(shù)據(jù)庫進行過寫入的命令(及其參數(shù))記錄到 AOF 文件,以此達到記錄數(shù)據(jù)庫狀態(tài)的目的,為了方便起見,我們稱呼這種記錄過程為同步。
舉個例子,如果執(zhí)行以下命令:
redis> RPUSH list 1 2 3 4
(integer) 4
redis> LRANGE list 0 -1
1) "1"
2) "2"
3) "3"
4) "4"
redis> KEYS *
1) "list"
redis> RPOP list
"4"
redis> LPOP list
"1"
redis> LPUSH list 1
(integer) 3
redis> LRANGE list 0 -1
1) "1"
2) "2"
3) "3"
那么其中四條對數(shù)據(jù)庫有修改的寫入命令就會被同步到 AOF 文件中:
RPUSH list 1 2 3 4
RPOP list
LPOP list
LPUSH list 1
為了處理的方便,AOF 文件使用網(wǎng)絡通訊協(xié)議的格式來保存這些命令。
比如說,上面列舉的四個命令在 AOF 文件中就實際保存如下:
*2
$6
SELECT
$1
0
*6
$5
RPUSH
$4
list
$1
1
$1
2
$1
3
$1
4
*2
$4
RPOP
$4
list
*2
$4
LPOP
$4
list
*3
$5
LPUSH
$4
list
$1
1
除了 SELECT 命令是 AOF 程序自己加上去的之外,其他命令都是之前我們在終端里執(zhí)行的命令。
同步命令到 AOF 文件的整個過程可以分為三個階段:
fsync
函數(shù)或者 fdatasync
函數(shù)會被調用,將寫入的內容真正地保存到磁盤中。以下幾個小節(jié)將詳細地介紹這三個步驟。
當一個 Redis 客戶端需要執(zhí)行命令時,它通過網(wǎng)絡連接,將協(xié)議文本發(fā)送給 Redis 服務器。
比如說,要執(zhí)行命令 SET KEY VALUE
,客戶端將向服務器發(fā)送文本 "*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n"
。
服務器在接到客戶端的請求之后,它會根據(jù)協(xié)議文本的內容,選擇適當?shù)拿詈瘮?shù),并將各個參數(shù)從字符串文本轉換為 Redis 字符串對象(StringObject
)。
比如說,針對上面的 SET 命令例子,Redis 將客戶端的命令指針指向實現(xiàn) SET 命令的 setCommand
函數(shù),并創(chuàng)建三個 Redis 字符串對象,分別保存 SET
、 KEY
和 VALUE
三個參數(shù)(命令也算作參數(shù))。
每當命令函數(shù)成功執(zhí)行之后,命令參數(shù)都會被傳播到 AOF 程序,以及 REPLICATION 程序(本節(jié)不討論這個,列在這里只是為了完整性的考慮)。
這個執(zhí)行并傳播命令的過程可以用以下偽代碼表示:
if (execRedisCommand(cmd, argv, argc) == EXEC_SUCCESS):
if aof_is_turn_on():
# 傳播命令到 AOF 程序
propagate_aof(cmd, argv, argc)
if replication_is_turn_on():
# 傳播命令到 REPLICATION 程序
propagate_replication(cmd, argv, argc)
以下是該過程的流程圖:
aof_choice; aof_choice -> propagate_aof [label = "是"]; propagate_aof -> replication_choice; aof_choice -> replication_choice [label = "否"]; replication_choice -> remaind_jobs [label = "否"]; replication_choice -> propagate_replication [label = "是"]; propagate_replication -> remaind_jobs;}" />
當命令被傳播到 AOF 程序之后,程序會根據(jù)命令以及命令的參數(shù),將命令從字符串對象轉換回原來的協(xié)議文本。
比如說,如果 AOF 程序接受到的三個參數(shù)分別保存著 SET
、 KEY
和 VALUE
三個字符串,那么它將生成協(xié)議文本 "*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n"
。
協(xié)議文本生成之后,它會被追加到 redis.h/redisServer
結構的 aof_buf
末尾。
redisServer
結構維持著 Redis 服務器的狀態(tài),aof_buf
域則保存著所有等待寫入到 AOF 文件的協(xié)議文本:
struct redisServer {
// 其他域...
sds aof_buf;
// 其他域...
};
至此,追加命令到緩存的步驟執(zhí)行完畢。
綜合起來,整個緩存追加過程可以分為以下三步:
aof_buf
末尾。每當服務器常規(guī)任務函數(shù)被執(zhí)行、或者事件處理器被執(zhí)行時,aof.c/flushAppendOnlyFile
函數(shù)都會被調用,這個函數(shù)執(zhí)行以下兩個工作:
WRITE:根據(jù)條件,將 aof_buf
中的緩存寫入到 AOF 文件。
SAVE:根據(jù)條件,調用 fsync
或 fdatasync
函數(shù),將 AOF 文件保存到磁盤中。
兩個步驟都需要根據(jù)一定的條件來執(zhí)行,而這些條件由 AOF 所使用的保存模式來決定,以下小節(jié)就來介紹 AOF 所使用的三種保存模式,以及在這些模式下,步驟 WRITE 和 SAVE 的調用條件。
Redis 目前支持三種 AOF 保存模式,它們分別是:
AOF_FSYNC_NO
:不保存。AOF_FSYNC_EVERYSEC
:每一秒鐘保存一次。AOF_FSYNC_ALWAYS
:每執(zhí)行一個命令保存一次。以下三個小節(jié)將分別討論這三種保存模式。
在這種模式下,每次調用 flushAppendOnlyFile
函數(shù),WRITE 都會被執(zhí)行,但 SAVE 會被略過。
在這種模式下, SAVE 只會在以下任意一種情況中被執(zhí)行:
這三種情況下的 SAVE 操作都會引起 Redis 主進程阻塞。
在這種模式中,SAVE 原則上每隔一秒鐘就會執(zhí)行一次,因為 SAVE 操作是由后臺子線程調用的,所以它不會引起服務器主進程阻塞。
注意,在上一句的說明里面使用了詞語“原則上”,在實際運行中,程序在這種模式下對 fsync
或 fdatasync
的調用并不是每秒一次,它和調用 flushAppendOnlyFile
函數(shù)時 Redis 所處的狀態(tài)有關。
每當 flushAppendOnlyFile
函數(shù)被調用時,可能會出現(xiàn)以下四種情況:
子線程正在執(zhí)行 SAVE ,并且:
- 這個 SAVE 的執(zhí)行時間未超過 2 秒,那么程序直接返回,并不執(zhí)行 WRITE 或新的 SAVE 。
- 這個 SAVE 已經(jīng)執(zhí)行超過 2 秒,那么程序執(zhí)行 WRITE ,但不執(zhí)行新的 SAVE 。注意,因為這時 WRITE 的寫入必須等待子線程先完成(舊的) SAVE ,因此這里 WRITE 會比平時阻塞更長時間。
子線程沒有在執(zhí)行 SAVE ,并且:
- 上次成功執(zhí)行 SAVE 距今不超過 1 秒,那么程序執(zhí)行 WRITE ,但不執(zhí)行 SAVE 。
- 上次成功執(zhí)行 SAVE 距今已經(jīng)超過 1 秒,那么程序執(zhí)行 WRITE 和 SAVE 。
可以用流程圖表示這四種情況:
over_2_second_choice [label = "是"]; over_2_second_choice -> not_over_2_second [label = "否"]; over_2_second_choice -> over_2_second [label = "是"]; finish_over_2_second [label = "距離上次 SAVE\n 執(zhí)行成功\n超過 1 秒?", shape = diamond, fillcolor = "#95BBE3"]; no [label = "情況 3 :\n 執(zhí)行 WRITE \n 但不執(zhí)行新的 SAVE "]; yes [label = "情況 4 :\n 執(zhí)行 WRITE 和\n新的 SAVE\n"]; SAVE_running_choice -> finish_over_2_second [label = "否"]; finish_over_2_second -> yes [label = "是"]; finish_over_2_second -> no [label = "否"];}" />
根據(jù)以上說明可以知道,在“每一秒鐘保存一次”模式下,如果在情況 1 中發(fā)生故障停機,那么用戶最多損失小于 2 秒內所產(chǎn)生的所有數(shù)據(jù)。
如果在情況 2 中發(fā)生故障停機,那么用戶損失的數(shù)據(jù)是可以超過 2 秒的。
Redis 官網(wǎng)上所說的,AOF 在“每一秒鐘保存一次”時發(fā)生故障,只丟失 1 秒鐘數(shù)據(jù)的說法,實際上并不準確。
在這種模式下,每次執(zhí)行完一個命令之后, WRITE 和 SAVE 都會被執(zhí)行。
另外,因為 SAVE 是由 Redis 主進程執(zhí)行的,所以在 SAVE 執(zhí)行期間,主進程會被阻塞,不能接受命令請求。
在上一個小節(jié),我們簡短地描述了三種 AOF 保存模式的工作方式,現(xiàn)在,是時候研究一下這三個模式在安全性和性能方面的區(qū)別了。
對于三種 AOF 保存模式,它們對服務器主進程的阻塞情況如下:
AOF_FSYNC_NO
):寫入和保存都由主進程執(zhí)行,兩個操作都會阻塞主進程。AOF_FSYNC_EVERYSEC
):寫入操作由主進程執(zhí)行,阻塞主進程。保存操作由子線程執(zhí)行,不直接阻塞主進程,但保存操作完成的快慢會影響寫入操作的阻塞時長。AOF_FSYNC_ALWAYS
):和模式 1 一樣。因為阻塞操作會讓 Redis 主進程無法持續(xù)處理請求,所以一般說來,阻塞操作執(zhí)行得越少、完成得越快,Redis 的性能就越好。
模式 1 的保存操作只會在AOF 關閉或 Redis 關閉時執(zhí)行,或者由操作系統(tǒng)觸發(fā),在一般情況下,這種模式只需要為寫入阻塞,因此它的寫入性能要比后面兩種模式要高,當然,這種性能的提高是以降低安全性為代價的:在這種模式下,如果運行的中途發(fā)生停機,那么丟失數(shù)據(jù)的數(shù)量由操作系統(tǒng)的緩存沖洗策略決定。
模式 2 在性能方面要優(yōu)于模式 3 ,并且在通常情況下,這種模式最多丟失不多于 2 秒的數(shù)據(jù),所以它的安全性要高于模式 1 ,這是一種兼顧性能和安全性的保存方案。
模式 3 的安全性是最高的,但性能也是最差的,因為服務器必須阻塞直到命令信息被寫入并保存到磁盤之后,才能繼續(xù)處理請求。
綜合起來,三種 AOF 模式的操作特性可以總結如下:
模式 | WRITE 是否阻塞? | SAVE 是否阻塞? | 停機時丟失的數(shù)據(jù)量 |
---|---|---|---|
AOF_FSYNC_NO |
阻塞 | 阻塞 | 操作系統(tǒng)最后一次對 AOF 文件觸發(fā) SAVE 操作之后的數(shù)據(jù)。 |
AOF_FSYNC_EVERYSEC |
阻塞 | 不阻塞 | 一般情況下不超過 2 秒鐘的數(shù)據(jù)。 |
AOF_FSYNC_ALWAYS |
阻塞 | 阻塞 | 最多只丟失一個命令的數(shù)據(jù)。 |
AOF 文件保存了 Redis 的數(shù)據(jù)庫狀態(tài),而文件里面包含的都是符合 Redis 通訊協(xié)議格式的命令文本。
這也就是說,只要根據(jù) AOF 文件里的協(xié)議,重新執(zhí)行一遍里面指示的所有命令,就可以還原 Redis 的數(shù)據(jù)庫狀態(tài)了。
Redis 讀取 AOF 文件并還原數(shù)據(jù)庫的詳細步驟如下:
完成第 4 步之后,AOF 文件所保存的數(shù)據(jù)庫就會被完整地還原出來。
注意,因為 Redis 的命令只能在客戶端的上下文中被執(zhí)行,而 AOF 還原時所使用的命令來自于 AOF 文件,而不是網(wǎng)絡,所以程序使用了一個沒有網(wǎng)絡連接的偽客戶端來執(zhí)行命令。偽客戶端執(zhí)行命令的效果,和帶網(wǎng)絡連接的客戶端執(zhí)行命令的效果,完全一樣。
整個讀取和還原過程可以用以下偽代碼表示:
def READ_AND_LOAD_AOF():
# 打開并讀取 AOF 文件
file = open(aof_file_name)
while file.is_not_reach_eof():
# 讀入一條協(xié)議文本格式的 Redis 命令
cmd_in_text = file.read_next_command_in_protocol_format()
# 根據(jù)文本命令,查找命令函數(shù),并創(chuàng)建參數(shù)和參數(shù)個數(shù)等對象
cmd, argv, argc = text_to_command(cmd_in_text)
# 執(zhí)行命令
execRedisCommand(cmd, argv, argc)
# 關閉文件
file.close()
作為例子,以下是一個簡短的 AOF 文件的內容:
*2
$6
SELECT
$1
0
*3
$3
SET
$3
key
$5
value
*8
$5
RPUSH
$4
list
$1
1
$1
2
$1
3
$1
4
$1
5
$1
6
當程序讀入這個 AOF 文件時,它首先執(zhí)行 SELECT 0
命令 ——這個 SELECT
命令是由 AOF 寫入程序自動生成的,它確保程序可以將數(shù)據(jù)還原到正確的數(shù)據(jù)庫上。
然后執(zhí)行后面的 SET key value
和 RPUSH 1 2 3 4
命令,還原 key
和 list
兩個鍵的數(shù)據(jù)。
Note
為了避免對數(shù)據(jù)的完整性產(chǎn)生影響,在服務器載入數(shù)據(jù)的過程中,只有和數(shù)據(jù)庫無關的訂閱與發(fā)布功能可以正常使用,其他命令一律返回錯誤。
AOF 文件通過同步 Redis 服務器所執(zhí)行的命令,從而實現(xiàn)了數(shù)據(jù)庫狀態(tài)的記錄,但是,這種同步方式會造成一個問題:隨著運行時間的流逝,AOF 文件會變得越來越大。
舉個例子,如果服務器執(zhí)行了以下命令:
RPUSH list 1 2 3 4 // [1, 2, 3, 4]
RPOP list // [1, 2, 3]
LPOP list // [2, 3]
LPUSH list 1 // [1, 2, 3]
那么光是記錄 list
鍵的狀態(tài),AOF 文件就需要保存四條命令。
另一方面,有些被頻繁操作的鍵,對它們所調用的命令可能有成百上千、甚至上萬條,如果這樣被頻繁操作的鍵有很多的話,AOF 文件的體積就會急速膨脹,對 Redis 、甚至整個系統(tǒng)的造成影響。
為了解決以上的問題,Redis 需要對 AOF 文件進行重寫(rewrite):創(chuàng)建一個新的 AOF 文件來代替原有的 AOF 文件,新 AOF 文件和原有 AOF 文件保存的數(shù)據(jù)庫狀態(tài)完全一樣,但新 AOF 文件的體積小于等于原有 AOF 文件的體積。
以下就來介紹 AOF 重寫的實現(xiàn)方式。
所謂的“重寫”其實是一個有歧義的詞語,實際上,AOF 重寫并不需要對原有的 AOF 文件進行任何寫入和讀取,它針對的是數(shù)據(jù)庫中鍵的當前值。
考慮這樣一個情況,如果服務器對鍵 list
執(zhí)行了以下四條命令:
RPUSH list 1 2 3 4 // [1, 2, 3, 4]
RPOP list // [1, 2, 3]
LPOP list // [2, 3]
LPUSH list 1 // [1, 2, 3]
那么當前列表鍵 list
在數(shù)據(jù)庫中的值就為 [1, 2, 3]
。
如果我們要保存這個列表的當前狀態(tài),并且盡量減少所使用的命令數(shù),那么最簡單的方式不是去 AOF 文件上分析前面執(zhí)行的四條命令,而是直接讀取 list
鍵在數(shù)據(jù)庫的當前值,然后用一條 RPUSH 1 2 3
命令來代替前面的四條命令。
再考慮這樣一個例子,如果服務器對集合鍵 animal
執(zhí)行了以下命令:
SADD animal cat // {cat}
SADD animal dog panda tiger // {cat, dog, panda, tiger}
SREM animal cat // {dog, panda, tiger}
SADD animal cat lion // {cat, lion, dog, panda, tiger}
那么使用一條 SADD animal cat lion dog panda tiger
命令,就可以還原 animal
集合的狀態(tài),這比之前的四條命令調用要大大減少。
除了列表和集合之外,字符串、有序集、哈希表等鍵也可以用類似的方法來保存狀態(tài),并且保存這些狀態(tài)所使用的命令數(shù)量,比起之前建立這些鍵的狀態(tài)所使用命令的數(shù)量要大大減少。
根據(jù)鍵的類型,使用適當?shù)膶懭朊顏碇噩F(xiàn)鍵的當前值,這就是 AOF 重寫的實現(xiàn)原理。整個重寫過程可以用偽代碼表示如下:
def AOF_REWRITE(tmp_tile_name):
f = create(tmp_tile_name)
# 遍歷所有數(shù)據(jù)庫
for db in redisServer.db:
# 如果數(shù)據(jù)庫為空,那么跳過這個數(shù)據(jù)庫
if db.is_empty(): continue
# 寫入 SELECT 命令,用于切換數(shù)據(jù)庫
f.write_command("SELECT " + db.number)
# 遍歷所有鍵
for key in db:
# 如果鍵帶有過期時間,并且已經(jīng)過期,那么跳過這個鍵
if key.have_expire_time() and key.is_expired(): continue
if key.type == String:
# 用 SET key value 命令來保存字符串鍵
value = get_value_from_string(key)
f.write_command("SET " + key + value)
elif key.type == List:
# 用 RPUSH key item1 item2 ... itemN 命令來保存列表鍵
item1, item2, ..., itemN = get_item_from_list(key)
f.write_command("RPUSH " + key + item1 + item2 + ... + itemN)
elif key.type == Set:
# 用 SADD key member1 member2 ... memberN 命令來保存集合鍵
member1, member2, ..., memberN = get_member_from_set(key)
f.write_command("SADD " + key + member1 + member2 + ... + memberN)
elif key.type == Hash:
# 用 HMSET key field1 value1 field2 value2 ... fieldN valueN 命令來保存哈希鍵
field1, value1, field2, value2, ..., fieldN, valueN =\
get_field_and_value_from_hash(key)
f.write_command("HMSET " + key + field1 + value1 + field2 + value2 +\
... + fieldN + valueN)
elif key.type == SortedSet:
# 用 ZADD key score1 member1 score2 member2 ... scoreN memberN
# 命令來保存有序集鍵
score1, member1, score2, member2, ..., scoreN, memberN = \
get_score_and_member_from_sorted_set(key)
f.write_command("ZADD " + key + score1 + member1 + score2 + member2 +\
... + scoreN + memberN)
else:
raise_type_error()
# 如果鍵帶有過期時間,那么用 EXPIREAT key time 命令來保存鍵的過期時間
if key.have_expire_time():
f.write_command("EXPIREAT " + key + key.expire_time_in_unix_timestamp())
# 關閉文件
f.close()
上一節(jié)展示的 AOF 重寫程序可以很好地完成創(chuàng)建一個新 AOF 文件的任務,但是,在執(zhí)行這個程序的時候,調用者線程會被阻塞。
很明顯,作為一種輔佐性的維護手段,Redis 不希望 AOF 重寫造成服務器無法處理請求,所以 Redis 決定將 AOF 重寫程序放到(后臺)子進程里執(zhí)行,這樣處理的最大好處是:
不過,使用子進程也有一個問題需要解決:因為子進程在進行 AOF 重寫期間,主進程還需要繼續(xù)處理命令,而新的命令可能對現(xiàn)有的數(shù)據(jù)進行修改,這會讓當前數(shù)據(jù)庫的數(shù)據(jù)和重寫后的 AOF 文件中的數(shù)據(jù)不一致。
為了解決這個問題,Redis 增加了一個 AOF 重寫緩存,這個緩存在 fork 出子進程之后開始啟用,Redis 主進程在接到新的寫命令之后,除了會將這個寫命令的協(xié)議內容追加到現(xiàn)有的 AOF 文件之外,還會追加到這個緩存中:
server [label = "命令請求"]; current_aof [label = "現(xiàn)有 AOF 文件", shape = box, fillcolor = "#FADCAD"]; aof_rewrite_buf [label = "AOF 重寫緩存", shape = box, fillcolor = "#FADCAD"]; server -> current_aof [label = "命令協(xié)議內容"]; server -> aof_rewrite_buf [label = "命令協(xié)議內容"];}" />
換言之,當子進程在執(zhí)行 AOF 重寫時,主進程需要執(zhí)行以下三個工作:
這樣一來可以保證:
當子進程完成 AOF 重寫之后,它會向父進程發(fā)送一個完成信號,父進程在接到完成信號之后,會調用一個信號處理函數(shù),并完成以下工作:
當步驟 1 執(zhí)行完畢之后,現(xiàn)有 AOF 文件、新 AOF 文件和數(shù)據(jù)庫三者的狀態(tài)就完全一致了。
當步驟 2 執(zhí)行完畢之后,程序就完成了新舊兩個 AOF 文件的交替。
這個信號處理函數(shù)執(zhí)行完畢之后,主進程就可以繼續(xù)像往常一樣接受命令請求了。在整個 AOF 后臺重寫過程中,只有最后的寫入緩存和改名操作會造成主進程阻塞,在其他時候,AOF 后臺重寫都不會對主進程造成阻塞,這將 AOF 重寫對性能造成的影響降到了最低。
以上就是 AOF 后臺重寫,也即是 BGREWRITEAOF 命令的工作原理。
AOF 重寫可以由用戶通過調用 BGREWRITEAOF 手動觸發(fā)。
另外,服務器在 AOF 功能開啟的情況下,會維持以下三個變量:
aof_current_size
。aof_rewrite_base_size
。aof_rewrite_perc
。每次當 serverCron
函數(shù)執(zhí)行時,它都會檢查以下條件是否全部滿足,如果是的話,就會觸發(fā)自動的 AOF 重寫:
server.aof_rewrite_min_size
(默認值為 1 MB)。默認情況下,增長百分比為 100%
,也即是說,如果前面三個條件都已經(jīng)滿足,并且當前 AOF 文件大小比最后一次 AOF 重寫時的大小要大一倍的話,那么觸發(fā)自動 AOF 重寫。
更多建議: