故障分析 | Redis AOF 重寫原始碼分析

愛可生雲資料庫發表於2023-02-09

作者:朱鵬舉

新人 DBA ,會點 MySQL ,Redis ,Oracle ,在知識的海洋中掙扎,活下來就算成功...

本文來源:原創投稿

*愛可生開源社群出品,原創內容未經授權不得隨意使用,轉載請聯絡小編並註明來源。


AOF 作為 Redis 的資料持久化方式之一,透過追加寫的方式將 Redis 伺服器所執行的寫命令寫入到 AOF 日誌中來記錄資料庫的狀態。但當一個鍵值對被多條寫命令反覆修改時,AOF 日誌會記錄相應的所有命令,這也就意味著 AOF 日誌中存在重複的"無效命令",造成的結果就是 AOF 日誌檔案越來越大,使用 AOF 日誌來進行資料恢復所需的時間越來越長。為瞭解決這個問題,Redis 推出了 AOF 重寫功能

什麼是 AOF 重寫

簡單來說,AOF 重寫就是根據當時鍵值對的最新狀態,為它生成對應的寫入命令,然後寫入到臨時 AOF 日誌中。在重寫期間 Redis 會將發生更改的資料寫入到重寫緩衝區 aof_rewrite_buf_blocks 中,於重寫結束後合併到臨時 AOF 日誌中,最後使用臨時 AOF 日誌替換原來的 AOF 日誌。當然,為了避免阻塞主執行緒,Redis 會 fork 一個程式來執行 AOF 重寫操作。

如何定義 AOF 重寫緩衝區

我知道你很急,但是你先別急,在瞭解AOF重寫流程之前你會先遇到第一個問題,那就是如何定義AOF重寫緩衝區。

一般來說我們會想到用malloc函式來初始化一塊記憶體用於儲存AOF重寫期間主程式收到的命令,當剩餘空間不足時再用realloc函式對其進行擴容。但是Redis並沒有這麼做,Redis定義了一個aofrwblock結構體,其中包含了一個10MB大小的字元陣列,當做一個資料塊,負責記錄AOF重寫期間主程式收到的命令,然後使用aof_rewrite_buf_blocks列表將這些資料塊連線起來,每次分配一個aofrwblock資料塊。

//AOF重寫緩衝區大小為10MB,每一次分配一個aofrwblock
typedef struct aofrwblock {
unsigned long used, free;
char buf[AOF_RW_BUF_BLOCK_SIZE]; //10MB
} aofrwblock;

那麼問題來了,為什麼 Redis 的開發者要選擇自己維護一個字元陣列呢,答案是在使用 realloc 函式進行擴容的時候,如果此時客戶端的寫請求涉及到正在持久化的資料,那麼就會觸發 Linux 核心的大頁機制,造成不必要的記憶體空間浪費,並且申請記憶體的時間變長。

Linux 核心從2.6.38開始支援大頁機制,該機制支援2MB大小的記憶體頁分配,而常規的記憶體頁分配是按4KB的粒度來執行的。這也就意味著在 AOF 重寫期間,客戶端的寫請求可能會修改正在進行持久化的資料,在這一過程中, Redis 就會採用寫時複製機制,一旦有資料要被修改, Redis 並不會直接修改記憶體中的資料,而是將這些資料複製一份,然後再進行修改。即使客戶端請求只修改100B的資料, Redis 也需要複製2MB的大頁。

AOF 重寫流程

不知道說什麼,貼個程式碼先。

