🕋【Redis 干货领域】从底层彻底吃透 AOF 重写 (原理篇)
🕋 每日一句
不要轻易去依赖一个人,它会成为你的习惯,当分别来临,你失去的不是某个人,而是你精神的支柱。无论何时何地,都要学会独立行走,它会让你走得更坦然些。
🕋 前提介绍
为了保证缓存数据的完整性和可靠性,Redis 提供两种持久化机制:
RDB: 将数据库的快照以二进制的方式保存到磁盘;
AOF: 将所有写入命令及相关参数以协议文本的方式写入文件并持久保存磁盘;
本文只关心 AOF,Redis Server 将所有写入的命令转换成 RESP 协议文本的方式写入 AOF 文件。
🕋 AOF 的实现
Redis 的 AOF 是类似于 log 的机制,每次写操作都会写到硬盘上,当系统崩溃时,可以通过 AOF 来恢复数据。每个带有写操作的命令被 Redis 服务器端收到运行时,该命令都会被记录到 AOF 文件上。由于只是一个 append 到文件操作,所以写到硬盘上的操作往往非常快。
其实 Redis AOF 机制包括了两件事,Rewrite 和 AOF,因为 AOF 主要采用的方式属于 Append Of File 的方式追加是文档存储,本质内容暂时不考虑,本章重点内容是 Rewrite 机制。
🕋 AOF 执行案例
Redis Server 收到set key value
的的写入命令,Redis server 会进行以下几步操作:
将命令转换成协议文本,转换后的结果:
将协议文本追加到 aof 缓存,也就是 aof_buf;
根据 sync 策略调用 fsync/fdatasync。
到目前为止已经成功保存数据,如果想要还原 AOF,只需要将 AOF 里命令读出来并重放就可以还原数据库。
🕋 重写的介绍
AOF 持久化机制存在一个致命的问题,随着时间推移,AOF 文件会膨胀,如果频繁写入 AOF 文件会膨胀到无限大,当 server 重启时严重影响数据库还原时间,影响系统可用性。
为解决此问题,系统需要定期重写 AOF 文件,目前采用的方式是创建一个新的 AOF 文件,将数据库里的全部数据转换成协议的方式保存到文件中,通过此操作达到减少 AOF 文件大小的目的,重写后的大小一定是小于等于旧 AOF 文件的大小。
🕋 重写的实现
rewrite 类似于普通数据库管理系统日志恢复点,当 AOF 文件随着写命令的运行膨胀时,当文件大小触碰到临界时,rewrite 会被运行。
rewrite(bgrewriteaof 相似)会像 replication 一样,fork 出一个子进程,创建一个临时文件,遍历数据库内存数据,将每个 key、value 对输出到临时文件。输出格式就是 Redis 的命令(RESP),但是为了减小文件大小,会将多个 key、value 对集合起来用一条命令表达。
rewrite 期间的写操作会保存在内存的 rewrite buffer 中,rewrite 成功后这些操作也会复制到临时文件中(指令传播),在最后临时文件会代替 AOF 文件。以上在 AOF 打开的情况下,如果 AOF 是关闭的,那么 rewrite 操作可以通过 bgrewriteaof 命令来进行。
🕋 重写的类型
本文只关心 BGREWRITE 的问题,因此只介绍此命令的实现机制。
REWRITE: 在主线程中重写 AOF,会阻塞工作线程,在生产环境中很少使用,处于废弃状态;
BGREWRITE: 在后台(子进程)重写 AOF, 不会阻塞工作线程,能正常服务,此方法最常用。
🕋 重写的流程
Redis Server 启动,如果 AOF 机制打开那么初始化 AOF 状态,并且如果存在 AOF 文件,读取 AOF 文件。
随着 Redis 不断接受命令,每个写命令都被添加到 AOF 文件,AOF 文件膨胀到需要 rewrite 阈值时又或者接收到客户端的 bgrewriteaof 命令。
fork 出一个子进程进行 rewrite,而父进程继续接受命令,现在的写操作命令都会被额外添加到一个 aof_rewrite_buf_blocks 缓冲中。
当子进程 rewrite 结束后,父进程收到子进程退出信号,把 aof_rewrite_buf_blocks 的缓冲添加到 rewrite 后的文件中,然后切换 AOF 的文件 fd。(此部分过程需要阻塞)
rewrite 任务完成,继续第二个步骤。
🕋 实现关键点
由于写操作通常是有缓冲的,所以有可能 AOF 操作并没有写到硬盘中,一般可以通过 fsync()来强制输出到硬盘中。而 fsync()的频率可以通过配置文件中的 flush 策略来指定,可以选择每次事件循环写操作都强制 fsync 或者每秒 fsync 至少运行一次。
当 rewrite 子进程开始后,父进程接受到的命令会添加到 aof_rewrite_buf_blocks 中,使得 rewrite 成功后,将这些命令添加到新文件中。在 rewrite 过程中,原来的 AOF 也可以选择是不是继续添加,由于存在性能上的问题,在 rewrite 过程中,如果 fsync()继续执行,会导致 IO 性能受损影响 Redis 性能。所以一般情况下 rewrite 期间禁止 fsync()到旧 AOF 文件。这策略可以在配置文件中修改。
在 rewrite 结束后,在将新 rewrite 文件重命名为配置中指定的文件时,如果旧 AOF 存在,那么会 unlink 掉旧文件。这是就存在一个问题,处理 rewrite 文件迁移的是主线程,rename(oldpath, newpath)过程会覆盖旧文件,这是 rename 会 unlink(oldfd),而 unlink 操作会导致 block 主线程。这时,我们就需要类似 libeio(http://software.schmorp.de/pkg/libeio.html)这样的库去进行异步的底层 IO。作者在 bio.c 有一个类似的机制,通过创建新线程来进行异步操作。
🕋 异步重写的支持
Redis Server 收到 BGREWRITE 命令或者系统自动触发 AOF 重写时,主进程创建一个子进程并进行 AOF 重写,主进程异步等待子进程结束(信号量),此时主进程能正常接收处理用户请求,用户请求会修改数据库里数据,会使得当前数据库的数据跟重写后 AOF 里不一致,需要有种机制保证数据的一致性。当前的做法是在重写 AOF 期间系统会新开一块内存用于缓存重写期间收到的命令,在重写完成以后再将缓存中的数据追加到新的 AOF。
在处理命令时既要将命令追加到 aof_buf,也要追加到重写 AOF Buffer。
🕋 Rewrite 存在的问题
重写 AOF Buffer 是个不限大小的 buffer,但用户写入的数据量较多时会出现以下两个问题:
占用过多内存,浪费资源;
主进程将 AOF buffer 数据写入到新 AOF 文件中时会阻塞工作线程,用户正常请求的延时会变高,严重情况下会超时,主备同步也会出问题,断开重连,重新同步等。
🕋 自动触发条件
AOF 里存放了所有的 redis 操作指令,文件达到条件或者手动 bgrewriteaof 命令都可以触发 rewrite。
rewrite 之后 aof 文件会保存 keys 的最后的状态,清除掉之前冗余的,来缩小这个文件.
在配置文件里设置过:
auto-aof-rewrite-percentage 100 (当前写入日志文件的大小超过上一次 rewrite 之后的文件大小的百分之 100 时就是 2 倍时触发 Rewrite)
此外那个 64m 的代码我就不列举了 我相信大家都知道 哈哈,以后的篇章我会从源码去分析介绍的,期待吧 哈哈。
🕋 后台 Rewrite 问题解决方案
🕋 官方解决方案(AOF_BUFFER_BLOCK 不进行阻塞)
主要思路是 AOF 重写期间,主进程跟子进程通过管道通信,主进程实时将新写入的数据发送给子进程,子进程从管道读出数据交缓存在 buffer 中,子进程等待存量数据全部写入 AOF 文件后,将缓存数据追加到 AOF 文件中(不是主进程写入),此方案只是解决阻塞工作线程问题,但占用内存过多问题并没有解决。
🕋 新解决方案(buffer 配对文件机制)
主要思路是 AOF 重写期间,主进程创建一个新的 aof_buf,新的 AOF 文件用于接收新写入的命令,sync 策略保持不变,在 AOF 重写期间,系统需要向两个 aof_buf,两个 AOF 文件同时追加新写入的命令。当主进程收到子进程重写 AOF 文件完成后,停止向老的 aof_buf,AOF 文件追加命令,然后删除旧的 AOF 文件(流程跟原来保持一致);将将子进程新生成的 AOF 文件重命名为 appendonly.aof.last,具体流程如下:
停止向旧的 aof_buf,AOF 文件追加命令;
删除旧的的 appendonly.aof.last 文件;
交换两个 aof_buf,AOF 文件指针;
回收旧的 aof_buf,AOF 文件;
重命令子进程生成的 AOF 文件为 appendonly.aof.last;
系统运行期间同时存在两个 AOF 文件,一个是当前正在写的 AOF,另一个是存量的 AOF 数据文件。因此需要修改数据库恢复相关逻辑,加载 AOF 时先要加载存量数据 appendonly.aof.last,再加载 appendonly.aof。
评论 (1 条评论)