5. 備份機制
Redis是內存數據庫,支持兩種方式——snapshot和aof,將數據從內存dump到磁盤。 snapshot是快照式備份,每次將Redis全庫dump到磁盤, aof是流水式備份,每次將Redis數據庫的修改日誌dump到磁盤,並定期整理日誌。
5.1. Snapshot
Redis支持Snapshot(快照)式備份。 Redis可以自動檢測備份時機,設置每N秒檢查,若發生M次以上數據更新操作,則開始Snapshot備份,也可以由客戶端發送save或bgsave命令手動啓動備份。save是阻塞式備份,備份過程中Redis會停止服務; bgsave是後臺備份, Redis將新建一個備份進程負責將全庫dump到磁盤。saveCommand()中,首先檢查是否有bgsave的後臺備份進程,若沒有,則執行rdbSave()進行全庫備份。bgsaveCommand()中,也會檢查是否有bgsave的後臺備份進程,若沒有,則執行rdbSaveBackground(),啓動備份子進程,在後臺全庫備份。備份子進程最終也是通過rdbSave()備份數據庫。rdbSaveBackground()中,首先通過前文所說的waitEmptyIOJobsQueue()檢查是否有vm的IO線程在進行數據交換,如果有則阻塞,等待所有vm的IO線程完成工作。然後fork()備份子進程。在父進程中, server.bgsavechildpid記錄了備份子進程的pid,並通過updateDictResizePolicy()禁止Hash表自動擴容。在子進程中,如果啓用了vm,則打開vm swap文件,然後調用rdbSave()執行備份。因爲之前已經檢查了所有vm的IO線程是否完成,因此這裏打開vm的swap文件並不會和vm的IO線程衝突。
int rdbSaveBackground(char *filename) {
pid_t childpid;
if (server.bgsavechildpid != -1) return REDIS_ERR;
if (server.vm_enabled) waitEmptyIOJobsQueue();
server.dirty_before_bgsave = server.dirty;
if ((childpid = fork()) == 0) {
/* Child */
if (server.vm_enabled) vmReopenSwapFile();
if (server.ipfd > 0) close(server.ipfd);
if (server.sofd > 0) close(server.sofd);
if (rdbSave(filename) == REDIS_OK) {
_exit(0);
} else {
_exit(1);
}
} else {
/* Parent */
if (childpid == -1) {
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.bgsavechildpid = childpid;
updateDictResizePolicy();
return REDIS_OK;
}
return REDIS_OK; /* unreached */
}
rdbSave()中首先通過waitEmptyIOJobsQueue()檢查vm的IO線程,並阻塞直到所有vm的IO線程完成。因爲備份過程中,需要備份vm的swap文件,如果有未完成的vm的IO線程,會導致數據不同步。首先建立一個tmp文件,向它寫入備份數據。依次循環每個db: server.db,server.db+1, ..., server.db+server.dbnum,通過dict的迭代器dictIterator訪問db中的所有數據,依次寫入tmp文件。Redis在寫備份文件時做了許多優化,例如rdbSaveLen()用於寫入長度信息,寫入的數字不超過32bit整數。 Redis將長度數字按二進制長度分三類:小於6bit, 6bit至14bit, 14bit至32bit,並用前綴REDIS_RDB_6BITLEN(00)、REDIS_RDB_14BITLEN(01)、 REDIS_RDB_32BITLEN(10)標示數字長度。將前綴與數字一起寫入磁盤。這個優化類似於Protobuf裏壓縮數字的方法。類似的壓縮還有很多,再此不一一分析。
int rdbSaveLen(FILE *fp, uint32_t len) {
unsigned char buf[2];
int nwritten;
if (len < (1<<6)) {
/* Save a 6 bit len */
buf[0] = (len&0xFF)|(REDIS_RDB_6BITLEN<<6);
if (rdbWriteRaw(fp,buf,1) == -1) return -1;
nwritten = 1;
} else if (len < (1<<14)) {
/* Save a 14 bit len */
buf[0] = ((len>>8)&0xFF)|(REDIS_RDB_14BITLEN<<6);
buf[1] = len&0xFF;
if (rdbWriteRaw(fp,buf,2) == -1) return -1;
nwritten = 2;
} else {
/* Save a 32 bit len */
buf[0] = (REDIS_RDB_32BITLEN<<6);
if (rdbWriteRaw(fp,buf,1) == -1) return -1;
len = htonl(len);
if (rdbWriteRaw(fp,&len,4) == -1) return -1;
nwritten = 1+4;
}
return nwritten;
}
在備份文件中, Redis寫入type/key/value數據,並放棄所有過期的數據。特別地,對於vm中的數據,會先讀入內存,再將數據寫入備份文件。父進程在serverCron中檢查備份進程,如果存在bgsave進程或者bgrewrite進程(AOF備份,下節分析),則wait3()等待進程完成,完成後調用backgroundSaveDoneHandler()或者backgroundRewriteDoneHandler()處理。snapshot式備份中, Redis會停止vm的換入換出操作,停更新LRU策略中的標記信息,但不會影響Redis對內存中的數據的讀寫。 Redis通過調整snapshot備份的粒度,適應各種應用需求。
5.2. AOF
除了Snapshot備份模式外Redis還支持AOF(流水式)備份,它保存所有操作的commit log,並能自動地進行全庫備份和刪除無用的commit log。Redis在寫commit log時, Redis並不是每次處理請求時都將請求寫入磁盤,它會用一段內存Buffer緩存commit log,並在一定時間將緩存中的內容統一寫入磁盤。 爲了避免磁盤緩存的影響,可以設置三種策略進行fsync():每次寫操作後均調用fsync(),每秒調用一次fsync(),從不調用fsync(),三種不同的策略獲得不同的性能和一致性。
Redis將所有操作保存在commit log中, commit log的文件是追加寫。當commitlog的大小無法承受時,可以手工通過bgrewriteaof命令產生一個快照(這個不同於Snapshot備份),具體構造方式後文分析。
AOF有四種操作:
•feedAppendOnlyFile()-將命令寫入AOF。該方法在call()中被調用,用於將
所有Redis處理的命令寫入AOF;該方法在Redis發現過期的Keys被調用,記錄清除過期Key的操作
call()是Redis中所有命令處理的入口,當啓用了AOF且數據有變化時
(server.dirty有變化),將命令用feedAppendOnlyFile()寫入AOF,該函數中,將命令編碼後寫入server.aofbuf,當後臺rewriteaof進程存在時,同時將編碼後的命令寫入server.bgrewritebuf, bgrewritebuf的作用後文解釋。
/* Append to the AOF buffer. This will be flushed on disk just before
* of re-entering the event loop, so before the client will get a
* positive reply about the operation performed. */
server.aofbuf = sdscatlen(server.aofbuf,buf,sdslen(buf));
/* If a background append only file rewriting is in progress we want to
* accumulate the differences between the child DB and the current one
* in a buffer, so that when the child process will do its work we
* can append the differences to the new append only file. */
if (server.bgrewritechildpid != -1)
server.bgrewritebuf = sdscatlen(server.bgrewritebuf,buf,sdslen(buf));
•rewriteAppendOnlyFile()-產生快照,並更新aof文件。
rewriteAppendOnlyFile()只在 rewriteAppendOnlyFileBackground()中
被調用。 Redis產生AOF快照主要分爲以下幾步:
(1) fork()一個rewrite子進程,調用rewriteAppendOnlyFile()產生快照。
(2) 當rewrite進程開始後, rewriteAppendOnlyFile()掃描數據庫中所有數
據,將他們編碼後寫入臨時文件。 AOF文件的編碼形式爲文本,即人肉可讀的編碼。
(3)rewrite時父進程照常接收請求,並將流水日誌寫入
server.bgrewriteaofbuf中。
(4)子進程完成工作,父進程在serverCron()中通過wait3()獲知其狀態後,調用
backgroundRewriteDoneHandler()將server.bgrewriteaofbuf中的內容作爲新
的commit log,覆蓋原有server.aofbuf。
(5) 父進程將rewrite子進程生成的臨時文件改名,作爲新的aof文件。用於未來恢
複數據。
Redis的AOF快照機制的基礎,是數據庫的大小一定小於操作日誌的大小,否則產生
的快照文件可能遠遠超過commit log文件的大小。因此,此處有一點值得考慮,
rewrite過程是否可以只寫入aofbuf中改變了的數據的快照?
•flushAppendOnlyFile()-將內存buffer中的commit log寫到磁盤上。
Redis在beforeSleep()中,及關閉AOF功能時,將內存buffer中的log寫入磁盤。
•loadAppendOnlyFile()-重放AOF文件。該方法在Redis啓動時被調用,從AOF
文件中重建數據庫。
Redis通過AOF重建時,構造一個虛擬的client(),向自己發送重建的命令。而恢復
數據只需要將AOF快照重新載入數據庫,並回放commit log中的操作。