Redis AOF持久化機制
1. AOF持久化介紹
Redis中支持RDB
和AOF
這兩種持久化機制,目的都是避免因進程退出,造成的數據丟失問題。
- RDB持久化:把當前進程數據生成時間點快照(point-in-time snapshot)保存到硬盤的過程,避免數據意外丟失。
- AOF持久化:以獨立日誌的方式記錄每次寫命令,重啓時在重新執行AOF文件中的命令達到恢復數據的目的。
AOF
的使用:在redis.conf
配置文件中,將appendonly
設置爲yes
,默認的爲no
。
2. AOF持久化的實現
AOF持久化所有註釋:Redis AOF持久化機制源碼註釋
2.1 命令寫入磁盤
2.1.1 命令寫入緩衝區
- 命令問什麼先寫入緩衝區
由於Redis
是單線程響應命令,所以每次寫AOF
文件都直接追加到硬盤中,那麼寫入的性能完全取決於硬盤的負載,所以Redis
會將命令寫入到緩衝區中,然後執行文件同步操作,再將緩衝區內容同步到磁盤中,這樣就很好的保持了高性能。
那麼緩衝區定義如下,它是一個簡單動態字符串(sds),因此很好的和C語言的字符串想兼容。
struct redisServer {
// AOF緩衝區,在進入事件loop之前寫入
sds aof_buf; /* AOF buffer, written before entering the event loop */
};
- 命令的寫入格式
Redis命令寫入的內容直接就是文本協議格式,例如:
*2\r\n$6\r\nSELECT\r\n$1\r\n0\r\n*5\r\n$4\r\nSADD\r\n$3\r\nkey\r\n$2\r\nm3\r\n$2\r\nm2\r\n$2\r\nm1\r\n
根據協議內容,大致可以得出:這是第0
號數據庫,執行了一個SADD key m1 m2 m3
命令。這就是Redis採用文件協議格式的原因之一,文本協議具有很高的可讀性,可以直接進行修改。而且,文本協議還具有很好的兼容性,而且協議採用了\r\n
換行符,所以每次寫入命令只需執行追加操作。
既然是追加操作,因此,源碼中的函數名字也是如此,catAppendOnlyGenericCommand()
函數實現了追加命令到緩衝區中,從這個函數中,可以清楚的看到協議是如何生成的。
// 根據傳入的命令和命令參數,將他們還原成協議格式
sds catAppendOnlyGenericCommand(sds dst, int argc, robj **argv) {
char buf[32];
int len, j;
robj *o;
// 格式:"*<argc>\r\n"
buf[0] = '*';
len = 1+ll2string(buf+1,sizeof(buf)-1,argc);
buf[len++] = '\r';
buf[len++] = '\n';
// 拼接到dst的後面
dst = sdscatlen(dst,buf,len);
// 遍歷所有的參數,建立命令的格式:$<command_len>\r\n<command>\r\n
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; //返回還原後的協議內容
}
這個函數只是追加一個普通的鍵,然而一個過期命令的鍵,需要全部轉換爲PEXPIREAT
,因爲必須將相對時間設置爲絕對時間,否則還原數據庫時,就無法得知該鍵是否過期,Redis的catAppendOnlyExpireAtCommand()
函數實現了這個功能。
// 用sds表示一個 PEXPIREAT 命令,seconds爲生存時間,cmd爲指定轉換的指令
// 這個函數用來轉換 EXPIRE and PEXPIRE 命令成 PEXPIREAT ,以便在AOF時,時間總是一個絕對值
sds catAppendOnlyExpireAtCommand(sds buf, struct redisCommand *cmd, robj *key, robj *seconds) {
long long when;
robj *argv[3];
/* Make sure we can use strtoll */
// 解碼成字符串對象,以便使用strtoll函數
seconds = getDecodedObject(seconds);
// 取出過期值,long long類型
when = strtoll(seconds->ptr,NULL,10);
/* Convert argument into milliseconds for EXPIRE, SETEX, EXPIREAT */
// 將 EXPIRE, SETEX, EXPIREAT 參數的秒轉換成毫秒
if (cmd->proc == expireCommand || cmd->proc == setexCommand ||
cmd->proc == expireatCommand)
{
when *= 1000;
}
/* Convert into absolute time for EXPIRE, PEXPIRE, SETEX, PSETEX */
// 將 EXPIRE, PEXPIRE, SETEX, PSETEX 命令的參數,從相對時間設置爲絕對時間
if (cmd->proc == expireCommand || cmd->proc == pexpireCommand ||
cmd->proc == setexCommand || cmd->proc == psetexCommand)
{
when += mstime();
}
decrRefCount(seconds);
// 創建一個 PEXPIREAT 命令對象
argv[0] = createStringObject("PEXPIREAT",9);
argv[1] = key;
argv[2] = createStringObjectFromLongLong(when);
// 將命令還原成協議格式,追加到buf
buf = catAppendOnlyGenericCommand(buf, 3, argv);
decrRefCount(argv[0]);
decrRefCount(argv[2]);
// 返回buf
return buf;
}
那麼,這兩個函數都是實現的底層功能,因此他們都被feedAppendOnlyFile()
函數最終調用。
這個函數,創建一個空的簡單動態字符串(sds),將當前所有追加命令操作都追加到這個sds中,最終將這個sds追加到server.aof_buf
。。還有就是,這個函數在寫入鍵之前,需要顯式的寫入一個SELECT
命令,以正確的將所有鍵還原到正確的數據庫中。
// 將命令追加到AOF文件中
void feedAppendOnlyFile(struct redisCommand *cmd, int dictid, robj **argv, int argc) {
sds buf = sdsempty(); //設置一個空sds
robj *tmpargv[3];
// 使用SELECT命令,顯式的設置當前數據庫
if (dictid != server.aof_selected_db) {
char seldb[64];
snprintf(seldb,sizeof(seldb),"%d",dictid);
// 構造SELECT命令的協議格式
buf = sdscatprintf(buf,"*2\r\n$6\r\nSELECT\r\n$%lu\r\n%s\r\n",
(unsigned long)strlen(seldb),seldb);
// 執行AOF時,當前的數據庫ID
server.aof_selected_db = dictid;
}
// 如果是 EXPIRE/PEXPIRE/EXPIREAT 三個命令,則要轉換成 PEXPIREAT 命令
if (cmd->proc == expireCommand || cmd->proc == pexpireCommand ||
cmd->proc == expireatCommand) {
/* Translate EXPIRE/PEXPIRE/EXPIREAT into PEXPIREAT */
buf = catAppendOnlyExpireAtCommand(buf,cmd,argv[1],argv[2]);
// 如果是 SETEX/PSETEX 命令,則轉換成 SET and PEXPIREAT
} else if (cmd->proc == setexCommand || cmd->proc == psetexCommand) {
/* Translate SETEX/PSETEX to SET and PEXPIREAT */
// SETEX key seconds value
// 構建SET命令對象
tmpargv[0] = createStringObject("SET",3);
tmpargv[1] = argv[1];
tmpargv[2] = argv[3];
// 將SET命令按協議格式追加到buf中
buf = catAppendOnlyGenericCommand(buf,3,tmpargv);
decrRefCount(tmpargv[0]);
// 將SETEX/PSETEX命令和鍵對象按協議格式追加到buf中
buf = catAppendOnlyExpireAtCommand(buf,cmd,argv[1],argv[2]);
// 其他命令直接按協議格式轉換,然後追加到buf中
} else {
buf = catAppendOnlyGenericCommand(buf,argc,argv);
}
// 如果正在進行AOF,則將命令追加到AOF的緩存中,在重新進入事件循環之前,這些命令會被沖洗到磁盤上,並向client回覆
if (server.aof_state == AOF_ON)
server.aof_buf = sdscatlen(server.aof_buf,buf,sdslen(buf));
// 如果後臺正在進行重寫,那麼將命令追加到重寫緩存區中,以便我們記錄重寫的AOF文件於當前數據庫的差異
if (server.aof_child_pid != -1)
aofRewriteBufferAppend((unsigned char*)buf,sdslen(buf));
sdsfree(buf);
}
2.1.2 緩衝區同步到文件
既然緩衝區提供了高性能的保障,那麼緩衝區中的數據安全問題如何解決呢?只要數據存在於緩衝區,那麼就有丟失的危險。那麼,如果控制同步的頻率呢?Redis中給出了3中緩衝區同步文件的策略。
可配置值 | 說明 |
---|---|
AOF_FSYNC_ALWAYS | 命令寫入aof_buf後調用系統fsync和操作同步到AOF文件,fsync完成後進程程返回 |
AOF_FSYNC_EVERYSEC | 命令寫入aof_buf後調用系統write操作,write完成後線程返回。fsync同步文件操作由進程每秒調用一次 |
AOF_FSYNC_NO | 命令寫入aof_buf後調用系統write操作,不對AOF文件做fsync同步,同步硬盤由操作由操作系統負責 |
我們來了解一下,write和fsync操作,在系統中都做了哪些事:
- write操作:會觸發延遲寫(delayed write)機制。Linux在內核提供頁緩衝區用來提高IO性能,因此,write操作在將數據寫入操作系統的緩衝區後就直接返回,而不一定觸發同步到磁盤的操作。只有在頁空間寫滿,或者達到特定的時間週期,纔會同步到磁盤。因此單純的write操作也是有數據丟失的風險。
- fsync操作:針對單個文件操作,做強制硬盤同步,fsync將阻塞直到寫入硬盤完成後返回。
雖然Redis提供了三種同步策略,兼顧安全和性能的同步策略是:AOF_FSYNC_EVERYSEC。但是仍有丟失數據的風險,而且不是一秒而是兩秒的數據,接下來就看同步的源碼實現:
// 將AOF緩存寫到磁盤中
// 因爲我們需要在回覆client之前對AOF執行寫操作,唯一的機會是在事件loop中,因此累計所有的AOF到緩存中,在下一次重新進入事件loop之前將緩存寫到AOF文件中
// 關於force參數
// 當fsync被設置爲每秒執行一次,如果後臺仍有線程正在執行fsync操作,我們可能會延遲flush操作,因爲write操作可能會被阻塞,當發生這種情況時,說明需要儘快的執行flush操作,會調用 serverCron() 函數。
// 然而如果force被設置爲1,我們會無視後臺的fsync,直接進行寫入操作
#define AOF_WRITE_LOG_ERROR_RATE 30 /* Seconds between errors logging. */
// 將AOF緩存沖洗到磁盤中
void flushAppendOnlyFile(int force) {
ssize_t nwritten;
int sync_in_progress = 0;
mstime_t latency;
// 如果緩衝區中沒有數據,直接返回
if (sdslen(server.aof_buf) == 0) return;
// 同步策略是每秒同步一次
if (server.aof_fsync == AOF_FSYNC_EVERYSEC)
// AOF同步操作是否在後臺正在運行
sync_in_progress = bioPendingJobsOfType(BIO_AOF_FSYNC) != 0;
// 同步策略是每秒同步一次,且不是強制同步的
if (server.aof_fsync == AOF_FSYNC_EVERYSEC && !force) {
/* With this append fsync policy we do background fsyncing.
* If the fsync is still in progress we can try to delay
* the write for a couple of seconds. */
// 根據這個同步策略,且沒有設置強制執行,我們在後臺執行同步
// 如果同步已經在後臺執行,那麼可以延遲兩秒,如果設置了force,那麼服務器會阻塞在write操作上
// 如果後臺正在執行同步
if (sync_in_progress) {
// 延遲執行flush操作的開始時間爲0,表示之前沒有延遲過write
if (server.aof_flush_postponed_start == 0) {
/* No previous write postponing, remember that we are
* postponing the flush and return. */
// 之前沒有延遲過write操作,那麼將延遲write操作的開始時間保存下來,然後就直接返回
server.aof_flush_postponed_start = server.unixtime;
return;
// 如果之前延遲過write操作,如果沒到2秒,直接返回,不執行write
} else if (server.unixtime - server.aof_flush_postponed_start < 2) {
/* We were already waiting for fsync to finish, but for less
* than two seconds this is still ok. Postpone again. */
return;
}
/* Otherwise fall trough, and go write since we can't wait
* over two seconds. */
// 執行到這裏,表示後臺正在執行fsync,但是延遲時間已經超過2秒
// 那麼執行write操作,此時write會被阻塞
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.");
}
}
/* We want to perform a single write. This should be guaranteed atomic
* at least if the filesystem we are writing is a real physical one.
* While this will save us against the server being killed I don't think
* there is much to do about the whole server stopping for power problems
* or alike */
// 執行write操作,保證寫操作是原子操作
// 設置延遲檢測開始的時間
latencyStartMonitor(latency);
// 將緩衝區的內容寫到AOF文件中
nwritten = write(server.aof_fd,server.aof_buf,sdslen(server.aof_buf));
// 設置延遲的時間 = 當前的時間 - 開始的時間
latencyEndMonitor(latency);
/* We want to capture different events for delayed writes:
* when the delay happens with a pending fsync, or with a saving child
* active, and when the above two conditions are missing.
* We also use an additional event name to save all samples which is
* useful for graphing / monitoring purposes. */
// 捕獲不同造成延遲write的事件
// 如果正在後臺執行同步fsync
if (sync_in_progress) {
// 將latency和"aof-write-pending-fsync"關聯到延遲診斷字典中
latencyAddSampleIfNeeded("aof-write-pending-fsync",latency);
// 如果正在執行AOF或正在執行RDB
} else if (server.aof_child_pid != -1 || server.rdb_child_pid != -1) {
// 將latency和"aof-write-active-child"關聯到延遲診斷字典中
latencyAddSampleIfNeeded("aof-write-active-child",latency);
} else {
// 將latency和"aof-write-alone"關聯到延遲診斷字典中
latencyAddSampleIfNeeded("aof-write-alone",latency);
}
// 將latency和"aof-write"關聯到延遲診斷字典中
latencyAddSampleIfNeeded("aof-write",latency);
/* We performed the write so reset the postponed flush sentinel to zero. */
// 執行了write,所以清零延遲flush的時間
server.aof_flush_postponed_start = 0;
// 如果寫入的字節數不等於緩存的字節數,發生異常錯誤
if (nwritten != (signed)sdslen(server.aof_buf)) {
static time_t last_write_error_log = 0;
int can_log = 0;
/* Limit logging rate to 1 line per AOF_WRITE_LOG_ERROR_RATE seconds. */
// 限制日誌的頻率每行30秒
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. */
// 如果寫入錯誤,寫errno到日誌
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));
}
// 將追加的內容截斷,刪除了追加的內容,恢復成原來的文件
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;
}
/* Handle the AOF write error. */
// 如果是寫入的策略爲每次寫入就同步,無法恢復這種策略的寫,因爲我們已經告知使用者,已經將寫的數據同步到磁盤了,因此直接退出程序
if (server.aof_fsync == AOF_FSYNC_ALWAYS) {
/* We can't recover when the fsync policy is ALWAYS since the
* reply for the client is already in the output buffers, and we
* have the contract with the user that on acknowledged write data
* is synced on disk. */
serverLog(LL_WARNING,"Can't recover from AOF write error when the AOF fsync policy is 'always'. Exiting...");
exit(1);
} else {
/* Recover from failed write leaving data into the buffer. However
* set an error to stop accepting writes as long as the error
* condition is not cleared. */
//設置執行write操作的狀態
server.aof_last_write_status = C_ERR;
/* Trim the sds buffer if there was a partial write, and there
* was no way to undo it with ftruncate(2). */
// 如果只寫入了局部,沒有辦法用ftruncate()函數去恢復原來的AOF文件
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... */
}
// nwritten == (signed)sdslen(server.aof_buf
// 執行write寫入成功
} else {
/* Successful write(2). If AOF was in error state, restore the
* OK state and log the event. */
// 更新最近一次寫的狀態爲 C_OK
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;
}
}
// 只能更新當前的AOF文件的大小
server.aof_current_size += nwritten;
/* 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). */
// 如果這個緩存足夠小,小於4K,那麼重用這個緩存,否則釋放AOF緩存
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. */
// 如果no-appendfsync-on-rewrite被設置爲yes,表示正在執行重寫,則不執行fsync
// 或者正在執行 BGSAVE 或 BGWRITEAOF,也不執行
if (server.aof_no_fsync_on_rewrite &&
(server.aof_child_pid != -1 || server.rdb_child_pid != -1))
return;
/* Perform the fsync if needed. */
// 執行fsync進行同步,每次寫入都同步
if (server.aof_fsync == AOF_FSYNC_ALWAYS) {
/* aof_fsync is defined as fdatasync() for Linux in order to avoid
* flushing metadata. */
// 設置延遲檢測開始的時間
latencyStartMonitor(latency);
// Linux下調用fdatasync()函數更高效的執行同步
aof_fsync(server.aof_fd); /* Let's try to get this data on the disk */
// 設置延遲的時間 = 當前的時間 - 開始的時間
latencyEndMonitor(latency);
// 將latency和"aof-fsync-always"關聯到延遲診斷字典中
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;
}
}
2.2 重寫機制
當一個數據庫的命令非常多時,AOF文件就會非常大,爲了解決這個問題,Redis引入了AOF重寫機制來壓縮文件的體積。
2.2.1 AOF重寫的方式
- 進程內已經超時的數據不在寫入文件。
- 無效命令不在寫入文件。
- 多條寫的命令合併成一個。
總之,AOF總是記錄數據庫的最終狀態的一個命令集。類似於物理中的位移與路程的關係,位移總是關心的是啓動到終點距離,而不關心是如何從起點到達終點。
2.2.2 觸發機制
- 手動觸發:BGREWRITEAOF 命令。
- 自動觸發:根據
redis.conf
的兩個參數確定觸發的時機。
- auto-aof-rewrite-percentage 100:當前AOF的文件空間(aof_current_size)和上一次重寫後AOF文件空間(aof_base_size)的比值。
- auto-aof-rewrite-min-size 64mb:表示運行AOF重寫時文件最小的體積。
- 自動觸發時機 = (aof_current_size > auto-aof-rewrite-min-size && (aof_current_size - aof_base_size) / aof_base_size >= auto-aof-rewrite-percentage)
2.2.3 AOF重寫的實現
AOF重寫操作有可能會長時間阻塞服務器主進程,因此會fork()
一個子進程在後臺進行重寫,然後父進程就可以繼續響應命令請求。雖然解決了阻塞問題,但是有產生了新問題:子進程在重寫期間,服務其還會處理新的命令請求,而這些命令可能灰度數據庫的狀態進行更改,從而使當前的數據庫狀態和AOF重寫之後保存的狀態不一致。
因此Redis設置了一個AOF重寫緩衝區的結構。
// AOF緩衝區大小
#define AOF_RW_BUF_BLOCK_SIZE (1024*1024*10) /* 10 MB per block */
// AOF塊緩衝區結構
typedef struct aofrwblock {
// 當前已經使用的和可用的字節數
unsigned long used, free;
// 緩衝區
char buf[AOF_RW_BUF_BLOCK_SIZE];
} aofrwblock;
重寫緩衝區並不是一個大塊的內存空間,而是一些內存塊的鏈表,沒個內存塊的大小爲10MB,這樣就組成了一個重寫緩衝區。
因此當客戶端發來命令時,會執行以下操作:
- 執行客戶端的命令。
- 將執行後的寫命令追加到AOF緩衝區(server.aof_buf)中。
- 將執行後的寫命令追加到AOF重寫緩衝區(server.aof_rewrite_buf_blocks)中。
這樣以來就不會丟失子進程重寫期間,父進程新處理的寫命令了。
於是,我們查看一下後臺執行重寫操作的源碼。
// 以下是BGREWRITEAOF的工作步驟
// 1. 用戶調用BGREWRITEAOF
// 2. Redis調用這個函數,它執行fork()
// 2.1 子進程在臨時文件中執行重寫操作
// 2.2 父進程將累計的差異數據追加到server.aof_rewrite_buf中
// 3. 當子進程完成2.1
// 4. 父進程會捕捉到子進程的退出碼,如果是OK,那麼追加累計的差異數據到臨時文件,並且對臨時文件rename,用它代替舊的AOF文件,然後就完成AOF的重寫。
int rewriteAppendOnlyFileBackground(void) {
pid_t childpid;
long long start;
// 如果正在進行重寫或正在進行RDB持久化操作,則返回C_ERR
if (server.aof_child_pid != -1 || server.rdb_child_pid != -1) return C_ERR;
// 創建父子進程間通信的管道
if (aofCreatePipes() != C_OK) return C_ERR;
// 記錄fork()開始時間
start = ustime();
// 子進程
if ((childpid = fork()) == 0) {
char tmpfile[256];
/* Child */
// 關閉監聽的套接字
closeListeningSockets(0);
// 設置進程名字
redisSetProcTitle("redis-aof-rewrite");
// 創建臨時文件
snprintf(tmpfile,256,"temp-rewriteaof-bg-%d.aof", (int) getpid());
// 對臨時文件進行AOF重寫
if (rewriteAppendOnlyFile(tmpfile) == C_OK) {
// 獲取子進程使用的內存空間大小
size_t private_dirty = zmalloc_get_private_dirty();
if (private_dirty) {
serverLog(LL_NOTICE,
"AOF rewrite: %zu MB of memory used by copy-on-write",
private_dirty/(1024*1024));
}
// 成功退出子進程
exitFromChild(0);
} else {
// 異常退出子進程
exitFromChild(1);
}
// 父進程
} else {
/* Parent */
// 設置fork()函數消耗的時間
server.stat_fork_time = ustime()-start;
// 計算fork的速率,GB/每秒
server.stat_fork_rate = (double) zmalloc_used_memory() * 1000000 / server.stat_fork_time / (1024*1024*1024); /* GB per second. */
// 將"fork"和fork消耗的時間關聯到延遲診斷字典中
latencyAddSampleIfNeeded("fork",server.stat_fork_time/1000);
if (childpid == -1) {
serverLog(LL_WARNING,
"Can't rewrite append only file in background: fork: %s",
strerror(errno));
return C_ERR;
}
// 打印日誌
serverLog(LL_NOTICE,
"Background append only file rewriting started by pid %d",childpid);
// 將AOF日程標誌清零
server.aof_rewrite_scheduled = 0;
// AOF開始的時間
server.aof_rewrite_time_start = time(NULL);
// 設置AOF重寫的子進程pid
server.aof_child_pid = childpid;
// 在AOF或RDB期間,不能對哈希表進行resize操作
updateDictResizePolicy();
// 將aof_selected_db設置爲-1,強制讓feedAppendOnlyFile函數執行時,執行一個select命令
server.aof_selected_db = -1;
// 清空腳本緩存
replicationScriptCacheFlush();
return C_OK;
}
return C_OK; /* unreached */
}
服務器主進程執行了fork
操作生成一個子進程執行rewriteAppendOnlyFile()
函數進行對臨時文件的重寫操作。
rewriteAppendOnlyFile()
函數源碼如下:
// 寫一系列的命令,用來完全重建數據集到filename文件中,被 REWRITEAOF and BGREWRITEAOF調用
// 爲了使重建數據集的命令數量最小,Redis會使用 可變參的命令,例如RPUSH, SADD 和 ZADD。
// 然而每次單個命令的元素數量不能超過AOF_REWRITE_ITEMS_PER_CMD
int rewriteAppendOnlyFile(char *filename) {
dictIterator *di = NULL;
dictEntry *de;
rio aof;
FILE *fp;
char tmpfile[256];
int j;
long long now = mstime();
char byte;
size_t processed = 0;
// 創建臨時文件的名字保存到tmpfile中
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;
}
// 設置一個空sds給 保存子進程AOF時差異累計數據的sds
server.aof_child_diff = sdsempty();
// 初始化rio爲文件io對象
rioInitWithFile(&aof,fp);
// 如果開啓了增量時同步,防止在緩存中累計太多命令,造成寫入時IO阻塞時間過長
if (server.aof_rewrite_incremental_fsync)
// 設置自動同步的字節數限制爲AOF_AUTOSYNC_BYTES = 32MB
rioSetAutoSync(&aof,AOF_AUTOSYNC_BYTES);
// 遍歷所有的數據庫
for (j = 0; j < server.dbnum; j++) {
// 按照格式構建 SELECT 命令內容
char selectcmd[] = "*2\r\n$6\r\nSELECT\r\n";
// 當前數據庫指針
redisDb *db = server.db+j;
// 數據庫的鍵值對字典
dict *d = db->dict;
// 如果數據庫中沒有鍵值對則跳過當前數據庫
if (dictSize(d) == 0) continue;
// 創建一個安全的字典迭代器
di = dictGetSafeIterator(d);
if (!di) {
// 創建失敗返回C_ERR
fclose(fp);
return C_ERR;
}
// 將SELECT 命令寫入AOF文件,確保後面的命令能正確載入到數據庫
if (rioWrite(&aof,selectcmd,sizeof(selectcmd)-1) == 0) goto werr;
// 將數據庫的ID吸入AOF文件
if (rioWriteBulkLongLong(&aof,j) == 0) goto werr;
// 遍歷保存當前數據的鍵值對的字典
while((de = dictNext(di)) != NULL) {
sds keystr;
robj key, *o;
long long expiretime;
// 當前節點保存的鍵值
keystr = dictGetKey(de);
// 當前節點保存的值對象
o = dictGetVal(de);
// 初始化一個在棧中分配的鍵對象
initStaticStringObject(key,keystr);
// 獲取該鍵值對的過期時間
expiretime = getExpire(db,&key);
// 如果當前鍵已經過期,則跳過該鍵
if (expiretime != -1 && expiretime < now) continue;
// 根據值的對象類型,將鍵值對寫到AOF文件中
// 值爲字符串類型對象
if (o->type == OBJ_STRING) {
char cmd[]="*3\r\n$3\r\nSET\r\n";
// 按格式寫入SET命令
if (rioWrite(&aof,cmd,sizeof(cmd)-1) == 0) goto werr;
/* Key and value */
// 按格式寫入鍵值對對象
if (rioWriteBulkObject(&aof,&key) == 0) goto werr;
if (rioWriteBulkObject(&aof,o) == 0) goto werr;
// 值爲列表類型對象
} else if (o->type == OBJ_LIST) {
// 重建一個列表對象命令,將鍵值對按格式寫入
if (rewriteListObject(&aof,&key,o) == 0) goto werr;
// 值爲集合類型對象
} else if (o->type == OBJ_SET) {
// 重建一個集合對象命令,將鍵值對按格式寫入
if (rewriteSetObject(&aof,&key,o) == 0) goto werr;
// 值爲有序集合類型對象
} else if (o->type == OBJ_ZSET) {
// 重建一個有序集合對象命令,將鍵值對按格式寫入
if (rewriteSortedSetObject(&aof,&key,o) == 0) goto werr;
// 值爲哈希類型對象
} else if (o->type == OBJ_HASH) {
// 重建一個哈希對象命令,將鍵值對按格式寫入
if (rewriteHashObject(&aof,&key,o) == 0) goto werr;
} else {
serverPanic("Unknown object type");
}
// 如果該鍵有過期時間,且沒過期,寫入過期時間
if (expiretime != -1) {
char cmd[]="*3\r\n$9\r\nPEXPIREAT\r\n";
// 將過期鍵時間全都以Unix時間寫入
if (rioWrite(&aof,cmd,sizeof(cmd)-1) == 0) goto werr;
if (rioWriteBulkObject(&aof,&key) == 0) goto werr;
if (rioWriteBulkLongLong(&aof,expiretime) == 0) goto werr;
}
// 在rio的緩存中每次寫了10M,就從父進程讀累計的差異,保存到子進程的aof_child_diff中
if (aof.processed_bytes > processed+1024*10) {
// 更新已寫的字節數
processed = aof.processed_bytes;
// 從父進程讀累計寫入的緩衝區的差異,在重寫結束時鏈接到文件的結尾
aofReadDiffFromParent();
}
}
dictReleaseIterator(di); //釋放字典迭代器
di = NULL;
}
// 當父進程仍然在發送數據時,先執行一個緩慢的同步,以便下一次最中的同步更快
if (fflush(fp) == EOF) goto werr;
if (fsync(fileno(fp)) == -1) goto werr;
// 再次從父進程讀取幾次數據,以獲得更多的數據,我們無法一直讀取,因爲服務器從client接受的數據總是比發送給子進程要快,所以當數據來臨的時候,我們嘗試從在循環中多次讀取。
// 如果在20ms之內沒有新的數據到來,那麼我們終止讀取
int nodata = 0;
mstime_t start = mstime(); //讀取的開始時間
// 在20ms之內等待數據到來
while(mstime()-start < 1000 && nodata < 20) {
// 在1ms之內,查看從父進程讀數據的fd是否變成可讀的,若不可讀則aeWait()函數返回0
if (aeWait(server.aof_pipe_read_data_from_parent, AE_READABLE, 1) <= 0)
{
nodata++; //更新新數據到來的時間,超過20ms則退出while循環
continue;
}
// 當管道的讀端可讀時,清零nodata
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;
// 將從父進程讀ack的fd設置爲非阻塞模式
if (anetNonBlock(NULL,server.aof_pipe_read_ack_from_parent) != ANET_OK)
goto werr;
// 在5000ms之內,從fd讀1個字節的數據保存在byte中,查看byte是否是'!'
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...");
// 最後一次從父進程讀累計寫入的緩衝區的差異
aofReadDiffFromParent();
serverLog(LL_NOTICE,
"Concatenating %.2f MB of AOF diff received from parent.",
(double) sdslen(server.aof_child_diff) / (1024*1024));
// 將子進程aof_child_diff中保存的差異數據寫到AOF文件中
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; //關閉文件
// 原子性的將臨時文件的名字,改成appendonly.aof
if (rename(tmpfile,filename) == -1) {
serverLog(LL_WARNING,"Error moving temp append only file on the final destination: %s", strerror(errno));
unlink(tmpfile);
return C_ERR;
}
// 打印日誌
serverLog(LL_NOTICE,"SYNC append only file rewrite performed");
return C_OK;
// 寫錯誤處理
werr:
serverLog(LL_WARNING,"Write error writing append only file on disk: %s", strerror(errno));
fclose(fp);
unlink(tmpfile);
if (di) dictReleaseIterator(di);
return C_ERR;
}
我們可以看到在關閉文件之前,多次執行了從重寫緩衝區做讀操作的aofReadDiffFromParent()
。在最後執行了rioWrite(&aof,server.aof_child_diff,sdslen(server.aof_child_diff)
操作,這就是把AOF重寫緩衝區保存服務器主進程新命令追加寫到AOF文件中,以此保證了AOF文件的數據狀態和數據庫的狀態一致。
2.3 父子進程間的通信
整個重寫的過程中,父子進行通信的地方只有一個,那就是最後父進程在子進程做重寫操作完成時,把子進程重寫操作期間所執行的新命令發送給子進程的重寫緩衝區,子進程然後將重寫緩衝區的數據追加到AOF文件中。
而父進程是如何將差異數據發送給子進程呢?Redis中使用了管道技術。進程間通信(IPC)之管道詳解
在上文提到的rewriteAppendOnlyFileBackground()
函數首先就創建了父子通信的管道。
父子進程間通信時共創建了三組管道
//下面兩個是發送差異數據管道
int aof_pipe_write_data_to_child; //父進程寫給子進程的文件描述符
int aof_pipe_read_data_from_parent; //子進程從父進程讀的文件描述符
//下面四個是應答ack的管道
int aof_pipe_write_ack_to_parent; //子進程寫ack給父進程的文件描述符
int aof_pipe_read_ack_from_child; //父進程從子進程讀ack的文件描述符
int aof_pipe_write_ack_to_child; //父進程寫ack給子進程的文件描述符
int aof_pipe_read_ack_from_parent; //子進程從父進程讀ack的文件描述符
當將feedAppendOnlyFile()
將命令追加到緩衝區的同時,還在最後調用了aofRewriteBufferAppend()
函數,這個函數就是將命令追加到AOF的緩衝區,然而,在追加完成後會執行這麼一段代碼
// 獲取當前事件正在監聽的類型,如果等於0,未設置,則設置管道aof_pipe_write_data_to_child爲可寫狀態
// 當然aof_pipe_write_data_to_child可以用的時候,調用aofChildWriteDiffDatah()函數寫數據
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_pipe_write_data_to_child
可以寫的時候,調用aofChildWriteDiffDatah()
函數寫數據,而在aofChildWriteDiffDatah()
函數中,則將重寫緩衝區數據寫到管道中。函數源碼如下:
// 事件處理程序發送一些數據給正在做AOF重寫的子進程,我們發送AOF緩衝區一部分不同的數據給子進程,當子進程完成重寫時,重寫的文件會比較小
void aofChildWriteDiffData(aeEventLoop *el, int fd, void *privdata, int mask) {
listNode *ln;
aofrwblock *block;
ssize_t nwritten;
UNUSED(el);
UNUSED(fd);
UNUSED(privdata);
UNUSED(mask);
while(1) {
// 獲取緩衝塊鏈表的頭節點地址
ln = listFirst(server.aof_rewrite_buf_blocks);
// 獲取緩衝塊地址
block = ln ? ln->value : NULL;
// 如果aof_stop_sending_diff爲真,則停止發送累計的不同數據給子進程,或者緩衝塊爲空
// 則將管道的寫端從服務器的監聽隊列中刪除
if (server.aof_stop_sending_diff || !block) {
aeDeleteFileEvent(server.el,server.aof_pipe_write_data_to_child,
AE_WRITABLE);
return;
}
// 如果已經有緩存的數據
if (block->used > 0) {
// 則將緩存的數據寫到管道中
nwritten = write(server.aof_pipe_write_data_to_child,
block->buf,block->used);
if (nwritten <= 0) return;
// 更新緩衝區的數據,覆蓋掉已經寫到管道的數據
memmove(block->buf,block->buf+nwritten,block->used-nwritten);
block->used -= nwritten;
}
// 如果當前節點的所緩衝的數據全部寫完,則刪除該節點
if (block->used == 0) listDelNode(server.aof_rewrite_buf_blocks,ln);
}
}
而在上面展示到的rewriteAppendOnlyFile()
函數中,則當aof_pipe_read_data_from_parent
可讀時,不斷調用aofReadDiffFromParent()
函數的從管道讀數據,這樣就實現了父子進程的通信。該函數源碼如下:
// 該函數在子進程正在進行重寫AOF文件時調用
// 用來讀從父進程累計寫入的緩衝區的差異,在重寫結束時鏈接到文件的結尾
ssize_t aofReadDiffFromParent(void) {
// 大多數Linux系統中默認的管道大小
char buf[65536]; /* Default pipe buffer size on most Linux systems. */
ssize_t nread, total = 0;
// 從父進程讀數據到buf中,讀了nread個字節
while ((nread =
read(server.aof_pipe_read_data_from_parent,buf,sizeof(buf))) > 0) {
// 將buf中的數據累計到子進程的差異累計的sds中
server.aof_child_diff = sdscatlen(server.aof_child_diff,buf,nread);
// 更新總的累計字節數
total += nread;
}
return total;
}
從中可以看到,子進程從管道讀的數據全部保存在server.aof_child_diff
中。
2.4 AOF文件的載入
因爲Redis命令總是在一個客戶端中執行,因此,爲了載入AOF文件,需要創建一個關閉監聽套接字的僞客戶端。AOF文件的載入和寫入是相反的過程,因此比較簡單,直接給出註釋的源碼:Redis AOF持久化機制源碼註釋
// 執行AOF文件中的命令
// 成功返回C_OK,出現非致命錯誤返回C_ERR,例如AOF文件長度爲0,出現致命錯誤打印日誌退出
int loadAppendOnlyFile(char *filename) {
struct client *fakeClient;
FILE *fp = fopen(filename,"r"); //以讀打開AOF文件
struct redis_stat sb;
int old_aof_state = server.aof_state; //備份當前AOF的狀態
long loops = 0;
off_t valid_up_to = 0; /* Offset of the latest well-formed command loaded. */
// 如果文件打開,但是大小爲0,則返回C_ERR
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);
}
/* Temporarily disable AOF, to prevent EXEC from feeding a MULTI
* to the same file we're about to read. */
// 暫時關閉AOF,防止在執行MULTI時,EXEC命令被傳播到AOF文件中
server.aof_state = AOF_OFF;
// 生成一個僞client
fakeClient = createFakeClient();
// 設置載入的狀態信息
startLoading(fp);
while(1) {
int argc, j;
unsigned long len;
robj **argv;
char buf[128];
sds argsds;
struct redisCommand *cmd;
/* Serve the clients from time to time */
// 間隔性的處理client請求
if (!(loops++ % 1000)) {
// ftello(fp)返回當前文件載入的偏移量
// 設置載入時server的狀態信息,更新當前載入的進度
loadingProgress(ftello(fp));
// 在服務器被阻塞的狀態下,仍然能處理請求
// 因爲當前處於載入狀態,當client的請求到來時,總是返回loading的狀態錯誤
processEventsWhileBlocked();
}
// 將一行文件內容讀到buf中,遇到"\r\n"停止
if (fgets(buf,sizeof(buf),fp) == NULL) {
if (feof(fp)) //如果文件已經讀完了或數據庫爲空,則跳出while循環
break;
else
goto readerr;
}
// 檢查文件格式 "*<argc>\r\n"
if (buf[0] != '*') goto fmterr;
if (buf[1] == '\0') goto readerr;
// 取出命令參數個數
argc = atoi(buf+1);
if (argc < 1) goto fmterr; //至少一個參數,就是當前命令
// 分配參數列表空間
argv = zmalloc(sizeof(robj*)*argc);
// 設置僞client的參數列表
fakeClient->argc = argc;
fakeClient->argv = argv;
// 遍歷參數列表
// "$<command_len>\r\n<command>\r\n"
for (j = 0; j < argc; j++) {
// 讀一行內容到buf中,遇到"\r\n"停止
if (fgets(buf,sizeof(buf),fp) == NULL) {
fakeClient->argc = j; /* Free up to j-1. */
freeFakeClientArgv(fakeClient);
goto readerr;
}
// 檢查格式
if (buf[0] != '$') goto fmterr;
// 讀出參數的長度len
len = strtol(buf+1,NULL,10);
// 初始化一個len長度的sds
argsds = sdsnewlen(NULL,len);
// 從文件中讀出一個len字節長度,將值保存到argsds中
if (len && fread(argsds,len,1,fp) == 0) {
sdsfree(argsds);
fakeClient->argc = j; /* Free up to j-1. */
freeFakeClientArgv(fakeClient);
goto readerr;
}
// 創建一個字符串對象保存讀出的參數argsds
argv[j] = createObject(OBJ_STRING,argsds);
// 讀兩個字節,跳過"\r\n"
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 */
// 調用僞client執行命令
cmd->proc(fakeClient);
/* The fake client should not have a reply */
// 僞client不應該有回覆
serverAssert(fakeClient->bufpos == 0 && listLength(fakeClient->reply) == 0);
/* The fake client should never get blocked */
// 僞client不應該是阻塞的
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. */
// 釋放僞client的參數列表
freeFakeClientArgv(fakeClient);
// 更新已載入且命令合法的當前文件的偏移量
if (server.aof_load_truncated) valid_up_to = ftello(fp);
}
/* This point can only be reached when EOF is reached without errors.
* If the client is in the middle of a MULTI/EXEC, log error and quit. */
// 執行到這裏,說明AOF文件的所有內容都被正確的讀取
// 如果僞client處於 MULTI/EXEC 的環境中,還有檢測文件是否包含正確事物的結束,調到uxeof
if (fakeClient->flags & CLIENT_MULTI) goto uxeof;
// 載入成功
loaded_ok: /* DB loaded, cleanup and return C_OK to the caller. */
fclose(fp); //關閉文件
freeFakeClient(fakeClient); //釋放僞client
server.aof_state = old_aof_state; //還原AOF狀態
stopLoading(); //設置載入完成的狀態
aofUpdateCurrentSize(); //更新服務器狀態,當前AOF文件的大小
server.aof_rewrite_base_size = server.aof_current_size; //更新重寫的大小
return C_OK;
// 載入時讀錯誤,如果feof(fp)爲真,則直接執行 uxeof
readerr: /* Read error. If feof(fp) is true, fall through to unexpected EOF. */
if (!feof(fp)) {
// 退出前釋放僞client的空間
if (fakeClient) freeFakeClient(fakeClient); /* avoid valgrind warning */
serverLog(LL_WARNING,"Unrecoverable error reading the append only file: %s", strerror(errno));
exit(1);
}
// 不被預期的AOF文件結束格式
uxeof: /* Unexpected AOF end of file. */
// 如果發現末尾結束格式不完整則自動截掉,成功加載前面正確的數據。
if (server.aof_load_truncated) {
serverLog(LL_WARNING,"!!! Warning: short read while loading the AOF file !!!");
serverLog(LL_WARNING,"!!! Truncating the AOF at offset %llu !!!",
(unsigned long long) valid_up_to);
// 截斷文件到正確加載的位置
if (valid_up_to == -1 || truncate(filename,valid_up_to) == -1) {
if (valid_up_to == -1) {
serverLog(LL_WARNING,"Last valid command offset is invalid");
} else {
serverLog(LL_WARNING,"Error truncating the AOF file: %s",
strerror(errno));
}
} else {
/* Make sure the AOF file descriptor points to the end of the
* file after the truncate call. */
// 確保截斷後的文件指針指向文件的末尾
if (server.aof_fd != -1 && lseek(server.aof_fd,0,SEEK_END) == -1) {
serverLog(LL_WARNING,"Can't seek the end of the AOF file: %s",
strerror(errno));
} else {
serverLog(LL_WARNING,
"AOF loaded anyway because aof-load-truncated is enabled");
goto loaded_ok; //跳轉到loaded_ok,表截斷成功,成功加載前面正確的數據。
}
}
}
// 退出前釋放僞client的空間
if (fakeClient) freeFakeClient(fakeClient); /* avoid valgrind warning */
serverLog(LL_WARNING,"Unexpected end of file reading the append only file. You can: 1) Make a backup of your AOF file, then use ./redis-check-aof --fix <filename>. 2) Alternatively you can set the 'aof-load-truncated' configuration option to yes and restart the server.");
exit(1);
// 格式錯誤
fmterr: /* Format error. */
// 退出前釋放僞client的空間
if (fakeClient) freeFakeClient(fakeClient); /* avoid valgrind warning */
serverLog(LL_WARNING,"Bad file format reading the append only file: make a backup of your AOF file, then use ./redis-check-aof --fix <filename>");
exit(1);
}