写点什么

Redis - AOF

用户头像
insight
关注
发布于: 2021 年 03 月 27 日
Redis - AOF

为什么要持久化

在 Redis 使用的大部分场景下,Redis 都是做为缓存来使用,为数据库分担压力。当 Redis 宕机之后,我们固然可以从数据库中恢复所有数据到内存中,但是由于数据库和缓存能承受的 QPS 是有一个数量级的差距的,所以如果直接从数据库中恢复数据,会对数据库造成很大的影响,并且恢复的速度比较慢。因此,Redis 自身提供了持久化机制,来让 Redis 出现宕机或者转移时能快速进行恢复。


Redis 的持久化机制包含两种:AOF 和 RDB。下面先介绍 AOF。

AOF

介绍

AOF 是 append only file 的缩写,由名字可以知道,它其实是一个追加写的文件。那么它追加写什么内容呢?那就是每一条 Redis 的操作命令,如果我们有一个记录了 Redis 所有操作命令的文件,那么我们就可以通过重放文件中的操作,将 Redis 的数据状态恢复到故障前的情况。

时机

那么 AOF 是什么时候做记录的呢?一般而言,记录命令有两个时机,一个是命令执行前,一个是命令执行后。如果在命令执行前就记录了命令,万一命令执行过程中发生了错误,就会导致重放的时候数据状态是不正确的,而且执行命令前做写操作,也会影响命令的执行速度,因此 AOF 是在命令执行后才会被记录。


然后就是,当 AOF 被记录的时候,并不是实时写入磁盘当中的,因为这样效率比较低,所以往往是将命令先写入缓冲区中,然后再找合适的时机来刷入磁盘,与 MySQL 中先记录操作在内存中,再统一刷写有异曲同工之妙。那么,什么时候将 AOF 的缓存区写入磁盘中,就决定了发生故障时,AOF 能回放的操作到什么时间点。


Redis 为 AOF 提供了三个级别的写入时机(appendfsync):

  • Always,同步写回:每个写命令执行完,立马同步地将日志写回磁盘;

  • Everysec,每秒写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;

  • No,操作系统控制的写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。


这三种写回策略都无法做到两全其美。我们来分析下其中的原因。

  • “同步写回”可以做到基本不丢数据,但是它在每一个写命令后都有一个慢速的落盘操作,不可避免地会影响主线程性能;

  • 虽然“操作系统控制的写回”在写完缓冲区后,就可以继续执行后续的命令,但是落盘的时机已经不在 Redis 手中了,只要 AOF 记录没有写回磁盘,一旦宕机对应的数据就丢失了;

  • “每秒写回”采用一秒写回一次的频率,避免了“同步写回”的性能开销,虽然减少了对系统性能的影响,但是如果发生宕机,上一秒内未落盘的命令操作仍然会丢失。所以,这只能算是,在避免影响主线程性能和避免数据丢失两者间取了个折中。

重写机制

当 Redis 的运行时间变得足够长,AOF 的文件就会变得极其庞大,这样不利于后续的追加和故障恢复,因此,Redis 设计了一个 AOF 重写机制来缩减 AOF 的文件大小。


具体的做法是:通过 fork 产生一个后台线程 bgrewriteaof,这个后台线程与主线程拥有同一张内存页表,因此也能看到内存中的所有数据,然后这个线程会将内存中的所有值以 set 的方式写入到新的 AOF 的文件中,比如有个键值对 "hello":"world",就会在 AOF 日志中记录成 set hello world。这样做,可以直接恢复最终值,压缩了之前的多次变动操作。


那么当 bgrewriteaof 进行 AOF 重写时,如果这时候数据有变化,会造成阻塞吗?答案是基本不会,当主线程收到新写或修改的操作时,主线程会申请新的内存空间,用来保存新写或修改的数据,这个操作被称为写时复制(Copy on Write)。这样逐渐地,父子进程内存数据开始分离,父子进程逐渐拥有各自独立的内存空间。而 Redis 也会将值的变动同时写入新旧两个 AOF 的内存缓冲区。这样当 AOF 重写完成之后,也会记录下当前的变动了。需要注意的是,如果操作的是 bigkey,也就是数据量大的集合类型数据,那么,主线程会因为申请大空间而面临阻塞风险。因为操作系统在分配内存空间时,有查找和锁的开销,这就会导致阻塞。

重写过程中会产生阻塞的点

Redis 采用 fork 子进程重写 AOF 文件时,潜在的阻塞风险包括:fork 子进程 和 AOF 重写过程中父进程产生写入的场景,下面依次介绍。


a、fork 子进程,fork 这个瞬间一定是会阻塞主线程的,fork 采用操作系统提供的写实复制(Copy On Write)机制,就是为了避免一次性拷贝大量内存数据给子进程造成的长时间阻塞问题,但 fork 子进程需要拷贝进程必要的数据结构,其中有一项就是拷贝内存页表(虚拟内存和物理内存的映射索引表),这个拷贝过程会消耗大量 CPU 资源,拷贝完成之前整个进程是会阻塞的,阻塞时间取决于整个实例的内存大小,实例越大,内存页表越大,fork 阻塞时间越久。拷贝内存页表完成后,子进程与父进程指向相同的内存地址空间,也就是说此时虽然产生了子进程,但是并没有申请与父进程相同的内存大小。那什么时候父子进程才会真正内存分离呢?“写实复制”顾名思义,就是在写发生时,才真正拷贝内存真正的数据,这个过程中,父进程也可能会产生阻塞的风险,就是下面介绍的场景。


b、fork 出的子进程指向与父进程相同的内存地址空间,此时子进程就可以执行 AOF 重写,把内存中的所有数据写入到 AOF 文件中。但是此时父进程依旧是会有流量写入的,如果父进程操作的是一个已经存在的 key,那么这个时候父进程就会真正拷贝这个 key 对应的内存数据,申请新的内存空间,这样逐渐地,父子进程内存数据开始分离,父子进程逐渐拥有各自独立的内存空间。因为内存分配是以页为单位进行分配的,默认 4k,如果父进程此时操作的是一个 bigkey,重新申请大块内存耗时会变长,可能会产阻塞风险。另外,如果操作系统开启了内存大页机制(Huge Page,页面大小 2M),那么父进程申请内存时阻塞的概率将会大大提高,所以在 Redis 机器上需要关闭 Huge Page 机制。Redis 每次 fork 生成 RDB 或 AOF 重写完成后,都可以在 Redis log 中看到父进程重新申请了多大的内存空间。


用户头像

insight

关注

不要混淆行动与进展、忙碌与多产。 2018.11.17 加入

永远都是初学者

评论

发布
暂无评论
Redis - AOF