日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢(xún)
選擇下列產(chǎn)品馬上在線(xiàn)溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問(wèn)題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營(yíng)銷(xiāo)解決方案
Redis7.0MultiPartAOF的設(shè)計(jì)和實(shí)現(xiàn)

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

為了解決這個(gè)問(wèn)題,Redis 提供了 RDB 和 AOF 兩種持久化方案,將內(nèi)存中的數(shù)據(jù)保存到磁盤(pán)中,避免數(shù)據(jù)丟失。本文將重點(diǎn)討論AOF持久化方案,以及其存在的一些問(wèn)題,并探討在Redis 7.0 (已發(fā)布RC1) 中Multi Part AOF(下文簡(jiǎn)稱(chēng)為MP-AOF,本特性由阿里云數(shù)據(jù)庫(kù)Tair團(tuán)隊(duì)貢獻(xiàn))設(shè)計(jì)和實(shí)現(xiàn)細(xì)節(jié)。

一 、AOF

AOF( append only file )持久化以獨(dú)立日志文件的方式記錄每條寫(xiě)命令,并在 Redis 啟動(dòng)時(shí)回放 AOF 文件中的命令以達(dá)到恢復(fù)數(shù)據(jù)的目的。

由于AOF會(huì)以追加的方式記錄每一條redis的寫(xiě)命令,因此隨著Redis處理的寫(xiě)命令增多,AOF文件也會(huì)變得越來(lái)越大,命令回放的時(shí)間也會(huì)增多,為了解決這個(gè)問(wèn)題,Redis引入了AOF rewrite機(jī)制(下文稱(chēng)之為AOFRW)。AOFRW會(huì)移除AOF中冗余的寫(xiě)命令,以等效的方式重寫(xiě)、生成一個(gè)新的AOF文件,來(lái)達(dá)到減少AOF文件大小的目的。

二、 AOFRW

圖1展示的是AOFRW的實(shí)現(xiàn)原理。當(dāng)AOFRW被觸發(fā)執(zhí)行時(shí),Redis首先會(huì)fork一個(gè)子進(jìn)程進(jìn)行后臺(tái)重寫(xiě)操作,該操作會(huì)將執(zhí)行fork那一刻Redis的數(shù)據(jù)快照全部重寫(xiě)到一個(gè)名為temp-rewriteaof-bg-pid.aof的臨時(shí)AOF文件中。

由于重寫(xiě)操作為子進(jìn)程后臺(tái)執(zhí)行,主進(jìn)程在AOF重寫(xiě)期間依然可以正常響應(yīng)用戶(hù)命令。因此,為了讓子進(jìn)程最終也能獲取重寫(xiě)期間主進(jìn)程產(chǎn)生的增量變化,主進(jìn)程除了會(huì)將執(zhí)行的寫(xiě)命令寫(xiě)入aof_buf,還會(huì)寫(xiě)一份到aof_rewrite_buf中進(jìn)行緩存。在子進(jìn)程重寫(xiě)的后期階段,主進(jìn)程會(huì)將aof_rewrite_buf中累積的數(shù)據(jù)使用pipe發(fā)送給子進(jìn)程,子進(jìn)程會(huì)將這些數(shù)據(jù)追加到臨時(shí)AOF文件中(詳細(xì)原理可參考[1])。

當(dāng)主進(jìn)程承接了較大的寫(xiě)入流量時(shí),aof_rewrite_buf中可能會(huì)堆積非常多的數(shù)據(jù),導(dǎo)致在重寫(xiě)期間子進(jìn)程無(wú)法將aof_rewrite_buf中的數(shù)據(jù)全部消費(fèi)完。此時(shí),aof_rewrite_buf剩余的數(shù)據(jù)將在重寫(xiě)結(jié)束時(shí)由主進(jìn)程進(jìn)行處理。

當(dāng)子進(jìn)程完成重寫(xiě)操作并退出后,主進(jìn)程會(huì)在backgroundRewriteDoneHandler 中處理后續(xù)的事情。首先,將重寫(xiě)期間aof_rewrite_buf中未消費(fèi)完的數(shù)據(jù)追加到臨時(shí)AOF文件中。其次,當(dāng)一切準(zhǔn)備就緒時(shí),Redis會(huì)使用rename 操作將臨時(shí)AOF文件原子的重命名為server.aof_filename,此時(shí)原來(lái)的AOF文件會(huì)被覆蓋。至此,整個(gè)AOFRW流程結(jié)束。

