新聞中心
Redis 作為一種非常流行的內(nèi)存數(shù)據(jù)庫,通過將數(shù)據(jù)保存在內(nèi)存中,Redis 得以擁有極高的讀寫性能。但是一旦進程退出,Redis 的數(shù)據(jù)就會全部丟失。

為了解決這個問題,Redis 提供了 RDB 和 AOF 兩種持久化方案,將內(nèi)存中的數(shù)據(jù)保存到磁盤中,避免數(shù)據(jù)丟失。本文將重點討論AOF持久化方案,以及其存在的一些問題,并探討在Redis 7.0 (已發(fā)布RC1) 中Multi Part AOF(下文簡稱為MP-AOF,本特性由阿里云數(shù)據(jù)庫Tair團隊貢獻)設(shè)計和實現(xiàn)細節(jié)。
一 、AOF
AOF( append only file )持久化以獨立日志文件的方式記錄每條寫命令,并在 Redis 啟動時回放 AOF 文件中的命令以達到恢復數(shù)據(jù)的目的。
由于AOF會以追加的方式記錄每一條redis的寫命令,因此隨著Redis處理的寫命令增多,AOF文件也會變得越來越大,命令回放的時間也會增多,為了解決這個問題,Redis引入了AOF rewrite機制(下文稱之為AOFRW)。AOFRW會移除AOF中冗余的寫命令,以等效的方式重寫、生成一個新的AOF文件,來達到減少AOF文件大小的目的。
二、 AOFRW
圖1展示的是AOFRW的實現(xiàn)原理。當AOFRW被觸發(fā)執(zhí)行時,Redis首先會fork一個子進程進行后臺重寫操作,該操作會將執(zhí)行fork那一刻Redis的數(shù)據(jù)快照全部重寫到一個名為temp-rewriteaof-bg-pid.aof的臨時AOF文件中。
由于重寫操作為子進程后臺執(zhí)行,主進程在AOF重寫期間依然可以正常響應(yīng)用戶命令。因此,為了讓子進程最終也能獲取重寫期間主進程產(chǎn)生的增量變化,主進程除了會將執(zhí)行的寫命令寫入aof_buf,還會寫一份到aof_rewrite_buf中進行緩存。在子進程重寫的后期階段,主進程會將aof_rewrite_buf中累積的數(shù)據(jù)使用pipe發(fā)送給子進程,子進程會將這些數(shù)據(jù)追加到臨時AOF文件中(詳細原理可參考[1])。
當主進程承接了較大的寫入流量時,aof_rewrite_buf中可能會堆積非常多的數(shù)據(jù),導致在重寫期間子進程無法將aof_rewrite_buf中的數(shù)據(jù)全部消費完。此時,aof_rewrite_buf剩余的數(shù)據(jù)將在重寫結(jié)束時由主進程進行處理。
當子進程完成重寫操作并退出后,主進程會在backgroundRewriteDoneHandler 中處理后續(xù)的事情。首先,將重寫期間aof_rewrite_buf中未消費完的數(shù)據(jù)追加到臨時AOF文件中。其次,當一切準備就緒時,Redis會使用rename 操作將臨時AOF文件原子的重命名為server.aof_filename,此時原來的AOF文件會被覆蓋。至此,整個AOFRW流程結(jié)束。
圖1 AOFRW實現(xiàn)原理
三、 AOFRW存在的問題
1. 內(nèi)存開銷
由圖1可以看到,在AOFRW期間,主進程會將fork之后的數(shù)據(jù)變化寫進aof_rewrite_buf中,aof_rewrite_buf和aof_buf中的內(nèi)容絕大部分都是重復的,因此這將帶來額外的內(nèi)存冗余開銷。
在Redis INFO中的aof_rewrite_buffer_length字段可以看到當前時刻aof_rewrite_buf占用的內(nèi)存大小。如下面顯示的,在高寫入流量下aof_rewrite_buffer_length幾乎和aof_buffer_length占用了同樣大的內(nèi)存空間,幾乎浪費了一倍的內(nèi)存。
aof_pending_rewrite:0
aof_buffer_length:35500
aof_rewrite_buffer_length:34000
aof_pending_bio_fsync:0
當aof_rewrite_buf占用的內(nèi)存大小超過一定閾值時,我們將在Redis日志中看到如下信息。可以看到,aof_rewrite_buf占用了100MB的內(nèi)存空間且主進程和子進程之間傳輸了2135MB的數(shù)據(jù)(子進程在通過pipe讀取這些數(shù)據(jù)時也會有內(nèi)部讀buffer的內(nèi)存開銷)。
對于內(nèi)存型數(shù)據(jù)庫Redis而言,這是一筆不小的開銷。
3351:M 25 Jan 2022 09:55:39.655 * Background append only file rewriting started by pid 6817
3351:M 25 Jan 2022 09:57:51.864 * AOF rewrite child asks to stop sending diffs.
6817:C 25 Jan 2022 09:57:51.864 * Parent agreed to stop sending diffs. Finalizing AOF...
6817:C 25 Jan 2022 09:57:51.864 * Concatenating 2135.60 MB of AOF diff received from parent.
3351:M 25 Jan 2022 09:57:56.545 * Background AOF buffer size: 100 MB
AOFRW帶來的內(nèi)存開銷有可能導致Redis內(nèi)存突然達到maxmemory限制,從而影響正常命令的寫入,甚至會觸發(fā)操作系統(tǒng)限制被OOM Killer殺死,導致Redis不可服務(wù)。
2. CPU開銷
CPU的開銷主要有三個地方,分別解釋如下:
在AOFRW期間,主進程需要花費CPU時間向aof_rewrite_buf寫數(shù)據(jù),并使用eventloop事件循環(huán)向子進程發(fā)送aof_rewrite_buf中的數(shù)據(jù):
/* Append data to the AOF rewrite buffer, allocating new blocks if needed. */
void aofRewriteBufferAppend(unsigned char *s, unsigned long len) {
// 此處省略其他細節(jié)...
/* Install a file event to send data to the rewrite child if there is
* not one already. */
if (!server.aof_stop_sending_diff &&
aeGetFileEvents(server.el,server.aof_pipe_write_data_to_child) == 0)
{
aeCreateFileEvent(server.el, server.aof_pipe_write_data_to_child,
AE_WRITABLE, aofChildWriteDiffData, NULL);
}
// 此處省略其他細節(jié)...
}
在子進程執(zhí)行重寫操作的后期,會循環(huán)讀取pipe中主進程發(fā)送來的增量數(shù)據(jù),然后追加寫入到臨時AOF文件:
int rewriteAppendOnlyFile(char *filename) {
// 此處省略其他細節(jié)...
/* Read again a few times to get more data from the parent.
* We can't read forever (the server may receive data from clients
* faster than it is able to send data to the child), so we try to read
* some more data in a loop as soon as there is a good chance more data
* will come. If it looks like we are wasting time, we abort (this
* happens after 20 ms without new data). */
int nodata = 0;
mstime_t start = mstime();
while(mstime()-start < 1000 && nodata < 20) {
if (aeWait(server.aof_pipe_read_data_from_parent, AE_READABLE, 1) <= 0)
{
nodata++;
continue;
}
nodata = 0; /* Start counting from zero, we stop on N *contiguous*
timeouts. */
aofReadDiffFromParent();
}
// 此處省略其他細節(jié)...
}
在子進程完成重寫操作后,主進程會在backgroundRewriteDoneHandler 中進行收尾工作。其中一個任務(wù)就是將在重寫期間aof_rewrite_buf中沒有消費完成的數(shù)據(jù)寫入臨時AOF文件。如果aof_rewrite_buf中遺留的數(shù)據(jù)很多,這里也將消耗CPU時間。
void backgroundRewriteDoneHandler(int exitcode, int bysignal) {
// 此處省略其他細節(jié)...
/* Flush the differences accumulated by the parent to the rewritten AOF. */
if (aofRewriteBufferWrite(newfd) == -1) {
serverLog(LL_WARNING,
"Error trying to flush the parent diff to the rewritten AOF: %s", strerror(errno));
close(newfd);
goto cleanup;
}
// 此處省略其他細節(jié)...
}
AOFRW帶來的CPU開銷可能會造成Redis在執(zhí)行命令時出現(xiàn)RT上的抖動,甚至造成客戶端超時的問題。
3 .磁盤IO開銷
如前文所述,在AOFRW期間,主進程除了會將執(zhí)行過的寫命令寫到aof_buf之外,還會寫一份到aof_rewrite_buf中。aof_buf中的數(shù)據(jù)最終會被寫入到當前使用的舊AOF文件中,產(chǎn)生磁盤IO。同時,aof_rewrite_buf中的數(shù)據(jù)也會被寫入重寫生成的新AOF文件中,產(chǎn)生磁盤IO。因此,同一份數(shù)據(jù)會產(chǎn)生兩次磁盤IO。
4. 代碼復雜度
Redis使用下面所示的六個pipe進行主進程和子進程之間的數(shù)據(jù)傳輸和控制交互,這使得整個AOFRW邏輯變得更為復雜和難以理解。
/* AOF pipes used to communicate between parent and child during rewrite. */
int aof_pipe_write_data_to_child;
int aof_pipe_read_data_from_parent;
int aof_pipe_write_ack_to_parent;
int aof_pipe_read_ack_from_child;
int aof_pipe_write_ack_to_child;
int aof_pipe_read_ack_from_parent;
四、 MP-AOF實現(xiàn)
1 .方案概述
顧名思義,MP-AOF就是將原來的單個AOF文件拆分成多個AOF文件。在MP-AOF中,我們將AOF分為三種類型,分別為:
BASE:表示基礎(chǔ)AOF,它一般由子進程通過重寫產(chǎn)生,該文件最多只有一個。
INCR:表示增量AOF,它一般會在AOFRW開始執(zhí)行時被創(chuàng)建,該文件可能存在多個。
HISTORY:表示歷史AOF,它由BASE和INCR AOF變化而來,每次AOFRW成功完成時,本次AOFRW之前對應(yīng)的BASE和INCR AOF都將變?yōu)镠ISTORY,HISTORY類型的AOF會被Redis自動刪除。
為了管理這些AOF文件,我們引入了一個manifest(清單)文件來跟蹤、管理這些AOF。同時,為了便于AOF備份和拷貝,我們將所有的AOF文件和manifest文件放入一個單獨的文件目錄中,目錄名由appenddirname配置(Redis 7.0新增配置項)決定。
圖2 MP-AOF Rewrite原理
圖2展示的是在MP-AOF中執(zhí)行一次AOFRW的大致流程。在開始時我們依然會fork一個子進程進行重寫操作,在主進程中,我們會同時打開一個新的INCR類型的AOF文件,在子進程重寫操作期間,所有的數(shù)據(jù)變化都會被寫入到這個新打開的INCR AOF中。子進程的重寫操作完全是獨立的,重寫期間不會與主進程進行任何的數(shù)據(jù)和控制交互,最終重寫操作會產(chǎn)生一個BASE AOF。新生成的BASE AOF和新打開的INCR AOF就代表了當前時刻Redis的全部數(shù)據(jù)。AOFRW結(jié)束時,主進程會負責更新manifest文件,將新生成的BASE AOF和INCR AOF信息加入進去,并將之前的BASE AOF和INCR AOF標記為HISTORY(這些HISTORY AOF會被Redis異步刪除)。一旦manifest文件更新完畢,就標志整個AOFRW流程結(jié)束。
由圖2可以看到,我們在AOFRW期間不再需要aof_rewrite_buf,因此去掉了對應(yīng)的內(nèi)存消耗。同時,主進程和子進程之間也不再有數(shù)據(jù)傳輸和控制交互,因此對應(yīng)的CPU開銷也全部去掉。對應(yīng)的,前文提及的六個pipe及其對應(yīng)的代碼也全部刪除,使得AOFRW邏輯更加簡單清晰。
2 .關(guān)鍵實現(xiàn)
Manifest
1)在內(nèi)存中的表示
MP-AOF強依賴manifest文件,manifest在內(nèi)存中表示為如下結(jié)構(gòu)體,其中:
- aofInfo:表示一個AOF文件信息,當前僅包括文件名、文件序號和文件類型
- base_aof_info:表示BASE AOF信息,當不存在BASE AOF時,該字段為NULL
- incr_aof_list:用于存放所有INCR AOF文件的信息,所有的INCR AOF都會按照文件打開順序排放
- history_aof_list:用于存放HISTORY AOF信息,history_aof_list中的元素都是從base_aof_info和incr_aof_list中move過來的
typedef struct {
sds file_name; /* file name */
long long file_seq; /* file sequence */
aof_file_type file_type; /* file type */
} aofInfo;
typedef struct {
aofInfo *base_aof_info; /* BASE file information. NULL if there is no BASE file. */
list *incr_aof_list; /* INCR AOFs list. We may have multiple INCR AOF when rewrite fails. */
list *history_aof_list; /* HISTORY AOF list. When the AOFRW success, The aofInfo contained in
`base_aof_info` and `incr_aof_list` will be moved to this list. We
will delete these AOF files when AOFRW finish. */
long long curr_base_file_seq; /* The sequence number used by the current BASE file. */
long long curr_incr_file_seq; /* The sequence number used by the current INCR file. */
int dirty; /* 1 Indicates that the aofManifest in the memory is inconsistent with
disk, we need to persist it immediately. */
} aofManifest;
為了便于原子性修改和回滾操作,我們在redisServer結(jié)構(gòu)中使用指針的方式引用aofManifest。
struct redisServer {
// 此處省略其他細節(jié)...
aofManifest *aof_manifest; /* Used to track AOFs. */
// 此處省略其他細節(jié)...
}
2)在磁盤上的表示
Manifest本質(zhì)就是一個包含多行記錄的文本文件,每一行記錄對應(yīng)一個AOF文件信息,這些信息通過key/value對的方式展示,便于Redis處理、易于閱讀和修改。下面是一個可能的manifest文件內(nèi)容:
file appendonly.aof.1.base.rdb seq 1 type b
file appendonly.aof.1.incr.aof seq 1 type i
file appendonly.aof.2.incr.aof seq 2 type i
Manifest格式本身需要具有一定的擴展性,以便將來添加或支持其他的功能。比如可以方便的支持新增key/value和注解(類似AOF中的注解),這樣可以保證較好的forward compatibility。
file appendonly.aof.1.base.rdb seq 1 type b newkey newvalue
file appendonly.aof.1.incr.aof type i seq 1
# this is annotations
seq 2 type i file appendonly.aof.2.incr.aof
文件命名規(guī)則
在MP-AOF之前,AOF的文件名為appendfilename參數(shù)的設(shè)置值(默認為appendonly.aof)。
在MP-AOF中,我們使用basename.suffix的方式命名多個AOF文件。其中,appendfilename配置內(nèi)容將作為basename部分,suffix則由三個部分組成,格式為seq.type.format ,其中:
- seq為文件的序號,由1開始單調(diào)遞增,BASE和INCR擁有獨立的文件序號
- type為AOF的類型,表示這個AOF文件是BASE還是INCR
- format用來表示這個AOF內(nèi)部的編碼方式,由于Redis支持RDB preamble機制,因此BASE AOF可能是RDB格式編碼也可能是AOF格式編碼:
#define BASE_FILE_SUFFIX ".base"
#define INCR_FILE_SUFFIX ".incr"
#define RDB_FORMAT_SUFFIX ".rdb"
#define AOF_FORMAT_SUFFIX ".aof"
#define MANIFEST_NAME_SUFFIX ".manifest"
因此,當使用appendfilename默認配置時,BASE、INCR和manifest文件的可能命名如下:
appendonly.aof.1.base.rdb // 開啟RDB preamble
appendonly.aof.1.base.aof // 關(guān)閉RDB preamble
appendonly.aof.1.incr.aof
appendonly.aof.2.incr.aof
兼容老版本升級
由于MP-AOF強依賴manifest文件,Redis啟動時會嚴格按照manifest的指示加載對應(yīng)的AOF文件。但是在從老版本Redis(指Redis 7.0之前的版本)升級到Redis 7.0時,由于此時并無manifest文件,因此如何讓Redis正確識別這是一個升級過程并正確、安全的加載舊AOF是一個必須支持的能力。
識別能力是這一重要過程的首要環(huán)節(jié),在真正加載AOF文件之前,我們會檢查Redis工作目錄下是否存在名為server.aof_filename的AOF文件。如果存在,那說明我們可能在從一個老版本Redis執(zhí)行升級,接下來,我們會繼續(xù)判斷,當滿足下面三種情況之一時我們會認為這是一個升級啟動:
- 如果appenddirname目錄不存在
- 或者appenddirname目錄存在,但是目錄中沒有對應(yīng)的manifest清單文件
- 如果appenddirname目錄存在且目錄中存在manifest清單文件,且清單文件中只有BASE AOF相關(guān)信息,且這個BASE AOF的名字和server.aof_filename相同,且appenddirname目錄中不存在名為server.aof_filename的文件
/* Load the AOF files according the aofManifest pointed by am. */
int loadAppendOnlyFiles(aofManifest *am) {
// 此處省略其他細節(jié)...
/* If the 'server.aof_filename' file exists in dir, we may be starting
* from an old redis version. We will use enter upgrade mode in three situations.
*
* 1. If the 'server.aof_dirname' directory not exist
* 2. If the 'server.aof_dirname' directory exists but the manifest file is missing
* 3. If the 'server.aof_dirname' directory exists and the manifest file it contains
* has only one base AOF record, and the file name of this base AOF is 'server.aof_filename',
* and the 'server.aof_filename' file not exist in 'server.aof_dirname' directory
* */
if (fileExist(server.aof_filename)) {
if (!dirExists(server.aof_dirname) ||
(am->base_aof_info == NULL && listLength(am->incr_aof_list) == 0) ||
(am->base_aof_info != NULL && listLength(am->incr_aof_list) == 0 &&
!strcmp(am->base_aof_info->file_name, server.aof_filename) && !aofFileExist(server.aof_filename)))
{
aofUpgradePrepare(am);
}
}
// 此處省略其他細節(jié)...
}
一旦被識別為這是一個升級啟動,我們會使用aofUpgradePrepare 函數(shù)進行升級前的準備工作。
升級準備工作主要分為三個部分:
使用server.aof_filename作為文件名來構(gòu)造一個BASE AOF信息
將該BASE AOF信息持久化到manifest文件
使用rename 將舊AOF文件移動到appenddirname目錄中
void aofUpgradePrepare(aofManifest *am) {
// 此處省略其他細節(jié)...
/* 1. Manually construct a BASE type aofInfo and add it to aofManifest. */
if (am->base_aof_info) aofInfoFree(am->base_aof_info);
aofInfo *ai = aofInfoCreate();
ai->file_name = sdsnew(server.aof_filename);
ai->file_seq = 1;
ai->file_type = AOF_FILE_TYPE_BASE;
am->base_aof_info = ai;
am->curr_base_file_seq = 1;
am->dirty = 1;
/* 2. Persist the manifest file to AOF directory. */
if (persistAofManifest(am) != C_OK) {
exit(1);
}
/* 3. Move the old AOF file to AOF directory. */
sds aof_filepath = makePath(server.aof_dirname, server.aof_filename);
if (rename(server.aof_filename, aof_filepath) == -1) {
sdsfree(aof_filepath);
exit(1);;
}
// 此處省略其他細節(jié)...
}
升級準備操作是Crash Safety的,以上三步中任何一步發(fā)生Crash我們都能在下一次的啟動中正確的識別并重試整個升級操作。
多文件加載及進度計算
Redis在加載AOF時會記錄加載的進度,并通過Redis INFO的loading_loaded_perc字段展示出來。在MP-AOF中,loadAppendOnlyFiles 函數(shù)會根據(jù)傳入的aofManifest進行AOF文件加載。在進行加載之前,我們需要提前計算所有待加載的AOF文件的總大小,并傳給startLoading 函數(shù),然后在loadSingleAppendOnlyFile 中不斷的上報加載進度。
接下來,loadAppendOnlyFiles 會根據(jù)aofManifest依次加載BASE AOF和INCR AOF。當前加載完所有的AOF文件,會使用stopLoading 結(jié)束加載狀態(tài)。
int loadAppendOnlyFiles(aofManifest *am) {
// 此處省略其他細節(jié)...
/* Here we calculate the total size of all BASE and INCR files in
* advance, it will be set to `server.loading_total_bytes`. */
total_size = getBaseAndIncrAppendOnlyFilesSize(am);
startLoading(total_size, RDBFLAGS_AOF_PREAMBLE, 0);
/* Load BASE AOF if needed. */
if (am->base_aof_info) {
aof_name = (char*)am->base_aof_info->file_name;
updateLoadingFileName(aof_name);
loadSingleAppendOnlyFile(aof_name);
}
/* Load INCR AOFs if needed. */
if (listLength(am->incr_aof_list)) {
listNode *ln;
listIter li;
listRewind(am->incr_aof_list, &li);
while ((ln = listNext(&li)) != NULL) {
aofInfo *ai = (aofInfo*)ln->value;
aof_name = (char*)ai->file_name;
updateLoadingFileName(aof_name);
loadSingleAppendOnlyFile(aof_name);
}
}
server.aof_current_size = total_size;
server.aof_rewrite_base_size = server.aof_current_size;
server.aof_fsync_offset = server.aof_current_size;
stopLoading();
// 此處省略其他細節(jié)...
}
AOFRW Crash Safety
當子進程完成重寫操作,子進程會創(chuàng)建一個名為temp-rewriteaof-bg-pid.aof的臨時AOF文件,此時這個文件對Redis而言還是不可見的,因為它還沒有被加入到manifest文件中。要想使得它能被Redis識別并在Redis啟動時正確加載,我們還需要將它按照前文提到的命名規(guī)則進行rename 操作,并將其信息加入到manifest文件中。
AOF文件rename 和manifest文件修改雖然是兩個獨立操作,但我們必須保證這兩個操作的原子性,這樣才能讓Redis在啟動時能正確的加載對應(yīng)的AOF。MP-AOF使用兩個設(shè)計來解決這個問題:
- BASE AOF的名字中包含文件序號,保證每次創(chuàng)建的BASE AOF不會和之前的BASE AOF沖突;
- 先執(zhí)行AOF的rename 操作,再修改manifest文件;
為了便于說明,我們假設(shè)在AOFRW開始之前,manifest文件內(nèi)容如下:
le appendonly.aof.1.base.rdb seq 1 type b
file appendonly.aof.1.incr.aof seq 1 type i
則在AOFRW開始執(zhí)行后manifest文件內(nèi)容如下:
file appendonly.aof.1.base.rdb seq 1 type b
file appendonly.aof.1.incr.aof seq 1 type i
file appendonly.aof.2.incr.aof seq 2 type i
子進程重寫結(jié)束后,在主進程中,我們會將temp-rewriteaof-bg-pid.aof重命名為appendonly.aof.2.base.rdb,并將其加入manifest中,同時會將之前的BASE和INCR AOF標記為HISTORY。此時manifest文件內(nèi)容如下:
file appendonly.aof.2.base.rdb seq 2 type b
file appendonly.aof.1.base.rdb seq 1 type h
file appendonly.aof.1.incr.aof seq 1 type h
file appendonly.aof.2.incr.aof seq 2 type i
此時,本次AOFRW的結(jié)果對Redis可見,HISTORY AOF會被Redis異步清理。
backgroundRewriteDoneHandler 函數(shù)通過七個步驟實現(xiàn)了上述邏輯:
- 在修改內(nèi)存中的server.aof_manifest前,先dup一份臨時的manifest結(jié)構(gòu),接下來的修改都將針對這個臨時的manifest進行。這樣做的好處是,一旦后面的步驟出現(xiàn)失敗,我們可以簡單的銷毀臨時manifest從而回滾整個操作,避免污染server.aof_manifest全局數(shù)據(jù)結(jié)構(gòu);
- 從臨時manifest中獲取新的BASE AOF文件名(記為new_base_filename),并將之前(如果有)的BASE AOF標記為HISTORY;
- 將子進程產(chǎn)生的temp-rewriteaof-bg-pid.aof臨時文件重命名為new_base_filename;
- 將臨時manifest結(jié)構(gòu)中上一次的INCR AOF全部標記為HISTORY類型;
- 將臨時manifest對應(yīng)的信息持久化到磁盤(persistAofManifest內(nèi)部會保證manifest本身修改的原子性);
- 如果上述步驟都成功了,我們可以放心的將內(nèi)存中的server.aof_manifest指針指向臨時的manifest結(jié)構(gòu)(并釋放之前的manifest結(jié)構(gòu)),至此整個修改對Redis可見;
- 清理HISTORY類型的AOF,該步驟允許失敗,因為它不會導致數(shù)據(jù)一致性問題。
void backgroundRewriteDoneHandler(int exitcode, int bysignal) {
snprintf(tmpfile, 256, "temp-rewriteaof-bg-%d.aof",
(int)server.child_pid);
/* 1. Dup a temporary aof_manifest for subsequent modifications. */
temp_am = aofManifestDup(server.aof_manifest);
/* 2. Get a new BASE file name and mark the previous (if we have)
* as the HISTORY type. */
new_base_filename = getNewBaseFileNameAndMarkPreAsHistory(temp_am);
/* 3. Rename the temporary aof file to 'new_base_filename'. */
if (rename(tmpfile, new_base_filename) == -1) {
aofManifestFree(temp_am);
goto cleanup;
}
/* 4. Change the AOF file type in 'incr_aof_list' from AOF_FILE_TYPE_INCR
* to AOF_FILE_TYPE_HIST, and move them to the 'history_aof_list'. */
markRewrittenIncrAofAsHistory(temp_am);
/* 5. Persist our modifications. */
if (persistAofManifest(temp_am) == C_ERR) {
bg_unlink(new_base_filename);
aofManifestFree(temp_am);
goto cleanup;
}
/* 6. We can safely let `server.aof_manifest` point to 'temp_am' and free the previous one. */
aofManifestFreeAndUpdate(temp_am);
/* 7. We don't care about the return value of `aofDelHistoryFiles`, because the history
* deletion failure will not cause any problems. */
aofDelHistoryFiles();
}
支持AOF truncate
在進程出現(xiàn)Crash時AOF文件很可能出現(xiàn)寫入不完整的問題,如一條事務(wù)里只寫了MULTI,但是還沒寫EXEC時Redis就Crash。默認情況下,Redis無法加載這種不完整的AOF,但是Redis支持AOF truncate功能(通過aof-load-truncated配置打開)。其原理是使用server.aof_current_size跟蹤AOF最后一個正確的文件偏移,然后使用ftruncate 函數(shù)將該偏移之后的文件內(nèi)容全部刪除,這樣雖然可能會丟失部分數(shù)據(jù),但可以保證AOF的完整性。
在MP-AOF中,server.aof_current_size已經(jīng)不再表示單個AOF文件的大小而是所有AOF文件的總大小。因為只有最后一個INCR AOF才有可能出現(xiàn)不完整寫入的問題,因此我們引入了一個單獨的字段server.aof_last_incr_size用于跟蹤最后一個INCR AOF文件的大小。當最后一個INCR AOF出現(xiàn)不完整寫入時,我們只需要將server.aof_last_incr_size之后的文件內(nèi)容刪除即可。
if (ftruncate(server.aof_fd, server.aof_last_incr_size) == -1) {
//此處省略其他細節(jié)...
}
AOFRW限流
Redis在AOF大小超過一定閾值時支持自動執(zhí)行AOFRW,當出現(xiàn)磁盤故障或者觸發(fā)了代碼bug導致AOFRW失敗時,Redis將不停的重復執(zhí)行AOFRW直到成功為止。在MP-AOF出現(xiàn)之前,這看似沒有什么大問題(頂多就是消耗一些CPU時間和fork開銷)。但是在MP-AOF中,因為每次AOFRW都會打開一個INCR AOF,并且只有在AOFRW成功時才會將上一個INCR和BASE轉(zhuǎn)為HISTORY并刪除。因此,連續(xù)的AOFRW失敗勢必會導致多個INCR AOF并存的問題。極端情況下,如果AOFRW重試頻率很高我們將會看到成百上千個INCR AOF文件。
為此,我們引入了AOFRW限流機制。即當AOFRW已經(jīng)連續(xù)失敗三次時,下一次的AOFRW會被強行延遲1分鐘執(zhí)行,如果下一次AOFRW依然失敗,則會延遲2分鐘,依次類推延遲4、8、16...,當前最大延遲時間為1小時。
在AOFRW限流期間,我們依然可以使用bgrewriteaof命令立即執(zhí)行一次AOFRW。
if (server.aof_state == AOF_ON &&
!hasActiveChildProcess() &&
server.aof_rewrite_perc &&
server.aof_current_size > server.aof_rewrite_min_size &&
!aofRewriteLimited())
{
long long base = server.aof_rewrite_base_size ?
server.aof_rewrite_base_size : 1;
long long growth = (server.aof_current_size*100/base) - 100;
if (growth >= server.aof_rewrite_perc) {
rewriteAppendOnlyFileBackground();
}
}
AOFRW限流機制的引入,還可以有效的避免AOFRW高頻重試帶來的CPU和fork開銷。Redis中很多的RT抖動都和fork有關(guān)系。
五、 總結(jié)
MP-AOF的引入,成功的解決了之前AOFRW存在的內(nèi)存和CPU開銷對Redis實例甚至業(yè)務(wù)訪問帶來的不利影響。同時,在解決這些問題的過程中,我們也遇到了很多未曾預料的挑戰(zhàn),這些挑戰(zhàn)主要來自于Redis龐大的使用群體、多樣化的使用場景,因此我們必須考慮用戶在各種場景下使用MP-AOF可能遇到的問題。如兼容性、易用性以及對Redis代碼盡可能的減少侵入性等。這都是Redis社區(qū)功能演進的重中之重。
同時,MP-AOF的引入也為Redis的數(shù)據(jù)持久化帶來了更多的想象空間。如在開啟aof-use-rdb-preamble時,BASE AOF本質(zhì)是一個RDB文件,因此我們在進行全量備份的時候無需在單獨執(zhí)行一次BGSAVE操作。直接備份BASE AOF即可。MP-AOF支持關(guān)閉自動清理HISTORY AOF的能力,因此那些歷史的AOF有機會得以保留,并且目前Redis已經(jīng)支持在AOF中加入timestamp annotation,因此基于這些我們甚至可以實現(xiàn)一個簡單的PITR能力( point-in-time recovery)。
MP-AOF的設(shè)計原型來自于Tair for redis企業(yè)版[2]的binlog實現(xiàn),這是一套在阿里云Tair服務(wù)上久經(jīng)驗證的核心功能,在這個核心功能上阿里云Tair成功構(gòu)建了全球多活、PITR等企業(yè)級能力,使用戶的更多業(yè)務(wù)場景需求得到滿足。今天我們將這個核心能力貢獻給Redis社區(qū),希望社區(qū)用戶也能享受這些企業(yè)級特性,并通過這些企業(yè)級特性更好的優(yōu)化,創(chuàng)造自己的業(yè)務(wù)代碼。有關(guān)MP-AOF的更多細節(jié),請移步參考相關(guān)PR(#9788),那里有更多的原始設(shè)計和完整代碼。
[1]http://mysql.taobao.org/monthly/2018/12/06/[2]https://help.aliyun.com/document_detail/145956.html
本文題目:Redis7.0MultiPartAOF的設(shè)計和實現(xiàn)
URL鏈接:http://m.fisionsoft.com.cn/article/cdhpjdj.html


咨詢
建站咨詢
