Redis 持久化之 AOF

happen發表於2019-01-29

除了 RDB 持久化功能之外,Redis 還提供了 AOF(Append Only File)持久化功能。與 RDB 持久化通過儲存資料庫中的鍵值對來記錄資料庫狀態不同,AOF 持久化是通過儲存 Redis 伺服器所執行的寫命令來記錄資料庫狀態的。

簡介

AOF 檔案中記錄了 Redis 伺服器所執行的寫命令,以此來儲存資料庫的狀態。AOF 檔案本質上是一個 redo log,通過它可以恢復資料庫狀態。

隨著執行命令的增多,AOF 檔案的大小會不斷增大,這會導致幾個問題,比如,磁碟佔用增加,重啟載入過慢等。因此, Redis 提供了 AOF 重寫機制來控制 AOF 檔案大小,下面會細說。

AOF 檔案中寫入的所有命令以 Redis 的命令請求協議格式去儲存,即 RESP 格式。

有兩種方式可以實現 AOF 功能的開關,如下,

  • 在 redis 配置檔案 redis.conf 中有配置項 appendonly, yes 開啟 AOF 功能,no 關閉 AOF 功能。
  • 使用客戶端命令config set appendonly yes/no

server 相關變數

與 AOF 相關的 server 成員變數很多,這裡只選擇幾個進行簡要說明。先看後面的章節,之後再回頭看本章節,也是個不錯的主意。

int aof_state;                  /* AOF_(ON|OFF|WAIT_REWRITE) */
int aof_fsync;                  /* Kind of fsync() policy */
char *aof_filename;             /* Name of the AOF file */
int aof_no_fsync_on_rewrite;    /* Don`t fsync if a rewrite is in prog. */
int aof_rewrite_perc;           /* Rewrite AOF if % growth is > M and... */
off_t aof_rewrite_min_size;     /* the AOF file is at least N bytes. */
off_t aof_rewrite_base_size;    /* AOF size on latest startup or rewrite. */
off_t aof_current_size;         /* AOF current size. */
int aof_rewrite_scheduled;      /* Rewrite once BGSAVE terminates. */
pid_t aof_child_pid;            /* PID if rewriting process */
list *aof_rewrite_buf_blocks;   /* Hold changes during an AOF rewrite. */
sds aof_buf;                   /* AOF buffer, written before entering the event loop */
int aof_fd;                     /* File descriptor of currently selected AOF file */
int aof_selected_db;            /* Currently selected DB in AOF */
time_t aof_flush_postponed_start; /* UNIX time of postponed AOF flush */
time_t aof_last_fsync;            /* UNIX time of last fsync() */
time_t aof_rewrite_time_last;     /* Time used by last AOF rewrite run. */
time_t aof_rewrite_time_start;    /* Current AOF rewrite start time. */
int aof_lastbgrewrite_status;     /* C_OK or C_ERR */
unsigned long aof_delayed_fsync;  /* delayed AOF fsync() counter */
int aof_rewrite_incremental_fsync;/* fsync incrementally while rewriting? */
int aof_last_write_status;        /* C_OK or C_ERR */
int aof_last_write_errno;         /* Valid if aof_last_write_status is ERR */
int aof_load_truncated;           /* Don`t stop on unexpected AOF EOF. */

aof_fsync

表示 AOF 刷盤策略,後面會細說

aof_child_pid

由於 aofrewrite 是個耗時操作,因此會 fork 一個子程式去做這件事, aof_child_pid 就標識了子程式的 pid。

aof_buf

該變數儲存著所有等待寫入到 AOF 檔案的協議文字。

aof_rewrite_buf_blocks

該變數用來儲存 aofrewrite 期間,server 處理過的需要寫入 AOF 檔案的協議文字。這個變數採用 list 結構,是考慮到分配到一個非常大的空間並不總是可能的,也可能產生大量的複製工作。

aof_rewrite_scheduled

可取值有 0 和 1。

取 1 時,表示此時有子程式正在做 aofrewrite 操作,本次任務後延,等到 serverCron 執行時,合適的情況再執行。或者是執行了 config set appendonly yes, 想把 AOF 功能開啟,此時執行的 aofrewrite 失敗了,aof_state 仍然處於 AOF_WAIT_REWRITE 狀態,此時 aof_rewrite_scheduled 也會置為 1,等下次再執行 aofrewrite。