圖1 AOFRW實(shí)現(xiàn)原理

三、 AOFRW存在的問(wèn)題

1. 內(nèi)存開(kāi)銷(xiāo)

由圖1可以看到,在AOFRW期間,主進(jìn)程會(huì)將fork之后的數(shù)據(jù)變化寫(xiě)進(jìn)aof_rewrite_buf中,aof_rewrite_buf和aof_buf中的內(nèi)容絕大部分都是重復(fù)的,因此這將帶來(lái)額外的內(nèi)存冗余開(kāi)銷(xiāo)。

在Redis INFO中的aof_rewrite_buffer_length字段可以看到當(dāng)前時(shí)刻aof_rewrite_buf占用的內(nèi)存大小。如下面顯示的,在高寫(xiě)入流量下aof_rewrite_buffer_length幾乎和aof_buffer_length占用了同樣大的內(nèi)存空間,幾乎浪費(fèi)了一倍的內(nèi)存。

aof_pending_rewrite:0
aof_buffer_length:35500
aof_rewrite_buffer_length:34000
aof_pending_bio_fsync:0

當(dāng)aof_rewrite_buf占用的內(nèi)存大小超過(guò)一定閾值時(shí),我們將在Redis日志中看到如下信息??梢钥吹?,aof_rewrite_buf占用了100MB的內(nèi)存空間且主進(jìn)程和子進(jìn)程之間傳輸了2135MB的數(shù)據(jù)(子進(jìn)程在通過(guò)pipe讀取這些數(shù)據(jù)時(shí)也會(huì)有內(nèi)部讀buffer的內(nèi)存開(kāi)銷(xiāo))。

對(duì)于內(nèi)存型數(shù)據(jù)庫(kù)Redis而言,這是一筆不小的開(kāi)銷(xiāo)。

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帶來(lái)的內(nèi)存開(kāi)銷(xiāo)有可能導(dǎo)致Redis內(nèi)存突然達(dá)到maxmemory限制,從而影響正常命令的寫(xiě)入,甚至?xí)|發(fā)操作系統(tǒng)限制被OOM Killer殺死,導(dǎo)致Redis不可服務(wù)。

2. CPU開(kāi)銷(xiāo)

CPU的開(kāi)銷(xiāo)主要有三個(gè)地方,分別解釋如下:

在AOFRW期間,主進(jìn)程需要花費(fèi)CPU時(shí)間向aof_rewrite_buf寫(xiě)數(shù)據(jù),并使用eventloop事件循環(huán)向子進(jì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) {
// 此處省略其他細(xì)節(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);
}

// 此處省略其他細(xì)節(jié)...
}

在子進(jìn)程執(zhí)行重寫(xiě)操作的后期,會(huì)循環(huán)讀取pipe中主進(jìn)程發(fā)送來(lái)的增量數(shù)據(jù),然后追加寫(xiě)入到臨時(shí)AOF文件:

int rewriteAppendOnlyFile(char *filename) {
// 此處省略其他細(xì)節(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();
}

// 此處省略其他細(xì)節(jié)...
}

在子進(jìn)程完成重寫(xiě)操作后,主進(jìn)程會(huì)在backgroundRewriteDoneHandler 中進(jìn)行收尾工作。其中一個(gè)任務(wù)就是將在重寫(xiě)期間aof_rewrite_buf中沒(méi)有消費(fèi)完成的數(shù)據(jù)寫(xiě)入臨時(shí)AOF文件。如果aof_rewrite_buf中遺留的數(shù)據(jù)很多,這里也將消耗CPU時(shí)間。

void backgroundRewriteDoneHandler(int exitcode, int bysignal) {
// 此處省略其他細(xì)節(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;
}

// 此處省略其他細(xì)節(jié)...
}

AOFRW帶來(lái)的CPU開(kāi)銷(xiāo)可能會(huì)造成Redis在執(zhí)行命令時(shí)出現(xiàn)RT上的抖動(dòng),甚至造成客戶(hù)端超時(shí)的問(wèn)題。