int rewriteAppendOnlyFileBackground(void) {
pid_t childpid;
long long start;
if (server.aof_child_pid != -1 || server.rdb_child_pid != -1) return C_ERR;
if (aofCreatePipes() != C_OK) return C_ERR;
openChildInfoPipe();
start = ustime();
if ((childpid = fork()) == 0) {
char tmpfile[256];
/* Child */
closeListeningSockets(0);
redisSetProcTitle("redis-aof-rewrite");
snprintf(tmpfile,256,
"temp-rewriteaof-bg-%d.aof"
, (int) getpid());
if (rewriteAppendOnlyFile(tmpfile) == C_OK) {
size_t private_dirty = zmalloc_get_private_dirty(-1);
if (private_dirty) {
serverLog(LL_NOTICE,
"AOF rewrite: %zu MB of memory used by copy-on-write"
,
private_dirty/(1024*1024));
}
server.child_info_data.cow_size = private_dirty;
sendChildInfo(CHILD_INFO_TYPE_AOF);
exitFromChild(0);
} else {
exitFromChild(1);
}
} else {
/* Parent */
server.stat_fork_time = ustime()-start;
/* GB per second. */
server.stat_fork_rate = (double) zmalloc_used_memory() * 1000000 / server.stat_fo
rk_time / (1024*1024*1024);
latencyAddSampleIfNeeded("fork"
,server.stat_fork_time/1000);
if (childpid == -1) {
closeChildInfoPipe();
serverLog(LL_WARNING,
"Can't rewrite append only file in background: fork: %s"
,
strerror(errno));
aofClosePipes();
return C_ERR;
}
serverLog(LL_NOTICE,
"Background append only file rewriting started by pid %d"
,childpid);
server.aof_rewrite_scheduled = 0;
server.aof_rewrite_time_start = time(NULL);
server.aof_child_pid = childpid;
updateDictResizePolicy();
server.aof_selected_db = -1;
replicationScriptCacheFlush();
return C_OK;
}
return C_OK; /* unreached */
}

一步到"胃"直接看原始碼相信不少同學都覺得很胃疼,但是整理過後理解起來就會輕鬆不少

  • 父程式
  1. 若當前有正在進行的AOF重寫子程式或者RDB持久化子程式,則退出AOF重寫流程
  2. 建立3個管道
  3. parent -> children data
  4. children -> parent ack
  5. parent -> children ack
  6. 將parent -> children data設定為非阻塞
  7. 在children -> parent ack上註冊讀事件的監聽
  8. 將陣列fds中的六個⽂件描述符分別複製給server變數的成員變數
  9. 開啟children->parent ack通道,用於將RDB/AOF儲存過程的資訊傳送給父程式
  10. 用start變數記錄當前時間
  11. fork出一個子程式,透過寫時複製的形式共享主執行緒的所有記憶體資料
  • 子程式
  • 關閉監聽socket,避免接收客戶端連線
  • 設定程式名
  • 生成AOF臨時檔名
  • 遍歷每個資料庫的每個鍵值對,以插入(命令+鍵值對)的方式寫到臨時AOF⽂件中
  • 父程式
  • 計算上一次fork已經花費的時間
  • 計算每秒寫了多少GB內容
  • 判斷上一次fork是否結束,沒結束則此次AOF重寫流程就此中止
  • 將aof_rewrite_scheduled設定為0(表示現在沒有待排程執⾏的AOF重寫操作)
  • 關閉rehash功能(Rehash會帶來較多的資料移動操作,這就意味著⽗程式中的記憶體修改會⽐較多,對於AOF重寫⼦程式來說,就需要更多的時間來執行寫時複製,進⽽完成AOF⽂件的寫⼊,這就會給Redis系統的效能造成負⾯影響)
  • 將aof_selected_db設定為-1(以強制在下一次呼叫feedAppendOnlyFile函式(寫AOF日誌)的時候將AOF重寫期間累計的內容合併到AOF日誌中)
  • 當發現正在進行AOF重寫任務的時候

    (1)將收到的新的寫命令快取在aofrwblock中

    (2)檢查parent -> children data上面有沒有寫監聽,沒有的話註冊一個

    (3)觸發寫監聽時從aof_rewrite_buf_blocks列表中逐個取出aofrwblock資料塊,透過parent -> children data傳送到AOF重寫子程式

  • 子程式重寫結束後,將重寫期間aof_rewrite_buf_blocks列表中沒有消費完成的資料追加寫入到臨時AOF檔案中

管道機制

Redis建立了3個管道用於AOF重寫時父子程式之間的資料傳輸,那麼管道之間的通訊機制就成為了我們需要了解的內容。

1.子程式從parent -> children data讀取資料 (觸發時機)

  • rewriteAppendOnlyFileRio

    由重寫⼦程式執⾏,負責遍歷Redis每個資料庫,⽣成AOF重寫⽇志,在這個過程中,會不時地調⽤ aofReadDiffFromParent

  • rewriteAppendOnlyFile

    重寫⽇志的主體函式,也是由重寫⼦程式執⾏的,本⾝會調⽤rewriteAppendOnlyFileRio,調⽤完後會調⽤ aofReadDiffFromParent 多次,儘可能多地讀取主程式在重寫⽇志期間收到的操作命令

  • rdbSaveRio

    建立RDB⽂件的主體函式,使⽤AOF和RDB混合持久化機制時,這個函式會調⽤aofReadDiffFromParent