aof_state

表示 AOF 功能現在的狀態,可取值如下,

#define AOF_OFF 0             /* AOF is off */
#define AOF_ON 1              /* AOF is on */
#define AOF_WAIT_REWRITE 2    /* AOF waits rewrite to start appending */

AOF_OFF 表示 AOF 功能處於關閉狀態,開關在上一節已經說過,預設 AOF 功能是關閉的。AOF 功能從 off switch 到 on 後,aof_state 會從 AOF_OFF 變為 AOF_WAIT_REWRITEstartAppendOnly 函式完成該邏輯。在 aofrewrite 一次之後,該變數才會從 AOF_WAIT_REWRITE 變為 AOF_ON

可以看到從 ON 切換到 OFF 時,要經歷一箇中間狀態 AOF_WAIT_REWRITE,那為何要這麼設計呢?再來分析一下 startAppendOnly 函式的邏輯(程式碼去掉了列印日誌的部分)。

server.aof_fd = open(server.aof_filename,O_WRONLY|O_APPEND|O_CREAT,0644);
serverAssert(server.aof_state == AOF_OFF);
if (server.aof_fd == -1) {
    char *cwdp = getcwd(cwd,MAXPATHLEN);
    return C_ERR;
}
if (server.rdb_child_pid != -1) {
    server.aof_rewrite_scheduled = 1;
} else if (rewriteAppendOnlyFileBackground() == C_ERR) {
    close(server.aof_fd);
    return C_ERR;
}
server.aof_state = AOF_WAIT_REWRITE;

【1】開啟 aof 檔案,預設名為 appendonly.aof,沒有的話就新建空檔案,失敗則返回。

【2】切換後,需要做一次 aofrewrite,將 server 中現有的資料轉換成協議文字,寫到 AOF 檔案。但是,這裡要注意,如果此時有子程式在做 bgrdb,那麼此次 aofrewrite 需要任務延緩,即 aof_rewrite_scheduled 置為 1。

【3】將 aof_state 置為 AOF_WAIT_REWRITE 狀態。

而做完第一次 aofrewrite 後,AOF_WAIT_REWRITE 轉換成 AOF_ON,如下,

void backgroundRewriteDoneHandler(int exitcode, int bysignal) {
    ...
    if (server.aof_state == AOF_WAIT_REWRITE)
        server.aof_state = AOF_ON;
    ...
}

仔細分析原始碼發現,在 AOF 持久化的命令追加階段(後面章節細講),有如下邏輯,

void feedAppendOnlyFile(struct redisCommand *cmd, int dictid, robj **argv, int argc) {
    ...
    if (server.aof_state == AOF_ON)
        server.aof_buf = sdscatlen(server.aof_buf,buf,sdslen(buf));
    if (server.aof_child_pid != -1)
        aofRewriteBufferAppend((unsigned char*)buf,sdslen(buf));
    ...
}

很明顯,剛開啟 AOF 時, aof_stateAOF_WAIT_REWRITE ,處理好的協議文字 buf 無法寫入 aof_buf 變數 ,但必須寫入 aof_rewrite_buf_blocks 變數(資料在 aofrewrite 的最後階段會被寫進 AOF 檔案)。

這裡是否將命令 append 到 aof_state 的判斷至關重要,如果修改條件為 server.aof_state != AOF_OFF考慮如下情況

AOF 狀態剛開啟,尚未完成第一次 aofrewrite,也即,一邊 Child 程式資料庫中現有資料還未寫進 AOF 檔案,另一邊 Parent 程式仍然持續處理 client 請求,於是,Parent 程式在指定的資料刷盤策略下,將 aof_buf 刷盤。如果這時當機了,當 server 重啟後,載入 AOF 檔案,在記憶體中塞入資料,實際上對於使用者來說,這部分資料算是髒資料了,因為 AOF 並沒有成功開啟,未開啟 AOF 狀態時,資料都在記憶體中,當機後,資料會全部丟掉。增加這個中間狀態就是為了應對這種情況。所以, AOF_WAIT_REWRITE 狀態存在的時間範圍起始於 startAppendOnly ,到完成第一次 aofrewrite 後切成 AOF_ON 。aofrewrite 後再發生當機,丟失的資料就少多了。

這只是我個人的理解,不一定正確,歡迎大家斧正。

