相信不少小夥伴對redis的持久化保證有疑問:node
因此簡單整理了這篇文章,對redis的持久化內部原理進行分析,同時可爲其它須要持久化的實現提供參考。 鑑於OS的差別,本文統一以linux 2.6+爲準。linux
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
在進入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是經過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的解決方案