//將從父級累積的差異讀取到緩衝區中,該緩衝區在重寫結束時連線
ssize_t aofReadDiffFromParent(void) {
char buf[65536]; //大多數Linux系統上的預設管道緩衝區大小
ssize_t nread, total = 0;
while ((nread =
read(server.aof_pipe_read_data_from_parent,buf,sizeof(buf))) > 0) {
server.aof_child_diff = sdscatlen(server.aof_child_diff,buf,nread);
total += nread;
}
return total;
}

2.子程式向children -> parent ack傳送ACK訊號

  • 在完成⽇志重寫,以及多次向⽗程式讀取操作命令後,向children -> parent ack傳送"!",也就是向主程式傳送ACK訊號,讓主程式停⽌傳送收到的新寫操作
int rewriteAppendOnlyFile(char *filename) {
rio aof;
FILE *fp;
char tmpfile[256];
char byte;
//注意,與rewriteAppendOnlyFileBackground()函式使用的臨時名稱相比,我們必須在此處使用不同的臨
時名稱
snprintf(tmpfile,256,
"temp-rewriteaof-%d.aof"
, (int) getpid());
fp = fopen(tmpfile,
"w");
if (!fp) {
serverLog(LL_WARNING,
"Opening the temp file for AOF rewrite in rewriteAppendOnly
File(): %s"
, strerror(errno));
return C_ERR;
}
server.aof_child_diff = sdsempty();
rioInitWithFile(&aof,fp);
if (server.aof_rewrite_incremental_fsync)
rioSetAutoSync(&aof,REDIS_AUTOSYNC_BYTES);
if (server.aof_use_rdb_preamble) {
int error;
if (rdbSaveRio(&aof,&error,RDB_SAVE_AOF_PREAMBLE,NULL) == C_ERR) {
errno = error;
goto werr;
}
} else {
if (rewriteAppendOnlyFileRio(&aof) == C_ERR) goto werr;
}
//當父程式仍在傳送資料時,在此處執行初始的慢速fsync,以便使下一個最終的fsync更快
if (fflush(fp) == EOF) goto werr;
if (fsync(fileno(fp)) == -1) goto werr;
//再讀幾次,從父級獲取更多資料。我們不能永遠讀取(伺服器從客戶端接收資料的速度可能快於它向子程式傳送數
據的速度),所以我們嘗試在迴圈中讀取更多的資料,只要有更多的資料出現。如果看起來我們在浪費時間,我們會中止
(在沒有新資料的情況下,這會在20ms後發生)。
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();
}
//傳送ACK資訊讓父程式停止傳送訊息
if (write(server.aof_pipe_write_ack_to_parent,
"!"
,1) != 1) goto werr;
if (anetNonBlock(NULL,server.aof_pipe_read_ack_from_parent) != ANET_OK)
goto werr;
//等待父程式返回的ACK資訊,超時時間為10秒。通常父程式應該儘快回覆,但萬一失去回覆,則確信子程式最終會
被終止。
if (syncRead(server.aof_pipe_read_ack_from_parent,&byte,1,5000) != 1 ||
byte != '!') goto werr;
serverLog(LL_NOTICE,
"Parent agreed to stop sending diffs. Finalizing AOF...
");
//如果存在最終差異資料,那麼將讀取
aofReadDiffFromParent();
//將收到的差異資料寫入檔案
serverLog(LL_NOTICE,
"Concatenating %.2f MB of AOF diff received from parent.
"
,
(double) sdslen(server.aof_child_diff) / (1024*1024));
if (rioWrite(&aof,server.aof_child_diff,sdslen(server.aof_child_diff)) == 0)
goto werr;
//確保資料不會保留在作業系統的輸出緩衝區中
if (fflush(fp) == EOF) goto werr;
if (fsync(fileno(fp)) == -1) goto werr;
if (fclose(fp) == EOF) goto werr;
//使用RENAME確保僅當生成DB檔案正常時,才自動更改DB檔案
if (rename(tmpfile,filename) == -1) {
serverLog(LL_WARNING,
"Error moving temp append only file on the final destinatio
n: %s"
, strerror(errno));
unlink(tmpfile);
return C_ERR;
}
serverLog(LL_NOTICE,
"SYNC append only file rewrite performed");
return C_OK;
werr:
serverLog(LL_WARNING,
"Write error writing append only file on disk: %s"
, strerror(err
no));
fclose(fp);
unlink(tmpfile);
return C_ERR;
}

