關於Redis的aof持久化的二三事

相信不少小夥伴對redis的持久化保證有疑問:node

  • redis不是內存性應用嗎?爲何磁盤擁堵的狀況下會影響讀寫呢?
  • redis不是支持持久化嗎?爲何會丟數據?
  • redis的持久化數據安全嗎?到什麼級別?能不能取代數據庫?

因此簡單整理了這篇文章,對redis的持久化內部原理進行分析,同時可爲其它須要持久化的實現提供參考。 鑑於OS的差別,本文統一以linux 2.6+爲準。linux

Redis的持久化工做原理

Redis的持久化有RDB和AOF兩種,RDB 能夠定時備分內存中的數據集。服務器啓動的時候,能夠從 RDB 文件中回覆數據集。AOF 能夠記錄服務器的全部寫操做。在服務器從新啓動的時候,會把全部的寫操做從新執行一遍,從而實現數據備份。當寫操做集過大(比原有的數據集還大),redis會重寫寫操做集。由於每次RDB都保存全量數據,這是一個開銷很大的操做,爲了不進行RDB時fork對主進程影響,以及儘可能減小發生故障時丟失的數據量,通常狀況你們採用數據持久化策略是AOF。
下面先來看一下AOF數據組織方式 假設redis中有foo:helloworld的string類型的key,那麼進行AOF持久化後,appendonly.aof文件有以下內容:c++

*2         # 表示這條命令的消息體共2行
$6         # 下一行的數據長度爲6
SELECT     # 消息體
$1         # 下一行數據長度爲1
0          # 消息體
*3         # 表示這條命令的消息體共2行
$3         # 下一行的數據長度爲3
set        # 消息體
$3         # 下一行的數據長度爲3
foo        # 消息體
$10        # 下一行的數據長度爲10
helloworld # 消息體

經過解析上面內容,能獲得熟悉的一條redis命令:SELECT 0; SET foo helloworld 咱們能夠經過執行命令:BGREWRITEAOF實現一次aof文件的重寫,這時redis會將內存中每個key按照上面格式寫入磁盤上appendonly.aof文件; 而當Redis啓動載入這個AOF文件時,會建立用於執行AOF文件包含Redis命令的僞客戶端,並在載入完成後關閉這個僞客戶端。另外,由於AOF持久化是經過記錄寫命令流水來記錄數據變化,這個文件會愈來愈大,爲了解決這個問題,Redis提供了AOF重寫功能,經過將當期內存中數據導出建立一個新的AOF文件替換現有AOF文件,這樣新文件的體積就會小不少,具體機制這裏就不過多展開了。redis

文件IO相關原理

在進入redis的具體實現以前,咱們先梳理一下文件IO相關函數及系統實現。這裏涉及的文件IO操做有以下幾個:數據庫

open緩存

int open(const char path, int oflag, ... / mode_t mode */ );
path參數是要打開的文件名,oflag參數指定一個或多個選項,例如: 安全

O_WRONLY 只寫打開
O_APPEND 每次寫操做以前,將文件偏移量設置在文件的當前結尾處,在一處成功寫以後,該文件的偏移量增長時間寫的字節數。
O_CREAT 若是文件不存在則先建立它。
write服務器

ssizet write(int fd, const void *buf, sizet nbytes);
write向打開的文件寫數據,返回值一般與nbytes值相同,不然表示出錯app

ftruncateless

int ftruncate(int fd, off_t length);
經過ftruncate能夠將文件長度截短或是增加,若是length小於原來長度,超過length的數據就不能在訪問,若是大於原來長度,文件長度則增長,若是以前文件尾端到length長度之間沒有數據則讀出的爲0,至關於在文件中建立了空洞

在咱們向文件中寫數據時,傳統Unix/Liunx系統內核一般現將數據複製到緩衝區中,而後排入隊列,晚些時候再寫入磁盤。這種方式被稱爲延遲寫(delayed write)。對磁盤文件的write操做,更新的只是內存中的page cache,由於write調用不會等到硬盤IO完成以後才返回,所以若是OS在write調用以後、硬盤同步以前崩潰,則數據可能丟失。爲了保證磁盤上時間文件系統與緩衝區內容的一致性,UNIX系統提供了sync、fsync和fdatasync三個函數:

