Redis线程模型解析

Redis的线程模型

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

Redis既然是单线程的,那么它是如何监听大量的客户端请求的呢?

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

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

另外, Redis 服务器是一个事件驱动程序,服务器需要处理两类事件: 1. 文件事件; 2. 时间事件。

时间事件不需要多花时间了解,我们接触最多的还是 文件事件(客户端进行读取写入等操作,涉及一系列网络通信)。

Redis 基于 Reactor 模式开发了自己的网络事件处理器:这个处理器被称为文件事件处理器(file event handler)。文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字,并根据 套接字目前执行的任务来为套接字关联不同的事件处理器。

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

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

以上的说法并不意味着redis只能以单线程的方式运行,在redis4.0之后的版本就已经加入了对多线程的支持(仅限于对大键值的删除操作,Redis6.0引入了多线程主要是为了提高IO读写能力,执行命令这种还是单线程顺序执行,默认Redis6.0是禁用多线程的。)。

持久化原理

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

快照(snapshotting)持久化(RDB)

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

RDB是redis默认的持久化方式,在redis.conf中有以下默认配置。

image-20201130141742522

1
2
3
save 900 1             #在900s之后,如果至少有一个key发生了变化,那么redis就会触发bgsave命令创建快照
save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发BGSAVE命令创建快照。

AOF(append-only file)持久化

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

image-20201130142034658

1
appendonly yes

开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入硬盘中的 AOF 文件。AOF 文件的保存位置和 RDB 文件的位置相同,都是通过 dir 参数设置的,默认的文件名是 appendonly.aof。

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

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

为了兼顾数据备份准确性和性能,一般采用 每秒钟同步一次(appendfsync everysec)的备份策略,

持久化机制的优化

混合持久化

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

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

aof重写

AOF 重写可以产生一个新的 AOF 文件,这个新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样,但体积更小。

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

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

无磁盘化复制

image-20201130142722714

Redis的主从复制,过程是master先将内存中的数据以RDB文件的形式保存在本地磁盘,然后将RDB文件传输给slave的磁盘,slave再将接受到的RDB文件加载到内存中。这是默认的主从复制过程,需要经过磁盘。但如果是普通机械硬盘,硬盘读写效率低下。这样就出现了无磁盘化复制,从而提高了效率。默认是关闭的。使用的时候把no改为yes

1
2
3
4
5
repl-diskless-sync no
# 这一点很重要,因为一旦传输开始,就不可能服务新的从服务器到达,它将排队等待下一次RDB传输,所以服
# 务器等待延迟以便让更多的从节点到达。延迟以秒为单位指定,默认为5秒。禁用它完全只是设置为0秒,传
# 输将尽快开始。
repl-diskless-sync-delay 5

内存淘汰策略和缓存过期机制

缓存过期机制

(主动)定期删除(1s中检查10次,可以配置)

设置的 hz参数过高,CPU占用率会越高,一般采用默认设置 10 即可。image-20201121121449523

(被动)惰性删除

客户端会获取到过期的key,这个时候会检测这个key有没有过期,过期了直接从内存中删除,这种策略不会

占用太多的CPU资源,缺点是如果一个过期的key没有被访问到,那么这个key会一直存在于内存中。

内存淘汰策略

如果某个时候Redis中所使用的内存,已经达到了设置的阈值,那么一些key会被清理掉。

Redis4.0之前提供以下六种数据淘汰策略。

  1. volatile-lru(least recently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
  2. volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
  3. volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
  4. allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)
  5. allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
  6. no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!

