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;
}



 

posted @ 2022-08-13 13:04  車輪の唄  阅读(17)  评论(0)    收藏  举报  来源