[redis 源碼走讀] aof 持久化 (下)

[redis 源碼走讀] aof 持久化,文章篇幅有點長,所以拆分上下爲兩章,可以先讀上一章,再讀這一章。

此博客將逐步遷移到作者新的博客,可以點擊此處進入。



應用場景

aof 持久化應用場景

啓動加載

redis 啓動,程序會模擬一個客戶端加載從 aof 文件讀出的命令。

aof 持久化支持 aof 和 rdb 混合模式,參考上面的 aof 和 rdb 混合結構

int main(int argc, char **argv) {
    ...
    loadDataFromDisk();
    ...
}

void loadDataFromDisk(void) {
    ...
    if (server.aof_state == AOF_ON) {
        if (loadAppendOnlyFile(server.aof_filename) == C_OK)
            serverLog(LL_NOTICE,"DB loaded from append only file: %.3f seconds",(float)(ustime()-start)/1000000);
    }
    ...
}

int loadAppendOnlyFile(char *filename) {
    ...
    // 程序模擬一個客戶端執行從 aof 文件讀出的命令。
    fakeClient = createAOFClient();
    ...
    // 檢查 aof 文件讀取數據方式。
    char sig[5];
    if (fread(sig,1,5,fp) != 5 || memcmp(sig,"REDIS",5) != 0) {
        // 通過 aof 方式加載數據。
        if (fseek(fp,0,SEEK_SET) == -1) goto readerr;
    } else {
        ...
        // 通過 rdb 方式加載數據。
        if (rdbLoadRio(&rdb,RDBFLAGS_AOF_PREAMBLE,NULL) != C_OK) {
            serverLog(LL_WARNING,"Error reading the RDB preamble of the AOF file, AOF loading aborted");
            goto readerr;
        }
    }

    /* Read the actual AOF file, in REPL format, command by command. */
    while(1) {
        // 根據 aof 文件數據結構,取出數據回寫內存。
        ...
    }
    ...
}

寫命令執行流程

  1. client 向 redis 服務發送寫命令。
  2. redis 服務接收命令,進行業務處理。
  3. redis 服務將新的寫命令追加到 aof 數據緩衝區。
  4. redis 服務會通過時鐘,(eventloop)事件處理前(beforeSleep)等方法將 aof 數據緩衝區落地,然後清空 aof 緩衝區。
  • 流程
call(client * c, int flags) (/Users/wenfh2020/src/other/redis/src/server.c:3266)
processCommand(client * c) (/Users/wenfh2020/src/other/redis/src/server.c:3552)
...
aeProcessEvents(aeEventLoop * eventLoop, int flags) (/Users/wenfh2020/src/other/redis/src/ae.c:457)
aeMain(aeEventLoop * eventLoop) (/Users/wenfh2020/src/other/redis/src/ae.c:515)
main(int argc, char ** argv) (/Users/wenfh2020/src/other/redis/src/server.c:5054)
  • 執行命令,填充 aof 數據緩衝區
/* Command propagation flags, see propagate() function
   + PROPAGATE_NONE (no propagation of command at all)
   + PROPAGATE_AOF (propagate into the AOF file if is enabled)
   + PROPAGATE_REPL (propagate into the replication link)
*/

#define PROPAGATE_NONE 0
#define PROPAGATE_AOF 1
#define PROPAGATE_REPL 2

void call(client *c, int flags) {
    ...
    c->cmd->proc(c);
    ...
    if (propagate_flags != PROPAGATE_NONE && !(c->cmd->flags & CMD_MODULE))
        propagate(c->cmd,c->db->id,c->argv,c->argc,propagate_flags);
    ...
}

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);
    ...
}

// aof 緩衝區
struct redisServer {
    ...
    sds aof_buf;      /* AOF buffer, written before entering the event loop */
    ...
}

// 追加內容到 aof 文件
void feedAppendOnlyFile(struct redisCommand *cmd, int dictid, robj **argv, int argc) {
    sds buf = sdsempty();
    robj *tmpargv[3];

    // 命令執行,需要指定到對應數據庫。
    if (dictid != server.aof_selected_db) {
        char seldb[64];

        snprintf(seldb,sizeof(seldb),"%d",dictid);
        buf = sdscatprintf(buf,"*2\r\n$6\r\nSELECT\r\n$%lu\r\n%s\r\n",
            (unsigned long)strlen(seldb),seldb);
        server.aof_selected_db = dictid;
    }
    ...
    // 將命令格式化爲 redis 命令格式,然後追加到 aof 數據緩衝區。
    buf = catAppendOnlyGenericCommand(buf,argc,argv);
    ...
    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));
    ...
}
  • 重寫過程中,父進程接收到新的命令,父進程發送給子進程,對重寫數據進行追加。

    父子進程通過管道進行通信交互。

