Redis 7.0 新特性 :共享复制缓存区与MPAOF

00 前情提要

    截止至2022年3月22号,稳定的Redis版本依然是Redis 6.x(截止发稿日当天,最稳定版本是Redis 6.2.6)。

    提及Redis 6.0, 相比于Redis 5.x以及之前版本,Redis 6.0最大的改进就是在网络IO上使用了多线程进行加速,但是Redis 6.0在执行数据的逻辑处理上面还是单线程操作的(关于Redis 6.0的多线程优化后面找个时间写写)。另外Redis 的多线程其实是Redis 4.0开始的,增加了一些异步删除相关的操作(主要的优化场景:大key的删除),这一点面试时候需要注意一下。

    技术的更新总是日新月异的,在很多人还没有对Redis 6.0甚至Redis 5.0、Redis 4.0有足够的了解的情况下,Redis 7.0又马上要出来了。

    往者不可谏,来者犹可追,我们先看看Redis 7.0的新特性吧。

    注意点:

    1.文章中标红的字段为编者自己的解释,黑色字段为官网文档翻译;

    2.文章主要介绍新版的新特性以及新特性导致的性能优化等,新命令会简单介绍,其他一些内容不介绍;

    3.如果有内容错误,欢迎拍砖。

(重新群发一遍)

         

    

01 Redis 7.0 新特性

    以下内容均来源于Redis官网。

  • Redis 7.0 RC1 2022年1月31号 (注:第一个Redis 7.0的RC版本)

 关于Redis 7.0的介绍
=====================================
Redis 7.0包含了众多用户侧的新特征和重要的性能优化,以及其他一些优化,当然也会导致一些功能回退回旧版本。(注:感觉有些话就是废话……)。
官方建议用户在使用Redis 7.0之前仔细阅读相关文档。
Redis 7.0主要特性:
 
--------------------------------------
* Redis 函数:新增了服务端脚本,拓展Redis;
* 访问控制技术:基于key的细粒度的权限管理,允许用户支持多个选择器的命令规则集合;
* 集群:支持基于分片(指定节点)的发布和订阅;
* 对子命令的一级处理(影响ACL类别和INFO命令的统计等);
* 新增Redis命令和文档(注:原文是“Command metadata and documentation”,并提供了参考链接:

https://redis.io/topics/command-tips

https://redis.io/topics/command-tips

https://redis.io/commands/command-docs

https://redis.io/commands/command-docs

目前看应该只是修改了文档

 
 
* key规范:优化客户端定位key,以及对key的读写操作(注:在Redis 7.0之前疑似是没有key规范的);
* AOF文件多文件化,并避免了重写开销(注:更早期的AOF文件有指令压缩,但都是基于单文件的,这个新特性的特定应该是说在AOF文件多文件化的情况下,也能避免命令的重复读写)
* 集群:支持host names,之前版本建立集群只支持ip地址(注:对k8s的支持似乎更好了?毕竟k8s环境下ip是动态的)
* 改进了Redis的网络缓存机制下的内存管理;当内存开销超过某个阈值,支持拒绝客户端请求(注:可能是Redis 7.0的最重要特性,后续可以深入挖掘);
* 集群:提供新机制,支持对集群总线的断连,防止不可控的缓存(buffer)增长;
* AOF:时间戳声明,支持某个时间点的恢复操作;
* Lua:EVAL脚本支持Functions 标志;
* Lua:支持对大数字类型数据和Verbatim(注:这个没用过,有谁了解的吗) 的REP3类型回复;
* Lua:可以通过 redis.REDIS_VERSION, redis.REDIS_VERSION_NUM 来获取redis版本;
 
新的命令和命令参数:
========================================
注:命令用法请自行查找官方文档
* ZMPOP, BZMPOP 命令
* LMPOP, BLMPOP 命令3)
* SINTERCARD, ZINTERCARD 命令
* SPUBLISH, SSUBSCRIBE, SUNSUBSCRIBE, PUBSUB SHARDCHANNELS/SHARDNUMSUB 
* EXPIRETIME and PEXPIRETIME 命令
* EXPIRE 命令组支持参数 NX/XX/GT/LT
* SET 命令支持NX和GET的整合
* BITPOS, BITCOUNT支持BIT索引
* EVAL_RO, EVALSHA_RO 命令变化,可以在只读(read only)副本运行;
* SORT_RO 命令,在只读(read only)副本运行
* SHUTDOWN 命令新增参数:NOW, FORCE, ABORT
* FUNCTION *, FCALL, FCALL_RO 
* CONFIG SET/GET命令:可以在一次请求中,原子性的处理多个配置
* QUIT 命令升级为可用命令, 
* XADD命令支持自动序列号(通过:<ms>-* 操作)
 
