深刻剖析 redis RDB 持久化策略

簡介 redis 持久化 RDB、AOF

redis 提供兩種持久化方式:RDB 和 AOF。redis 容許二者結合,也容許二者同時關閉。git

  • RDB 能夠定時備分內存中的數據集。服務器啓動的時候,能夠從 RDB 文件中回覆數據集。
  • AOF 能夠記錄服務器的全部寫操做。在服務器從新啓動的時候,會把全部的寫操做從新執行一遍,從而實現數據備份。當寫操做集過大(比原有的數據集還大),redis 會重寫寫操做集。

本篇主要講的是 RDB 持久化,瞭解 RDB 的數據保存結構和運做機制。redis 主要在 rdb.h 和 rdb.c 兩個文件中實現 RDB 的操做。github

數據結構 rio

持久化的 IO 操做在 rio.h 和 rio.c 中實現,核心數據結構是 struct rio。RDB 中的幾乎每個函數都帶有 rio 參數。struct rio 既適用於文件,又適用於內存緩存,從 struct rio 的實現可見一斑。redis

struct _rio {
    // 函數指針,包括讀操做,寫操做和文件指針移動操做
    /* Backend functions.
     * Since this functions do not tolerate short writes or reads the return
     * value is simplified to: zero on error, non zero on complete success. */
    size_t (*read)(struct _rio *, void *buf, size_t len);
    size_t (*write)(struct _rio *, const void *buf, size_t len);
    off_t (*tell)(struct _rio *);

    // 校驗和計算函數
    /* The update_cksum method if not NULL is used to compute the checksum of
     * all the data that was read or written so far. The method should be
     * designed so that can be called with the current checksum, and the buf
     * and len fields pointing to the new block of data to add to the checksum
     * computation. */
    void (*update_cksum)(struct _rio *, const void *buf, size_t len);

    // 校驗和
    /* The current checksum */
    uint64_t cksum;

    // 已經讀取或者寫入的字符數
    /* number of bytes read or written */
    size_t processed_bytes;

    // 每次最多能處理的字符數
    /* maximum single read or write chunk size */
    size_t max_processing_chunk;

    // 能夠是一個內存總的字符串,也能夠是一個文件描述符
    /* Backend-specific vars. */
    union {
        struct {
            sds ptr;
            // 偏移量
            off_t pos;
        } buffer;
        struct {
            FILE *fp;
            // 偏移量
            off_t buffered; /* Bytes written since last fsync. */
            off_t autosync; /* fsync after 'autosync' bytes written. */
        } file;
    } io;
};

typedef struct _rio rio;

redis 定義兩個 struct rio,分別是 rioFileIO 和 rioBufferIO,前者用於內存緩存,後者用於文件 IO:數據庫

// 適用於內存緩存
static const rio rioBufferIO = {
    rioBufferRead,
    rioBufferWrite,
    rioBufferTell,
    NULL,           /* update_checksum */
    0,              /* current checksum */
    0,              /* bytes read or written */
    0,              /* read/write chunk size */
    { { NULL, 0 } } /* union for io-specific vars */
};

// 適用於文件 IO
static const rio rioFileIO = {
    rioFileRead,
    rioFileWrite,
    rioFileTell,
    NULL,           /* update_checksum */
    0,              /* current checksum */
    0,              /* bytes read or written */
    0,              /* read/write chunk size */
    { { NULL, 0 } } /* union for io-specific vars */
};

RDB 持久化的運做機制

rdb_persistence

redis 支持兩種方式進行 RDB:當前進程執行和後臺執行(BGSAVE)。RDB BGSAVE 策略是 fork 出一個子進程,把內存中的數據集整個 dump 到硬盤上。兩個場景舉例:緩存

  1. redis 服務器初始化過程當中,設定了定時事件,每隔一段時間就會觸發持久化操做;進入定時事件處理程序中,就會 fork 產生子進程執行持久化操做。
  2. redis 服務器預設了 save 指令,客戶端可要求服務器進程中斷服務,執行持久化操做。

這裏主要展開的內容是 RDB 持久化操做的寫文件過程,讀過程和寫過程相反。子進程的產生髮生在 rdbSaveBackground() 中,真正的 RDB 持久化操做是在 rdbSave(),想要直接進行 RDB 持久化,調用 rdbSave() 便可。服務器

如下主要以代碼的方式來展開 RDB 的運做機制:數據結構