3 .磁盤(pán)IO開(kāi)銷(xiāo)

如前文所述,在AOFRW期間,主進(jìn)程除了會(huì)將執(zhí)行過(guò)的寫(xiě)命令寫(xiě)到aof_buf之外,還會(huì)寫(xiě)一份到aof_rewrite_buf中。aof_buf中的數(shù)據(jù)最終會(huì)被寫(xiě)入到當(dāng)前使用的舊AOF文件中,產(chǎn)生磁盤(pán)IO。同時(shí),aof_rewrite_buf中的數(shù)據(jù)也會(huì)被寫(xiě)入重寫(xiě)生成的新AOF文件中,產(chǎn)生磁盤(pán)IO。因此,同一份數(shù)據(jù)會(huì)產(chǎn)生兩次磁盤(pán)IO。

4. 代碼復(fù)雜度

Redis使用下面所示的六個(gè)pipe進(jìn)行主進(jìn)程和子進(jìn)程之間的數(shù)據(jù)傳輸和控制交互,這使得整個(gè)AOFRW邏輯變得更為復(fù)雜和難以理解。

/* 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實(shí)現(xiàn)

1 .方案概述

顧名思義,MP-AOF就是將原來(lái)的單個(gè)AOF文件拆分成多個(gè)AOF文件。在MP-AOF中,我們將AOF分為三種類(lèi)型,分別為:

BASE:表示基礎(chǔ)AOF,它一般由子進(jìn)程通過(guò)重寫(xiě)產(chǎn)生,該文件最多只有一個(gè)。

INCR:表示增量AOF,它一般會(huì)在AOFRW開(kāi)始執(zhí)行時(shí)被創(chuàng)建,該文件可能存在多個(gè)。

HISTORY:表示歷史AOF,它由BASE和INCR AOF變化而來(lái),每次AOFRW成功完成時(shí),本次AOFRW之前對(duì)應(yīng)的BASE和INCR AOF都將變?yōu)镠ISTORY,HISTORY類(lèi)型的AOF會(huì)被Redis自動(dòng)刪除。

為了管理這些AOF文件,我們引入了一個(gè)manifest(清單)文件來(lái)跟蹤、管理這些AOF。同時(shí),為了便于AOF備份和拷貝,我們將所有的AOF文件和manifest文件放入一個(gè)單獨(dú)的文件目錄中,目錄名由appenddirname配置(Redis 7.0新增配置項(xiàng))決定。

圖2 MP-AOF Rewrite原理

圖2展示的是在MP-AOF中執(zhí)行一次AOFRW的大致流程。在開(kāi)始時(shí)我們依然會(huì)fork一個(gè)子進(jìn)程進(jìn)行重寫(xiě)操作,在主進(jìn)程中,我們會(huì)同時(shí)打開(kāi)一個(gè)新的INCR類(lèi)型的AOF文件,在子進(jìn)程重寫(xiě)操作期間,所有的數(shù)據(jù)變化都會(huì)被寫(xiě)入到這個(gè)新打開(kāi)的INCR AOF中。子進(jìn)程的重寫(xiě)操作完全是獨(dú)立的,重寫(xiě)期間不會(huì)與主進(jìn)程進(jìn)行任何的數(shù)據(jù)和控制交互,最終重寫(xiě)操作會(huì)產(chǎn)生一個(gè)BASE AOF。新生成的BASE AOF和新打開(kāi)的INCR AOF就代表了當(dāng)前時(shí)刻Redis的全部數(shù)據(jù)。AOFRW結(jié)束時(shí),主進(jìn)程會(huì)負(fù)責(zé)更新manifest文件,將新生成的BASE AOF和INCR AOF信息加入進(jìn)去,并將之前的BASE AOF和INCR AOF標(biāo)記為HISTORY(這些HISTORY AOF會(huì)被Redis異步刪除)。一旦manifest文件更新完畢,就標(biāo)志整個(gè)AOFRW流程結(jié)束。

由圖2可以看到,我們?cè)贏OFRW期間不再需要aof_rewrite_buf,因此去掉了對(duì)應(yīng)的內(nèi)存消耗。同時(shí),主進(jìn)程和子進(jìn)程之間也不再有數(shù)據(jù)傳輸和控制交互,因此對(duì)應(yīng)的CPU開(kāi)銷(xiāo)也全部去掉。對(duì)應(yīng)的,前文提及的六個(gè)pipe及其對(duì)應(yīng)的代碼也全部刪除,使得AOFRW邏輯更加簡(jiǎn)單清晰。

2 .關(guān)鍵實(shí)現(xiàn)

Manifest

1)在內(nèi)存中的表示

MP-AOF強(qiáng)依賴(lài)manifest文件,manifest在內(nèi)存中表示為如下結(jié)構(gòu)體,其中:

  • aofInfo:表示一個(gè)AOF文件信息,當(dāng)前僅包括文件名、文件序號(hào)和文件類(lèi)型
  • base_aof_info:表示BASE AOF信息,當(dāng)不存在BASE AOF時(shí),該字段為NULL
  • incr_aof_list:用于存放所有INCR AOF文件的信息,所有的INCR AOF都會(huì)按照文件打開(kāi)順序排放
  • history_aof_list:用于存放HISTORY AOF信息,history_aof_list中的元素都是從base_aof_info和incr_aof_list中move過(guò)來(lái)的
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;

為了便于原子性修改和回滾操作,我們?cè)趓edisServer結(jié)構(gòu)中使用指針的方式引用aofManifest。

struct redisServer {
// 此處省略其他細(xì)節(jié)...

aofManifest *aof_manifest; /* Used to track AOFs. */

