【云原生进阶之PaaS中间件】第一章Redis-2.1架构综述

2023-10-16 12:37:47 浏览数 (2)

1 Redis组件模型

        Redis 组件的系统架构如图所示,主要包括事件处理、数据存储及管理、用于系统扩展的主从复制/集群管理,以及为插件化功能扩展的 Module System 模块。

        Redis的客户端与服务端的交互过程如下所示:

1.1 事件处理机制

        Redis 中的事件处理模块,采用的是作者自己开发的 ae 事件驱动模型,可以进行高效的网络 IO 读写、命令执行,以及时间事件处理。

        其中,网络 IO 读写处理采用的是 IO 多路复用技术,通过对 evport、epoll、kqueue、select 等进行封装,同时监听多个 socket,并根据 socket 目前执行的任务,来为 socket 关联不同的事件处理器。

        当监听端口对应的 socket 收到连接请求后,就会创建一个 client 结构,通过 client 结构来对连接状态进行管理。在请求进入时,将请求命令读取缓冲并进行解析,并存入到 client 的参数列表。

        然后根据请求命令找到 对应的redisCommand ,最后根据命令协议,对请求参数进一步的解析、校验并执行。Redis 中时间事件比较简单,目前主要是执行 serverCron,来做一些统计更新、过期 key 清理、AOF 及 RDB 持久化等辅助操作。

1.1.1 redis单线程模型

        Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型 (Netty 的线程模型也基于 Reactor 模式,Reactor 模式不愧是高性能 IO 的基石),这套事件处理模型对应的是 Redis 中的文件事件处理器(file event handler)。由于文件事件处理器(file event handler)是单线程方式运行的,所以我们一般都说 Redis 是单线程模型。

        Redis 基于 Reactor 模式开发了自己的网络事件处理器:这个处理器被称为文件事件处理器(file event handler)。

        文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。

        当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关 闭(close)等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。

        虽然文件事件处理器以单线程方式运行,但通过使用 I/O 多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通信模型,又可以很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接,这保持了 Redis 内部单线程设计的简单性。

1.1.2 既然是单线程,那怎么监听大量的客户端连接呢?

        Redis 通过 IO 多路复用程序来监听来自客户端的大量连接(或者说是监听多个 socket),它会将感兴趣的事件及类型(读、写)注册到内核中并监听每个事件是否发生。

        这样的好处非常明显: I/O 多路复用技术的使用让 Redis 不需要额外创建多余的线程来监听客户端的大量连接,降低了资源的消耗(和 NIO 中的 Selector 组件很像)。

         文件事件处理器(file event handler)主要是包含 4 个部分:

  1. 多个 socket(客户端连接)
  2. IO 多路复用程序(支持多个客户端连接的关键)
  3. 文件事件分派器(将 socket 关联到相应的事件处理器)
  4. 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)

事件种类:

  • AE_READABLE:与两个事件处理器结合使用。
    • 当客户端连接服务器端时,服务器端会将连接应答处理器与socket的AE_READABLE事件关联起来;
    • 当客户端向服务端发送命令的时候,服务器端将命令请求处理器与AE_READABLE事件关联起来;
  • AE_WRITABLE:当服务端有数据需要回传给客户端时,服务端将命令回复处理器与socket的AE_WRITABLE事件关联起来。
1.1.3 Redis6.0 之后为何引入了多线程?

        Redis6.0 引入多线程主要是为了提高网络 IO 读写性能,因为这个算是 Redis 中的一个性能瓶颈(Redis 的瓶颈主要受限于内存和网络)。

        虽然,Redis6.0 引入了多线程,但是 Redis 的多线程只是在网络数据的读写这类耗时操作上使用了,执行命令仍然是单线程顺序执行。因此,你也不需要担心线程安全问题。

        Redis6.0 的多线程默认是禁用的,只使用主线程。如需开启需要修改 redis 配置文件 redis.conf :

代码语言:javascript复制
io-threads-do-reads yes

        开启多线程后,还需要设置线程数,否则是不生效的。同样需要修改 redis 配置文件 redis.conf :

代码语言:javascript复制
io-threads 4 #官网建议4核的机器建议设置为2或3个线程,8核的建议设置为6个线程