3.父程式從children -> parent ack讀取ACK

  • 當children -> parent ack上有了資料,就會觸發之前註冊的讀監聽
  • 判斷這個資料是不是"!"
  • 是就向parent -> children ack寫入"!",表⽰主程式已經收到重寫⼦程式傳送的ACK資訊,同時給重寫⼦程式回覆⼀個ACK資訊
void aofChildPipeReadable(aeEventLoop *el, int fd, void *privdata, int mask) {
char byte;
UNUSED(el);
UNUSED(privdata);
UNUSED(mask);
if (read(fd,&byte,1) == 1 && byte == '!') {
serverLog(LL_NOTICE,
"AOF rewrite child asks to stop sending diffs.
");
server.aof_stop_sending_diff = 1;
if (write(server.aof_pipe_write_ack_to_child,
"!"
,1) != 1) {
//如果我們無法傳送ack,請通知使用者,但不要重試,因為在另一側,如果核心無法緩衝我們的寫入,或
者子級已終止,則子級將使用超時
serverLog(LL_WARNING,
"Can't send ACK to AOF child: %s"
,
strerror(errno));
}
}
//刪除處理程式,因為在重寫期間只能呼叫一次
aeDeleteFileEvent(server.el,server.aof_pipe_read_ack_from_child,AE_READABLE);
}

什麼時候觸發AOF重寫

開啟AOF重寫功能以後Redis會自動觸發重寫,花費精力去了解觸發機制感覺意義不大。想法很不錯,下次別想了。不然當你手動
執行Bgrewriteaof命令卻發現總是報錯時,疼的不只有你的頭,還有你的胃。

1.手動觸發
  • 當前沒有正在執⾏AOF重寫的⼦程式
  • 當前沒有正在執⾏建立RDB的⼦程式,有會將aof_rewrite_scheduled設定為1(AOF重寫操作被設定為了待排程執⾏)
void bgrewriteaofCommand(client *c) {
if (server.aof_child_pid != -1) {
addReplyError(c,
"Background append only file rewriting already in progress");
} else if (server.rdb_child_pid != -1) {
server.aof_rewrite_scheduled = 1;
addReplyStatus(c,
"Background append only file rewriting scheduled");
} else if (rewriteAppendOnlyFileBackground() == C_OK) {
addReplyStatus(c,
"Background append only file rewriting started");
} else {
addReply(c,shared.err);

   }
}
2.開啟AOF與主從複製
  • 開啟AOF功能以後,執行一次AOF重寫
  • 主從節點在進⾏複製時,如果從節點的AOF選項被開啟,那麼在載入解析RDB⽂件時,AOF選項會被關閉,⽆論從節點是否成功載入RDB⽂件,restartAOFAfterSYNC函式都會被調⽤,⽤來恢復被關閉的AOF功能,在這個過程中會執行一次AOF重寫
int startAppendOnly(void) {
    char cwd[MAXPATHLEN]; //錯誤訊息的當前工作目錄路徑
    int newfd;
    newfd = open(server.aof_filename,O_WRONLY|O_APPEND|O_CREAT,0644);
    serverAssert(server.aof_state == AOF_OFF);
    if (newfd == -1) {
        char *cwdp = getcwd(cwd,MAXPATHLEN);
        serverLog(LL_WARNING,
            "Redis needs to enable the AOF but can't open the "
            "append only file %s (in server root dir %s): %s",
            server.aof_filename,
            cwdp ? cwdp : "unknown",
            strerror(errno));
        return C_ERR;
    }
    if (server.rdb_child_pid != -1) {
        server.aof_rewrite_scheduled = 1;
        serverLog(LL_WARNING,"AOF was enabled but there is already a child process saving an RDB file on disk. An AOF background was scheduled to start when possible.");
    } else {
        //關閉正在進行的AOF重寫程式,並啟動一個新的AOF:舊的AOF無法重用,因為它沒有累積AOF緩衝區。
        if (server.aof_child_pid != -1) {
            serverLog(LL_WARNING,"AOF was enabled but there is already an AOF rewriting in background. Stopping background AOF and starting a rewrite now.");
            killAppendOnlyChild();
        }
        if (rewriteAppendOnlyFileBackground() == C_ERR) {
            close(newfd);
            serverLog(LL_WARNING,"Redis needs to enable the AOF but can't trigger a background AOF rewrite operation. Check the above logs for more info about the error.");
            return C_ERR;
        }
    }
    //我們正確地開啟了AOF,現在等待重寫完成,以便將資料附加到磁碟上
    server.aof_state = AOF_WAIT_REWRITE;
    server.aof_last_fsync = server.unixtime;
    server.aof_fd = newfd;
    return C_OK;
}
3.定時任務
  1. 每100毫秒觸發一次,由server.hz控制,預設10
  2. 當前沒有在執⾏的RDB⼦程式 && AOF重寫⼦程式 && aof_rewrite_scheduled=1
  3. 當前沒有在執⾏的RDB⼦程式 && AOF重寫⼦程式 && aof_rewrite_scheduled=0

    AOF功能已啟⽤ && AOF⽂件⼤⼩⽐例超出auto-aof-rewrite-percentage && AOF⽂件⼤⼩絕對值超出auto-aofrewrite-min-size

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
......
//判斷當前沒有在執⾏的RDB⼦程式 && AOF重寫⼦程式 && aof_rewrite_scheduled=1
if (server.rdb_child_pid == -1 && server.aof_child_pid == -1 &&
server.aof_rewrite_scheduled)
{
rewriteAppendOnlyFileBackground();
}
//檢查正在進行的後臺儲存或AOF重寫是否終止
if (server.rdb_child_pid != -1 || server.aof_child_pid != -1 ||
ldbPendingChildren())
{
int statloc;
pid_t pid;
if ((pid = wait3(&statloc,WNOHANG,NULL)) != 0) {
int exitcode = WEXITSTATUS(statloc);
int bysignal = 0;
if (WIFSIGNALED(statloc)) bysignal = WTERMSIG(statloc);
if (pid == -1) {
serverLog(LL_WARNING,
"wait3() returned an error: %s.
"
"rdb_child_pid = %d, aof_child_pid = %d"
,
strerror(errno),
(int) server.rdb_child_pid,
(int) server.aof_child_pid);
} else if (pid == server.rdb_child_pid) {
backgroundSaveDoneHandler(exitcode,bysignal);
if (!bysignal && exitcode == 0) receiveChildInfo();
} else if (pid == server.aof_child_pid) {
backgroundRewriteDoneHandler(exitcode,bysignal);
if (!bysignal && exitcode == 0) receiveChildInfo();
} else {
if (!ldbRemoveChild(pid)) {
serverLog(LL_WARNING,
"Warning, detected child with unmatched pid: %ld"
,
(long)pid);
}
}
updateDictResizePolicy();
closeChildInfoPipe();
}
} else {
//如果沒有正在進行的後臺save/rewrite,請檢查是否必須立即save/rewrite
for (j = 0; j < server.saveparamslen; j++) {
struct saveparam *sp = server.saveparams+j;
//如果我們達到了給定的更改量、給定的秒數,並且最新的bgsave成功,或者如果發生錯誤,至少已經
過了CONFIG_bgsave_RETRY_DELAY秒,則儲存。
if (server.dirty >= sp->changes &&
server.unixtime-server.lastsave > sp->seconds &&
(server.unixtime-server.lastbgsave_try >
CONFIG_BGSAVE_RETRY_DELAY ||
server.lastbgsave_status == C_OK))
      {
serverLog(LL_NOTICE,
"%d changes in %d seconds. Saving...
"
,
sp->changes, (int)sp->seconds);
rdbSaveInfo rsi, *rsiptr;
rsiptr = rdbPopulateSaveInfo(&rsi);
rdbSaveBackground(server.rdb_filename,rsiptr);
break;
}
}
//判斷AOF功能已啟⽤ && AOF⽂件⼤⼩⽐例超出auto-aof-rewrite-percentage && AOF⽂件⼤⼩絕對
值超出auto-aof-rewrite-min-size
if (server.aof_state == AOF_ON &&
server.rdb_child_pid == -1 &&
server.aof_child_pid == -1 &&
server.aof_rewrite_perc &&
server.aof_current_size > server.aof_rewrite_min_size)
{
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) {
serverLog(LL_NOTICE,
"Starting automatic rewriting of AOF on %lld%% growt
h"
,growth);
rewriteAppendOnlyFileBackground();
}
}
}
......
return 1000/server.hz;
}