// 此處省略其他細(xì)節(jié)...
}

2)在磁盤(pán)上的表示

Manifest本質(zhì)就是一個(gè)包含多行記錄的文本文件,每一行記錄對(duì)應(yīng)一個(gè)AOF文件信息,這些信息通過(guò)key/value對(duì)的方式展示,便于Redis處理、易于閱讀和修改。下面是一個(gè)可能的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格式本身需要具有一定的擴(kuò)展性,以便將來(lái)添加或支持其他的功能。比如可以方便的支持新增key/value和注解(類(lèi)似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è)置值(默認(rèn)為appendonly.aof)。

在MP-AOF中,我們使用basename.suffix的方式命名多個(gè)AOF文件。其中,appendfilename配置內(nèi)容將作為basename部分,suffix則由三個(gè)部分組成,格式為seq.type.format ,其中:

  • seq為文件的序號(hào),由1開(kāi)始單調(diào)遞增,BASE和INCR擁有獨(dú)立的文件序號(hào)
  • type為AOF的類(lèi)型,表示這個(gè)AOF文件是BASE還是INCR
  • format用來(lái)表示這個(gè)AOF內(nèi)部的編碼方式,由于Redis支持RDB preamble機(jī)制,因此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"

因此,當(dāng)使用appendfilename默認(rèn)配置時(shí),BASE、INCR和manifest文件的可能命名如下:

appendonly.aof.1.base.rdb // 開(kāi)啟RDB preamble
appendonly.aof.1.base.aof // 關(guān)閉RDB preamble
appendonly.aof.1.incr.aof
appendonly.aof.2.incr.aof

兼容老版本升級(jí)

由于MP-AOF強(qiáng)依賴(lài)manifest文件,Redis啟動(dòng)時(shí)會(huì)嚴(yán)格按照manifest的指示加載對(duì)應(yīng)的AOF文件。但是在從老版本Redis(指Redis 7.0之前的版本)升級(jí)到Redis 7.0時(shí),由于此時(shí)并無(wú)manifest文件,因此如何讓Redis正確識(shí)別這是一個(gè)升級(jí)過(guò)程并正確、安全的加載舊AOF是一個(gè)必須支持的能力。

識(shí)別能力是這一重要過(guò)程的首要環(huán)節(jié),在真正加載AOF文件之前,我們會(huì)檢查Redis工作目錄下是否存在名為server.aof_filename的AOF文件。如果存在,那說(shuō)明我們可能在從一個(gè)老版本Redis執(zhí)行升級(jí),接下來(lái),我們會(huì)繼續(xù)判斷,當(dāng)滿(mǎn)足下面三種情況之一時(shí)我們會(huì)認(rèn)為這是一個(gè)升級(jí)啟動(dòng):

  • 如果appenddirname目錄不存在
  • 或者appenddirname目錄存在,但是目錄中沒(méi)有對(duì)應(yīng)的manifest清單文件
  • 如果appenddirname目錄存在且目錄中存在manifest清單文件,且清單文件中只有BASE AOF相關(guān)信息,且這個(gè)BASE AOF的名字和server.aof_filename相同,且appenddirname目錄中不存在名為server.aof_filename的文件