sync
只是將全部修改過的塊緩衝區排查寫隊列,而後就返回,它並不等待時間寫磁盤操做結束。一般稱爲update的系統守護進程會週期性地(通常每隔30秒)調用sync函數。這就保證了按期flush內核的塊緩衝區。
fsync
只對由文件描述符fd指定的單一文件起做用,而且等待寫磁盤操做結束才返回。fsync可用於數據庫這樣的應用程序,這種應用程序須要確保將修改過的塊當即寫到磁盤上。
fdatasync
相似於fsync,但它隻影響文件的數據部分。而除數據外,fsync還會同步更新文件的屬性。

如今來看一下fsync的性能問題,與fdatasync不一樣,fsync除了同步文件的修改內容(髒頁),fsync還會同步文件的描述信息(metadata,包括size、訪問時間statime & stmtime等等),由於文件的數據和metadata一般存在硬盤的不一樣地方,所以fsync至少須要兩次IO寫操做,這個在fsync的man page有說明:
Applications that access databases or log files often write a tiny data fragment (e.g., one line in a log file) and then call fsync()
immediately in order to ensure that the written data is physically
stored on the harddisk. Unfortunately, fsync() will always initiate
two write operations: one for the newly written data and another one
in order to update the modification time stored in the inode. If the
modification time is not a part of the transaction concept fdatasync()
can be used to avoid unnecessary inode disk write operations.
fdatasync不會同步metadata,所以能夠減小一次IO寫操做。fdatasync的man page中的解釋:

fdatasync() is similar to fsync(), but does not flush modified
metadata unless that metadata is needed in order to allow a subsequent
data retrieval to be correctly handled. For example, changes to
st_atime or st_mtime (respectively, time of last access and time of
last modification; see stat(2)) do not require flushing because they
are not necessary for a subsequent data read to be handled correctly.
On the other hand, a change to the file size (st_size, as made by say
ftruncate(2)), would require a metadata flush. The aim of fdatasync()
is to reduce disk activity for applications that do not require all
metadata to be synchronized with the disk.

具體來講,若是文件的尺寸(st_size)發生變化,是須要當即同步,不然OS一旦崩潰,即便文件的數據部分已同步,因爲metadata沒有同步,依然讀不到修改的內容。而最後訪問時間(atime)/修改時間(mtime)是不須要每次都同步的,只要應用程序對這兩個時間戳沒有苛刻的要求,基本沒有影響。在Redis的源文件src/config.h中能夠看到在Redis針對Linux實際使用了fdatasync()來進行刷盤操做

源文件:src/config.h

91 #ifdef __linux__
 92 #define aof_fsync fdatasync
 93 #else
 94 #define aof_fsync fsync
 95 #endif

Redis的AOF刷盤工做原理

Redis是經過apendfsync參數來設置不一樣刷盤策略,apendfsync主要有下面三個選項:

always
每次有新命令追加到AOF文件是就執行一次同步到AOF文件的操做,安全性最高,可是性能影響最大。
everysec
每秒執行一次同步到AOF文件的操做,redis會在一個單獨線程中執行同步操做。
no
將數據同步操做交給操做系統來處理,性能最好,可是數據可靠性最差。 加入在配置文件設置appendonly=yes後,沒有指定apendfsync,默認會使用everysec選項,通常都是採用的這個選項。
下面咱們來具體分析一下Redis代碼中關於AOF刷盤操做的工做原理:

在appendonly yes激活AOF時,會調用startAppendOnly()函數來打開appendonly.aof文件句柄。

241 server.aof_fd = open(server.aof_filename,O_WRONLY|O_APPEND|O_CREAT,0644);
同時在Redis啓動時也會建立專門的bio線程處理aof持久化,在src/server.c文件的initServer()中會調用bioInit()函數建立兩個線程,分別用來處理刷盤和關閉文件的任務。代碼以下:

源文件:src/bio.h

38 /* Background job opcodes */
39 #define BIO_CLOSE_FILE    0 /* Deferred close(2) syscall. */
40 #define BIO_AOF_FSYNC     1 /* Deferred AOF fsync. */
41 #define BIO_NUM_OPS       2

源文件: src/bio.c

116     for (j = 0; j < BIO_NUM_OPS; j++) {
117         void *arg = (void*)(unsigned long) j;
118         if (pthread_create(&thread,&attr,bioProcessBackgroundJobs,arg) != 0) {
119             serverLog(LL_WARNING,"Fatal: Can't initialize Background Jobs.");
120             exit(1);
121         }
122         bio_threads[j] = thread;
123     }
  當redis服務器執行寫命令時,例如SET foo helloworld,不只僅會修改內存數據集,也會記錄此操做,記錄的方式就是前面所說的數據組織方式。redis將一些內容被追加到server.aofbuf緩衝區中,能夠把它理解爲一個小型臨時中轉站,全部累積的更新緩存都會先放入這裏,它會在特定時機寫入文件或者插入到server.aofrewritebufblocks,同時每次寫操做後先寫入緩存,而後按期fsync到磁盤,在到達某些時機(主要是受auto-aof-rewrite-percentage/auto-aof-rewrite-min-size這兩個參數影響)後,還會fork子進程執行rewrite。爲了不在服務器忽然崩潰時丟失過多的數據,在redis會在下列幾個特定時機調用flushAppendOnlyFile函數進行寫盤操做:

