redis--aof&rdb
RDB持久化机制
RDB持久化是把当前进程数据生成时间点快照(point-in-time snapshot)保存到硬盘的过程,避免数据意外丢失
1.1 RDB触发机制
RDB触发机制分为手动触发和自动触发。
手动触发的两条命令:
SAVE:阻塞当前Redis服务器,知道RDB过程完成为止。
BGSAVE:Redis 进程执行fork()操作创建出一个子进程,在后台完成RDB持久化的过程。(主流)
自动触发的配置:
save 900 1 //服务器在900秒之内,对数据库执行了至少1次修改
save 300 10 //服务器在300秒之内,对数据库执行了至少10修改
save 60 1000 //服务器在60秒之内,对数据库执行了至少1000修改
// 满足以上三个条件中的任意一个,则自动触发 BGSAVE 操作
// 或者使用命令CONFIG SET 命令配置
1.2 RDB持久化的流程
我们用图来表示 BGSAVE命令 的触发流程,如下图所示:
1.3 RDB的优点:
RDB是一个紧凑压缩的二进制文件,代表Redis在某个时间点上的数据快照。非常适用于备份,全景复制等场景。
Redis 加载RDB恢复数据远远快于AOF的方式。
RDB的缺点:
RDB没有办法做到实时持久化或秒级持久化。因为BGSAVE每次运行的又要进行fork()的调用创建子进程,这属于重量级操作,频繁执行成本过高,因为虽然Linux支持读时共享,写时拷贝(copy-on-write)的技术,但是仍然会有大量的父进程的空间内存页表,信号控制表,寄存器资源等等的复制。
RDB文件使用特定的二进制格式保存,Redis版本演进的过程中,有多个RDB版本,这导致版本兼容的问题。
AOF持久化机制
2.1 命令写入到 server.aof_buf (这个过程没有找到, 估计是在写入命令执行时, 执行的该操作)
2.2 server.aof_buf写入到操作系统内核缓冲区, 缓冲区刷新到磁盘(使用后台线程或者在主线程下完成)
Redis中给出了3中缓冲区同步文件的策略:
AOF_FSYNC_ALWAYS 命令写入aof_buf后调用系统fsync和操作同步到AOF文件,fsync完成后进程程返回
AOF_FSYNC_EVERYSEC 命令写入aof_buf后调用系统write操作,write完成后线程返回。fsync同步文件操作由进程每秒调用一次
AOF_FSYNC_NO 命令写入aof_buf后调用系统write操作,不对AOF文件做fsync同步,同步硬盘由操作由操作系统负责
write和fsync操作,在系统中都做了哪些事:
write操作:会触发延迟写(delayed write)机制。Linux在内核提供页缓冲区用来提高IO性能,因此,write操作在将数据写入操作系统的缓冲区后就直接返回,而不一定触发同步到磁盘的操作。只有在页空间写满,或者达到特定的时间周期,才会同步到磁盘。因此单纯的write操作也是有数据丢失的风险。
fsync操作:针对单个文件操作,做强制硬盘同步,fsync将阻塞直到写入硬盘完成后返回。
在serverCron()函数中调用 flushAppendOnlyFile()
/**
* 1.将server.aof_buf 写到系统内核缓冲的server.aof_fd文件描述符中(调用write方法)
* 2.根据配置项, 如果是AOF_FSYNC_ALWAYS配置,则在主线程中(事件循环线程中)将内核缓冲文件写入磁盘(调用fsnyc方法)
* 如果是AOF_FSYNC_EVERYSEC配置,则在后台线程中将内核缓冲写入到磁盘(即bio文件中实现的线程)
*
* 3.flushAppendOnlyFile()这个函数被serverCron()调用,在主线程中执行
*/
#define AOF_WRITE_LOG_ERROR_RATE 30 /* Seconds between errors logging. */
void flushAppendOnlyFile(int force) {
ssize_t nwritten;
int sync_in_progress = 0;
/* 缓冲区中没有任何内容,直接返回 */
if (sdslen(server.aof_buf) == 0) return;
if (server.aof_fsync_strategy == AOF_FSYNC_EVERYSEC) /* aof 文件的写入是每秒写入一次 */
sync_in_progress = bioPendingJobsOfType(REDIS_BIO_AOF_FSYNC) != 0; /* 是否有文件同步在后台执行 */
if (server.aof_fsync_strategy == AOF_FSYNC_EVERYSEC && !force) {
/*
* 当 fsync 策略为每秒钟一次时, fsync 在后台执行。
*
* 如果后台仍在执行 FSYNC ,那么我们可以延迟写操作一两秒
* (如果强制执行 write 的话,服务器主线程将阻塞在 write 上面)
*/
if (sync_in_progress) {
if (server.aof_flush_postponed_start == 0) {
/* 前面没有推迟过 write 操作,这里将推迟写操作的时间记录下来
* 然后就返回,不执行 write 或者 fsync
*/
server.aof_flush_postponed_start = server.unixtime;
return;
}
else if (server.unixtime - server.aof_flush_postponed_start < 2) {
/*
* 如果之前已经因为 fsync 而推迟了 write 操作
* 但是推迟的时间不超过 2 秒,那么直接返回
* 不执行 write 或者 fsync
*/
return;
}
/*
* 如果后台还有 fsync 在执行,并且 write 已经推迟 >= 2 秒
* 那么执行写操作(write 将被阻塞)
*/
server.aof_delayed_fsync++; // 被阻塞的文件同步的数目
mylog("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.");
}
}
/*
* 执行到这里,程序会对 AOF 文件进行写入。
*
* 清零延迟 write 的时间记录
*/
server.aof_flush_postponed_start = 0;
/*
* 执行单个 write 操作,如果写入设备是物理的话,那么这个操作应该是原子的
*
* 当然,如果出现像电源中断这样的不可抗现象,那么 AOF 文件也是可能会出现问题的
* 这时就要用 redis-check-aof 程序来进行修复。
*/
nwritten = write(server.aof_fd, server.aof_buf, sdslen(server.aof_buf));
if (nwritten != (signed)sdslen(server.aof_buf)) {
static time_t last_write_error_log = 0;
int can_log = 0;
/* 将日志的记录频率限制在每行 AOF_WRITE_LOG_ERROR_RATE 秒. */
if ((server.unixtime - last_write_error_log) > AOF_WRITE_LOG_ERROR_RATE) {
can_log = 1;
last_write_error_log = server.unixtime;
}
/* Lof the AOF write error and record the error code.
* 如果写入出错,那么尝试将该情况写入到日志里面 */
if (nwritten == -1) {
if (can_log) {
mylog("Error writing to the AOF file: %s", strerror(errno));
server.aof_last_write_errno = errno;
}
}
else {
if (can_log) {
mylog("Short write while writing to "
"the AOF file: (nwritten=%lld, "
"expected=%lld)",
(long long)nwritten,
(long long)sdslen(server.aof_buf));
}
/* 尝试移除新追加的不完整内容 */
if (ftruncate(server.aof_fd, server.aof_current_size) == -1) { /* ftruncate表示截断文件的内容 */
if (can_log) {
mylog("Could not remove short write "
"from the append-only file. Redis may refuse "
"to load the AOF the next time it starts. "
"ftruncate: %s", strerror(errno));
}
}
else { /* If the ftrunacate() succeeded we can set nwritten to
* -1 since there is no longer partial data into the AOF. */
nwritten = -1;
}
server.aof_last_write_errno = ENOSPC; /* 记录下出错原因 */
}
/* 处理写入 AOF 文件时出现的错误. */
if (server.aof_fsync_strategy == AOF_FSYNC_ALWAYS) {
/* We can't recover when the fsync policy is ALWAYS since the
* reply for the client is already in the output buffers, and we
* have the contract with the user that on acknowledged write data
* is synched on disk. */
mylog("%s", "Can't recover from AOF write error when the AOF fsync policy is 'always'. Exiting...");
exit(1);
}
else {
/* Recover from failed write leaving data into the buffer. However
* set an error to stop accepting writes as long as the error
* condition is not cleared. */
server.aof_last_write_status = REDIS_ERR;
/* Trim the sds buffer if there was a partial write, and there
* was no way to undo it with ftruncate(2). */
if (nwritten > 0) {
server.aof_current_size += nwritten;
sdsrange(server.aof_buf, nwritten, -1);
}
return; /* We'll try again on the next call... */
}
}
else {
/* 写入成功,更新最后写入状态 */
if (server.aof_last_write_status == REDIS_ERR) {
mylog("%s", "AOF write error looks solved, Redis can write again.");
server.aof_last_write_status = REDIS_OK;
}
}
/* 更新写入后的 AOF 文件大小 */
server.aof_current_size += nwritten;
/*
* 如果 AOF 缓存的大小足够小的话,那么重用这个缓存,
* 否则的话,释放 AOF 缓存。
*/
if ((sdslen(server.aof_buf) + sdsavail(server.aof_buf)) < 4000) {
/* 清空缓存中的内容,等待重用 */
sdsclear(server.aof_buf);
}
else {
/* 释放缓存 */
sdsfree(server.aof_buf);
server.aof_buf = sdsempty();
}
/*
* 如果 no-appendfsync-on-rewrite 选项为开启状态,
* 并且有 BGSAVE 或者 BGREWRITEAOF 正在进行的话,
* 那么不执行 fsync
*/
if (server.aof_no_fsync_on_rewrite &&
(server.aof_child_pid != -1 || server.rdb_child_pid != -1))
return;
/* 总是执行 fsnyc */
if (server.aof_fsync_strategy == AOF_FSYNC_ALWAYS) {
/* aof_fsync is defined as fdatasync() for Linux in order to avoid
* flushing metadata. */
aof_fsync(server.aof_fd); /* Let's try to get this data on the disk */
}
else if ((server.aof_fsync_strategy == AOF_FSYNC_EVERYSEC &&
server.unixtime > server.aof_last_fsync)) {
/* 放到后台执行 */
if (!sync_in_progress) aof_background_fsync(server.aof_fd);
}
/* 更新最后一次执行 fsync 的时间 */
server.aof_last_fsync = server.unixtime;
}
2.3 当一个数据库的命令非常多时,AOF文件就会非常大,为了解决这个问题,Redis引入了AOF重写机制来压缩文件的体积
AOF重写操作有可能会长时间阻塞服务器主进程,因此会fork()一个子进程在后台进行重写,然后父进程就可以继续响应命令请求。虽然解决了阻塞问题,但是有产生了新问题:子进程在重写期间,服务其还会处理新的命令请求,而这些命令可能灰度数据库的状态进行更改,从而使当前的数据库状态和AOF重写之后保存的状态不一致
// 以下是BGREWRITEAOF的工作步骤
// 1. 用户调用BGREWRITEAOF
// 2. Redis调用这个函数,它执行fork()
// 2.1 子进程在临时文件中执行重写操作
// 2.2 父进程将累计的差异数据追加到server.aof_rewrite_buf中
// 3. 当子进程完成2.1
// 4. 父进程会捕捉到子进程的退出码,如果是OK,那么追加累计的差异数据到临时文件,并且对临时文件rename,用它代替旧的AOF文件,然后就完成AOF的重写。
int rewriteAppendOnlyFileBackground(void) {
pid_t childpid;
long long start;
// 如果正在进行重写或正在进行RDB持久化操作,则返回C_ERR
if (server.aof_child_pid != -1 || server.rdb_child_pid != -1) return C_ERR;
// 创建父子进程间通信的管道
if (aofCreatePipes() != C_OK) return C_ERR;
// 记录fork()开始时间
start = ustime();
// 子进程
if ((childpid = fork()) == 0) {
char tmpfile[256];
/* Child */
// 关闭监听的套接字
closeListeningSockets(0);
// 设置进程名字
redisSetProcTitle("redis-aof-rewrite");
// 创建临时文件
snprintf(tmpfile,256,"temp-rewriteaof-bg-%d.aof", (int) getpid());
// 对临时文件进行AOF重写
if (rewriteAppendOnlyFile(tmpfile) == C_OK) {
// 获取子进程使用的内存空间大小
size_t private_dirty = zmalloc_get_private_dirty();
if (private_dirty) {
serverLog(LL_NOTICE,
"AOF rewrite: %zu MB of memory used by copy-on-write",
private_dirty/(1024*1024));
}
// 成功退出子进程
exitFromChild(0);
} else {
// 异常退出子进程
exitFromChild(1);
}
// 父进程
} else {
/* Parent */
// 设置fork()函数消耗的时间
server.stat_fork_time = ustime()-start;
// 计算fork的速率,GB/每秒
server.stat_fork_rate = (double) zmalloc_used_memory() * 1000000 / server.stat_fork_time / (1024*1024*1024); /* GB per second. */
// 将"fork"和fork消耗的时间关联到延迟诊断字典中
latencyAddSampleIfNeeded("fork",server.stat_fork_time/1000);
if (childpid == -1) {
serverLog(LL_WARNING,
"Can't rewrite append only file in background: fork: %s",
strerror(errno));
return C_ERR;
}
// 打印日志
serverLog(LL_NOTICE,
"Background append only file rewriting started by pid %d",childpid);
// 将AOF日程标志清零
server.aof_rewrite_scheduled = 0;
// AOF开始的时间
server.aof_rewrite_time_start = time(NULL);
// 设置AOF重写的子进程pid
server.aof_child_pid = childpid;
// 在AOF或RDB期间,不能对哈希表进行resize操作
updateDictResizePolicy();
// 将aof_selected_db设置为-1,强制让feedAppendOnlyFile函数执行时,执行一个select命令
server.aof_selected_db = -1;
// 清空脚本缓存
replicationScriptCacheFlush();
return C_OK;
}
return C_OK; /* unreached */
}
2.4 关于父子进程通信
整个重写的过程中,父子进行通信的地方只有一个,那就是最后父进程在子进程做重写操作完成时,把子进程重写操作期间所执行的新命令发送给子进程的重写缓冲区,子进程然后将重写缓冲区的数据追加到AOF文件中
而父进程是如何将差异数据发送给子进程呢?Redis中使用了管道技术
在上文提到的rewriteAppendOnlyFileBackground()函数首先就创建了父子通信的管道。
父子进程间通信时共创建了三组管道
//下面两个是发送差异数据管道
int aof_pipe_write_data_to_child; //父进程写给子进程的文件描述符
int aof_pipe_read_data_from_parent; //子进程从父进程读的文件描述符
//下面四个是应答ack的管道
int aof_pipe_write_ack_to_parent; //子进程写ack给父进程的文件描述符
int aof_pipe_read_ack_from_child; //父进程从子进程读ack的文件描述符
int aof_pipe_write_ack_to_child; //父进程写ack给子进程的文件描述符
int aof_pipe_read_ack_from_parent; //子进程从父进程读ack的文件描述符
当将feedAppendOnlyFile()将命令追加到缓冲区的同时,还在最后调用了aofRewriteBufferAppend()函数,这个函数就是将命令追加到AOF的缓冲区,然而,在追加完成后会执行这么一段代码
// 获取当前事件正在监听的类型,如果等于0,未设置,则设置管道aof_pipe_write_data_to_child为可写状态
// 当然aof_pipe_write_data_to_child可以用的时候,调用aofChildWriteDiffDatah()函数写数据
if (aeGetFileEvents(server.el,server.aof_pipe_write_data_to_child) == 0) {
aeCreateFileEvent(server.el, server.aof_pipe_write_data_to_child,
AE_WRITABLE, aofChildWriteDiffData, NULL);
}
当然aof_pipe_write_data_to_child可以写的时候,调用aofChildWriteDiffDatah()函数写数据,而在aofChildWriteDiffDatah()函数中,则将重写缓冲区数据写到管道中。函数源码如下:// 事件处理程序发送一些数据给正在做AOF重写的子进程,我们发送AOF缓冲区一部分不同的数据给子进程,当子进程完成重写时,重写的文件会比较小
void aofChildWriteDiffData(aeEventLoop *el, int fd, void *privdata, int mask) {
listNode *ln;
aofrwblock *block;
ssize_t nwritten;
UNUSED(el);
UNUSED(fd);
UNUSED(privdata);
UNUSED(mask);
while(1) {
// 获取缓冲块链表的头节点地址
ln = listFirst(server.aof_rewrite_buf_blocks);
// 获取缓冲块地址
block = ln ? ln->value : NULL;
// 如果aof_stop_sending_diff为真,则停止发送累计的不同数据给子进程,或者缓冲块为空
// 则将管道的写端从服务器的监听队列中删除
if (server.aof_stop_sending_diff || !block) {
aeDeleteFileEvent(server.el,server.aof_pipe_write_data_to_child,
AE_WRITABLE);
return;
}
// 如果已经有缓存的数据
if (block->used > 0) {
// 则将缓存的数据写到管道中
nwritten = write(server.aof_pipe_write_data_to_child,
block->buf,block->used);
if (nwritten <= 0) return;
// 更新缓冲区的数据,覆盖掉已经写到管道的数据
memmove(block->buf,block->buf+nwritten,block->used-nwritten);
block->used -= nwritten;
}
// 如果当前节点的所缓冲的数据全部写完,则删除该节点
if (block->used == 0) listDelNode(server.aof_rewrite_buf_blocks,ln);
}
}
而在上面展示到的rewriteAppendOnlyFile()函数中,则当aof_pipe_read_data_from_parent可读时,不断调用aofReadDiffFromParent()函数的从管道读数据,这样就实现了父子进程的通信。该函数源码如下:
// 该函数在子进程正在进行重写AOF文件时调用
// 用来读从父进程累计写入的缓冲区的差异,在重写结束时链接到文件的结尾
ssize_t aofReadDiffFromParent(void) {
// 大多数Linux系统中默认的管道大小
char buf[65536]; /* Default pipe buffer size on most Linux systems. */
ssize_t nread, total = 0;
// 从父进程读数据到buf中,读了nread个字节
while ((nread =
read(server.aof_pipe_read_data_from_parent,buf,sizeof(buf))) > 0) {
// 将buf中的数据累计到子进程的差异累计的sds中
server.aof_child_diff = sdscatlen(server.aof_child_diff,buf,nread);
// 更新总的累计字节数
total += nread;
}
return total;
}

浙公网安备 33010602011771号