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

aof (Append Only File) 是 redis 持久化的其中一種方式。

服務器接收的每個寫入操作命令,都會追加記錄到 aof 文件末尾,當服務器重新啓動時,記錄的命令會重新載入到服務器內存還原數據。這一章我們走讀一下源碼,看看 aof 持久化的數據結構和應用場景是怎樣的。

主要源碼邏輯在 aof.c 文件中。
此博客將逐步遷移到作者新的博客,可以點擊此處進入。



在瞭解 redis 持久化功能前,可以先看看 redis 作者這兩篇文章:

鏈接可能被牆,可以用國內搜索引擎搜索下對應的文章題目。


開啓 aof 持久化模式

可以看一下 redis.conf 有關 aof 持久化配置,有redis 作者豐富的註釋內容。

# 持久化方式 (yes - aof) / (no - rdb)
appendonly yes

# aof 文件名,默認 "appendonly.aof"
appendfilename "appendonly.aof"

結構

aof 文件結構

aof 文件結構

aof 文件可以由 redis 協議命令組成文本文件。 第一次啓動 redis,執行第一個寫命令: set key1111 1111。我們觀察一下 aof 文件:

  • redis 記錄了 select 數據庫命令,^Mcat 命令打印的 \r\n
# cat -v appendonly.aof
*2^M
$6^M
SELECT^M
$1^M
0^M
*3^M
$3^M
set^M
$7^M
key1111^M
$4^M
1111^M
  • 命令存儲文本。
# set key1111 1111
*3\r\n$3\r\nset\r\n$7\r\nkey1111$4\r\n$1111\r\n
  • RESP 協議格式,以 \r\n 作爲分隔符,有一個作用:可以用 fgets,將文件數據一行一行讀出來。
*<命令參數個數>\r\n$<第1個參數字符串長度>\r\n$<第1個參數字符串>\r\n$<第2個參數字符串長度>\r\n$<第2個參數字符串>\r\n$<第n個參數字符串長度>\r\n$<第n個參數字符串>
  • aof 追加命令記錄源碼。
sds catAppendOnlyGenericCommand(sds dst, int argc, robj **argv) {
    char buf[32];
    int len, j;
    robj *o;

    // 命令參數個數
    buf[0] = '*';
    len = 1+ll2string(buf+1,sizeof(buf)-1,argc);
    buf[len++] = '\r';
    buf[len++] = '\n';
    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++] = '\r';
        buf[len++] = '\n';
        dst = sdscatlen(dst,buf,len);
        // 參數
        dst = sdscatlen(dst,o->ptr,sdslen(o->ptr));
        dst = sdscatlen(dst,"\r\n",2);
        decrRefCount(o);
    }
    return dst;
}

aof 和 rdb 混合結構

rdb aof 混合結構

redis 支持 aof 和 rdb 持久化同時使用,rdb 和 aof 存儲格式同時存儲在一個 aof 文件中。

rdb 持久化速度快,而且落地文件小,這個優勢理應加強使用。redis 持久化目前有兩種方式,最終結合爲一種方式,使其更加高效,這是 redis 作者一直努力的目標。

有關 rdb 持久化,可以參考我的帖子:

[redis 源碼走讀] rdb 持久化 - 文件結構

[redis 源碼走讀] rdb 持久化 - 應用場景

  • 可以通過配置,aof 持久化模式下,內存數據可以重寫存儲爲 rdb 格式的 aof 文件。
# redis.conf

# 開啓 aof 持久化模式
appendonly yes

# [RDB file][AOF tail] 支持 aof 和 rdb 混合持久化。
aof-use-rdb-preamble yes
// rdb 持久化時,添加 aof 標識。
int rdbSaveInfoAuxFields(rio *rdb, int rdbflags, rdbSaveInfo *rsi) {
    ...
    if (rdbSaveAuxFieldStrInt(rdb,"aof-preamble",aof_preamble) == -1) return -1;
    ...
}
  • redis 第一次啓動後,執行第二個命令 bgrewriteaof 重寫 aof 文件。
# cat -v appendonly.aof
REDIS0009�      redis-ver^K999.999.999�
redis-bits�@�^Ectime�M-^J�}^�^Hused-mem�^Pl^Q^@�^Laof-preamble
�^A�^@�^A^@^@^Gkey1111�W^D��^L�6Afi�
  • redis 第一次啓動後,執行第三個命令 set key2222 2222,aof 文件結構展示了 rdb 和 aof 結合存儲方式。