進入事件循環以前
服務器定時函數serverCron()中,在Redis運行期間主要是在這裏調用flushAppendOnlyFile
中止AOF策略的stopAppendOnly()函數中
注:因 serverCron 函數中的全部代碼每秒都會調用 server.hz 次,爲了對部分代碼的調用次數進行限制,Redis使用了一個宏 runwithperiod(milliseconds) { ... } ,這個宏能夠將被包含代碼的執行次數下降爲每 milliseconds 執行一次。

源文件: src/server.c

1099 int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
 1260     /* AOF postponed flush: Try at every cron cycle if the slow fsync
 1261      * completed. */
 1262     if (server.aof_flush_postponed_start) flushAppendOnlyFile(0);
 1263
 1264     /* AOF write errors: in this case we have a buffer to flush as well and
 1265      * clear the AOF error in case of success to make the DB writable again,
 1266      * however to try every second is enough in case of 'hz' is set to
 1267      * an higher frequency. */
 1268     run_with_period(1000) {
 1269         if (server.aof_last_write_status == C_ERR)
 1270             flushAppendOnlyFile(0);
 1271     }
 1316 }
  經過下面的代碼能夠看到flushAppendOnlyFile函數中,在write寫盤以後根據apendfsync選項來執行刷盤策略,若是是AOFFSYNCALWAYS,就當即執行刷盤操做,若是是AOFFSYNCEVERYSEC,則建立一個後臺異步刷盤任務。 在函數bioCreateBackgroundJob()會建立bio後臺任務,在函數bioProcessBackgroundJobs()會執行bio後臺任務的處理。

源文件:src/aof.c

