Redis7.0MultiPartAOF的設計和實現

Redis 作為一種非常流行的記憶體資料庫,透過將資料儲存在記憶體中,Redis 得以擁有極高的讀寫效能。但是一旦程序退出,Redis 的資料就會全部丟失。
為了解決這個問題,Redis 提供了 RDB 和 AOF 兩種持久化方案,將記憶體中的資料儲存到磁碟中,避免資料丟失。本文將重點討論AOF持久化方案,以及其存在的一些問題,並探討在Redis 7.0 (已釋出RC1) 中Multi Part AOF(下文簡稱為MP-AOF,本特性由阿里雲資料庫Tair團隊貢獻)設計和實現細節。

一  AOF

AOF( append only file )持久化以獨立日誌檔案的方式記錄每條寫命令,並在 Redis 啟動時回放 AOF 檔案中的命令以達到恢復資料的目的。
由於AOF會以追加的方式記錄每一條redis的寫命令,因此隨著Redis處理的寫命令增多,AOF檔案也會變得越來越大,命令回放的時間也會增多,為了解決這個問題,Redis引入了AOF rewrite機制(下文稱之為AOFRW)。AOFRW會移除AOF中冗餘的寫命令,以等效的方式重寫、生成一個新的AOF檔案,來達到減少AOF檔案大小的目的。

二  AOFRW

圖1展示的是AOFRW的實現原理。當AOFRW被觸發執行時,Redis首先會fork一個子程序進行後臺重寫操作,該操作會將執行fork那一刻Redis的資料快照全部重寫到一個名為temp-rewriteaof-bg-pid.aof的臨時AOF檔案中。 
由於重寫操作為子程序後臺執行,主程序在AOF重寫期間依然可以正常響應使用者命令。因此,為了讓子程序最終也能獲取重寫期間主程序產生的增量變化,主程序除了會將執行的寫命令寫入aof_buf,還會寫一份到aof_rewrite_buf中進行快取。在子程序重寫的後期階段,主程序會將aof_rewrite_buf中累積的資料使用pipe傳送給子程序,子程序會將這些資料追加到臨時AOF檔案中(詳細原理可參考[1])。
當主程序承接了較大的寫入流量時,aof_rewrite_buf中可能會堆積非常多的資料,導致在重寫期間子程序無法將aof_rewrite_buf中的資料全部消費完。此時,aof_rewrite_buf剩餘的資料將在重寫結束時由主程序進行處理。
當子程序完成重寫操作並退出後,主程序會在backgroundRewriteDoneHandler 中處理後續的事情。首先,將重寫期間aof_rewrite_buf中未消費完的資料追加到臨時AOF檔案中。其次,當一切準備就緒時,Redis會使用rename 操作將臨時AOF檔案原子的重新命名為server.aof_filename,此時原來的AOF檔案會被覆蓋。至此,整個AOFRW流程結束。
圖1 AOFRW實現原理

三  AOFRW存在的問題

1  記憶體開銷

由圖1可以看到,在AOFRW期間,主程序會將fork之後的資料變化寫進aof_rewrite_buf中,aof_rewrite_buf和aof_buf中的內容絕大部分都是重複的,因此這將帶來額外的記憶體冗餘開銷。
在Redis INFO中的aof_rewrite_buffer_length欄位可以看到當前時刻aof_rewrite_buf佔用的記憶體大小。如下面顯示的,在高寫入流量下aof_rewrite_buffer_length幾乎和aof_buffer_length佔用了同樣大的記憶體空間,幾乎浪費了一倍的記憶體。
aof_pending_rewrite:0aof_buffer_length:35500aof_rewrite_buffer_length:34000aof_pending_bio_fsync:0
當aof_rewrite_buf佔用的記憶體大小超過一定閾值時,我們將在Redis日誌中看到如下資訊。可以看到,aof_rewrite_buf佔用了100MB的記憶體空間且主程序和子程序之間傳輸了2135MB的資料(子程序在透過pipe讀取這些資料時也會有內部讀buffer的記憶體開銷)。
對於記憶體型資料庫Redis而言,這是一筆不小的開銷。
3351:M 25 Jan 2022 09:55:39.655 * Backgroundappendonlyfilerewritingstartedbypid 68173351:M 25 Jan 2022 09:57:51.864 * AOFrewritechildaskstostopsendingdiffs.6817:C 25 Jan 2022 09:57:51.864 * Parentagreedtostopsendingdiffs. FinalizingAOF...6817:C 25 Jan 2022 09:57:51.864 * Concatenating 2135.60MBofAOFdiffreceivedfromparent.3351:M 25 Jan 2022 09:57:56.545 * BackgroundAOFbuffersize: 100 MB
AOFRW帶來的記憶體開銷有可能導致Redis記憶體突然達到maxmemory限制,從而影響正常命令的寫入,甚至會觸發作業系統限制被OOM Killer殺死,導致Redis不可服務。