AOF重寫功能的缺點

哪怕是你心中的她,也並非是完美無缺的存在,更別說Redis這個人工產物了。但不去發現也就自然而然不存在缺點,對吧~

1.記憶體開銷
  • 在AOF重寫期間,主程式會將fork之後的資料變化寫進aof_rewrite_buf與aof_buf中,其內容絕大部分是重複的,在高流量寫入的場景下兩者消耗的空間幾乎一樣大。
  • AOF重寫帶來的記憶體開銷有可能導致Redis記憶體突然達到maxmemory限制,甚至會觸發作業系統限制被OOM Killer殺死,導致Redis不可服務。
2.CPU開銷
  • 在AOF重寫期間主程式需要花費CPU時間向aof_rewrite_buf寫資料,並使用eventloop事件迴圈向子程式傳送aof_rewrite_buf中的資料。
//將資料附加到AOF重寫緩衝區,如果需要,分配新的塊
void aofRewriteBufferAppend(unsigned char *s, unsigned long len) {
......
//建立事件以便向子程式傳送資料
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);
 }
  ......
}
  • 在子程式執行AOF重寫操作的後期,會迴圈讀取pipe中主程式傳送來的增量資料,然後追加寫入到臨時AOF檔案。
int rewriteAppendOnlyFile(char *filename) {
......
//再次讀取幾次以從父程式獲取更多資料。我們不能永遠讀取(伺服器從客戶端接收資料的速度可能快於它向子級發
送資料的速度),因此我們嘗試在迴圈中讀取更多資料,只要有很好的機會會有更多資料。如果看起來我們在浪費時間,
我們會中止(在沒有新資料的情況下,這會在20ms後發生)
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();
 }
 ......
}