1.2 数据管理

        Redis 的内存数据都存在 redisDB 中。Redis 支持多 DB,每个 DB 都对应一个 redisDB 结构。Redis 的 8 种数据类型,每种数据类型都采用一种或多种内部数据结构进行存储。同时这些内部数据结构及数据相关的辅助信息,都以 key/value 的格式存在 redisDB 中的各个dict字典中。

        数据在写入redisDB后,这些执行的写指令还会及时追加到 AOF 中,追加的方式是先实时写入AOF 缓冲,然后按策略刷缓冲数据到文件。由于 AOF 记录每个写操作,所以一个 key 的大量中间状态也会呈现在 AOF 中,导致 AOF 冗余信息过多,因此 Redis 还设计了一个 RDB 快照操作,可以通过定期将内存里所有的数据快照落地到 RDB 文件,来以最简洁的方式记录 Redis 的所有内存数据。

        Redis 进行数据读写的核心处理线程是单线程模型,为了保持整个系统的高性能,必须避免任何kennel 导致阻塞的操作。为此,Redis 增加了BIO线程,来处理容易导致阻塞的文件close、fsync等操作,确保系统处理的性能和稳定性。

        在 server 端,存储内存永远是昂贵且短缺的,Redis中,过期的key需要及时清理,不活跃的key在内存不足时也可能需要进行淘汰。为此,Redis 设计了 8 种淘汰策略,借助新引入的 eviction pool,进行高效的 key 淘汰和内存回收。

1.3 功能扩展

        Redis 在 4.0 版本之后引入了 Module System 模块,可以方便使用者在不修改核心功能的同时,进行插件化功能开发。使用者可以将新的 feature 封装成动态链接库,Redis 可以在启动时加载,也可以在运行过程中随时按需加载和启用。

        在扩展模块中,开发者可以通过 RedisModule_init 初始化新模块,用 RedisModule_CreateCommand 扩展各种新模块指令,以可插拔的方式为 Redis 引入新的数据结构和访问命令。

1.4 系统扩展

        Redis作者在架构设计中对系统的扩展也倾注了大量关注。在主从复制功能中,psyn 在不断的优化,不仅在 slave 闪断重连后可以进行增量复制,而且在 slave 通过主从切换成为 master 后,其他 slave 仍然可以与新晋升的 master 进行增量复制,另外,其他一些场景,如 slave 重启后,也可以进行增量复制,大大提升了主从复制的可用性。使用者可以更方便的使用主从复制,进行业务数据的读写分离,大幅提升 Redis 系统的稳定读写能力。

        通过主从复制可以较好的解决 Redis 的单机读写问题,但所有写操作都集中在 master 服务器,很容易达到 Redis 的写上限,同时 Redis 的主从节点都保存了业务的所有数据,随着业务发展,很容易出现内存不够用的问题。

        为此,Redis 分区无法避免。虽然业界大多采用在 client 和 proxy 端分区,但 Redis 自己也早早推出了 cluster 功能,并不断进行优化。Redis cluster 预先设定了 16384 个 slot 槽,在 Redis 集群启动时,通过手动或自动将这些 slot 分配到不同服务节点上。在进行 key 读写定位时,首先对 key 做 hash,并将 hash 值对 16383 ,做 按位与运算,确认 slot,然后确认服务节点,最后再对 对应的 Redis 节点,进行常规读写。如果 client 发送到错误的 Redis 分片,Redis 会发送重定向回复。如果业务数据大量增加,Redis 集群可以通过数据迁移,来进行在线扩容。

2 数据流

 3 redis数据结构

        数据结构总览:

3.1 全局hash表

         redis会采用dict来保存全局hash,会有2个dict,dict0和dict1。其中dict1用于扩容用。

        redis有扩容和缩容,缩容流程与扩容基本一致。除了什么时候扩会有差异:扩容是负载因子>=1或负载因子>=5;缩容是负载因子小于等于0.1。

redis扩容:

        什么时候扩容:当负载因子>=1并且此时没在做重写aof或进行快照,或者负载因子>=5则马上会做。

怎么扩:

        采用的是渐进式hash。这种思路可以借鉴。

  1. 对于增删改查都会将一个元素从dict0放到dict1&定时任务迁移,对于增加只会往dict1放;
  2. 迁移完成后将dict0置为dict1,同时清空dict1;

        整体redis的渐近式hash细节步骤可以用到数据库扩容上。

 3.2 简单动态字符串

应用场景:用于表示key,value是字符串的场景。

数据结构:长度,未使用长度,字符数组。

代码语言:javascript复制
struct sdshdr{
    //总字符长度
    int len ;
    //记录buf数组中未使用字节的数量
    int free;
    //字节数组,用来保存字符串
    char buf[];
}

3.3 数组

        redis整数数组比较简单,就是等长的数组。如果数组元素是8字节,而有10个元素,但每个元素实际存储内容为1字节。那此时空间整体会浪费70字节。数组有个优势是内存比较紧贴,能够利用好局部性原理,从而使用cpu缓存行加速访问速度。但如果数组是随机访问,则无法利用cpu缓存行了,会有一定的影响。

