Redis源碼閱讀 (備份機制)

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中的操作。

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