相信不少小伙伴对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的解决方案