# cat -v appendonly.aof
REDIS0009�      redis-ver^K999.999.999�
redis-bits�@�^Ectime�M-^J�}^�^Hused-mem�^Pl^Q^@�^Laof-preamble
�^A�^@�^A^@^@^Gkey1111�W^D��^L�6Afi�*2^M
$6^M
SELECT^M
$1^M
0^M
*3^M
$3^M
set^M
$7^M
key2222^M
$4^M
2222^M

持久化策略

策略

磁盤 I/O 速度慢,redis 作爲高性能的緩存數據庫,在平衡性能和持久化上,提供了幾個存儲策略:

aof 持久化,每秒刷新一次緩存到磁盤,這是 redis aof 持久化默認的操作,兼顧性能和持久化。如果使用場景數據很重要,可以設置每條命令刷新磁盤一次,但是速度會非常慢。如果 redis 只作爲緩存,持久化不那麼重要,那麼刷盤行爲交給 Linux 系統管理。

  • 每秒將新命令緩存刷新到磁盤。速度足夠快,如果 redis 發生異常,您可能會丟失1秒的數據。
# redis.conf
appendfsync everysec
  • 每次將新命令刷新到磁盤,非常非常慢,但是非常安全。
# redis.conf
appendfsync always
  • redis 不主動刷新文件緩存到磁盤,只需將數據交給操作系統即可。速度更快,但是更不安全。一般情況下,Linux 使用此配置每30秒刷新一次數據。
# redis.conf
appendfsync no

流程原理

  • 文件數據刷新到磁盤原理:

    傳統的 UNIX 實現在內核中設有緩衝存儲器,⼤多數磁盤 I/O 都通過緩存進⾏。

    當將數據寫到文件上時,通常該數據先由內核複製到緩存中,如果該緩存尚未寫滿,則並不將其排入輸出隊列,⽽是等待其寫滿或者當內核需要重⽤該緩存以便存放其他磁盤塊數據時,再將該緩存排入輸出隊列,然後待其到達隊首時,才進⾏實際的 I/O 操作。這種輸出⽅式被稱之爲延遲寫(delayed write)。

    延遲寫減少了磁盤讀寫次數,但是卻降低了文件內容的更新速度,使得欲寫到⽂件中的數據在⼀段時間內並沒有寫到磁盤上。當系統發⽣生故障時,這種延遲可能造成⽂件更新內容的丟失。爲了保證磁盤上實際文件系統與緩存中內容的一致性,UNIX系統提供了 sync 和 fsync 兩個系統調⽤函數。

    sync 只是將所有修改過的塊的緩存排入寫隊列,然後就返回,它並不等待實際 I/O操作結束。系統精靈進程 (通常稱爲 update)一般每隔 30秒調⽤一次 sync 函數。這就保證了定期刷新內核的塊緩存。

    函數fsync 只引⽤單個文件,它等待I/O結束,然後返回。fsync 可用於數據庫這樣的應用程序,它確保修改過的塊⽴即寫到磁盤上。

    上文引用自 《UNINX 環境高級編程》 4.24

數據持久化流程

  • 文件數據刷新到磁盤流程。
  1. client 向 redis 服務發送寫命令。

  2. redis 服務接收到 client 發送的寫命令,存儲於 redis 進程內存中(redis 服務緩存)。

  3. redis 服務調用接口 write 將進程內存數據寫入文件。

    void flushAppendOnlyFile(int force) {
        ...
        nwritten = aofWrite(server.aof_fd,server.aof_buf,sdslen(server.aof_buf));
        ...
    }
    
  4. redis 服務調用接口(redis_fsync),將文件在內核緩衝區的數據刷新到磁盤緩衝區中。

    /* Define redis_fsync to fdatasync() in Linux and fsync() for all the rest */
    #ifdef __linux__
    #define redis_fsync fdatasync
    #else
    #define redis_fsync fsync
    #endif
    
  5. 磁盤控制器將磁盤緩衝區數據寫入到磁盤物理介質中。


流程走到第 5 步,數據纔算真正持久化成功。其中 2-4 步驟,一般情況下,系統會提供對外接口給服務控制,但是第 5 步沒有接口,redis 服務控制不了磁盤緩存寫入物理介質。一般情況下,進程正常退出或者崩潰退出,第 5 步機器系統會執行的。但是如果斷電情況或其他物理異常,這樣磁盤數據還是會丟失一部分。