3.4 hash表

        hash表结构也比较简单。和java的hash是差不多的,都会有一个entry数组,每个entry会有key和value。查询&put&删除操作时都先将key hash到具体的数组元素位置,然后如果存在冲突再遍历链表进行查询。

        hash表当元素个数没有超过hash-max-entries及单个元素大小没有超过hash-max-value时会采用压缩链表。

3.5 压缩链表

        压缩链表就是为了避免不定长的元素造成的空间浪费。整体从原理上就是有一个链表总长度,尾地址偏移量,元素个数,以及链表结束位zlen。netty解法粘包问题时,也有这种元素长度的解决方案。从结构上看,要查找尾结点时是O(1)的查询耗时。如果要查找数组中第4个元素,假设总共10个元素。那么需要从头0开始找,O(5)查询次数。

        hash表当元素较小时会以zipList存储,可以用hash-max-ziplist-entries来控制,小于hash-max-ziplist-entries则以zipList存储。zipList当在元素前插入新的元素,会导致插入位置后面的n个元素内存的重分配。这个是因为每个元素保存了前一个元素的大小。所以当元素较多时,插入操作性能就会变得很差,而由于redis是单线程执行命令模型所以查询也会阻塞。

zipList数据结构:

 3.6 跳跃表

        利用多级索引加速元素查找。和红黑树比: 跳跃表简单。跳表的插入使用随机性决定是否需要在上n层加入该元素。

 4 redis IO模型

        redis网络IO及处理命令是单线程的,但后台会有相应的清理过期key线程,主从同步相关线程。

        详细6.0处理流程:

5 redis存储持久化

        Redis 支持持久化,而且支持两种不同的持久化操作,一种持久化方式叫快照(snapshotting,RDB),另一种方式是只追加文件(append-only file, AOF)。

5.1 RDB持久化

        Redis 可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis 创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器的时候使用。

        快照持久化是 Redis 默认采用的持久化方式,在 redis.conf 配置文件中默认有此下配置:

  • save 900 1 #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发bgsave命令创建快照。
  • save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发bgsave命令创建快照。
  • save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发bgsave命令创建快照。

        RDB 创建快照时会阻塞主线程吗?

        Redis 提供了两个命令来生成 RDB 快照文件:

  • save : 主线程执行,会阻塞主线程;
  • bgsave : 子线程执行,不会阻塞主线程,默认选项。

缺点:

  • rdb整个流程中也涉及fork子进程,所以也会有阻塞。
  • 并且相应的rdb的快照不能太频繁(会导致频繁fork子进程及写磁盘文件从而引起阻塞)。

5.2 AOF

        与快照持久化相比,AOF 持久化的实时性更好,因此已成为主流的持久化方案。默认情况下 Redis 没有开启 AOF(append only file)方式的持久化,可以通过 appendonly 参数开启:

代码语言:javascript复制
appendonly yes

        开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入到内存缓存 server.aof_buf 中,然后再根据 appendfsync 配置来决定何时将其同步到硬盘中的 AOF 文件。AOF 文件的保存位置和 RDB 文件的位置相同,都是通过 dir 参数设置的,默认的文件名是 appendonly.aof。

        在 Redis 的配置文件中存在三种不同的 AOF 持久化方式,它们分别是:

  1. appendfsync always #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
  2. appendfsync everysec #每秒钟同步一次,显式地将多个写命令同步到硬盘
  3. appendfsync no #让操作系统决定何时进行同步

        为了兼顾数据读取和写入性能,用户可以考虑 appendfsync everysec 选项 ,让 Redis 每秒同步一次 AOF 文件,Redis 性能几乎没受到任何影响。而且这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操作的时候,Redis 还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。

5.2.1 AOF日志实现

        关系型数据库(如 MySQL)通常都是执行命令之前记录日志(方便故障恢复),而 Redis AOF 持久化机制是在执行完命令之后再记录日志。

         为什么是在执行完命令之后记录日志呢?

  • 避免额外的检查开销,AOF 记录日志不会对命令进行语法检查;
  • 在命令执行完之后再记录,不会阻塞当前的命令执行。

        这样也带来了风险(我在前面介绍 AOF 持久化的时候也提到过):

  • 如果刚执行完命令 Redis 就宕机会导致对应的修改丢失;
  • 可能会阻塞后续其他命令的执行(AOF 记录日志是在 Redis 主线程中进行的)。