2  CPU開銷

CPU的開銷主要有三個地方,分別解釋如下:
  1. 在AOFRW期間,主程序需要花費CPU時間向aof_rewrite_buf寫資料,並使用eventloop事件迴圈向子程序傳送aof_rewrite_buf中的資料:
/* Append data to the AOF rewrite buffer, allocating new blocks if needed. */voidaofRewriteBufferAppend(unsignedchar *s, unsignedlong len){// 此處省略其他細節.../* 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); } // 此處省略其他細節...}
  1. 在子程序執行重寫操作的後期,會迴圈讀取pipe中主程序傳送來的增量資料,然後追加寫入到臨時AOF檔案:
intrewriteAppendOnlyFile(char *filename){// 此處省略其他細節.../* 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(); }// 此處省略其他細節...}
  1. 在子程序完成重寫操作後,主程序會在backgroundRewriteDoneHandler 中進行收尾工作。其中一個任務就是將在重寫期間aof_rewrite_buf中沒有消費完成的資料寫入臨時AOF檔案。如果aof_rewrite_buf中遺留的資料很多,這裡也將消耗CPU時間。
voidbackgroundRewriteDoneHandler(int exitcode, int bysignal) {// 此處省略其他細節.../* 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; }// 此處省略其他細節...}
AOFRW帶來的CPU開銷可能會造成Redis在執行命令時出現RT上的抖動,甚至造成客戶端超時的問題。

3  磁碟IO開銷

如前文所述,在AOFRW期間,主程序除了會將執行過的寫命令寫到aof_buf之外,還會寫一份到aof_rewrite_buf中。aof_buf中的資料最終會被寫入到當前使用的舊AOF檔案中,產生磁碟IO。同時,aof_rewrite_buf中的資料也會被寫入重寫生成的新AOF檔案中,產生磁碟IO。因此,同一份資料會產生兩次磁碟IO。

4  程式碼複雜度

Redis使用下面所示的六個pipe進行主程序和子程序之間的資料傳輸和控制互動,這使得整個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實現

1  方案概述

顧名思義,MP-AOF就是將原來的單個AOF檔案拆分成多個AOF檔案。在MP-AOF中,我們將AOF分為三種類型,分別為:
  • BASE:表示基礎AOF,它一般由子程序透過重寫產生,該檔案最多隻有一個。
  • INCR:表示增量AOF,它一般會在AOFRW開始執行時被建立,該檔案可能存在多個。
  • HISTORY:表示歷史AOF,它由BASE和INCR AOF變化而來,每次AOFRW成功完成時,本次AOFRW之前對應的BASE和INCR AOF都將變為HISTORY,HISTORY型別的AOF會被Redis自動刪除。
為了管理這些AOF檔案,我們引入了一個manifest(清單)檔案來跟蹤、管理這些AOF。同時,為了便於AOF備份和複製,我們將所有的AOF檔案和manifest檔案放入一個單獨的檔案目錄中,目錄名由appenddirname配置(Redis 7.0新增配置項)決定。
圖2 MP-AOF Rewrite原理
圖2展示的是在MP-AOF中執行一次AOFRW的大致流程。在開始時我們依然會fork一個子程序進行重寫操作,在主程序中,我們會同時開啟一個新的INCR型別的AOF檔案,在子程序重寫操作期間,所有的資料變化都會被寫入到這個新開啟的INCR AOF中。子程序的重寫操作完全是獨立的,重寫期間不會與主程序進行任何的資料和控制互動,最終重寫操作會產生一個BASE AOF。新生成的BASE AOF和新開啟的INCR AOF就代表了當前時刻Redis的全部資料。AOFRW結束時,主程序會負責更新manifest檔案,將新生成的BASE AOF和INCR AOF資訊加入進去,並將之前的BASE AOF和INCR  AOF標記為HISTORY(這些HISTORY AOF會被Redis非同步刪除)。一旦manifest檔案更新完畢,就標誌整個AOFRW流程結束。
由圖2可以看到,我們在AOFRW期間不再需要aof_rewrite_buf,因此去掉了對應的記憶體消耗。同時,主程序和子程序之間也不再有資料傳輸和控制互動,因此對應的CPU開銷也全部去掉。對應的,前文提及的六個pipe及其對應的程式碼也全部刪除,使得AOFRW邏輯更加簡單清晰。

2  關鍵實現

Manifest

1)在記憶體中的表示

MP-AOF強依賴manifest檔案,manifest在記憶體中表示為如下結構體,其中:
  • 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過來的
typedefstruct { sds file_name; /* file name */longlong file_seq; /* file sequence */ aof_file_type file_type; /* file type */} aofInfo;typedefstruct { 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. */longlong curr_base_file_seq; /* The sequence number used by the current BASE file. */longlong 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結構中使用指標的方式引用aofManifest。
structredisServer {// 此處省略其他細節... aofManifest *aof_manifest; /* Used to track AOFs. */// 此處省略其他細節...}

2)在磁碟上的表示

Manifest本質就是一個包含多行記錄的文字檔案,每一行記錄對應一個AOF檔案資訊,這些資訊透過key/value對的方式展示,便於Redis處理、易於閱讀和修改。下面是一個可能的manifest檔案內容:
fileappendonly.aof.1.base.rdbseq 1 typebfileappendonly.aof.1.incr.aofseq 1 typeifileappendonly.aof.2.incr.aofseq 2 typei
Manifest格式本身需要具有一定的擴充套件性,以便將來新增或支援其他的功能。比如可以方便的支援新增key/value和註解(類似AOF中的註解),這樣可以保證較好的forward compatibility。
fileappendonly.aof.1.base.rdbseq 1 typebnewkeynewvaluefileappendonly.aof.1.incr.aoftypeiseq 1 # thisisannotationsseq 2 typeifileappendonly.aof.2.incr.aof

檔案命名規則

在MP-AOF之前,AOF的檔名為appendfilename引數的設定值(預設為appendonly.aof)。
在MP-AOF中,我們使用basename.suffix的方式命名多個AOF檔案。其中,appendfilename配置內容將作為basename部分,suffix則由三個部分組成,格式為seq.type.format ,其中:
  • seq為檔案的序號,由1開始單調遞增,BASE和INCR擁有獨立的檔案序號
  • type為AOF的型別,表示這個AOF檔案是BASE還是INCR
  • format用來表示這個AOF內部的編碼方式,由於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 preambleappendonly.aof.1.base.aof // 關閉RDB preambleappendonly.aof.1.incr.aofappendonly.aof.2.incr.aof

相容老版本升級

由於MP-AOF強依賴manifest檔案,Redis啟動時會嚴格按照manifest的指示載入對應的AOF檔案。但是在從老版本Redis(指Redis 7.0之前的版本)升級到Redis 7.0時,由於此時並無manifest檔案,因此如何讓Redis正確識別這是一個升級過程並正確、安全的載入舊AOF是一個必須支援的能力。
識別能力是這一重要過程的首要環節,在真正載入AOF檔案之前,我們會檢查Redis工作目錄下是否存在名為server.aof_filename的AOF檔案。如果存在,那說明我們可能在從一個老版本Redis執行升級,接下來,我們會繼續判斷,當滿足下面三種情況之一時我們會認為這是一個升級啟動:
  1. 如果appenddirname目錄不存在
  2. 或者appenddirname目錄存在,但是目錄中沒有對應的manifest清單檔案
  3. 如果appenddirname目錄存在且目錄中存在manifest清單檔案,且清單檔案中只有BASE AOF相關資訊,且這個BASE AOF的名字和server.aof_filename相同,且appenddirname目錄中不存在名為server.aof_filename的檔案
/* Load the AOF files according the aofManifest pointed by am. */int loadAppendOnlyFiles(aofManifest *am) {// 此處省略其他細節.../* 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); } }// 此處省略其他細節... }
一旦被識別為這是一個升級啟動,我們會使用aofUpgradePrepare 函式進行升級前的準備工作。
升級準備工作主要分為三個部分:
  1. 使用server.aof_filename作為檔名來構造一個BASE AOF資訊
  2. 將該BASE AOF資訊持久化到manifest檔案
  3. 使用rename 將舊AOF檔案移動到appenddirname目錄中
void aofUpgradePrepare(aofManifest *am) {// 此處省略其他細節.../* 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);; }// 此處省略其他細節...}
升級準備操作是Crash Safety的,以上三步中任何一步發生Crash我們都能在下一次的啟動中正確的識別並重試整個升級操作。

多檔案載入及進度計算

Redis在載入AOF時會記錄載入的進度,並透過Redis INFO的loading_loaded_perc欄位展示出來。在MP-AOF中,loadAppendOnlyFiles函式會根據傳入的aofManifest進行AOF檔案載入。在進行載入之前,我們需要提前計算所有待載入的AOF檔案的總大小,並傳給startLoading 函式,然後在loadSingleAppendOnlyFile 中不斷的上報載入進度。
接下來,loadAppendOnlyFiles 會根據aofManifest依次載入BASE AOF和INCR AOF。當前載入完所有的AOF檔案,會使用stopLoading 結束載入狀態。
int loadAppendOnlyFiles(aofManifest *am) {// 此處省略其他細節.../* 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();// 此處省略其他細節...}

AOFRW Crash Safety

當子程序完成重寫操作,子程序會建立一個名為temp-rewriteaof-bg-pid.aof的臨時AOF檔案,此時這個檔案對Redis而言還是不可見的,因為它還沒有被加入到manifest檔案中。要想使得它能被Redis識別並在Redis啟動時正確載入,我們還需要將它按照前文提到的命名規則進行rename 操作,並將其資訊加入到manifest檔案中。
AOF檔案rename 和manifest檔案修改雖然是兩個獨立操作,但我們必須保證這兩個操作的原子性,這樣才能讓Redis在啟動時能正確的載入對應的AOF。MP-AOF使用兩個設計來解決這個問題:
  1. BASE AOF的名字中包含檔案序號,保證每次建立的BASE AOF不會和之前的BASE AOF衝突;
  2. 先執行AOF的rename 操作,再修改manifest檔案;
為了便於說明,我們假設在AOFRW開始之前,manifest檔案內容如下:
fileappendonly.aof.1.base.rdbseq 1 typebfileappendonly.aof.1.incr.aofseq 1 typei
則在AOFRW開始執行後manifest檔案內容如下:
fileappendonly.aof.1.base.rdbseq 1 typebfileappendonly.aof.1.incr.aofseq 1 typeifileappendonly.aof.2.incr.aofseq 2 typei
子程序重寫結束後,在主程序中,我們會將temp-rewriteaof-bg-pid.aof重新命名為appendonly.aof.2.base.rdb,並將其加入manifest中,同時會將之前的BASE和INCR AOF標記為HISTORY。此時manifest檔案內容如下:
fileappendonly.aof.2.base.rdbseq 2 typebfileappendonly.aof.1.base.rdbseq 1 typehfileappendonly.aof.1.incr.aofseq 1 typehfileappendonly.aof.2.incr.aofseq 2 typei
此時,本次AOFRW的結果對Redis可見,HISTORY AOF會被Redis非同步清理。
backgroundRewriteDoneHandler 函式透過七個步驟實現了上述邏輯:
  1. 在修改記憶體中的server.aof_manifest前,先dup一份臨時的manifest結構,接下來的修改都將針對這個臨時的manifest進行。這樣做的好處是,一旦後面的步驟出現失敗,我們可以簡單的銷燬臨時manifest從而回滾整個操作,避免汙染server.aof_manifest全域性資料結構;
  2. 從臨時manifest中獲取新的BASE AOF檔名(記為new_base_filename),並將之前(如果有)的BASE AOF標記為HISTORY;
  3. 將子程序產生的temp-rewriteaof-bg-pid.aof臨時檔案重新命名為new_base_filename;
  4. 將臨時manifest結構中上一次的INCR  AOF全部標記為HISTORY型別;
  5. 將臨時manifest對應的資訊持久化到磁碟(persistAofManifest內部會保證manifest本身修改的原子性);
  6. 如果上述步驟都成功了,我們可以放心的將記憶體中的server.aof_manifest指標指向臨時的manifest結構(並釋放之前的manifest結構),至此整個修改對Redis可見;
  7. 清理HISTORY型別的AOF,該步驟允許失敗,因為它不會導致資料一致性問題。
voidbackgroundRewriteDoneHandler(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 

在程序出現Crash時AOF檔案很可能出現寫入不完整的問題,如一條事務裡只寫了MULTI,但是還沒寫EXEC時Redis就Crash。預設情況下,Redis無法載入這種不完整的AOF,但是Redis支援AOF truncate功能(透過aof-load-truncated配置開啟)。其原理是使用server.aof_current_size跟蹤AOF最後一個正確的檔案偏移,然後使用ftruncate 函式將該偏移之後的檔案內容全部刪除,這樣雖然可能會丟失部分資料,但可以保證AOF的完整性。
在MP-AOF中,server.aof_current_size已經不再表示單個AOF檔案的大小而是所有AOF檔案的總大小。因為只有最後一個INCR AOF才有可能出現不完整寫入的問題,因此我們引入了一個單獨的欄位server.aof_last_incr_size用於跟蹤最後一個INCR AOF檔案的大小。當最後一個INCR AOF出現不完整寫入時,我們只需要將server.aof_last_incr_size之後的檔案內容刪除即可。
if (ftruncate(server.aof_fd, server.aof_last_incr_size) == -1) {//此處省略其他細節... }

AOFRW限流

Redis在AOF大小超過一定閾值時支援自動執行AOFRW,當出現磁碟故障或者觸發了程式碼bug導致AOFRW失敗時,Redis將不停的重複執行AOFRW直到成功為止。在MP-AOF出現之前,這看似沒有什麼大問題(頂多就是消耗一些CPU時間和fork開銷)。但是在MP-AOF中,因為每次AOFRW都會開啟一個INCR AOF,並且只有在AOFRW成功時才會將上一個INCR和BASE轉為HISTORY並刪除。因此,連續的AOFRW失敗勢必會導致多個INCR AOF並存的問題。極端情況下,如果AOFRW重試頻率很高我們將會看到成百上千個INCR AOF檔案。
為此,我們引入了AOFRW限流機制。即當AOFRW已經連續失敗三次時,下一次的AOFRW會被強行延遲1分鐘執行,如果下一次AOFRW依然失敗,則會延遲2分鐘,依次類推延遲4、8、16…,當前最大延遲時間為1小時。
在AOFRW限流期間,我們依然可以使用bgrewriteaof命令立即執行一次AOFRW。
if (server.aof_state == AOF_ON && !hasActiveChildProcess() && server.aof_rewrite_perc && server.aof_current_size > server.aof_rewrite_min_size && !aofRewriteLimited()){longlongbase = server.aof_rewrite_base_size ? server.aof_rewrite_base_size : 1;longlong growth = (server.aof_current_size*100/base) - 100;if (growth >= server.aof_rewrite_perc) { rewriteAppendOnlyFileBackground(); }}
AOFRW限流機制的引入,還可以有效的避免AOFRW高頻重試帶來的CPU和fork開銷。Redis中很多的RT抖動都和fork有關係。

五  總結

MP-AOF的引入,成功的解決了之前AOFRW存在的記憶體和CPU開銷對Redis例項甚至業務訪問帶來的不利影響。同時,在解決這些問題的過程中,我們也遇到了很多未曾預料的挑戰,這些挑戰主要來自於Redis龐大的使用群體、多樣化的使用場景,因此我們必須考慮使用者在各種場景下使用MP-AOF可能遇到的問題。如相容性、易用性以及對Redis程式碼儘可能的減少侵入性等。這都是Redis社群功能演進的重中之重。
同時,MP-AOF的引入也為Redis的資料持久化帶來了更多的想象空間。如在開啟aof-use-rdb-preamble時,BASE AOF本質是一個RDB檔案,因此我們在進行全量備份的時候無需在單獨執行一次BGSAVE操作。直接備份BASE AOF即可。MP-AOF支援關閉自動清理HISTORY AOF的能力,因此那些歷史的AOF有機會得以保留,並且目前Redis已經支援在AOF中加入timestamp annotation,因此基於這些我們甚至可以實現一個簡單的PITR能力( point-in-time recovery)。
MP-AOF的設計原型來自於Tair for redis企業版[2]的binlog實現,這是一套在阿里雲Tair服務上久經驗證的核心功能,在這個核心功能上阿里雲Tair成功構建了全球多活、PITR等企業級能力,使使用者的更多業務場景需求得到滿足。今天我們將這個核心能力貢獻給Redis社群,希望社群使用者也能享受這些企業級特性,並透過這些企業級特性更好的最佳化,創造自己的業務程式碼。有關MP-AOF的更多細節,請移步參考相關PR(#9788),那裡有更多的原始設計和完整程式碼。
[1]http://mysql.taobao.org/monthly/2018/12/06/
[2]https://help.aliyun.com/document_detail/145956.html

搜尋與推薦技術實戰訓練營

點選閱讀原文檢視詳情

相關文章