// 備份主程序
/* Save the DB on disk. Return REDIS_ERR on error, REDIS_OK on success */
int rdbSave(char *filename) {
    dictIterator *di = NULL;
    dictEntry *de;
    char tmpfile[256];
    char magic[10];
    int j;
    long long now = mstime();
    FILE *fp;
    rio rdb;
    uint64_t cksum;

    // 打開文件,準備寫
    snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid());
    fp = fopen(tmpfile,"w");
    if (!fp) {
        redisLog(REDIS_WARNING, "Failed opening .rdb for saving: %s",
            strerror(errno));
        return REDIS_ERR;
    }

    // 初始化 rdb 結構體。rdb 結構體內指定了讀寫文件的函數,已寫/讀字符統計等數據
    rioInitWithFile(&rdb,fp);

    if (server.rdb_checksum) // 校驗和
        rdb.update_cksum = rioGenericUpdateChecksum;

    // 先寫入版本號
    snprintf(magic,sizeof(magic),"REDIS%04d",REDIS_RDB_VERSION);
    if (rdbWriteRaw(&rdb,magic,9) == -1) goto werr;

    for (j = 0; j < server.dbnum; j++) {
        // server 中保存的數據
        redisDb *db = server.db+j;

        // 字典
        dict *d = db->dict;
        if (dictSize(d) == 0) continue;

        // 字典迭代器
        di = dictGetSafeIterator(d);
        if (!di) {
            fclose(fp);
            return REDIS_ERR;
        }

        // 寫入 RDB 操做碼
        /* Write the SELECT DB opcode */
        if (rdbSaveType(&rdb,REDIS_RDB_OPCODE_SELECTDB) == -1) goto werr;

        // 寫入數據庫序號
        if (rdbSaveLen(&rdb,j) == -1) goto werr;

        // 寫入數據庫中每個數據項
        /* Iterate this DB writing every entry */
        while((de = dictNext(di)) != NULL) {
            sds keystr = dictGetKey(de);
            robj key,
                *o = dictGetVal(de);
            long long expire;

            // 將 keystr 封裝在 robj 裏
            initStaticStringObject(key,keystr);

            // 獲取過時時間
            expire = getExpire(db,&key);

            // 開始寫入磁盤
            if (rdbSaveKeyValuePair(&rdb,&key,o,expire,now) == -1) goto werr;
        }
        dictReleaseIterator(di);
    }
    di = NULL; /* So that we don't release it again on error. */

    // RDB 結束碼
    /* EOF opcode */
    if (rdbSaveType(&rdb,REDIS_RDB_OPCODE_EOF) == -1) goto werr;

    // 校驗和
    /* CRC64 checksum. It will be zero if checksum computation is disabled, the
     * loading code skips the check in this case. */
    cksum = rdb.cksum;
    memrev64ifbe(&cksum);
    rioWrite(&rdb,&cksum,8);

    // 同步到磁盤
    /* Make sure data will not remain on the OS's output buffers */
    fflush(fp);
    fsync(fileno(fp));
    fclose(fp);

    // 修改臨時文件名爲指定文件名
    /* Use RENAME to make sure the DB file is changed atomically only
     * if the generate DB file is ok. */
    if (rename(tmpfile,filename) == -1) {
        redisLog(REDIS_WARNING,"Error moving temp DB file on the final destination: %s", strerror(errno));
        unlink(tmpfile);
        return REDIS_ERR;
    }
    redisLog(REDIS_NOTICE,"DB saved on disk");
    server.dirty = 0;

    // 記錄成功執行保存的時間
    server.lastsave = time(NULL);

    // 記錄執行的結果狀態爲成功
    server.lastbgsave_status = REDIS_OK;
    return REDIS_OK;

werr:
    // 清理工做,關閉文件描述符等
    fclose(fp);
    unlink(tmpfile);
    redisLog(REDIS_WARNING,"Write error saving DB on disk: %s", strerror(errno));
    if (di) dictReleaseIterator(di);
    return REDIS_ERR;
}