200 // 調用bio的建立異步線程任務函數,添加後臺刷盤任務
 201 void aof_background_fsync(int fd) {
 202     bioCreateBackgroundJob(BIO_AOF_FSYNC,(void*)(long)fd,NULL,NULL);
 203 }
 
  238 int startAppendOnly(void) {
 239     char cwd[MAXPATHLEN];
 240     // 經過appendonly yes激活AOF時,會調用startAppendOnly()函數來打開appendonly.aof文件句柄。
 241     server.aof_last_fsync = server.unixtime;
 242     server.aof_fd = open(server.aof_filename,O_WRONLY|O_APPEND|O_CREAT,0644);
 243     serverAssert(server.aof_state == AOF_OFF);
 244     if (server.aof_fd == -1) {
 245         char *cwdp = getcwd(cwd,MAXPATHLEN);
 246
 247         serverLog(LL_WARNING,
 248             "Redis needs to enable the AOF but can't open the "
 249             "append only file %s (in server root dir %s): %s",
 250             server.aof_filename,
 251             cwdp ? cwdp : "unknown",
 252             strerror(errno));
 253         return C_ERR;
 254     }
 255     if (server.rdb_child_pid != -1) {
 256         server.aof_rewrite_scheduled = 1;
 257         serverLog(LL_WARNING,"AOF was enabled but there is already a child process saving an RDB file on disk. An AOF background was scheduled to start when possible.");
 258     } else if (rewriteAppendOnlyFileBackground() == C_ERR) {
 259         close(server.aof_fd);
 260         serverLog(LL_WARNING,"Redis needs to enable the AOF but can't trigger a background AOF rewrite operation. Check the above logs for more info about the error.");
 261         return C_ERR;
 262     }
 263     /* We correctly switched on AOF, now wait for the rewrite to be complete
 264      * in order to append data on disk. */
 265     server.aof_state = AOF_WAIT_REWRITE;
 266     return C_OK;
 267 }
 
     // 執行write和fsync操做
 288 void flushAppendOnlyFile(int force) {
 289     ssize_t nwritten;
 290     int sync_in_progress = 0;
 291     mstime_t latency;
 292     // 沒有數據,無需寫盤
 293     if (sdslen(server.aof_buf) == 0) return;
 294     /* 經過bio的任務計數器bio_pending來判斷是否有後臺fsync操做正在進行
          * 若是有就要標記下sync_in_progress
          */
 295     if (server.aof_fsync == AOF_FSYNC_EVERYSEC)
 296         sync_in_progress = bioPendingJobsOfType(BIO_AOF_FSYNC) != 0;
 297     /* 若是沒有設置強制刷盤的選項,可能不會當即進行,而是延遲執行AOF刷盤
          * 由於 Linux 上的 write(2) 會被後臺的 fsync 阻塞, 若是強制執行 
          * write 的話,服務器主線程將阻塞在 write 上面
          */         
 298     if (server.aof_fsync == AOF_FSYNC_EVERYSEC && !force) {
 302         if (sync_in_progress) {
 303             if (server.aof_flush_postponed_start == 0) {
 306                 server.aof_flush_postponed_start = server.unixtime;
 307                 return;
                 // 若是距離上次執行刷盤操做沒有超過2秒,直接返回,
 308             } else if (server.unixtime - server.aof_flush_postponed_start < 2) {
 311                 return;
 312             }
                 /* 若是後臺還有 fsync 在執行,而且 write 已經推遲 >= 2 秒
                  * 那麼執行寫操做(write 將被阻塞)
                  * 假如此時出現死機等故障,可能存在丟失2秒左右的AOF日誌數據
                  */              
 315             server.aof_delayed_fsync++;
 316             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.");
 317         }
 318     }
 324     // 將server.aof_buf中緩存的AOF日誌數據進行寫盤
 325     latencyStartMonitor(latency);
 326     nwritten = write(server.aof_fd,server.aof_buf,sdslen(server.aof_buf));
 327     latencyEndMonitor(latency);
         // 重置延遲刷盤時間
 343     server.aof_flush_postponed_start = 0;
 344     // 若是write失敗,那麼嘗試將該狀況寫入到日誌裏面
 345     if (nwritten != (signed)sdslen(server.aof_buf)) {
 346         static time_t last_write_error_log = 0;
 347         int can_log = 0;
 348
 350         if ((server.unixtime - last_write_error_log) > AOF_WRITE_LOG_ERROR_RATE) {
 351             can_log = 1;
 352             last_write_error_log = server.unixtime;
 353         }
 354
 356         if (nwritten == -1) {
 357             if (can_log) {
 358                 serverLog(LL_WARNING,"Error writing to the AOF file: %s",
 359                     strerror(errno));
 360                 server.aof_last_write_errno = errno;
 361             }
 362         } else {
 363             if (can_log) {
 364                 serverLog(LL_WARNING,"Short write while writing to "
 365                                        "the AOF file: (nwritten=%lld, "
 366                                        "expected=%lld)",
 367                                        (long long)nwritten,
 368                                        (long long)sdslen(server.aof_buf));
 369             }
 370             // 經過ftruncate嘗試刪除新追加到AOF中的不完整的數據內容
 371             if (ftruncate(server.aof_fd, server.aof_current_size) == -1) {
 372                 if (can_log) {
 373                     serverLog(LL_WARNING, "Could not remove short write "
 374                              "from the append-only file.  Redis may refuse "
 375                              "to load the AOF the next time it starts.  "
 376                              "ftruncate: %s", strerror(errno));
 377                 }
 378             } else {
 381                 nwritten = -1;
 382             }
 383             server.aof_last_write_errno = ENOSPC;
 384         }
             // 處理寫入AOF文件是出現的錯誤
 387         if (server.aof_fsync == AOF_FSYNC_ALWAYS) {
 392             serverLog(LL_WARNING,"Can't recover from AOF write error when the AOF fsync policy is 'always'. Exiting...");
 393             exit(1);
 394         } else {
 398             server.aof_last_write_status = C_ERR;
                 // 若是是已經寫入了部分數據,是不能經過ftruncate進行撤銷的
                 // 這裏經過sdsrange清除掉aof_buf中已經寫入磁盤的那部分數據
 402             if (nwritten > 0) {
 403                 server.aof_current_size += nwritten;
 404                 sdsrange(server.aof_buf,nwritten,-1);
 405             }
 406             return; 
 407         }
 408     } else {
 411         if (server.aof_last_write_status == C_ERR) {
 412             serverLog(LL_WARNING,
 413                 "AOF write error looks solved, Redis can write again.");
 414             server.aof_last_write_status = C_OK;
 415         }
 416     }
         // 更新寫入後的 AOF 文件大小
 417     server.aof_current_size += nwritten;
 418
 419      /* 當 server.aof_buf 足夠小,從新利用空間,防止頻繁的內存分配。
           * 相反,當 server.aof_buf 佔據大量的空間,採起的策略是釋放空間。
           */
 420      
 421     if ((sdslen(server.aof_buf)+sdsavail(server.aof_buf)) < 4000) {
 422         sdsclear(server.aof_buf);
 423     } else {
 424         sdsfree(server.aof_buf);
 425         server.aof_buf = sdsempty();
 426     }
 427
 428     /* 若是 no-appendfsync-on-rewrite 選項激活狀態
 429      * 並有BGSAVE或BGREWRITEAOF正在進行,那麼不執行fsync
          */
 430     if (server.aof_no_fsync_on_rewrite &&
 431         (server.aof_child_pid != -1 || server.rdb_child_pid != -1))
 432             return;
 433
 434     // 執行 fysnc
 435     if (server.aof_fsync == AOF_FSYNC_ALWAYS) {
 436         /* aof_fsync is defined as fdatasync() for Linux in order to avoid
 437          * flushing metadata. */
 438         latencyStartMonitor(latency);
 439         aof_fsync(server.aof_fd); /* Let's try to get this data on the disk */
 440         latencyEndMonitor(latency);
 441         latencyAddSampleIfNeeded("aof-fsync-always",latency);
 442         server.aof_last_fsync = server.unixtime;
 443     } else if ((server.aof_fsync == AOF_FSYNC_EVERYSEC &&
 444                 server.unixtime > server.aof_last_fsync)) {
 445         if (!sync_in_progress) aof_background_fsync(server.aof_fd);
 446         server.aof_last_fsync = server.unixtime;
 447     }
 448 }
 449