如果用 appendfsync everysec 配置,正常情況程序退出可能會丟失 1 - 2 秒數據,但是斷電等物理情況導致系統終止,丟失的數據就不可預料了。

參考 Redis persistence demystified


策略實現

#define AOF_WRITE_LOG_ERROR_RATE 30 /* Seconds between errors logging. */

// 刷新緩存到磁盤。
void flushAppendOnlyFile(int force) {
    ssize_t nwritten;
    int sync_in_progress = 0;
    mstime_t latency;

    // 新的命令數據是先寫入 aof 緩衝區的,所以先判斷緩衝區是否有數據需要刷新到磁盤。
    if (sdslen(server.aof_buf) == 0) {
        /* 每秒刷新策略,有可能存在緩衝區是空的,但是還有數據沒刷新磁盤的情況,需要執行刷新操作。
         * 當異步線程還有刷盤任務沒有完成,新的刷盤任務是不會執行的,但是 aof_buf 已經寫進了
         * 文件緩存,aof_buf 緩存任務已經完成需要清空。只是文件緩存還沒刷新到磁盤,數據只在文件緩存
         * 裏,還算不上最終落地,需要調用 redis_fsync 纔會將文件緩存刷新到磁盤。* aof_fsync_offset 纔會最後更新到刷盤的位置*/
        if (server.aof_fsync == AOF_FSYNC_EVERYSEC &&
            server.aof_fsync_offset != server.aof_current_size &&
            server.unixtime > server.aof_last_fsync &&
            !(sync_in_progress = aofFsyncInProgress())) {
            goto try_fsync;
        } else {
            return;
        }
    }

    // 每秒刷新策略,採用的是後臺線程刷新方式,檢查後臺線程是否還有刷新任務沒完成。
    if (server.aof_fsync == AOF_FSYNC_EVERYSEC)
        sync_in_progress = aofFsyncInProgress();

    // 部分操作需要 force 強制寫入,不接受延時。例如退出 redis 服務。
    if (server.aof_fsync == AOF_FSYNC_EVERYSEC && !force) {
        if (sync_in_progress) {
            if (server.aof_flush_postponed_start == 0) {
                // 如果後臺線程還有刷新任務,當前刷新需要延後操作。
                server.aof_flush_postponed_start = server.unixtime;
                return;
            } else if (server.unixtime - server.aof_flush_postponed_start < 2) {
                // 延時操作不能超過 2 秒,否則強制執行。
                return;
            }

            // 延時超時,強制執行。
            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.");
        }
    }

    ...

    // 寫緩衝區數據到文件。
    nwritten = aofWrite(server.aof_fd,server.aof_buf,sdslen(server.aof_buf));
    ...
    /* We performed the write so reset the postponed flush sentinel to zero. */
    server.aof_flush_postponed_start = 0;

    // 處理寫文件異常
    if (nwritten != (ssize_t)sdslen(server.aof_buf)) {
        static time_t last_write_error_log = 0;
        int can_log = 0;

        // 設置異常日誌打印頻率
        if ((server.unixtime - last_write_error_log) > AOF_WRITE_LOG_ERROR_RATE) {
            can_log = 1;
            last_write_error_log = server.unixtime;
        }

        /* Log the AOF write error and record the error code. */
        if (nwritten == -1) {
            if (can_log) {
                serverLog(LL_WARNING,"Error writing to the AOF file: %s",
                    strerror(errno));
                server.aof_last_write_errno = errno;
            }
        } else {
            if (can_log) {
                serverLog(LL_WARNING,"Short write while writing to "
                                       "the AOF file: (nwritten=%lld, "
                                       "expected=%lld)",
                                       (long long)nwritten,
                                       (long long)sdslen(server.aof_buf));
            }

            /* 寫入了部分數據,新寫入的數據有可能是不完整的命令。這樣會導致 redis 啓動時,
             * 解析 aof 文件失敗,所以需要將文件截斷到上一次有效寫入的位置。*/
            if (ftruncate(server.aof_fd, server.aof_current_size) == -1) {
                if (can_log) {
                    serverLog(LL_WARNING, "Could not remove short write "
                             "from the append-only file.  Redis may refuse "
                             "to load the AOF the next time it starts.  "
                             "ftruncate: %s", strerror(errno));
                }
            } else {
                /* If the ftruncate() succeeded we can set nwritten to
                 * -1 since there is no longer partial data into the AOF. */
                nwritten = -1;
            }
            server.aof_last_write_errno = ENOSPC;
        }

        // 處理錯誤
        if (server.aof_fsync == AOF_FSYNC_ALWAYS) {
            // 命令實時更新策略下,如果出現寫文件錯誤,需要關閉服務。
            serverLog(LL_WARNING,"Can't recover from AOF write error when the AOF fsync policy is 'always'. Exiting...");
            exit(1);
        } else {
            /* 其它策略,出現寫入錯誤,更新寫入成功部分,沒寫成功部分則在時鐘裏定時檢查,重新寫入。*/
            server.aof_last_write_status = C_ERR;

            if (nwritten > 0) {
                server.aof_current_size += nwritten;
                sdsrange(server.aof_buf,nwritten,-1);
            }
            return; /* We'll try again on the next call... */
        }
    } else {
        // 之前持久化異常,現在已經正常恢復,解除異常標識。
        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;
        }
    }
    server.aof_current_size += nwritten;

    // 持久化成功,清空 aof 緩衝區。
    if ((sdslen(server.aof_buf)+sdsavail(server.aof_buf)) < 4000) {
        sdsclear(server.aof_buf);
    } else {
        sdsfree(server.aof_buf);
        server.aof_buf = sdsempty();
    }