另外,如果開啟了 AOF,在 redis 啟動 載入 AOF 檔案時,aof_state 也會暫時設定成 AOF_OFF,載入完畢之後設定為 AOF_ON

aof_pipe_*

為了提高 aofrewrite 效率,Redis 通過在父子程式間建立管道,把 aofrewrite 期間的寫命令通過管道同步給子程式,追加寫盤的操作也就轉交給了子程式。aof_pipe_* 變數就是這部分會用到的管道。

AOF 持久化

命令追加

AOF 功能開啟後,每次導致資料庫狀態發生變化的命令都會經過函式 feedAppendOnlyFile 累積到 aof_buf 變數中。如果後臺有正在執行的 aofrewrite 任務,還會寫一份資料到 aof_rewrite_buf_blocks 變數中。

feedAppendOnlyFile 函式

在該函式中,首先要將資料庫切換到當前資料庫( aof_selected_db 更新),在 buf 中插入一條 SELECT 命令。

sds buf = sdsempty();
if (dictid != server.aof_selected_db) {
    char seldb[64];
    snprintf(seldb,sizeof(seldb),"%d",dictid);
    buf = sdscatprintf(buf,"*2
$6
SELECT
$%lu
%s
", (unsigned        long)strlen(seldb),seldb);
    server.aof_selected_db = dictid;
}

然後在對需要加入 buf 的命令進行分類處理。

【1】帶有過期時間的命令,呼叫函式 catAppendOnlyExpireAtCommand 進行協議文字 buf 組裝。EXPIRE/PEXPIRE/EXPIREAT 這三個命令直接呼叫該函式,而 SETEX/PSETEX 這兩個命令需要在呼叫之前加入一個 SET 命令。即,

tmpargv[0] = createStringObject("SET",3);
tmpargv[1] = argv[1];
tmpargv[2] = argv[3];
buf = catAppendOnlyGenericCommand(buf,3,tmpargv);

decrRefCount(tmpargv[0]);
buf = catAppendOnlyExpireAtCommand(buf,cmd,argv[1],argv[2]);

【2】普通命令,直接呼叫函式 catAppendOnlyGenericCommand 進行協議文字 buf 組裝。

catAppendOnlyExpireAtCommand 函式

該函式其實就是將所有與過期時間相關的命令轉成 PEXPIREAT 命令,細化到毫秒。最後呼叫普通命令組裝 buf 函式 catAppendOnlyGenericCommand

// 構建 PEXPIREAT 命令
argv[0] = createStringObject("PEXPIREAT",9);
argv[1] = key;
argv[2] = createStringObjectFromLongLong(when);

// 呼叫 aof 公共函式
buf = catAppendOnlyGenericCommand(buf, 3, argv);

catAppendOnlyGenericCommand 函式

該函式用來把 redis 命令轉換成 RESP 協議文字。

sds catAppendOnlyGenericCommand(sds dst, int argc, robj **argv) {
    char buf[32];
    int len, j;
    robj *o;

    // 比如 *3

    buf[0] = `*`;
    len = 1+ll2string(buf+1,sizeof(buf)-1,argc);
    buf[len++] = `
`;
    buf[len++] = `
`;
    dst = sdscatlen(dst,buf,len);

    for (j = 0; j < argc; j++) {
        o = getDecodedObject(argv[j]);
        buf[0] = `$`;
        len = 1+ll2string(buf+1,sizeof(buf)-1,sdslen(o->ptr));
        buf[len++] = `
`;
        buf[len++] = `
`;
        dst = sdscatlen(dst,buf,len);
        dst = sdscatlen(dst,o->ptr,sdslen(o->ptr));
        dst = sdscatlen(dst,"
",2);
        decrRefCount(o);
    }
    return dst;
}

可以看到,定義了一個 buf 陣列,反覆使用,通過 len 精確控制 append 到 dst 後的長度。

aofRewriteBufferAppend 函式

aof_rewrite_buf_blocks 變數是一個 list 結構,其中每一個元素都是一個大小為 10M 的 block

#define AOF_RW_BUF_BLOCK_SIZE (1024*1024*10)    /* 10 MB per block */
typedef struct aofrwblock {
    unsigned long used, free;
    char buf[AOF_RW_BUF_BLOCK_SIZE];
} aofrwblock;

這個函式做了兩件事情。

一是,將 catAppendOnlyGenericCommand 獲得的協議文字 buf 存到 aof_rewrite_buf_blocks 變數,首先拿出來 list 最後一個 block,如果裝不下,那先把最後一個 block 填滿,剩下的再申請記憶體。

listNode *ln = listLast(server.aof_rewrite_buf_blocks); // 指向最後一個快取塊
aofrwblock *block = ln ? ln->value : NULL;
while(len) {
    if (block) { // 如果已經有至少一個快取塊,那麼嘗試將內容追加到這個快取塊裡面
        unsigned long thislen = (block->free < len) ? block->free : len;
        if (thislen) {  /* The current block is not already full. */
            memcpy(block->buf+block->used, s, thislen);
            block->used += thislen;
            block->free -= thislen;
            s += thislen;
            len -= thislen;
        }
    }
    if (len) {  // 最後一個快取塊沒有放得下本次 data,那再申請一個 block
        int numblocks;
        block = zmalloc(sizeof(*block));
        block->free = AOF_RW_BUF_BLOCK_SIZE;
        block->used = 0;
        listAddNodeTail(server.aof_rewrite_buf_blocks,block);
        ... ...
    }
}

二是,給 aof_pipe_write_data_to_child 這個 fd 註冊寫事件,回撥函式為 aofChildWriteDiffData

/* Install a file event to send data to the rewrite child if there is
     * not one already. */
if (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 重寫的邏輯,後面章節會細說,這裡先留個心。

何時進行命令追加

也就是說,什麼時候會呼叫feedAppendOnlyFile 呢?有以下兩個時機。

propagate 函式

大家都知道,Redis 中命令執行的流程,即 processCommand -> call 。在 call 函式中會把某些命令寫入 AOF 檔案。如何判斷某個命令是否需要寫入 AOF 呢?

在 server 結構體中維持了一個 dirty 計數器,dirty 記錄的是伺服器狀態進行了多少次修改,每次做完 save/bgsave 執行完成後,會將 dirty 清 0,而使得伺服器狀態修改的命令一般都需要寫入 AOF 檔案和主從同步(排除某些特殊情況)。

dirty = server.dirty;
c->cmd->proc(c);
dirty = server.dirty-dirty;
...
if (propagate_flags != PROPAGATE_NONE)
    propagate(c->cmd,c->db->id,c->argv,c->argc,propagate_flags);

propagate 函式中就會呼叫到 feedAppendOnlyFile

void propagate(struct redisCommand *cmd, int dbid, robj **argv, int argc,
               int flags)
{
    if (server.aof_state != AOF_OFF && flags & PROPAGATE_AOF)
        feedAppendOnlyFile(cmd,dbid,argv,argc);
    if (flags & PROPAGATE_REPL)
        replicationFeedSlaves(server.slaves,dbid,argv,argc);
}
propagateExpire 函式

當記憶體中帶有過期時間的 key 過期時,會向 AOF 寫入 del 命令。

void propagateExpire(redisDb *db, robj *key) {
    ...
    if (server.aof_state != AOF_OFF)
        feedAppendOnlyFile(server.delCommand,db->id,argv,2);
    replicationFeedSlaves(server.slaves,db->id,argv,2);
    ...
}

propagateExpire 函式在一些檢查 key 是否過期時會呼叫。

檔案的寫入與同步

上一步中,將需要寫入 AOF 檔案的資料先寫到了 aof_buf 變數中,那麼,接下來說一下如何將 aof_buf 的內容寫進 AOF 檔案。

同步策略

為了提高檔案的寫入效率,在現代作業系統中,當使用者呼叫 write 函式試,將一些資料寫入到檔案的時候,作業系統通常會將寫入的資料儲存在一個記憶體緩衝區裡,等到緩衝區的空間被填滿,或者超過了指定的時限後,才真正地將緩衝區中的資料寫入磁碟。

這種做法雖然提高了效率,但也為寫入資料帶來了安全問題,因為如果計算機當機,那麼儲存在記憶體緩衝區裡面的寫入資料將會丟失。

為此,系統提供了 fsyncfdatasync 兩個同步函式,它們可以強制讓作業系統立即將快取區中的資料寫入到硬碟裡面,從而確保寫入資料的安全性。

要知道,這兩個系統呼叫函式都是阻塞式的,針對如何協調檔案寫入與同步的關係,該版本 Redis 支援 3 種同步策略,可在配置檔案中使用 appendfsync 項進行配置,有如下取值,

  • always。每次有新命令追加到 AOF檔案 時就執行一次同步,,安全性最高,但是效能影響最大。
  • everysec。每秒執行一次同步。當機只會丟失一秒鐘的命令。這算是一個折中方案。
  • no。將資料同步操作完全交由作業系統處理,效能最好,但是資料可靠性最差。當機將丟失同步 AOF 檔案後的所有寫命令。

在 Redis 原始碼中, 當程式執行在 Linux 系統上時,執行的是 fdatasync 函式,而在其他系統上,則會執行 fsync 函式,即,

#ifdef __linux__
#define aof_fsync fdatasync
#else
#define aof_fsync fsync
#endif

:以下敘述均以 fsync 代稱。

如何寫入檔案

寫入檔案的邏輯在 flushAppendOnlyFile 函式中實現。下面分兩部分來看主要程式碼。

檔案寫入write 系統呼叫
...

// aof 快取區內沒有資料需要寫入 disk,無需處理
if (sdslen(server.aof_buf) == 0) return;

// 如果 sync policy 設定成 everysec,
// sync_in_progress 表示是否有 fsync 任務在後臺
if (server.aof_fsync == AOF_FSYNC_EVERYSEC)
    sync_in_progress = bioPendingJobsOfType(BIO_AOF_FSYNC) != 0;

// force=0(非強制寫入)時,如果後臺有 fsync 任務,推遲此次寫入,但推遲時間不超過 2s
if (server.aof_fsync == AOF_FSYNC_EVERYSEC && !force) {
    if (sync_in_progress) {
        if (server.aof_flush_postponed_start == 0) { // 首次推遲 write,一次推遲 2s
            server.aof_flush_postponed_start = server.unixtime;
            return;
        } else if (server.unixtime - server.aof_flush_postponed_start < 2) {
            return;
        }

        // 否則,通過,繼續寫,因為我們不能等待超過 2s
        server.aof_delayed_fsync++;
        serverLog(LL_NOTICE,"Asynchronous AOF fsync is taking too long (disk is busy?). Writing the AOF buffer without waiting for fsync to complete, this may slow down Redis.");
    }
}
...
// 將 aof 緩衝區的內容寫到系統快取區
nwritten = write(server.aof_fd, server.aof_buf, sdslen(server.aof_buf)); 
...
// 執行了 write 操作,所以要清零延遲 flush 的時間
server.aof_flush_postponed_start = 0;

首先會判斷 aof_buf 是否為空,如果是,那麼不需要執行下面的邏輯,直接返回。

如果同步策略為 everysec,那麼需要檢視是否有 fsync 任務在後臺,呼叫 fsync 使用的是 Redis 中 bio ,如果對這個還不瞭解,可以參考我之前的文章 《 Redis Bio 詳解 》。為什麼要做這個判斷呢?

fsyncwrite 同一個 fd 時,write 必然阻塞。 當系統 IO 非常繁忙時, fsync() 可能會阻塞, 即使系統 IO 不繁忙, fsync 也會因為資料量大而慢。

因此對於 everysec 策略,需要儘量保證 fsyncwrite 不同時操作同一個 fd。no 策略完全把 fsync 交給了作業系統,作業系統什麼時候 fsync ,無從得知。always 策略則是每次都要主從呼叫 fsync,也沒必要做判斷。因此,這裡的判斷,只針對 everysec 策略有效。

對於 everysec 策略,如果有 fsync 在執行,那麼本次 write 推遲 2 秒鐘,等到下次在進入本函式時,如果推遲時間超過 2 秒,那麼更新 aof_delayed_fsync 值(info 裡可以查到),列印日誌 ” Asynchronous AOF fsync is taking too long (disk is busy?). Writing the AOF buffer without waiting for fsync to complete, this may slow down Redis. “ ,之後進行 write 系統呼叫。當然了,系統也提供了 force 選項,去跳過這項是否要推遲 write 的檢查。

write 之後,將 aof_flush_postponed_start 推遲開始計時值清零,迎接下次檢查。

所以說,AOF 執行 everysec 策略時,如果恰好有 fsync 在長時間的執行,Redis 意外關閉會丟失最多兩秒的資料。如果 fsync 執行正常,只有當作業系統 crash 時才會造成最多 1 秒的資料丟失。

收尾工作, write 結果處理

write 呼叫結果可能是正常的,也可能是異常的,那麼需要做不同的處理。首先主要看異常處理,

if (nwritten != (signed)sdslen(server.aof_buf)) {
    ...
    /* Log the AOF write error and record the error code. */
    if (nwritten == -1) {
        ...
    } else { // 如果僅寫了一部分,發生錯誤
    // 將追加的內容截斷,刪除了追加的內容,恢復成原來的檔案
        if (ftruncate(server.aof_fd, server.aof_current_size) == -1) {
            ...
        } else {
            nwritten = -1;
        }
        server.aof_last_write_errno = ENOSPC;
    }

    // 如果是寫入的策略為每次寫入就同步,無法恢復這種策略的寫,因為我們已經告知使用者,已經將寫的資料同步到磁碟了,因此直接退出程式
    if (server.aof_fsync == AOF_FSYNC_ALWAYS) {
        ...
        exit(1);
    } else {
        // 設定執行write操作的狀態
        server.aof_last_write_status = C_ERR;
        if (nwritten > 0) {
            // 只能更新當前的 AOF 檔案的大小
            server.aof_current_size += nwritten;
            // 刪除 AOF 緩衝區寫入的位元組
            sdsrange(server.aof_buf,nwritten,-1);
        }
        return; /* We`ll try again on the next call... */
    }
} else {
    /* Successful write(2). If AOF was in error state, restore the
     * OK state and log the event. 
     */
    if (server.aof_last_write_status == C_ERR) {
        serverLog(LL_WARNING, "AOF write error looks solved, Redis can write again.");
        server.aof_last_write_status = C_OK;
    }
}

寫入異常的判斷,nwritten != (signed)sdslen(server.aof_buf)write 的資料量與 aof_buf 的大小不同。當完全沒寫入時,打個日誌就算了;當僅寫入了一部分資料時,使用 ftruncate 函式把 AOF 檔案的內容恢復成原來的大小,以備下次重新寫入,nwritten 置為 -1。使用 ftruncate 的原因是怕作業系統執行了 fsync,因此需要把 AOF 檔案的大小恢復。

如果執行的是 always 同步策略,那麼需要返回會客戶端錯誤。對於其他策略,更新 aof_last_write_status ,以便知道上一次做 write 的結果,對於未完全寫入的情況,如果上面執行的 ftruncate 失敗,此時 nwritten > 0,需要更新 aof_current_size,從 aof_buf 中減去已經寫入的,防止下次有重複資料寫入,然後返回。

如果寫入成功,那麼視情況更新 aof_last_write_status,表示此次 write 成功。

下面主要是正常情況的處理。

/* nwritten = -1 時走不到這個步驟 */ 
server.aof_current_size += nwritten; // 正常 write,更新 aof_current_size

/* Re-use AOF buffer when it is small enough. The maximum comes from the
 * arena size of 4k minus some overhead (but is otherwise arbitrary).
 */
if ((sdslen(server.aof_buf)+sdsavail(server.aof_buf)) < 4000) {
    sdsclear(server.aof_buf);
} else {
    sdsfree(server.aof_buf);
    server.aof_buf = sdsempty();
}

/* Don`t fsync if no-appendfsync-on-rewrite is set to yes and there are
 * children doing I/O in the background. */
if (server.aof_no_fsync_on_rewrite && (server.aof_child_pid != -1 || server.rdb_child_pid != -1)) return;

/* Perform the fsync if needed. */
if (server.aof_fsync == AOF_FSYNC_ALWAYS) {
    /* aof_fsync is defined as fdatasync() for Linux in order to avoid
         * flushing metadata. */
    latencyStartMonitor(latency);
    aof_fsync(server.aof_fd); /* Let`s try to get this data on the disk */
    latencyEndMonitor(latency);
    latencyAddSampleIfNeeded("aof-fsync-always",latency);
    server.aof_last_fsync = server.unixtime;
} else if ((server.aof_fsync == AOF_FSYNC_EVERYSEC && 
            server.unixtime > server.aof_last_fsync)) {
    if (!sync_in_progress) aof_background_fsync(server.aof_fd); // 如果沒有正在執行同步,那麼建立一個後臺任務
    server.aof_last_fsync = server.unixtime;
}

aof_buf 清空,然後根據不同策略進行同步。always 策略時,主動呼叫 fsync; everysec 策略,則建立 fsync bio 任務。

另外,有配置項 no-appendfsync-on-rewrite 去決定,當子程式在做 aofrewrite/bgsave 時是否要進行 fsync

何時進行檔案寫入

也就是,什麼時候會呼叫 flushAppendOnlyFile 函式,有以下三個時機。

beforeSleep 函式

Redis 的伺服器程式就是一個事件迴圈,這個迴圈中的檔案事件負責接收客戶端請求,以及向客戶端傳送命令回覆,而時間事件則負責像 serverCron 函式這樣需要定時執行的函式。

對於 Redis 的事件機制可以參考我之前的文章 《Redis 中的事件》。

因為伺服器在處理檔案事件時可能會執行寫命令,使得一些內容被追加到 aof_buf 緩衝區裡面,所以在伺服器每次結束一個事件迴圈之前,都會呼叫 flushAppendOnlyFile 函式,考慮是否需要將 aof_buf 緩衝區中的內容寫入和同步到 AOF 檔案裡面。即,

void beforeSleep(struct aeEventLoop *eventLoop) {
    ...
    /* Write the AOF buffer on disk */
    flushAppendOnlyFile(0);
    ...
}

這裡的呼叫是非強制寫入(force = 0)。

serverCron 函式

Redis 中的時間事件,定期執行 serverCron 函式(從 Redis 2.8 開始,使用者可以通過修改 hz 選項來調整 serverCron的每秒執行次數),做一些雜事,比如更新伺服器各項統計資訊、關閉清理客戶端、做 AOF 和 RDB 等。

  /* AOF postponed flush: Try at every cron cycle if the slow fsync completed. */
if (server.aof_flush_postponed_start) flushAppendOnlyFile(0);

如果上次 AOF 寫入推遲了,那麼再次嘗試非強制寫入。

run_with_period(1000) {
    if (server.aof_last_write_status == C_ERR)
        flushAppendOnlyFile(0);
}

每秒鐘檢查,如果上次寫入 AOF 檔案失敗了,再次嘗試非強制寫入。因為需要及時去處理 aof_buf,以及重置 AOF 寫入狀態的變數 aof_last_write_status,每秒做檢查,這個頻率是足夠的。

stopAppendOnly 函式

當 AOF 功能要關閉時,會呼叫 stopAppendOnly 函式,嘗試一次強制寫入,即盡最大努力去儲存最多的資料。

void stopAppendOnly(void) {
    serverAssert(server.aof_state != AOF_OFF);
    flushAppendOnlyFile(1);
    aof_fsync(server.aof_fd);
    close(server.aof_fd);
}

強制寫入,並刷盤。

AOF 檔案載入

當 Redis 伺服器程式啟動時,需要呼叫 loadDataFromDisk 函式去載入資料。

void loadDataFromDisk(void) {
    long long start = ustime();
    if (server.aof_state == AOF_ON) { // 開啟了 aof
        if (loadAppendOnlyFile(server.aof_filename) == C_OK)
            serverLog(LL_NOTICE,"DB loaded from append only file: %.3f seconds",(float)(ustime()-start)/1000000);
    } else {
        if (rdbLoad(server.rdb_filename) == C_OK) {
            serverLog(LL_NOTICE,"DB loaded from disk: %.3f seconds",
                (float)(ustime()-start)/1000000);
        } else if (errno != ENOENT) {
            serverLog(LL_WARNING,"Fatal error loading the DB: %s. Exiting.",strerror(errno));
            exit(1);
        }
    }
}

可以看到,如果開啟了 AOF 功能,就會呼叫 loadAppendOnlyFile 函式,載入 AOF 檔案中的資料到記憶體中。否則,會去呼叫 rdbLoad 函式,載入 RDB 檔案。載入 AOF 檔案的設計很有意思。

FILE *fp = fopen(filename,"r");
struct redis_stat sb;
int old_aof_state = server.aof_state;
long loops = 0;
off_t valid_up_to = 0; /* Offset of the latest well-formed command loaded. */

// 檢查檔案的正確性, 存在,並且不為空
if (fp && redis_fstat(fileno(fp),&sb) != -1 && sb.st_size == 0) {
    server.aof_current_size = 0;
    fclose(fp);
    return C_ERR;
}
if (fp == NULL) {
    serverLog(LL_WARNING,"Fatal error: can`t open the append log file for reading: %s",strerror(errno));
    exit(1);
}
// 暫時關掉 AOF, 防止向該 filename 中寫入新的 AOF 資料
server.aof_state = AOF_OFF;

首先,空檔案沒有必要再去載入了,提前返回。

然後,暫時關閉 AOF 功能,這是為了防止在載入 AOF 檔案的過程中,又有新的資料寫進來

fakeClient = createFakeClient(); // 建立一個不帶網路連線的偽客戶端
startLoading(fp);                // 標記正在 load db,loading = 1

// 讀 AOF 檔案
while(1) {
    int argc, j;
    unsigned long len;
    robj **argv;
    char buf[128];
    sds argsds;
    struct redisCommand *cmd;
    ... ...
        // 如執行命令 SET keytest val,那麼寫入 AOF 檔案中的格式為
        // *3
$3
SET
$7
keytest
$3
val

        if (fgets(buf,sizeof(buf),fp) == NULL) { // 按行讀取 AOF 檔案,*3
            if (feof(fp))
                break;
            else
                goto readerr;
        }

    if (buf[0] != `*`) goto fmterr; // 判斷協議是否正確
    if (buf[1] == ` `) goto readerr; // 資料完整判斷
    argc = atoi(buf+1);
    if (argc < 1) goto fmterr;

    argv = zmalloc(sizeof(robj*)*argc);
    fakeClient->argc = argc;
    fakeClient->argv = argv;

    for (j = 0; j < argc; j++) {
        if (fgets(buf,sizeof(buf),fp) == NULL) { // 依次讀到 $3, $7, $3
            fakeClient->argc = j; /* Free up to j-1. */
            freeFakeClientArgv(fakeClient);
            goto readerr;
        }
        if (buf[0] != `$`) goto fmterr;
        len = strtol(buf+1,NULL,10); // 引數長度
        argsds = sdsnewlen(NULL,len);
        if (len && fread(argsds,len,1,fp) == 0) { // 依次讀到 SET/ keytest/ val
            sdsfree(argsds);
            fakeClient->argc = j; /* Free up to j-1. */
            freeFakeClientArgv(fakeClient);
            goto readerr;
        }
        argv[j] = createObject(OBJ_STRING,argsds);
        if (fread(buf,2,1,fp) == 0) { // 讀到 

            fakeClient->argc = j+1; /* Free up to j. */
            freeFakeClientArgv(fakeClient);
            goto readerr; /* discard CRLF */
        }
    }

    /* Command lookup */
    cmd = lookupCommand(argv[0]->ptr);
    if (!cmd) {
        serverLog(LL_WARNING,"Unknown command `%s` reading the append only file", (char*)argv[0]->ptr);
        exit(1);
    }

    /* Run the command in the context of a fake client */
    cmd->proc(fakeClient);

    /* The fake client should not have a reply */
    serverAssert(fakeClient->bufpos == 0 && listLength(fakeClient->reply) == 0);
    /* The fake client should never get blocked */
    serverAssert((fakeClient->flags & CLIENT_BLOCKED) == 0);

    /* Clean up. Command code may have changed argv/argc so we use the
         * argv/argc of the client instead of the local variables. */
    freeFakeClientArgv(fakeClient);
    if (server.aof_load_truncated) valid_up_to = ftello(fp);
}

上面這部分是載入 AOF 檔案的關鍵,以 SET keytest val 命令對應的 AOF 檔案內容 *3
$3
SET
$7
keytest
$3
val
為例,可以更好地理解上面的邏輯。由於 AOF 檔案中儲存的資料與客戶端傳送的請求格式相同完全符合 Redis 的通訊協議,因此 Redis Server 建立偽客戶端 fakeClient,將解析後的 AOF 檔案資料像客戶端請求一樣呼叫各種指令,cmd->proc(fakeClient),將 AOF 檔案中的資料重現到 Redis Server 資料庫中。

完成以上邏輯後,進行一些收尾工作,如改回 AOF 狀態為 ON,釋放偽客戶端等,並處理一些異常情況,這裡就不展開細講了。

參考

  1. Copy On Write 機制瞭解一下
  2. Redis · 原理介紹 · 利用管道優化aofrewrite

相關文章