/* Load the AOF files according the aofManifest pointed by am. */
int loadAppendOnlyFiles(aofManifest *am) {
// 此處省略其他細(xì)節(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);
}
}

// 此處省略其他細(xì)節(jié)...
}

一旦被識(shí)別為這是一個(gè)升級(jí)啟動(dòng),我們會(huì)使用aofUpgradePrepare 函數(shù)進(jìn)行升級(jí)前的準(zhǔn)備工作。

升級(jí)準(zhǔn)備工作主要分為三個(gè)部分:

使用server.aof_filename作為文件名來(lái)構(gòu)造一個(gè)BASE AOF信息

將該BASE AOF信息持久化到manifest文件

使用rename 將舊AOF文件移動(dòng)到appenddirname目錄中

void aofUpgradePrepare(aofManifest *am) {
// 此處省略其他細(xì)節(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);;
}

// 此處省略其他細(xì)節(jié)...
}

升級(jí)準(zhǔn)備操作是Crash Safety的,以上三步中任何一步發(fā)生Crash我們都能在下一次的啟動(dòng)中正確的識(shí)別并重試整個(gè)升級(jí)操作。

多文件加載及進(jìn)度計(jì)算

Redis在加載AOF時(shí)會(huì)記錄加載的進(jìn)度,并通過(guò)Redis INFO的loading_loaded_perc字段展示出來(lái)。在MP-AOF中,loadAppendOnlyFiles 函數(shù)會(huì)根據(jù)傳入的aofManifest進(jìn)行AOF文件加載。在進(jìn)行加載之前,我們需要提前計(jì)算所有待加載的AOF文件的總大小,并傳給startLoading 函數(shù),然后在loadSingleAppendOnlyFile 中不斷的上報(bào)加載進(jìn)度。

接下來(lái),loadAppendOnlyFiles 會(huì)根據(jù)aofManifest依次加載BASE AOF和INCR AOF。當(dāng)前加載完所有的AOF文件,會(huì)使用stopLoading 結(jié)束加載狀態(tài)。

int loadAppendOnlyFiles(aofManifest *am) {
// 此處省略其他細(xì)節(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();

// 此處省略其他細(xì)節(jié)...
}

AOFRW Crash Safety

當(dāng)子進(jìn)程完成重寫(xiě)操作,子進(jìn)程會(huì)創(chuàng)建一個(gè)名為temp-rewriteaof-bg-pid.aof的臨時(shí)AOF文件,此時(shí)這個(gè)文件對(duì)Redis而言還是不可見(jiàn)的,因?yàn)樗€沒(méi)有被加入到manifest文件中。要想使得它能被Redis識(shí)別并在Redis啟動(dòng)時(shí)正確加載,我們還需要將它按照前文提到的命名規(guī)則進(jìn)行rename 操作,并將其信息加入到manifest文件中。

AOF文件rename 和manifest文件修改雖然是兩個(gè)獨(dú)立操作,但我們必須保證這兩個(gè)操作的原子性,這樣才能讓Redis在啟動(dòng)時(shí)能正確的加載對(duì)應(yīng)的AOF。MP-AOF使用兩個(gè)設(shè)計(jì)來(lái)解決這個(gè)問(wèn)題:

  • BASE AOF的名字中包含文件序號(hào),保證每次創(chuàng)建的BASE AOF不會(huì)和之前的BASE AOF沖突;
  • 先執(zhí)行AOF的rename 操作,再修改manifest文件;

為了便于說(shuō)明,我們假設(shè)在AOFRW開(kāi)始之前,manifest文件內(nèi)容如下:

le appendonly.aof.1.base.rdb seq 1 type b
file appendonly.aof.1.incr.aof seq 1 type i