try_fsync:
    // 檢查當有子進程在操作時是否允許刷新文件緩存到磁盤。
    if (server.aof_no_fsync_on_rewrite && hasActiveChildProcess())
        return;

    // 刷新文件緩存到磁盤。
    if (server.aof_fsync == AOF_FSYNC_ALWAYS) {
        latencyStartMonitor(latency);
        redis_fsync(server.aof_fd); /* Let's try to get this data on the disk */
        latencyEndMonitor(latency);
        latencyAddSampleIfNeeded("aof-fsync-always",latency);
        server.aof_fsync_offset = server.aof_current_size;
        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_fsync_offset = server.aof_current_size;
        }
        server.aof_last_fsync = server.unixtime;
    }
}

異步持久化

redis 作爲高性能緩存系統,它的主邏輯都在主進程主線程中實現運行的。而持久化寫磁盤是一個低效緩慢操作,因此redis 一般情況下不允許這個操作在主線程中運行。這樣 redis 開啓了後臺線程,用來異步處理任務,保障主線程可以高速運行。

  • 添加異步任務
/* Define redis_fsync to fdatasync() in Linux and fsync() for all the rest */
#ifdef __linux__
#define redis_fsync fdatasync
#else
#define redis_fsync fsync
#endif

void flushAppendOnlyFile(int force) {
    ...
    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_fsync_offset = server.aof_current_size;
        }
        server.aof_last_fsync = server.unixtime;
    }
    ...
}

// 添加異步任務
void aof_background_fsync(int fd) {
    bioCreateBackgroundJob(BIO_AOF_FSYNC,(void*)(long)fd,NULL,NULL);
}
  • 異步線程刷新緩存到磁盤。
// 後臺異步線程創建
void bioInit(void) {
    ...
    for (j = 0; j < BIO_NUM_OPS; j++) {
        void *arg = (void*)(unsigned long) j;
        // 創建線程
        if (pthread_create(&thread,&attr,bioProcessBackgroundJobs,arg) != 0) {
            serverLog(LL_WARNING,"Fatal: Can't initialize Background Jobs.");
            exit(1);
        }
        bio_threads[j] = thread;
    }
}

// 添加異步任務
void bioCreateBackgroundJob(int type, void *arg1, void *arg2, void *arg3) {
    struct bio_job *job = zmalloc(sizeof(*job));

    job->time = time(NULL);
    job->arg1 = arg1;
    job->arg2 = arg2;
    job->arg3 = arg3;
    pthread_mutex_lock(&bio_mutex[type]);
    listAddNodeTail(bio_jobs[type],job);
    bio_pending[type]++;
    pthread_cond_signal(&bio_newjob_cond[type]);
    pthread_mutex_unlock(&bio_mutex[type]);
}

// 線程處理
void *bioProcessBackgroundJobs(void *arg) {
    ...
    else if (type == BIO_AOF_FSYNC) {
        // 刷新內核緩存到磁盤。
        redis_fsync((long)job->arg1);
    }
    ...
}

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