新的管理员和内省命令和命令参数:
========================================
* COMMAND 命令和COMMAND INFO命令 扩展;
* ACL CAT, COMMAND LIST 的子命令;
* MODULE LIST :增加新的返回信息;
* OBJECT ENCODING 增加新的返回信息;
* CLUSTER SLOTS:支持hostname;
* COMMAND命令:添加 `blocking` 和 `module` 标志
 
性能和资源利用优化:=================================================
* 在Redis集群模式下,显著优化了内存消耗和延迟;
* 在众多hash或zset数据类型的key场景下,显著优化了内存消耗;
* 新增了副本集回退日志,各个副本在复制时候只使用一份副本缓存
* 显著优化了写时复制(copy-on-write)的内存消耗;
* 集群发送缓存(buffer)中的未使用空间会被释放;
* 提高了内存利用率,充分利用客户端的结构缓存来优化恢复缓存开销;
* 使用listpack替换了Hash,List和Zset中的ziplist(压缩列表);
* 支持list类型的数据结构存储大于4GB的内存(注:SDS泪流满面,只支持最大512MB,只能在夜里偷偷地哭泣);
* 在clients(客户端)阻塞时候,重复利用临时client节点;
* 移除了命令行参数的数量限制,使用了动态增长的参数缓存;
* 优化了list类型数据的操作:优化list在“从最近结束点开始查找”场景下的查找效率;
* 优化了fsync,避免了对磁盘的大量写(large writes)操作;
* BITSET 和 BITFIELD SET 两种数据类型,o只会在数据改变时候进行传播(注:猜测是说集群同步时候的优化?);
* 在“模块计数器解锁客户端(when a client is unblocked by module timer)”的场景下。
 
* Improve latency when a client is unblocked by module timer
 
  1. Redis 7 将AOF文件存储为一个文件夹下的多个文件,具体查看后文;

  2. Redis 7使用了新版本的RDB文件(版本号:10)来存储RDB文件,这和旧版本不兼容;
  3. 在载入一个旧版本(Redis 6.x 或者更老的版本)时候,Redis 7将ziplist的编码后的key转为listpackss。这个转化过程需要从磁盘加载文件或是从Redis的master节点加载副本,会轻微增加加载时间;
  4. 其他的新特性参考后文的“重要新特性”。
    新特性(New Features):
--------------------------------------

* 增加了Redis Stream 消费组的滞后跟踪信息和滞后报告;

* 为某些函数增加了API,以及显示评估ACL(访问控制技术)的LUA脚本;