// bgsaveCommand(),serverCron(),syncCommand(),updateSlavesWaitingBgsave() 會調用 rdbSaveBackground()
int rdbSaveBackground(char *filename) {
    pid_t childpid;
    long long start;

    // 已經有後臺程序了,拒絕再次執行
    if (server.rdb_child_pid != -1) return REDIS_ERR;

    server.dirty_before_bgsave = server.dirty;

    // 記錄此次嘗試執行持久化操做的時間
    server.lastbgsave_try = time(NULL);

    start = ustime();
    if ((childpid = fork()) == 0) {
        int retval;

        // 取消監聽
        /* Child */
        closeListeningSockets(0);
        redisSetProcTitle("redis-rdb-bgsave");

        // 執行備份主程序
        retval = rdbSave(filename);

        // 髒數據,其實就是子進程所消耗的內存大小
        if (retval == REDIS_OK) {
            // 獲取髒數據大小
            size_t private_dirty = zmalloc_get_private_dirty();

            // 記錄髒數據
            if (private_dirty) {
                redisLog(REDIS_NOTICE,
                    "RDB: %zu MB of memory used by copy-on-write",
                    private_dirty/(1024*1024));
            }
        }

        // 退出子進程
        exitFromChild((retval == REDIS_OK) ? 0 : 1);
    } else {
        /* Parent */
        // 計算 fork 消耗的時間
        server.stat_fork_time = ustime()-start;

        // fork 出錯
        if (childpid == -1) {
            // 記錄執行的結果狀態爲失敗
            server.lastbgsave_status = REDIS_ERR;
            redisLog(REDIS_WARNING,"Can't save in background: fork: %s",
                strerror(errno));
            return REDIS_ERR;
        }
        redisLog(REDIS_NOTICE,"Background saving started by pid %d",childpid);

        // 記錄保存的起始時間
        server.rdb_save_time_start = time(NULL);

        // 子進程 ID
        server.rdb_child_pid = childpid;
        updateDictResizePolicy();
        return REDIS_OK;
    }
    return REDIS_OK; /* unreached */
}

若是採用 BGSAVE 策略,且內存中的數據集很大,fork() 會由於要爲子進程產生一份虛擬空間表而花費較長的時間;若是此時客戶端請求數量很是大的話,會致使較多的寫時拷貝操做;在 RDB 持久化操做過程當中,每個數據都會致使 write() 系統調用,CPU 資源很緊張。所以,若是在一臺物理機上部署多個 redis,應該避免同時持久化操做。函數

那如何知道 BGSAVE 佔用了多少內存?子進程在結束以前,讀取了自身私有髒數據 Private_Dirty 的大小,這樣作是爲了讓用戶看到 redis 的持久化進程所佔用了有多少的空間。在父進程 fork 產生子進程事後,父子進程雖然有不一樣的虛擬空間,但物理空間上是共存的,直至父進程或者子進程修改內存數據爲止,因此髒數據 Private_Dirty 能夠近似的認爲是子進程,即持久化進程佔用的空間。ui

RDB 數據的組織方式

RDB 的文件組織方式爲:數據集序號1:操做碼:數據1:結束碼:校驗和----數據集序號2:操做碼:數據2:結束碼:校驗和......this

其中,數據的組織方式爲:過時時間:數據類型:鍵:值,即 TVL(type,length,value)。

舉兩個字符串存儲的例子,其餘的大概都以致於的形式來組織數據:

rdb_datastruct_sample

可見,RDB 持久化的結果是一個很是緊湊的文件,幾乎每一位都是有用的信息。若是對 redis RDB 數據組織方式的細則感興趣,能夠參看 rdb.h 和 rdb.c 兩個文件的實現。

對於每個鍵值對都會調用 rdbSaveKeyValuePair(),以下:

int rdbSaveKeyValuePair(rio *rdb, robj *key, robj *val,
                        long long expiretime, long long now)
{
    // 過時時間
    /* Save the expire time */
    if (expiretime != -1) {
        /* If this key is already expired skip it */
        if (expiretime < now) return 0;
        if (rdbSaveType(rdb,REDIS_RDB_OPCODE_EXPIRETIME_MS) == -1) return -1;
        if (rdbSaveMillisecondTime(rdb,expiretime) == -1) return -1;
    }

    /* Save type, key, value */
    // 數據類型
    if (rdbSaveObjectType(rdb,val) == -1) return -1;

    // 鍵
    if (rdbSaveStringObject(rdb,key) == -1) return -1;

    // 值
    if (rdbSaveObject(rdb,val) == -1) return -1;
    return 1;
}

若是對 redis RDB 數據格式細則感興趣,歡迎訪問個人 github & 歡迎討論。

參考文檔

http://redis.io/topics/persistence

----

搗亂 2014-3-26

http://daoluan.net

相關文章
相關標籤/搜索