則在AOFRW開(kāi)始執(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

子進(jìn)程重寫(xiě)結(jié)束后,在主進(jìn)程中,我們會(huì)將temp-rewriteaof-bg-pid.aof重命名為appendonly.aof.2.base.rdb,并將其加入manifest中,同時(shí)會(huì)將之前的BASE和INCR AOF標(biāo)記為HISTORY。此時(shí)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

此時(shí),本次AOFRW的結(jié)果對(duì)Redis可見(jiàn),HISTORY AOF會(huì)被Redis異步清理。

backgroundRewriteDoneHandler 函數(shù)通過(guò)七個(gè)步驟實(shí)現(xiàn)了上述邏輯:

  • 在修改內(nèi)存中的server.aof_manifest前,先dup一份臨時(shí)的manifest結(jié)構(gòu),接下來(lái)的修改都將針對(duì)這個(gè)臨時(shí)的manifest進(jìn)行。這樣做的好處是,一旦后面的步驟出現(xiàn)失敗,我們可以簡(jiǎn)單的銷(xiāo)毀臨時(shí)manifest從而回滾整個(gè)操作,避免污染server.aof_manifest全局?jǐn)?shù)據(jù)結(jié)構(gòu);
  • 從臨時(shí)manifest中獲取新的BASE AOF文件名(記為new_base_filename),并將之前(如果有)的BASE AOF標(biāo)記為HISTORY;
  • 將子進(jìn)程產(chǎn)生的temp-rewriteaof-bg-pid.aof臨時(shí)文件重命名為new_base_filename;
  • 將臨時(shí)manifest結(jié)構(gòu)中上一次的INCR AOF全部標(biāo)記為HISTORY類(lèi)型;
  • 將臨時(shí)manifest對(duì)應(yīng)的信息持久化到磁盤(pán)(persistAofManifest內(nèi)部會(huì)保證manifest本身修改的原子性);
  • 如果上述步驟都成功了,我們可以放心的將內(nèi)存中的server.aof_manifest指針指向臨時(shí)的manifest結(jié)構(gòu)(并釋放之前的manifest結(jié)構(gòu)),至此整個(gè)修改對(duì)Redis可見(jiàn);
  • 清理HISTORY類(lèi)型的AOF,該步驟允許失敗,因?yàn)樗粫?huì)導(dǎo)致數(shù)據(jù)一致性問(wèn)題。
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

在進(jìn)程出現(xiàn)Crash時(shí)AOF文件很可能出現(xiàn)寫(xiě)入不完整的問(wèn)題,如一條事務(wù)里只寫(xiě)了MULTI,但是還沒(méi)寫(xiě)EXEC時(shí)Redis就Crash。默認(rèn)情況下,Redis無(wú)法加載這種不完整的AOF,但是Redis支持AOF truncate功能(通過(guò)aof-load-truncated配置打開(kāi))。其原理是使用server.aof_current_size跟蹤AOF最后一個(gè)正確的文件偏移,然后使用ftruncate 函數(shù)將該偏移之后的文件內(nèi)容全部刪除,這樣雖然可能會(huì)丟失部分?jǐn)?shù)據(jù),但可以保證AOF的完整性。

在MP-AOF中,server.aof_current_size已經(jīng)不再表示單個(gè)AOF文件的大小而是所有AOF文件的總大小。因?yàn)橹挥凶詈笠粋€(gè)INCR AOF才有可能出現(xiàn)不完整寫(xiě)入的問(wèn)題,因此我們引入了一個(gè)單獨(dú)的字段server.aof_last_incr_size用于跟蹤最后一個(gè)INCR AOF文件的大小。當(dāng)最后一個(gè)INCR AOF出現(xiàn)不完整寫(xiě)入時(shí),我們只需要將server.aof_last_incr_size之后的文件內(nèi)容刪除即可。

if (ftruncate(server.aof_fd, server.aof_last_incr_size) == -1) {
//此處省略其他細(xì)節(jié)...
}

AOFRW限流

