开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第4天,点击查看活动详情
Redis除了使用内存快照RDB来保证数据可靠性之外,还可以使用AOF日志来进行持久化。不同的是RDB文件是将某一时刻的内存数据保存成一个文件,而AOF日志则会记录接收到的所有写操作。
AOF 持久化
要想开启 AOF 持久化需要将 appendonly 设置为 yes。开启 AOF 持久化后,每次执行写命令都会被记录在 AOF 文件中。
写入与同步
在现代操作系统中,当用户调用 write 函数时会将一些数据写入到文件的时候,操作系统通常会将写入数据暂时保存在一个内存缓冲区里面,等到缓冲区的空间被填满、或者超过了指定的时限之后,才真正地将缓冲区中的数据写入到磁盘里面。这种做法虽然提高了效率,但也为写入数据带来了安全问题,因为如果计算机发生停机,那么保存在内存缓冲区里面的写入数据将会丢失。为此,系统提供了 fsync 和 fdatasync 两个同步函数,它们可以强制让操作系统立即将缓冲区中的数据写入到硬盘里面,从而确保写入数据的安全性。
因为服务器在处理文件事件时可能会执行写命令,使得一些内容被追加到 aof_buf 缓冲区里面,所以在服务器每次结束一个事件循环之前,它都会调用 flushAppendOnlyFile 函数,flushAppendOnlyFile 函数会根据 appendfsync 的值来决定落盘策略。
落盘策略
AOF 的落盘策略是由 appendfsync 参数来控制,它有三个值可以设置:
- always :服务器在每个事件循环都要将aof_buf缓冲区中的所有内容写入到AOF文件,并且同步AOF文件。所以always的效率是appendfsync选项三个值当中最慢的一个,但从安全性来说,always也是最安全的,因为即使出现故障停机,AOF持久化也只会丢失一个事件循环中所产生的命令数据。
- everysec :服务器在每个事件循环都要将aof_buf缓冲区中的所有内容写入到AOF文件,并且每隔一秒就要在子线程中对AOF文件进行一次同步。从效率上来讲,everysec模式足够快,并且就算出现故障停机,数据库也只丢失一秒钟的命令数据。
- no :服务器在每个事件循环都要将aof_buf缓冲区中的所有内容写入到AOF文件,至于何时对AOF文件进行同步,则由操作系统控制。因为处于no模式下的flushAppendOnlyFile调用无须执行同步操作,所以该模式下的AOF文件写入速度总是最快的,不过因为这种模式会在系统缓存中积累一段时间的写入数据,所以该模式的单次同步时长通常是三种模式中时间最长的。从平摊操作的角度来看,no模式和everysec模式的效率类似,当出现故障停机时,使用no模式的服务器将丢失上次同步AOF文件之后的所有写命令数据
AOF 重写
随着 Redis 的写请求越来越多, AOF 文件会变得越来越大。为了避免 AOF 文件过大,Redis 会对 AOF 文件进行重写操作,即根据当前最新的数据记录它的插入操作,不再记录之前的写入记录,这样来缩小 AOF 文件的大小。
AOF 重写函数
AOF 的重写函数是 rewriteAppendOnlyFileBackground,可以看下面的官方注释,这个函数的作用是创建一个子进程,子进程会将当前的 AOF 文件重写到一个临时文件,而父进程将继续接收客户端的命令,并将写命令写入 AOF 重写缓冲区也就是 server.aof_rewrite_buf 中。父进程也会尽可能的将重写缓冲区中的数据写入临时文件,最后将临时文件重命名为真实的 AOF 文件名。
/* ----------------------------------------------------------------------------
* AOF background rewrite
* ------------------------------------------------------------------------- */
/* This is how rewriting of the append only file in background works:
*
* 1) The user calls BGREWRITEAOF
* 2) Redis calls this function, that forks():
* 2a) the child rewrite the append only file in a temp file.
* 2b) the parent accumulates differences in server.aof_rewrite_buf.
* 3) When the child finished '2a' exists.
* 4) The parent will trap the exit code, if it's OK, will append the
* data accumulated into server.aof_rewrite_buf into the temp file, and
* finally will rename(2) the temp file in the actual file name.
* The the new file is reopened as the new append only file. Profit!
*/
int rewriteAppendOnlyFileBackground(void);
重写时机
下面我们看一下 Redis 会在什么情况下触发 AOF 重写操作。我们首先看一下哪些函数会调用 rewriteAppendOnlyFileBackground,经过查找我们发现 bgrewriteaofCommand、startAppendOnly 和 serverCron 这三个函数会调用,下面我们来分析这三个函数的调用时机。
bgrewriteaofCommand
bgrewriteaofCommand 对应 bgrewriteaof 命令,即当我们手动执行 bgrewriteaof 命令时会进行 AOF 重写,不过是否真正执行 AOF 重写还需要判断两个条件:
- 是否已经有
AOF重写的子进程正在执行 - 是否已经有创建
RDB的子进程正在执行,如果有则会将aof_rewrite_scheduled设置为 1,这个参数后面会用到,这里简单提一下。
只有这两个条件都为否的情况下才会执行 AOF 重写。下面是 bgrewriteaofCommand 的函数实现:
void bgrewriteaofCommand(client *c) {
// 是否已经有 AOF 重写的子进程正在执行
if (server.aof_child_pid != -1) {
addReplyError(c,"Background append only file rewriting already in progress");
// 是否已经有创建 RDB 的子进程正在执行
} else if (server.rdb_child_pid != -1) {
server.aof_rewrite_scheduled = 1;
addReplyStatus(c,"Background append only file rewriting scheduled");
} else if (rewriteAppendOnlyFileBackground() == C_OK) {
addReplyStatus(c,"Background append only file rewriting started");
} else {
addReply(c,shared.err);
}
}
startAppendOnly
我们首先看一下 startAppendOnly 函数的代码实现:
/* Called when the user switches from "appendonly no" to "appendonly yes"
* at runtime using the CONFIG command. */
int startAppendOnly(void) {
...
// 是否已经有创建 RDB 的子进程正在执行
if (server.rdb_child_pid != -1) {
server.aof_rewrite_scheduled = 1;
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.");
} else {
/* If there is a pending AOF rewrite, we need to switch it off and
* start a new one: the old one cannot be reused because it is not
* accumulating the AOF buffer. */
// 是否已经有 AOF 重写的子进程正在执行
if (server.aof_child_pid != -1) {
serverLog(LL_WARNING,"AOF was enabled but there is already an AOF rewriting in background. Stopping background AOF and starting a rewrite now.");
killAppendOnlyChild();
}
if (rewriteAppendOnlyFileBackground() == C_ERR) {
close(newfd);
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.");
return C_ERR;
}
}
...
}
从注释看 startAppendOnly 会在我们使用 config 命令启用 AOF 时调用,这里对应了 configSetCommand 函数调用 startAppendOnly 的情况。
config set appendonly yes
其实在主从复制时 restartAOFAfterSYNC 函数也会调用 startAppendOnly,即当主从节点在进行复制时,如果从节点的 AOF 选项被打开,那么在加载解析 RDB 文件时,AOF 选项就会被关闭。然后无论从节点是否成功加载了 RDB 文件,restartAOFAfterSYNC 函数都会被调用,用来恢复被关闭的 AOF 功能。
再看回 startAppendOnly 函数,发现与 bgrewriteaofCommand 函数类似,它也会校验是否已经有创建 RDB 的子进程正在执行,不同的是在校验是否已经有 AOF 重写的子进程正在执行时,如果有,它会杀死子进程然后再进行 AOF 重写。
serverCron
serverCron 函数是一个周期函数,默认每 100 毫秒执行一次。然后它在执行的过程中,有两次调用了 rewriteAppendOnlyFileBackground 函数,我们来分析一下别是在哪种情况下会调用。
...
/* Start a scheduled AOF rewrite if this was requested by the user while
* a BGSAVE was in progress. */
if (server.rdb_child_pid == -1 && server.aof_child_pid == -1 &&
server.aof_rewrite_scheduled)
{
rewriteAppendOnlyFileBackground();
}
...
第一种是如果没有RDB子进程,也没有 AOF 重写子进程且 AOF 重写被设置为待调度执行,则会调用 rewriteAppendOnlyFileBackground 进行 AOF 重写。前面两个函数在执行时遇到创建 RDB 文件时会将 aof_rewrite_scheduled 变量被设置为 1,serverCron 函数就默认会每 100 毫秒执行并检测这个变量值。如果正在执行的 RDB 子进程和 AOF 重写子进程结束了之后,被调度执行的 AOF 重写就可以很快得到执行。
...
/* Trigger an AOF rewrite if needed. */
if (server.aof_state == AOF_ON &&
server.rdb_child_pid == -1 &&
server.aof_child_pid == -1 &&
server.aof_rewrite_perc &&
server.aof_current_size > server.aof_rewrite_min_size)
{
// 计算 AOF 当前文件大小超过基础大小的比例
long long base = server.aof_rewrite_base_size ?
server.aof_rewrite_base_size : 1;
long long growth = (server.aof_current_size*100/base) - 100;
// 如果 AOF 当前文件大小超过基础大小的比例超过预设值,就进行 AOF 重写
if (growth >= server.aof_rewrite_perc) {
serverLog(LL_NOTICE,"Starting automatic rewriting of AOF on %lld%% growth",growth);
rewriteAppendOnlyFileBackground();
}
}
...
第二种是 serverCron 会周期性的检查是否要进行 AOF 重写,主要根据是否开启 AOF 功能、AOF 当前文件大小超过基础大小的比例超过预设值和 AOF 文件大小是否超过阈值。
小结
到这我们就知道了 AOF 重写触发的四个时机,分别是:
- 手动执行
bgrewriteaof命令。 - 主从复制完成
RDB文件的加载和解析。 AOF重写被设置为待调度执行等待serverCron执行。serverCron周期判断是否开启AOF功能且AOF当前文件大小超过基础大小的比例超过预设值以及AOF文件大小是否超过阈值。
AOF 重写过程
下面我们再来看一下 rewriteAppendOnlyFileBackground 函数到底干了什么事。
int rewriteAppendOnlyFileBackground(void) {
pid_t childpid;
long long start;
if (server.aof_child_pid != -1 || server.rdb_child_pid != -1) return C_ERR;
// 创建父子进程通信的管道
if (aofCreatePipes() != C_OK) return C_ERR;
openChildInfoPipe();
start = ustime();
if ((childpid = fork()) == 0) {
...
// 调用 rewriteAppendOnlyFile 进行 AOF 重写
if (rewriteAppendOnlyFile(tmpfile) == C_OK) {
size_t private_dirty = zmalloc_get_private_dirty(-1);
if (private_dirty) {
serverLog(LL_NOTICE,
"AOF rewrite: %zu MB of memory used by copy-on-write",
private_dirty/(1024*1024));
}
server.child_info_data.cow_size = private_dirty;
sendChildInfo(CHILD_INFO_TYPE_AOF);
exitFromChild(0);
} else {
exitFromChild(1);
}
} else {
/* Parent */
server.stat_fork_time = ustime()-start;
server.stat_fork_rate = (double) zmalloc_used_memory() * 1000000 / server.stat_fork_time / (1024*1024*1024); /* GB per second. */
latencyAddSampleIfNeeded("fork",server.stat_fork_time/1000);
if (childpid == -1) {
closeChildInfoPipe();
serverLog(LL_WARNING,
"Can't rewrite append only file in background: fork: %s",
strerror(errno));
aofClosePipes();
return C_ERR;
}
serverLog(LL_NOTICE,
"Background append only file rewriting started by pid %d",childpid);
server.aof_rewrite_scheduled = 0;
server.aof_rewrite_time_start = time(NULL);
server.aof_child_pid = childpid;
// 禁用 rehash
updateDictResizePolicy();
/* We set appendseldb to -1 in order to force the next call to the
* feedAppendOnlyFile() to issue a SELECT command, so the differences
* accumulated by the parent into server.aof_rewrite_buf will start
* with a SELECT statement and it will be safe to merge. */
server.aof_selected_db = -1;
replicationScriptCacheFlush();
return C_OK;
}
return C_OK; /* unreached */
}
可以看到子进程主要是调用 rewriteAppendOnlyFile函数,而 rewriteAppendOnlyFile 主要会调用 rewriteAppendOnlyFileRio 函数完成 AOF 日志文件的重写。rewriteAppendOnlyFileRio 函数会遍历 Redis 的每一个数据库,把其中的每个键值对读取出来,然后记录该键值对类型对应的插入命令,以及键值对本身的内容。
在父进程中会把 aof_rewrite_scheduled 变量设置为 0,同时记录 AOF 重写开始的时间以及记录 AOF 子进程的进程号。此外,rewriteAppendOnlyFileBackground 函数还会调用 updateDictResizePolicy 函数来禁止在 AOF 重写期间进行 rehash 操作。这是因为 rehash 操作会带来较多的数据移动操作,对于 AOF 重写子进程来说,这就意味着父进程中的内存修改会比较多。因此 AOF 重写子进程就需要执行更多的写时复制,进而完成 AOF 文件的写入,这就会给 Redis 系统的性能造成负面影响。
小结
这篇文章主要介绍了 AOF 文件以及 AOF 重写机制。其中重点介绍了一下 AOF 重写的时机以及 AOF 重写的基本流程。注意一点 AOF 重写过程中父进程收到的写操作也会尽量写入 AOF 重写日志,Redis 使用管道机制来实现父子进程之间的通信。