在子程式完成AOF重寫操作後,主程式會在backgroundRewriteDoneHandler中進行收尾工作,其中一個任務就是將在重
寫期間aof_rewrite_buf中沒有消費完成的資料寫入臨時AOF檔案,消耗的CPU時間與aof_rewrite_buf中遺留的資料量成正
比。

3.磁碟IO開銷

在AOF重寫期間,主程式會將fork之後的資料變化寫進aof_rewrite_buf與aof_buf中,在業務高峰期間其內容絕大部分是重複的,一次操作產生了兩次IO開銷。

4.Fork

雖說 AOF 重寫期間不會阻塞主程式,但是 fork 這個瞬間一定是會阻塞主程式的。因此 fork 操作花費的時間越長,Redis 操作延遲的時間就越長。即使在一臺普通的機器上,Redis 也可以處理每秒50K到100K的操作,那麼幾秒鐘的延遲可能意味著數十萬次操作的速度減慢,這可能會給應用程式帶來嚴重的穩定性問題。

為了避免一次性複製大量記憶體資料給子程式造成的長時間阻塞問題,fork 採用作業系統提供的寫時複製(Copy-On-Write)機制,但 fork 子程式需要複製程式必要的資料結構,其中有一項就是複製記憶體頁表(虛擬記憶體和實體記憶體的對映索引表)。這個複製過程會消耗大量 CPU 資源,複製完成之前整個程式是會阻塞的,阻塞時間取決於整個例項的記憶體大小,例項越大,記憶體頁表越大,fork 阻塞時間越久。複製記憶體頁表完成後,子程式與父程式指向相同的記憶體地址空間,也就是說此時雖然產生了子程式,但是並沒有申請與父程式相同的記憶體大小。

參考資料:

1.極客時間專欄《Redis原始碼剖析與實戰》.蔣德鈞.2021

2.極客時間專欄《Redis核心技術與實戰》.蔣德鈞.2020

3.Redis 7.0 Multi Part AOF的設計和實現.驅動 qd.2022 :
https://developer.aliyun.com/...

4.Redis 5.0.8原始碼:https://github.com/redis/redi...

相關文章