void feedAppendOnlyFile(struct redisCommand *cmd, int dictid, robj **argv, int argc) {
    ...
    // 如果有子進程正在重寫,父進程將新的數據發送給正在重寫的子進程,使得重寫文件數據更完備。
    if (server.aof_child_pid != -1)
        aofRewriteBufferAppend((unsigned char*)buf,sdslen(buf));
    ...
}

// 將數據保存到重寫緩衝區鏈表。然後通過父子進程管道進行數據傳輸
void aofRewriteBufferAppend(unsigned char *s, unsigned long len) {}

// 父進程通過管道把重寫緩衝區數據,發送到子進程
void aofChildWriteDiffData(aeEventLoop *el, int fd, void *privdata, int mask) {}

// 子進程讀取父進程發送的數據。
ssize_t aofReadDiffFromParent(void) {...}

// 創建父子進程通信管道
int aofCreatePipes(void) {...}

// 父子結束通信
void aofChildPipeReadable(aeEventLoop *el, int fd, void *privdata, int mask) {}

定時保存

主要對延時刷新和寫磁盤出現錯誤回寫的檢查刷新。

/* Using the following macro you can run code inside serverCron() with the
 * specified period, specified in milliseconds.
 * The actual resolution depends on server.hz. */
#define run_with_period(_ms_)         \
    if ((_ms_ <= 1000 / server.hz) || \
        !(cronloops % ((_ms_) / (1000 / server.hz))))

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
    ...
    // 如果有延時任務,定時檢查刷新。
    if (server.aof_flush_postponed_start) flushAppendOnlyFile(0);

    // 刷新緩存到磁盤出現錯誤(例如:磁盤滿了),定時檢查回寫。
    // hz 頻率爲 10 ,這裏一般每十次時鐘檢查一次。
    run_with_period(1000) {
        if (server.aof_last_write_status == C_ERR)
            flushAppendOnlyFile(0);
    }
    ...
    server.cronloops++;
    return 1000/server.hz;
}

重寫

服務器接收到寫入操作命令會追加到 aof 文件,那麼 aof 文件相當於一個流水文件。隨着時間推移,文件將會越來越大。然而 aof 文件主要目的是爲了持久化,並不是爲了記錄服務器流水。這些流水命令有可能很多是冗餘的,需要重新整理——通過重寫來減小 aof 文件體積。

例如下面 4 條命令,會追加記錄到 aof 文件,因爲對同一個 key 操作,內存裏最終數據 key1 對應的數據是 4,這樣前面 3 條歷史命令是冗餘的,通過重寫功能,aof 文件只留下 key 對應的最新的 value。

set key1 1
set key1 2
set key1 3
set key1 4

重寫方式

void bgrewriteaofCommand(client *c) {
    if (server.aof_child_pid != -1) {
        // 當重寫正在進行時,返回錯誤。
        addReplyError(c,"Background append only file rewriting already in progress");
    } else if (hasActiveChildProcess()) {
        // 當有其它子進程正在進行工作時,延後執行。
        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 {
        // 重寫操作失敗,檢查原因。
        addReplyError(c,"Can't execute an AOF background rewriting. "
                        "Please check the server logs for more information.");
    }
}
  • 時鐘定期檢查 redis 使用內存大小,當超過配置的閾值,觸發自動重寫。
# redis.conf

# 當前增加的內存超過上一次重寫後的內存百分比,纔會觸發自動重寫。
auto-aof-rewrite-percentage 100

# 內存重寫下限
auto-aof-rewrite-min-size 64mb
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
    ...
    /* Trigger an AOF rewrite if needed. */
    if (server.aof_state == AOF_ON &&
        !hasActiveChildProcess() &&
        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%% growth",growth);
            rewriteAppendOnlyFileBackground();
        }
    }
    ...
}