Redis4.0之后增加了以下两种。

  1. volatile-lfu(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
  2. allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key

Redis高可用策略和集群

Redis 高可用策略(主从&哨兵)

基本概念

高可用(High Availability),是当一台服务器停止服务后,对于业务及用户毫无影响。 停止服务的原因可能由于网卡、路由器、机房、CPU负载过高、内存溢出、自然灾害等不可预期的原因导致,在很多时候也称单点问题。

主备方式(简单情景)
这种通常是一台主机、一台或多台备机,在正常情况下主机对外提供服务,并把数据同步到备机,当主机宕机后,备机立刻开始服务。 Redis HA中使用比较多的是keepalived,它使主机备机对外提供同一个虚拟IP,客户端通过虚拟IP进行数据操作,正常期间主机一直对外提供服务,宕机后VIP自动漂移到备机上。优点是对客户端毫无影响,仍然通过VIP操作。 缺点也很明显,在绝大多数时间内备机是一直没使用,被浪费着的。

主从方式(推荐)
这种采取一主多从的办法,主从之间进行数据同步。 当Master宕机后,通过选举算法(Paxos、Raft)从slave中选举出新Master继续对外提供服务,主机恢复后以slave的身份重新加入。 主从另一个目的是进行读写分离,这是当单机读写压力过高的一种通用型解决方案。 其主机的角色只提供写操作或少量的读,把多余读请求通过负载均衡算法分流到单个或多个slave服务器上。

缺点是主机宕机后,Slave虽然被选举成新Master了,但对外提供的IP服务地址却发生变化了,意味着会影响到客户端。 解决这种情况需要一些额外的工作,在当主机地址发生变化后及时通知到客户端,客户端收到新地址后,使用新地址继续发送新请求。

方案选择
主备(keepalived)方案配置简单、人力成本小,在数据量少、压力小的情况下推荐使用。 如果数据量比较大,不希望过多浪费机器,还希望在宕机后,做一些自定义的措施,比如报警、记日志、数据迁移等操作,推荐使用主从方式,因为和主从搭配的一般还有个管理监控中心。

主从拓扑

Redis的主从拓扑支持单层或者多层结构,可分为一主一从,一主多从,树状主从
一主一从:用于主节点故障转移从节点,当主节点的“写”命令并发高且需要持久化,可以只在从节点开启AOF(主节点不需要),这样即保证了数据的安全性,也避免持久化对主节点的影响。

一主多从:针对“读”较多的场景,“读”由多个从节点来分担,但节点越多,主节点同步到多节点的次数也越多,影响带宽,也加重主节点的负担。

树状主从:一主多从的缺点(主节点推送次数多压力大)可用些方案解决,主节点只推送一次数据到从节点1,再由从节点2推送到11,减轻主节点推送的压力。

哨兵机制

前面介绍了主从机制,但是从运维角度来看,主节点出现了问题我们还需要通过人工干预的方式把从节点设为主节点,还要通知应用程序更新主节点地址,这种方式非常繁琐笨重, 而且主节点的读写能力都十分有限,有没有较好的办法解决这两个问题,哨兵机制就是针对第一个问题的有效解决方案,第二个问题则有赖于集群!哨兵的作用就是监控Redis系统的运行状况,其功能主要是包括以下三个:

  • 监控(Monitoring): 哨兵(sentinel) 会不断地检查你的Master和Slave是否运作正常。
  • 提醒(Notification): 当被监控的某个 Redis出现问题时, 哨兵(sentinel) 可以通过 API 向管理员或者其他应用程序发送通知。
  • 自动故障迁移(Automatic failover): 当主数据库出现故障时自动将从数据库转换为主数据库。

哨兵的原理:Redis哨兵的三个定时任务,Redis哨兵判定一个Redis节点故障不可达主要就是通过三个定时监控任务来完成的:

  • 每隔10秒每个哨兵节点会向主节点和从节点发送"info replication" 命令来获取最新的拓扑结构

  • 每隔2秒每个哨兵节点会向Redis节点的_sentinel_:hello频道发送自己对主节点是否故障的判断以及自身的节点信息,并且其他的哨兵节点也会订阅这个频道来了解其他哨兵节点的信息以及对主节点的判断

  • 每隔1秒每个哨兵会向主节点、从节点、其他的哨兵节点发送一个 “ping” 命令来做心跳检测。

  • 如果在定时Job3检测不到节点的心跳,会判断为“主观下线”。如果该节点还是主节点那么还会通知到其他的哨兵对该主节点进行心跳检测,这时主观下线的票数超过了<quorum>数时,那么这个主节点确实就可能是故障不可达了,这时就由原来的主观下线变为了“客观下线”

    故障转移和Leader选举
    如果主节点被判定为客观下线之后,就要选取一个哨兵节点来完成后面的故障转移工作,选举出一个leader,这里面采用的选举算法为Raft。选举出来的哨兵leader就要来完成故障转移工作,也就是在从节点中选出一个节点来当新的主节点,这部分的具体流程可参考引用.

Redis 分布式集群

image-20201121163243822

集群至少部署两台Redis服务器构成一个小的集群,主要有2个目的:

  • 高可用性:在主机挂掉后,自动故障转移,使前端服务对用户无影响。
  • 读写分离:将主机读压力分流到从机上。

在Redis集群设计包括2部分:哈希Slot和节点主从

节点主从

这部分实际在前面的主从拓扑中已经提及,1个Master配备有N个slaver,而且Slaver也可以有自己的Slaver,由于这种主从的关系决定他们是在配置阶段就要指定他们的上下级关系,而不是Zookeeper那种平行关系。节点主从上可以实现读写分离,Master只负责写和同步数据给Slaver,Slaver承担了被读的任务,所以Slaver的扩容只能提高读效率不能提高写效率

  • 优点:读写分离,通过增加Slaver可以提高并发读的能力。
  • 缺点:Master写能力是瓶颈。维护Slaver开销总将会变成瓶颈,同时Master的Disk大小也将会成为整个Redis集群存储容量的瓶颈。

哈希Slot

说白了就像是数据库的分库分表,把整个数据按分区规则映射到多个节点,即把数据划分到多个节点上,每个节点负责整体数据的一个子集。每个Node节点被平均分配了一个Slot段,对应着0-16384,Slot不能重复也不能缺失,否则会导致对象重复存储或无法存储。Node之间也互相监听,一旦有Node退出或者加入,会按照Slot为单位做数据的迁移。

  • 优点:将Redis的写操作分摊到了多个节点上,提高写的并发能力,扩容简单。
  • 缺点:每个Node承担着互相监听、高并发数据写入、高并发数据读出,工作任务繁重

大集群时代

集群3.0时代:主从和哈希的设计优缺点正好是相互弥补的,可先Hash分逻辑节点,然后每个逻辑节点内部是主从结构,想扩展并发读就添加Slaver,想扩展并发写就添加Master,想扩容也就是添加Master,任何一个Slaver或者几个Master挂了都不会是灾难性的故障。

总结以下其优缺点:

  • 优点:无中心节点;数据按照 slot 存储分布在多个 Redis 实例上;平滑的进行扩容/缩容节点;自动故障转移(节点之间通过 Gossip 协议交换状态信息,进行投票机制完成 Slave 到 Master 角色的提升);降低运维成本,提高了系统的可扩展性和高可用性。
  • 缺点:严重依赖外部 Redis-Trib;缺乏监控管理;需要依赖 Smart Client(连接维护, 缓存路由表, MultiOp 和 Pipeline 支持);Failover 节点的检测过慢,不如“中心节点 ZooKeeper”及时;Gossip 消息的开销;无法根据统计区分冷热数据;Slave“冷备”,不能缓解读压力。

Redis集群+Proxy:当数据量持续增加时,应用可根据不同场景下的业务申请对应的分布式集群。 这块最关键的是缓存治理这块,其中最重要的部分是加入了代理服务。 应用通过代理访问真实的Redis服务器进行读写,这样做的好处是:

  • 避免越来越多的客户端直接访问Redis服务器难以管理,而造成风险。
  • 在代理这一层可以做对应的安全措施,比如限流、授权、分片。
  • 避免客户端越来越多的逻辑代码,不但臃肿升级还比较麻烦。
  • 代理这层无状态的,可任意扩展节点,对于客户端来说,访问代理跟访问单机Redis一样。

目前有公司使用的是客户端组件和代理两种方案并存,因为通过代理会影响一定的性能。 代理这块对应的方案实现有Twitter的Twemproxy和豌豆荚的codis。

参考

  1. Redis高可用策略与集群方案
  2. 三张图秒懂Redis集群设计原理_咖啡男孩之SRE之路-CSDN博客_redis集群原理
  3. 这可能是目前最全的Redis高可用技术解决方案_时光钟摆-CSDN博客_redis高可用方案
  4. 深入理解Redis哨兵搭建及原理_Nuomizhende45-CSDN博客_redis哨兵工作原理
  5. Redis集群架构及对比_u013473512的博客-CSDN博客_redis集群架构
  6. Redis面试题及分布式集群_yajlv的专栏-CSDN博客_redis面试题
  7. Redis Sentinel Documentation – Redis
显示评论