Redis深度歷險 - 持久化機制 Redis深度歷險 - 持久化實現

Redis深度歷險 - 持久化實現

Redis支持兩種持久化的方式:快照和AOF;快照指的是會將當前數據庫中所有的數據記錄下來,是一次性全量備份;AOF日誌記錄的是內存數據修改的指令文本記錄,是連續的增量備份。


Redis - 快照

快照是一次性的全量備份,執行bgsave命令就會開始全量備份,在此過程中並不會阻塞Redis的正常使用;從這一點上來看,很容易猜到其底層應該是fork了一個新的進程來實現的。

快照命令

docker run -d -p 6379:6379 --name=redis redis
docker exec -it redis /bin/bash
    最終會在目錄下生成dump.rdb,dump.rdb中的內容並非是明文的

fork的原理

    `fork`是Linux中用來創建子進程的函數,在歷史版本中`fork`創建子進程時會對父進程做一份複製操作,即子進程擁有父進程完全一樣的數據,Redis正是利用此原理實現的,子進程擁有所有的數據進行備份操作,父進程依然執行業務。

    在高版本中的`fork`中實現了`COW`的特性,即寫時複製,過去的版本中此特性由vfork實現;在Redis環境中,寫時複製的含義是:子進程創建完畢後,父子進程共享一片內存空間,數據段在操作系統中由很多頁面組成,當父進程對某個頁面進行操作寫入時會先複製一份分離出來進行修改。

快照原理

void bgsaveCommand(client *c)
................
    else if (rdbSaveBackground(server.rdb_filename,rsiptr) == C_OK) {
        addReplyStatus(c,"Background saving started");
................
    
createStringConfig("dbfilename", NULL, MODIFIABLE_CONFIG, ALLOW_EMPTY_STRING, server.rdb_filename, "dump.rdb", isValidDBfilename, NULL),
    默認情況下備份文件名就是dump.rdb。


int rdbSaveBackground(char *filename, rdbSaveInfo *rsi)
..................
    openChildInfoPipe();
    if ((childpid = redisFork()) == 0) {
        ....
        retval = rdbSave(filename,rsi);
        ....
.................
int redisFork() {
    int childpid;
    long long start = ustime();
    if ((childpid = fork()) == 0) {
        /* Child */
        closeListeningSockets(0);               //子進程關閉不需要的監聽句柄
        setupChildSignalHandlers();             //創建信號將領
    } else {
        /* Parent */
        server.stat_fork_time = ustime()-start;
        server.stat_fork_rate = (double) zmalloc_used_memory() * 1000000 / server.stat_fork_time / (1024*1024*1024); /* GB per second. */
        latencyAddSampleIfNeeded("fork",server.stat_fork_time/1000);
        if (childpid == -1) {
            return -1;
        }
        updateDictResizePolicy();       //避免大量的內存頁面的複製,所以禁止hash表的rehash操作
    }
    return childpid;
}
    開始執行快照時先進行創建管道負責後續的通信,之後就執行`fork`操作。父進程在執行快照的時候會禁止`dict`的`rehash`操作,但是這段代碼好像有問題,實際上無法生效。

    `Redis`的快照後續的存儲並沒有涉及到非常複雜的算法技術等,但是也並非文本協議存儲的;比如存儲時會記錄數據的類型,在`Redis`中以宏定義表示,存儲時並沒有說轉成字符串表示,以文本方式打開的話可能會看到亂碼之類的。

快照相關配置

  • rdbcompression yes:rdb文件是否壓縮
  • Rdbchecksum yes:恢復數據時是否校驗完整性,實質上時進行了一些crc64計算;每次進行寫入文件時,將上一次crc64的值和此次寫入的數據進行crc64計算
  • dbfilename dump.rdb:到處文件名
  • dir ./:存放路徑

AOF存儲

AOF存儲的是Redis服務器的順序指令記錄,記錄對內存有修改的記錄;如果需要恢復Redis數據,只需要對所有的執行進行重放就可以恢復了

AOF配置

  • appendonly yes:是否開啓AOF持久化
  • appendfilename appendonly.aof:文件名
  • appendfsync:磁盤寫入策略,主要是因爲文件IO有緩衝區可能數據沒有真正寫入到文件
    • always:每次寫入磁盤,性能較差
    • everysec:每秒寫入一次
    • no:系統處理緩存回寫

AOF的效果

*2
$6
SELECT
$1
0
*3
$3
set
$4
name
$8
    在指定的文件中可以看到AOF是以文本的形式存儲的操作記錄,這部分基本與Redis的文本傳輸協議是類似的。

AOF的實現

createBoolConfig("appendonly", NULL, MODIFIABLE_CONFIG, server.aof_enabled, 0, NULL, updateAppendonly),
.........
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);
}
    首先讀取配置文件`appendonly`決定是否開啓,服務器執行完寫命令就會調用`propagate`繼續追加記錄


void feedAppendOnlyFile(struct redisCommand *cmd, int dictid, robj **argv, int argc) {
    sds buf = sdsempty();
    robj *tmpargv[3];
    
    //如果數據庫進行了切換就會記錄SELECT命令,Redis默認是有16個數據庫的
    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;
    }

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

    sdsfree(buf);
}
    由於Redis有多個數據庫db文件, 所有在數據庫切換的情況下就需要記錄SELECT命令;同時會對命令做一些處理,最終調用`aofRewriteBufferAppend`存儲內容。


void aofRewriteBufferAppend(unsigned char *s, unsigned long len) {
    listNode *ln = listLast(server.aof_rewrite_buf_blocks);
    aofrwblock *block = ln ? ln->value : NULL;

    while(len) {
        /* If we already got at least an allocated block, try appending
         * at least some piece into it. */
        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) { /* First block to allocate, or need another block. */
            int numblocks;

            block = zmalloc(sizeof(*block));
            block->free = AOF_RW_BUF_BLOCK_SIZE;
            block->used = 0;
            listAddNodeTail(server.aof_rewrite_buf_blocks,block);

            /* Log every time we cross more 10 or 100 blocks, respectively
             * as a notice or warning. */
            numblocks = listLength(server.aof_rewrite_buf_blocks);
            if (((numblocks+1) % 10) == 0) {
                int level = ((numblocks+1) % 100) == 0 ? LL_WARNING :
                                                         LL_NOTICE;
                serverLog(level,"Background AOF buffer size: %lu MB",
                    aofRewriteBufferSize()/(1024*1024));
            }
        }
    }

    /* 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的優缺點

  • AOF隨着進程的執行,整個文件會越來越龐大;需要進行重寫瘦身
  • AOF在恢復時需要重新執行一邊所有的命令,耗時較長

總結

    AOF和RDB各有各的長處,在實際部署時也非一成不變的;一種很常用的方法就是同時進行RDB和AOF記錄,RDB記錄鏡像,AOF記錄從記錄鏡像開始的日誌,這樣雙方的優勢都可以利用起來。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章