5.2.2 AOF 重写

        AOF过大会会由后台线程进行重写。AOF 重写可以产生一个新的 AOF 文件,这个新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样,但体积更小。

        AOF 重写是一个有歧义的名字,该功能是通过读取数据库中的键值对来实现的,程序无须对现有 AOF 文件进行任何读入、分析或者写入操作。

        在执行 BGREWRITEAOF 命令时,Redis 服务器会维护一个 AOF 重写缓冲区,该缓冲区会在子进程创建新 AOF 文件期间,记录服务器执行的所有写命令。当子进程完成创建新 AOF 文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新 AOF 文件的末尾,使得新的 AOF 文件保存的数据库状态与现有的数据库状态一致。最后,服务器用新的 AOF 文件替换旧的 AOF 文件,以此来完成 AOF 文件重写操作。

        重写的大致流程:

  1. fork子线程,此时redis实例的内存数据与子线程共享。这个过程会阻塞redis服务客户操作。采用的是写时复制,在fork的时候拷贝了主线程的页表到子线程。数据还是共享的。
  2. 启动重写线程;
  3. 期间新的命令双写:以前的aof日志,重写日志。此时主线程会为要操作的key申请内存空间,所以这里也是阻塞的。这里需要注意的是操作系统如果开启了Huge Page机制,那么这个阻塞的风险会更大。
  4. 重写线程完成重写任务后,替换原先的aof日志。这步会阻塞主线程。

        AOF刷盘3种机制:

5.3 Redis 4.0 对于持久化机制做了什么优化?

        Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 aof-use-rdb-preamble 开启)。

        如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。

6 Redis如何实现高可用

6.1 主从复制

        为了分担压力,Redis支持主从复制,Redis的主从结构可以采用一主多从或者级联结构,Redis主从同步策略的策略就是先是全量同步,再为增量同步。

         全量同步:Redis全量复制一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份。具体步骤如下:

  1. 从服务器连接主服务器;
  2. 主服务器接收到命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令;
  3. 主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令;
  4. 从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;
  5. 主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;
  6. 从服务器完成对快照的载入,开始接收命令请求,此后主服务器每执行一次写命令,就向从服务器发送相同的写命令(也就是增量同步)。

        增量同步:Redis增量复制是指Slave初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程。 增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。

        主从复制的作用:主从复制,读写分离,容灾恢复。一台主机负责写入数据,多台从机负责备份数据。在高并发的场景下,即便是主机挂了,可以用从机代替主机继续工作,避免单点故障导致系统性能问题。读写分离,让读多写少的应用性能更佳。

6.2 哨兵(Sentinel)

        使用 Redis 主从服务的时候,会有一个问题,就是当 Redis 的主从服务器出现故障宕机时,需要手动进行恢复,为了解决这个问题,Redis 增加了哨兵模式(因为哨兵模式做到了可以监控主从服务器,并且提供自动容灾恢复的功能)。 Sentinel(哨兵)可以监听集群中的服务器,并在主服务器进入下线状态时,自动从从服务器中选举出新的主服务器。

 6.3 集群 (Redis Cluster)

        使用哨兵模式在数据上有副本数据做保证,在可用性上又有哨兵监控,一旦master宕机会选举salve节点为master节点,这种已经满足了我们的生产环境需要,那为什么还需要使用集群模式呢?

        答:因为主服务器挂掉的时候,要进行主从切换,这瞬间存在访问瞬断的情况。虽然在主从模式下我们可以通过增加salve节点来扩展读并发能力,但是没办法扩展写能力和存储能力,所以为了扩展写能力和存储能力,我们就需要引入集群模式。

        redis集群是一个由多个主从节点群组成的分布式服务器群,它具有复制、高可用和分片特性。Redis集群不需要哨兵也能完成节点移除和故障转移的功能。需要将每个节点设置成集群模式,这种集群模式没有中心节点,可水平扩展。

         集群中那么多Master节点,redis cluster在存储的时候如何确定选择哪个节点呢?

        答:Redis Cluster采用的是类一致性哈希算法实现节点选择的。

        Redis Cluster将自己分成了16384个Slot(槽位),哈希槽类似于数据分区,每个键值对都会根据它的 key,被映射到一个哈希槽中,具体执行过程分为两大步。

        根据键值对的 key,按照 CRC16 算法计算一个 16 bit 的值。

        再用 16bit 值对 16384 取模,得到0~16383 范围内的模数,每个模数代表一个相应编号的哈希槽。

        每个Redis节点负责处理一部分槽位,假如你有三个master节点 ABC,每个节点负责的槽位如下:

参考链接

一文搞懂 Redis 架构演化之路 

Redis设计与实现

redis架构_剑八-的博客-CSDN博客

Redis高可用方案—主从(masterslave)架构

Redis高可用架构—哨兵(sentinel)机制详细介绍

Redis高可用架构—Redis集群(Redis Cluster)详细介绍

Redis基本概念知识_redis基础概念_Gatsby_codeLife的博客-CSDN博客

Redis-基本概念_redis基础概念_SeaDhdhdhdhdh的博客-CSDN博客

redis架构_剑八-的博客-CSDN博客

03 Redis 网络IO模型简介_redis的io模型_天秤座的架构师的博客-CSDN博客

0 人点赞