最後咱們從新回顧一下關於aof的寫盤操做:

主線程操做完內存數據後,會執行write,以後根據配置決定是當即仍是延遲fdatasync
redis在啓動時,會建立專門的bio線程用於處理aof持久化
若是是apendfsync=everysec,時機到達後,會建立異步任務(bio)
bio線程輪詢任務池,拿到任務後同步執行fdatasync

結論:

關於數據可靠性:

若是是always每次寫命令後都是刷盤,故障時丟失數據最少,若是是everysec,會丟失大概2秒的數據,在bio延遲刷盤時若是後臺刷盤操做卡住,在ServerCron裏面每一輪循環(頻率取決於hz參數,咱們設置爲100,也就是一秒執行100次循環)都檢查是否上一次後臺刷盤操做是否超過2秒,若是超過當即進行一次強制刷盤,所以能夠粗略的認爲最大可能丟失2.01秒的數據。
若是在進行bgrewriteaof期間出現故障,因rewrite會阻塞fdatasync刷盤,可能丟失的數據量更大,這個就不太容易量化評估了。

關於aof對延遲的影響

關於AOF對訪問延遲的影響,Redis做者曾經專門寫過一篇博客 fsync() on a different thread: apparently a useless trick,結論是bio對延遲的改善並非很大,由於雖然apendfsync=everysec時fdatasync在後臺運行,wirte的aof_buf並不大,基本上不會致使阻塞,而是後臺的fdatasync會致使write等待datasync完成了以後才調用write致使阻塞,fdataysnc會握住文件句柄,fwrite也會用到文件句柄,這裏write會致使了主線程阻塞。這也就是爲何以前浪潮服務器的RAID出現性能問題時,雖然對大部分應用沒有影響,可是對於Redis這種對延遲很是敏感的應用卻形成了影響的緣由。

是否能夠關閉AOF?

既然開啓AOF會形成訪問延遲,那麼是能夠關閉呢,答案是確定的,對應純緩存場景,例如數據Missed後會自動訪問數據庫,或是能夠快速從數據庫重建的場景,徹底能夠關閉,從而獲取最優的性能。其實即便關閉了AOF也不意味着當一個分片實例Crash時會丟掉這個分片的數據,咱們實際生產環境中每一個分片都是會有主備(Master/Slave)兩個實例,經過Redis的Replication機制保持同步,當主實例Crash時會自動進行主從切換,將備實例切換爲主,從而保證了數據可靠性,爲了不主備同時Crash,實際生產環境都是將主從分佈在不一樣物理機和不一樣交換機下。

Redis的持久化是否具有數據庫能力

目前還不能代替數據庫,更不具有關係型數據庫的功能,若是是對數據可靠性要求高的業務須要慎重,建議考慮使用基於RocksDB的解決方案

相關文章
相關標籤/搜索