重寫實現

  1. 父進程 fork 子進程實現重寫邏輯。
  2. 子進程創建 aof 臨時文件存儲重寫子進程fork-on-write 內存到 aof 文件。
  3. 子進程重寫完成 fork 內存數據內容後,追加在重寫過程中父進程發送的新的內容。
  4. 子進程結束父子進程管道通信。
  5. 更新臨時文件覆蓋舊的文件。
// 父進程 fork 子進程進行 aof 重寫
int rewriteAppendOnlyFileBackground(void) {
    ...
    if ((childpid = redisFork()) == 0) {
        ...
        if (rewriteAppendOnlyFile(tmpfile) == C_OK) {
            sendChildCOWInfo(CHILD_INFO_TYPE_AOF, "AOF rewrite");
            exitFromChild(0);
        } else {
            exitFromChild(1);
        }
    } else {
        /* Parent */
        ...
    }
    return C_OK; /* unreached */
}

// 重寫 aof 實現邏輯
int rewriteAppendOnlyFile(char *filename) {
    rio aof;
    FILE *fp;
    char tmpfile[256];
    char byte;

    // 創建 aof 臨時文件。
    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 rewriteAppendOnlyFile(): %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);

    startSaving(RDBFLAGS_AOF_PREAMBLE);

    // 根據配置,重寫文件內容方式,rdb 或者 aof,aof 存儲方式支持 rdb 和 aof 內容兼容在同一個 aof 文件。
    if (server.aof_use_rdb_preamble) {
        int error;
        if (rdbSaveRio(&aof,&error,RDBFLAGS_AOF_PREAMBLE,NULL) == C_ERR) {
            errno = error;
            goto werr;
        }
    } else {
        if (rewriteAppendOnlyFileRio(&aof) == C_ERR) goto werr;
    }

    // 進程內存更新完畢,刷新文件到磁盤。
    if (fflush(fp) == EOF) goto werr;
    if (fsync(fileno(fp)) == -1) goto werr;

    // 子進程接收父進程發送的新數據。
    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();
    }

    // 子進程通知父進程不要發新的數據了。
    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;

    // 父進程收到子進程的結束通知,發送確認給子進程。
    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...");

    /* Read the final diff if any. */
    aofReadDiffFromParent();

    // 子進程接收父進程發送的內容緩存在緩衝區,將緩衝區內容追加到重寫 aof 文件後。
    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;

    // 新的重寫 aof 文件,覆蓋舊的文件。
    if (rename(tmpfile,filename) == -1) {
        serverLog(LL_WARNING,"Error moving temp append only file on the final destination: %s", strerror(errno));
        unlink(tmpfile);
        stopSaving(0);
        return C_ERR;
    }
    serverLog(LL_NOTICE,"SYNC append only file rewrite performed");
    stopSaving(1);
    return C_OK;

werr:
    serverLog(LL_WARNING,"Write error writing append only file on disk: %s", strerror(errno));
    fclose(fp);
    unlink(tmpfile);
    stopSaving(0);
    return C_ERR;
}

調試

我一直認爲:看文檔和結合源碼調試是理解一個項目的最好方法。

  • gdb 調試,在自己感興趣的地方設下斷點,通過調試熟悉 redis aof 持久化工作流程。

    調試方法可以參考我的帖子: 用 gdb 調試 redis

調試流程

  • 開啓日誌
# redis.conf

# Specify the server verbosity level.
# This can be one of:
# debug (a lot of information, useful for development/testing)
# verbose (many rarely useful info, but not a mess like the debug level)
# notice (moderately verbose, what you want in production probably)
# warning (only very important / critical messages are logged)
loglevel notice

# Specify the log file name. Also the empty string can be used to force
# Redis to log on the standard output. Note that if you use standard
# output for logging but daemonize, logs will be sent to /dev/null
logfile "redis.log"

總結

  • aof 文件存儲 RESP 命令,新數據追加到文件末。
  • aof 存儲爲了避免冗餘,需要設置重寫處理。
  • aof 有三種存儲策略,默認每秒存盤一次。根據自己的使用場景,選擇存儲策略。
  • 每秒存盤策略和重寫功能通過多線程異步處理,保證主線程高性能。
  • 關注 redis 的博客,多看 redis.conf 配置項,裏面有很多信息量。
  • aof 持久化文件支持 aof 和 rdb 方式混合存儲,可以快速重寫,並且減少 aof 體積。
  • aof 與 rdb 相比文件體積大,但是容災能力強,出現問題丟失數據少。

參考


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章