新增加的用户命令或用户命令参数:
--------------------------------------
命令:GETKEYSANDFLAGS sub-command 
* INFO:命令可以添加多个参数 (#6891);
* XGROUP CREATE and SETID: 添加了新参数 ENTRIESREAD  ;
* XSETID:添加了新的可选参数 ENTRIESADDED和MAXDELETEDID
扩展了以下命令的返回信息:
---------------------------------------
* XINFO:增加消费组的延迟信息和其他一些字段;
* XAUTOCLAIM:增加了一个新元素,返回一个列表,列表信息为被删除的id信息;
性能和资源利用优化:
=================================================
  • Redis 7.0 RC1 2022年2月28号(注:这里怀疑写错了,应该是RC2)

    新特性(New Features):

    * 减少了返回客户端信息时候的系统请求次数和回复packet数;

    * 对稳定的客户端,减少了内存消耗

    * 回归修复了Redis 6.2中新增加的Z[REV]RANGE 命令。

02 共享复制缓存区的设计与实现

        参考了下文链接:

稀土掘金

https://www.talkwithtrend.com/Article/260171

1 Redis 主从复制原理简介

尽管本文的目的不是讲解 Redis 主从复制的原理,但在开始进入主题之前,我们先简单回顾一下 Redis 主从复制的基本原理。Redis 的主从复制主要分为两种情况:

  • 全量同步

当主库收到从库的同步请求时,如果从库的复制历史与主库不一致,或者未能在复制积压区中找到从库请求的同步点时,则会与从库进行全量同步。主库通过 fork 子进程产生内存快照,然后将数据序列化为 RDB 格式同步到从库,使从库的数据与主库某一时刻的数据一致。

  • 命令传播

当从库与主库完成全量同步后,进入命令传播阶段,主库将变更数据的命令发送到从库,从库将执行相应命令,使从库与主库数据持续保持一致。

2 Redis 复制缓存区相关问题分析

2.1 多从库时主库内存占用过多

图1 Redis 主从复制

        如上图所示,对于 Redis 主库,当用户的写请求到达时,主库会将变更命令分别写入所有从库的缓存区(OutputBuffer),以及复制积压区 (ReplicationBacklog)。需要指出的是,全量同步时依然会执行该逻辑,所以在全量同步阶段经常会触发 client-output-buffer-limit,主库断开与从库的连接,导致主从同步失败,甚至出现循环持续失败的情况。

        该实现一个明显的问题是内存占用过多,所有从库的连接在主库上是独立的,也就是说每个从库 OutputBuffer 占用的内存空间也是独立的(注:这么神奇的嘛……一直以为都是共享内存方式的,那么主从复制消耗的内存就是所有从库缓冲区内存大小之和。如果我们设定从库的 client-output-buffer-limit 为 1GB,如果有三个从库,则在主库上可能会消耗 3GB 的内存用于主从复制。另外,真实环境中从库的数量不是确定的,这也导致 Redis 实例的内存消耗不可控。

2.2 OutputBuffer 拷贝和释放的堵塞问题

        Redis 为了提升多从库全量复制的效率和减少 fork 产生 RDB 的次数,会尽可能的让多个从库共用一个 RDB,如下代码所示,当已经有一个从库触发 RDB BGSAVE 时,后续需要全量同步的从库会共享这次 BGSAVE 的 RDB,为了从库复制数据的完整性,会将之前从库的 OutputBuffer 拷贝到请求全量同步从库的 OutputBuffer 中。

        copyClientOutputBuffer 看似只是一个简单的 buffer 拷贝,但可能存在堵塞问题,因为 OutputBuffer 链表上的数据可达数百 MB 甚至数 GB 之多,对其拷贝可能使用百毫秒甚至秒级的时间,而且该堵塞问题没法通过日志或者 latency 观察到,但影响却很大。

        同样地,当 OutputBuffer 大小触发 limit 限制时,Redis 就是关闭该从库链接,而在释放 OutputBuffer 时,也需要释放数百 MB 甚至数 GB 的数据,其耗时对 Redis 而言也很长。

2.3 ReplicationBacklog 的限制

        我们知道复制积压缓冲区 ReplicationBacklog 是 Redis 实现部分重同步的基础,如果从库可以进行增量同步,则主库会从 ReplicationBacklog 中拷贝从库缺失的数据到其 Out

putBuffer。拷贝的数据最多是 ReplicationBacklog 的大小,为了避免拷贝数据过多的问题,我们通常不会让该值过大,一般百兆左右。但在大容量实例中,为了避免由于主从网络中断导致的全量同步,我们又希望该值大一些,这就存在矛盾了。

        此外,我们在重新设置 ReplicationBacklog 大小时,会导致 ReplicationBacklog 中的内容全部清空,所以如果在变更该配置期间发生主从断链重连,则很有可能导致全量同步。

        

3 共享复制缓存区的设计与实现

3.1 方案简述

每个从库在主库上单独拥有自己的 OutputBuffer,但其存储的内容却是一样的,一个最直观的想法就是主库在命令传播时,将这些命令放在一个全局的复制数据缓冲区中,多个从库共享这份数据,不同的从库对引用复制数据缓冲区中不同的内容,这就是『共享复制缓存区』方案的核心思想。实际上,复制积压缓冲区(ReplicationBacklog)中的内容与从库 OutputBuffer 中的数据也是一样的,所以该方案中,ReplicationBacklog 和从库一样共享一份复制缓冲区的数据,也避免了 ReplicationBacklog 的内存开销。

借鉴了 Redis 客户端输出缓冲区的实现方案,『共享复制缓存区』方案中复制缓冲区 (ReplicationBuffer) 的表示也采用链表的表示方法,将 ReplicationBuffer 数据切割为多个 16KB 的数据块 (replBufBlock),然后使用链表来维护起来。为了维护不同从库的对 ReplicationBuffer 的使用信息,在 replBufBlock 中增加了如下字段:

  • refcount:block 的引用计数

  • id:block 的唯一标识,单调递增的数值

  • repl_offset:block 开始的复制偏移

图2 ReplicationBacklog 和 从库对 ReplicationBuffer 的引用描述

        如上图所示,ReplicationBuffer 是由多个 replBufBlock 组成的链表,当 ReplicatonBacklog 或从库对某个 block 使用时,便对正在使用的 replBufBlock 增加引用计数,可以看到,ReplicatonBacklog 正在使用的 replBufBlock refcount 是 1,从库 A 和 B 正在使用的 replBufBlock refcount 是 2。当从库使用完当前的 replBufBlock(已经将数据发送给从库)时,就会对其 refcount 减 1 而且移动到下一个 replBufBlock,并对其 refcount 加 1。(注:设计思路似乎有点类似Kafka的Offset?)

        2.1 提到的多从库消耗内存过多的问题通过共享复制缓存区方案得到了解决,对于 2.2 和 2.3 提出的问题是否解决了呢?

        首先来看 2.2 中的问题, OutputBuffer 拷贝问题,当前从库的 OutputBuffer 的描述只有对共享 ReplicationBuffer 的引用信息,如上代码所示,所以之前的数据深拷贝变成了更新引用信息,即对正在使用的 replBufBlock refcount 加 1,这仅仅是一条简单的赋值操作,非常轻量。OutputBuffer 释放问题呢?在当前的方案中释放从库 OutputBuffer 就变成了对其正在使用的 replBufBlock refcount 减 1,也是一条赋值操作,不会有任何阻塞。

        对于 2.3 提出的问题也很容易解决了,因为 ReplicatonBacklog 也只是记录了对 ReplicationBuffer 的引用信息,如上代码所示,对 ReplicatonBacklog 的拷贝也仅仅成了找到正确的 replBufBlock,然后对其 refcount 加 1。无需担心 ReplicatonBacklog 过大导致的拷贝堵塞问题。而且对 ReplicatonBacklog 大小的变更也仅仅是配置的变更,不会清掉数据。

        ReplicationBuffer 的释放也需要注意,因为如果一个从库引用的 replBufBlock 过多,它断开时释放的 replBufBlock 可能很多,这会造成堵塞问题,所以我们会限制一次释放的个数,未及时释放的内存在 ServerCron 中渐进式释放。

  • ReplicatonBacklog 和从库只对正在使用的 replBufBlock 增减 refcount 的原因

        在对 replBufBlock 增减 refcount 中,一个直观的思路是,对所有要使用的 replBufBlock 都增减引用计数,同样释放 replBufBlock 时,只需要检查 refcount 是否为 0。但这种思路的最大问题是从库在断开链接时需要对其所有增加过引用计数的 replBufBlock 都减 1,这又会出现了 2.2 提出的释放 OutputBuffer 造成堵塞的问题。从库只对正在使用的 replBufBlock 增减 refcount,直觉上这套机制看似有些脆弱,那么如何保证不会错误释放 replBufBlock 呢,尤其是那些不是正在使用但之后会使用,即 refcount == 0 的 replBufBlock?

        首先 replBufBlock 通过链表维护,天然具有有序性,也保证了 ReplicationBuffer 的正确性,其次,与我们上述提到的释放策略有关,我们只释放链表第一个 refcount 为 0 的 replBufBlock,若不为 0 则终止对 replBufBlock 链表的遍历从而避免对中间 refcount 为 0 的 replBufBlock 的释放。

  • ReplicatonBacklog 使用 ReplicationBuffer 的问题和解决方案

        当从库尝试与主库进行增量重同步时,会发送自己的 repl_offset, 若从库可满足增量重同步条件,则主库会从 ReplicationBacklog 中拷贝从库缺失的数据到从库 OutputBuffer,虽然现在的拷贝变成了仅仅是对特定 replBufBlock 引用计数的改变,在每个 replBufBlock 中记录了该其第一个字节对应的 repl_offset,但如何高效地从数万个 replBufBlock 的链表中找到特定的那个, 仍然一件棘手的事情。


图3 查找包含特定 repl_offset 的 replBufBlock

        直接从头到位遍历链表查找对应的 replBufBlock 必然会耗费较多时间而堵塞服务。起初,我额外使用了一个链表用于索引固定区间间隔的 replBufBlock,每 1000 个 replBufBlock 记录一个索引信息,当查找 repl_offset 时,会先从索引链表中查起,然后再查找 replBufBlock 链表,类似于跳表的查找实现,如上图所示。但这个方案在极端场景下可能会查找超过千次,有 10 毫秒以上的延迟,与 Redis 的维护者 Oran Agra 讨论后,我们对此仍不满足,遂放弃。

        最终使用 rax 树(注:面试可能会考)实现了对 replBufBlock 固定区间间隔的索引,每 64 个记录一个索引点。一方面,rax 索引占用的内存较少;另一方面,查询效率也是非常高,理论上查找比较次数不会超过 100,耗时在 1 毫秒以内。

3.3 其他相关问题

  • mem_total_replication_buffers

        在 Redis 的 INFO 命令输出中增加了 mem_total_replication_buffers 字段,用以展示全局复制缓存区 ReplicationBuffer 的大小。因共享复制缓冲区,只有所有从库引用的复制数据超过 ReplicationBacklog 时,我们才认为超过的内存是从库消耗的内存,即 mem_clients_slaves 字段的数值,否则 mem_clients_slaves 为 0。

  • Key 驱逐

        在 Redis 进行 key 驱逐时, 从库的 OutputBuffer 内存是不算入 Redis 的内存消耗,因共享复制缓冲区机制,我们认为只有超出 ReplicationBacklog 大小的部分是从库额外的内存消耗,也就是说 ReplicationBacklog 消耗的内存仍然会导致 key 的驱逐。

  • IO 多线程

        由于所有从库和 ReplicationBacklog 使用全局复制缓冲区,如果启用 IO 多线程,为了保证数据访问线程安全,我们必须让主线程处理发送数据到所有从库的工作。但在此之前,其他 IO 线程可以处理从库的发送 OutputBuffer 工作,无差别对待普通客户端和从库客户端。

4 总结

        本文主要分析了 Redis 主从复制缓存区存在的几个问题:内存占用过多,OutputBuffer 拷贝和释放造成的不易察觉的堵塞问题,以及 ReplicaitonBacklog 的不足,然后提出了共享复制缓存区的方案,并分析了该方案的几个关键问题。希望该方案能够减少大家在运维 Redis 过程中遇到的主从复制问题,降低内存消耗,避免线上稳定性问题,Cheers!

事实上,本文主要是对 PR: Replication backlog and replicas use one global shared replication buffer  动机和实现的中文描述。也感谢 sundb 和 zhaozhao.zz 对这个 PR 的 BugFix 和 索引优化 。如有问题,欢迎大家指正!


 

3.2 关键问题

  • ReplicationBuffer 的裁剪和释放

        ReplicationBuffer 不可能无限增长,Redis 会有相应的逻辑对其进行裁剪,简单来说,Redis 会从头访问 replBufBlock 链表,如果发现 replBufBlock refcount 为 0,则会释放它,直到迭代到第一个 replBufBlock refcount 不为 0 才停止,对应实现函数实现为 incrementalTrimReplicationBacklog。所以想要释放 ReplicationBuffer,只需要减少相应 ReplBufBlock 的 refcount,有以下主要会减少 refcount 的情况:

  • 当从库使用完当前的 replBufBlock 会对其 refcount 减 1

  • 当从库断开链接时会对正在引用的 replBufBlock refcount 减 1,无论是因为超过 client-output-buffer-limit 导致的断开还是网络原因导致的断开

  • 当 ReplicationBacklog 引用的 replBufBlock 数据量超过设置的该值大小时,会对正在引用的 replBufBlock refcount 减 1,以尝试释放内存


 

                        03 Multi Part AOF的设计和实现

        参考了下文链接:

        (黑色为原文,红字为自己见解)

51CTO

https://www.51cto.com/article/701106.html

疑似阿里云原文

https://developer.aliyun.com/article/869214

        Redis 作为一种非常流行的内存数据库,通过将数据保存在内存中,Redis 得以拥有极高的读写性能。但是一旦进程退出,Redis 的数据就会全部丢失。

        为了解决这个问题,Redis 提供了 RDB 和 AOF 两种持久化方案,将内存中的数据保存到磁盘中,避免数据丢失。本文将重点讨论AOF持久化方案,以及其存在的一些问题,并探讨在Redis 7.0 (已发布RC1) 中Multi Part AOF(下文简称为MP-AOF,本特性由阿里云数据库Tair团队贡献)(注:那可是阿里啊,我一个朋友在去年春招被不同部门面试了14轮!简直就是轮X!)设计和实现细节。

AOF

        AOF( append only file )持久化以独立日志文件的方式记录每条写命令,并在 Redis 启动时回放 AOF 文件中的命令以达到恢复数据的目的。

        由于AOF会以追加的方式记录每一条redis的写命令,因此随着Redis处理的写命令增多,AOF文件也会变得越来越大,命令回放的时间也会增多,为了解决这个问题,Redis引入了AOF rewrite机制(下文称之为AOFRW)。AOFRW会移除AOF中冗余的写命令,以等效的方式重写、生成一个新的AOF文件,来达到减少AOF文件大小的目的(注:即AOF的压缩)。

AOFRW(AFO rewrite)

        图1展示的是AOFRW的实现原理。当AOFRW被触发执行时,Redis首先会fork一个子进程进行后台重写操作,该操作会将执行fork那一刻Redis的数据快照全部重写到一个名为temp-rewriteaof-bg-pid.aof的临时AOF文件中。

        由于重写操作为子进程后台执行,主进程在AOF重写期间依然可以正常响应用户命令。因此,为了让子进程最终也能获取重写期间主进程产生的增量变化,主进程除了会将执行的写命令写入aof_buf,还会写一份到aof_rewrite_buf中进行缓存。在子进程重写的后期阶段,主进程会将aof_rewrite_buf中累积的数据使用pipe发送给子进程,子进程会将这些数据追加到临时AOF文件中。

        当主进程承接了较大的写入流量时,aof_rewrite_buf中可能会堆积非常多的数据,导致在重写期间子进程无法将aof_rewrite_buf中的数据全部消费完。此时,aof_rewrite_buf剩余的数据将在重写结束时由主进程进行处理。

        当子进程完成重写操作并退出后,主进程会在backgroundRewriteDoneHandler 中处理后续的事情。首先,将重写期间aof_rewrite_buf中未消费完的数据追加到临时AOF文件中。其次,当一切准备就绪时,Redis会使用rename 操作将临时AOF文件原子的重命名为server.aof_filename,此时原来的AOF文件会被覆盖。至此,整个AOFRW流程结束。

    补充:参考阿里云链接https://developer.aliyun.com/article/547677,这张图里面的子进程只负责写一个临时文件保存现有的k-v的最新数据,并没有做数据持久化操作。真正持久化操作是在写入AOF_buf后,后续开启的。

图1 AOFRW实现原理

 MP-AOFRW实现(只复制一下方案概述

        顾名思义,MP-AOF就是将原来的单个AOF文件拆分成多个AOF文件。在MP-AOF中,我们将AOF分为三种类型,分别为:

        BASE:表示基础AOF,它一般由子进程通过重写产生,该文件最多只有一个。

        INCR:表示增量AOF,它一般会在AOFRW开始执行时被创建,该文件可能存在多个。

        HISTORY:表示历史AOF,它由BASE和INCR AOF变化而来,每次AOFRW成功完成时,本次AOFRW之前对应的BASE和INCR AOF都将变为HISTORY,HISTORY类型的AOF会被Redis自动删除。

        为了管理这些AOF文件,我们引入了一个manifest(清单)文件来跟踪、管理这些AOF。同时,为了便于AOF备份和拷贝,我们将所有的AOF文件和manifest文件放入一个单独的文件目录中,目录名由appenddirname配置(Redis 7.0新增配置项)决定。

  图2 MP-AOF rewrite原理


            图2展示的是在MP-AOF中执行一次AOFRW的大致流程。在开始时我们依然会fork一个子进程进行重写操作,在主进程中,我们会同时打开一个新的INCR类型的AOF文件,在子进程重写操作期间,所有的数据变化都会被写入到这个新打开的INCR AOF中。子进程的重写操作完全是独立的,重写期间不会与主进程进行任何的数据和控制交互,最终重写操作会产生一个BASE AOF。新生成的BASE AOF和新打开的INCR AOF就代表了当前时刻Redis的全部数据。AOFRW结束时,主进程会负责更新manifest文件,将新生成的BASE AOF和INCR AOF信息加入进去,并将之前的BASE AOF和INCR AOF标记为HISTORY(这些HISTORY AOF会被Redis异步删除)。一旦manifest文件更新完毕,就标志整个AOFRW流程结束。

        由图2可以看到,我们在AOFRW期间不再需要aof_rewrite_buf,因此去掉了对应的内存消耗。同时,主进程和子进程之间也不再有数据传输和控制交互,因此对应的CPU开销也全部去掉。对应的,前文提及的六个pipe及其对应的代码也全部删除,使得AOFRW逻辑更加简单清晰。

        注:大致理解,就是用多个持久化到磁盘的文件替代原有的aof_rewrite_buf。每次fork子进程之后,子进程先写BASE_AOF(也就是老版本的临时文件)。在子进程写BASE_AOF文件期间,新的写命令先写到aof_buf再写入到INCR_AOF文件中。之后BASE_AOF和INCR_AOF会成为新的HISTORY文件,并在下次AOFRW时候迭代替换。

总结

        MP-AOF的引入,成功的解决了之前AOFRW存在的内存和CPU开销对Redis实例甚至业务访问带来的不利影响。同时,在解决这些问题的过程中,我们也遇到了很多未曾预料的挑战,这些挑战主要来自于Redis庞大的使用群体、多样化的使用场景,因此我们必须考虑用户在各种场景下使用MP-AOF可能遇到的问题。如兼容性、易用性以及对Redis代码尽可能的减少侵入性等。这都是Redis社区功能演进的重中之重。

        同时,MP-AOF的引入也为Redis的数据持久化带来了更多的想象空间。如在开启aof-use-rdb-preamble时,BASE AOF本质是一个RDB文件,因此我们在进行全量备份的时候无需再单独执行一次BGSAVE操作。直接备份BASE AOF即可。MP-AOF支持关闭自动清理HISTORY AOF的能力,因此那些历史的AOF有机会得以保留,并且目前Redis已经支持在AOF中加入timestamp annotation,因此基于这些我们甚至可以实现一个简单的PITR能力( point-in-time recovery)(注:的确是对混合持久化的一种升级)。

        MP-AOF的设计原型来自于Tair for redis企业版的binlog实现,这是一套在阿里云Tair服务上久经验证的核心功能,在这个核心功能上阿里云Tair成功构建了全球多活、PITR等企业级能力,使用户的更多业务场景需求得到满足。今天我们将这个核心能力贡献给Redis社区,希望社区用户也能享受这些企业级特性,并通过这些企业级特性更好的优化,创造自己的业务代码。有关MP-AOF的更多细节,请移步参考相关PR(#9788)(链接:https://github.com/redis/redis/pull/9788#issuecomment-1004133640),那里有更多的原始设计和完整代码。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值