Redis在AOF大小超過(guò)一定閾值時(shí)支持自動(dòng)執(zhí)行AOFRW,當(dāng)出現(xiàn)磁盤(pán)故障或者觸發(fā)了代碼bug導(dǎo)致AOFRW失敗時(shí),Redis將不停的重復(fù)執(zhí)行AOFRW直到成功為止。在MP-AOF出現(xiàn)之前,這看似沒(méi)有什么大問(wèn)題(頂多就是消耗一些CPU時(shí)間和fork開(kāi)銷(xiāo))。但是在MP-AOF中,因?yàn)槊看蜛OFRW都會(huì)打開(kāi)一個(gè)INCR AOF,并且只有在AOFRW成功時(shí)才會(huì)將上一個(gè)INCR和BASE轉(zhuǎn)為HISTORY并刪除。因此,連續(xù)的AOFRW失敗勢(shì)必會(huì)導(dǎo)致多個(gè)INCR AOF并存的問(wèn)題。極端情況下,如果AOFRW重試頻率很高我們將會(huì)看到成百上千個(gè)INCR AOF文件。

為此,我們引入了AOFRW限流機(jī)制。即當(dāng)AOFRW已經(jīng)連續(xù)失敗三次時(shí),下一次的AOFRW會(huì)被強(qiáng)行延遲1分鐘執(zhí)行,如果下一次AOFRW依然失敗,則會(huì)延遲2分鐘,依次類(lèi)推延遲4、8、16...,當(dāng)前最大延遲時(shí)間為1小時(shí)。

在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限流機(jī)制的引入,還可以有效的避免AOFRW高頻重試帶來(lái)的CPU和fork開(kāi)銷(xiāo)。Redis中很多的RT抖動(dòng)都和fork有關(guān)系。

五、 總結(jié)

MP-AOF的引入,成功的解決了之前AOFRW存在的內(nèi)存和CPU開(kāi)銷(xiāo)對(duì)Redis實(shí)例甚至業(yè)務(wù)訪(fǎng)問(wèn)帶來(lái)的不利影響。同時(shí),在解決這些問(wèn)題的過(guò)程中,我們也遇到了很多未曾預(yù)料的挑戰(zhàn),這些挑戰(zhàn)主要來(lái)自于Redis龐大的使用群體、多樣化的使用場(chǎng)景,因此我們必須考慮用戶(hù)在各種場(chǎng)景下使用MP-AOF可能遇到的問(wèn)題。如兼容性、易用性以及對(duì)Redis代碼盡可能的減少侵入性等。這都是Redis社區(qū)功能演進(jìn)的重中之重。

同時(shí),MP-AOF的引入也為Redis的數(shù)據(jù)持久化帶來(lái)了更多的想象空間。如在開(kāi)啟aof-use-rdb-preamble時(shí),BASE AOF本質(zhì)是一個(gè)RDB文件,因此我們?cè)谶M(jìn)行全量備份的時(shí)候無(wú)需在單獨(dú)執(zhí)行一次BGSAVE操作。直接備份BASE AOF即可。MP-AOF支持關(guān)閉自動(dòng)清理HISTORY AOF的能力,因此那些歷史的AOF有機(jī)會(huì)得以保留,并且目前Redis已經(jīng)支持在AOF中加入timestamp annotation,因此基于這些我們甚至可以實(shí)現(xiàn)一個(gè)簡(jiǎn)單的PITR能力( point-in-time recovery)。

MP-AOF的設(shè)計(jì)原型來(lái)自于Tair for redis企業(yè)版[2]的binlog實(shí)現(xiàn),這是一套在阿里云Tair服務(wù)上久經(jīng)驗(yàn)證的核心功能,在這個(gè)核心功能上阿里云Tair成功構(gòu)建了全球多活、PITR等企業(yè)級(jí)能力,使用戶(hù)的更多業(yè)務(wù)場(chǎng)景需求得到滿(mǎn)足。今天我們將這個(gè)核心能力貢獻(xiàn)給Redis社區(qū),希望社區(qū)用戶(hù)也能享受這些企業(yè)級(jí)特性,并通過(guò)這些企業(yè)級(jí)特性更好的優(yōu)化,創(chuàng)造自己的業(yè)務(wù)代碼。有關(guān)MP-AOF的更多細(xì)節(jié),請(qǐng)移步參考相關(guān)PR(#9788),那里有更多的原始設(shè)計(jì)和完整代碼。

[1]http://mysql.taobao.org/monthly/2018/12/06/[2]https://help.aliyun.com/document_detail/145956.html


網(wǎng)站標(biāo)題:Redis7.0MultiPartAOF的設(shè)計(jì)和實(shí)現(xiàn)
文章轉(zhuǎn)載:http://www.5511xx.com/article/cdhpjdj.html