Redis 三种集群方式
主从复制
- Redis主从模式,就是部署多台Redis服务器,有主库和从库,它们之间通过主从复制,以保证数据副本的一致。
- 主从库之间采用的是读写分离的方式,其中主库负责读操作和写操作,从库则负责读操作。
- 如果Redis主库挂了,切换其中的从库成为主库。
Redis 主从同步过程
Redis主从同步包括三个阶段:
- 第一阶段:主从库间建立连接、协商同步
- 从库向主库发送psync 命令,告诉它要进行数据同步。
- 主库收到 psync 命令后,响应FULLRESYNC命令(它表示第一次复制采用的是全量复制),并带上主库runID和主库目前的复制进度offset。
- 第二阶段:主库把数据同步到从库,从库收到数据后,完成本地加载
- 主库执行bgsave命令,生成RDB文件,接着将文件发给从库。从库接收到RDB 文件后,会先清空当前数据库,然后加载 RDB 文件。
- 主库把数据同步到从库的过程中,新来的写操作,会记录到replication buffer
- 第三阶段:主库把新写的命令,发送到从库
- 主库完成RDB发送后,会把replication buffer中的修改操作发给从库,从库再重新执行这些操作。这样主从库就实现同步啦。
主从复制优缺点
优点:
- 支持主从复制,主机会自动将数据同步到从机,可以进行读写分离
- 为了分载Master的读操作压力,Slave服务器可以为客户端提供只读操作的服务,写服务仍然必须由Master来完成
- Slave同样可以接受其它Slaves的连接和同步请求,这样可以有效的分载Master的同步压力。
- Master Server是以非阻塞的方式为Slaves提供服务。所以在Master-Slave同步期间,客户端仍然可以提交查询或修改请求。
- Slave Server同样是以非阻塞的方式完成数据同步。在同步期间,如果有客户端提交查询请求,Redis则返回同步之前的数据
缺点:
- Redis不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复。
- 主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性。
- Redis较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。
主从数据不一致
因为主从复制是异步进行的,如果从库滞后执行,则会导致主从数据不一致。
问题:主从数据不一致一般有两个原因:
- 主从库网路延迟。
- 从库收到了主从命令,但是它正在执行阻塞性的命令(如
hgetall
等)。
问题:如何解决主从数据不一致问题呢?
- 惰性删除:只有当访问一个key时,才会判断该key是否已过期,过期则清除。
- 定期删除:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。
- 主动删除:当前已用内存超过最大限定时,触发主动清理策略。
如果使用Redis版本低于3.2,读从库时,并不会判断数据是否过期,而是会返回过期数据。而3.2 版本后,Redis做了改进,如果读到的数据已经过期了,从库不会删除,却会返回空值,避免了客户端读到过期数据。
因此,在主从Redis模式下,尽量使用 Redis 3.2以上的版本。
一主多从,全量复制时主库压力问题
如果是一主多从模式,从库很多的时候,如果每个从库都要和主库进行全量复制的话,主库的压力是很大的。因为主库fork进程生成RDB,这个fork的过程是会阻塞主线程处理正常请求的。同时,传输大的RDB文件也会占用主库的网络宽带。
可以使用主-从-从模式解决。什么是主从从模式呢?其实就是部署主从集群时,选择硬件网络配置比较好的一个从库,让它跟部分从库再建立主从关系。如图:
主从断网
问题:主从库完成了全量复制后,它们之间会维护一个网络长连接,用于主库后续收到写命令传输到从库,它可以避免频繁建立连接的开销。但是,如果网络断开重连后,是否还需要进行一次全量复制呢?
如果是Redis 2.8之前,从库和主库重连后,确实会再进行一次全量复制,但是这样开销就很大。 而Redis 2.8之后做了优化,重连后采用增量复制方式,即把主从库网络断连期间主库收到的写命令,同步给从库。
主从库重连后,就是利用repl_backlog_buffer实现增量复制。
当主从库断开连接后,主库会把断连期间收到的写操作命令,写入replication buffer,同时也会把这些操作命令写入repl_backlog_buffer这个缓冲区。repl_backlog_buffer是一个环形缓冲区,主库会记录自己写到的位置,从库则会记录自己已经读到的位置。
哨兵模式
当主服务器中断服务后,可以将一个从服务器升级为主服务器,以便继续提供服务,但是这个过程需要人工手动来操作。 为此,Redis 2.8中提供了哨兵工具来实现自动化的系统监控和故障恢复功能。
哨兵的作用就是监控Redis系统的运行状况。它的功能包括以下两个:
- 监控主服务器和从服务器是否正常运行。
- 主服务器出现故障时自动将从服务器转换为主服务器。
哨兵的工作方式
- 每个Sentinel(哨兵)进程以每秒钟一次的频率向整个集群中的Master主服务器,Slave从服务器以及其他Sentinel(哨兵)进程发送一个 PING 命令。
- 如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被 Sentinel(哨兵)进程标记为主观下线(SDOWN)
- 如果一个Master主服务器被标记为主观下线(SDOWN),则正在监视这个Master主服务器的所有 Sentinel(哨兵)进程要以每秒一次的频率确认Master主服务器的确进入了主观下线状态
- 当有足够数量的 Sentinel(哨兵)进程(大于等于配置文件指定的值)在指定的时间范围内确认Master主服务器进入了主观下线状态(SDOWN), 则Master主服务器会被标记为客观下线(ODOWN)
- 在一般情况下, 每个 Sentinel(哨兵)进程会以每 10 秒一次的频率向集群中的所有Master主服务器、Slave从服务器发送 INFO 命令。
- 当Master主服务器被 Sentinel(哨兵)进程标记为客观下线(ODOWN)时,Sentinel(哨兵)进程向下线的 Master主服务器的所有 Slave从服务器发送 INFO 命令的频率会从 10 秒一次改为每秒一次。
- 若没有足够数量的 Sentinel(哨兵)进程同意 Master主服务器下线, Master主服务器的客观下线状态就会被移除。若 Master主服务器重新向 Sentinel(哨兵)进程发送 PING 命令返回有效回复,Master主服务器的主观下线状态就会被移除。
哨兵判定主库下线
问题:哨兵是如何判断主库是否下线的呢?
我们先来了解两个基础概念哈:主观下线和客观下线。
- 哨兵进程向主库、从库发送PING命令,如果主库或者从库没有在规定的时间内响应PING命令,哨兵就把它标记为主观下线。
- 如果是主库被标记为主观下线,则正在监视这个主库的所有哨兵要以每秒一次的频率,以确认主库是否真的进入了主观下线。 当有多数的哨兵(一般少数服从多数,由 Redis 管理员自行设定的一个值)在指定的时间范围内确认主库的确进入了主观下线状态,则主库会被标记为客观下线。这样做的目的就是避免对主库的误判,以减少没有必要的主从切换,减少不必要的开销。
假设我们有
N
个哨兵实例,如果有N/2+1
个实例判断主库主观下线,此时就可以把节点标记为客观下线,就可以做主从切换了。
哨兵选举
如果明确主库已经客观下线了,哨兵就开始了选主模式。
哨兵选主包括两大过程,分别是:过滤和打分。其实就是在多个从库中,先按照一定的筛选条件,把不符合条件的从库过滤掉。然后再按照一定的规则,给剩下的从库逐个打分,将得分最高的从库选为新主库
- 选主时,会判断从库的状态,如果已经下线,就直接过滤。
- 如果从库网络不好,老是超时,也会被过滤掉。看这个参数
down-after-milliseconds
,它表示我们认定主从库断连的最大连接超时时间。 - 过滤掉了不适合做主库的从库后,就可以给剩下的从库打分,按这三个规则打分:从库优先级、从库复制进度以及从库ID号。
- 从库优先级最高的话,打分就越高,优先级可以通过
slave-priority
配置。如果优先级一样,就选与旧的主库复制进度最快的从库。如果优先级和从库进度都一样,从库ID 号小的打分高。
哨兵执行主从切换
一个哨兵标记主库为主观下线后,它会征求其他哨兵的意见,确认主库是否的确进入了主观下线状态。它向其他实例哨兵发送
is-master-down-by-addr
命令。其他哨兵会根据自己和主库的连接情况,回应Y
或N
(Y 表示赞成,N表示反对票)。如果这个哨兵获取得足够多的赞成票数(quorum
配置),主库会被标记为客观下线。
标记主库客观下线的这个哨兵,紧接着向其他哨兵发送命令,再发起投票,希望它可以来执行主从切换。这个投票过程称为Leader 选举。因为最终执行主从切换的哨兵称为Leader,投票过程就是确定Leader。一个哨兵想成为Leader需要满足两个条件:
- 需要拿到
**num(sentinels)/2+1**
的赞成票。 - 并且拿到的票数需要大于等于哨兵配置文件中的
**quorum**
值。
举个例子,假设有3个哨兵。配置的quorum值为2。即一个一个哨兵想成为Leader至少需要拿到2张票。为了更好理解,大家可以看下
- 在t1时刻,哨兵A1判断主库为客观下线,它想成为主从切换的Leader,于是先给自己投一张赞成票,然后分别向哨兵A2 和A3发起投票命令,表示想成为 Leader。
- 在 t2 时刻,A3 判断主库为客观下线,它也想成为 Leader,所以也先给自己投一张赞成票,再分别向 A1 和 A2 发起投票命令,表示也要成为 Leader。
- 在 t3 时刻,哨兵A1 收到了A3 的Leader投票请求。因为A1已经把票Y投给自己了,所以它不能再给其他哨兵投赞成票了,所以A1投票
N
给A3。 - 在 t4时刻,哨兵A2收到A3 的Leader投票请求,因为哨兵A2之前没有投过票,它会给第一个向它发送投票请求的哨兵回复赞成票
Y
。 - 在 t5时刻,哨兵A2收到A1 的Leader投票请求,因为哨兵A2之前已经投过赞成票给A3了,所以它只能给A1投反对票
N
。 - 最后t6时刻,哨兵A1只收到自己的一票
Y
赞成票,而哨兵A3得到两张赞成票(A2和A3投的),因此哨兵A3成为了Leader。
假设网络故障等原因,哨兵A3也没有收到两张票,那么这轮投票就不会产生Leader。哨兵集群会等待一段时间(一般是哨兵故障转移超时时间的2倍),再进行重新选举。
哨兵模式的优缺点
优点:
- 哨兵模式是基于主从模式的,所有主从的优点,哨兵模式都具有。
- 主从可以自动切换,系统更健壮,可用性更高。
缺点:
当哨兵检测到Redis主库M1出现故障,那么哨兵需要对集群进行故障转移。假设选出了哨兵3作为Leader。故障转移流程如下:
- 从库S1解除从节点身份,升级为新主库
- 从库S2成为新主库的从库
- 原主节点恢复也变成新主库的从节点
- 通知客户端应用程序新主节点的地址。
故障转移后:
Redis-Cluster 集群
redis的哨兵模式基本已经可以实现高可用,读写分离 ,但是在这种模式下每台redis服务器都存储相同的数据,很浪费内存,所以在redis3.0上加入了cluster模式,通过分片,虚拟槽,实现的redis的分布式存储,也就是说每台redis节点上存储不同的内容。哨兵和集群是两个独立的功能,当不需要对数据进行分片使用哨兵就够了,如果要进行水平扩容,集群是一个比较好的方式
Redis-Cluster采用无中心结构,它的特点如下:
- 所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽。
- 节点的fail是通过集群中超过半数的节点检测失效时才生效。
- 客户端与redis节点直连,不需要中间代理层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。
Redis Cluster
一般由多个节点组成,节点数量至少为 6 个才能保证组成完整高可用的集群,其中三个为主节点,三个为从节点。三个主节点会分配槽,处理客户端的命令请求,而从节点可用在主节点故障后,顶替主节点
如上图所示,该集群中包含 6 个 Redis 节点,3主3从,分别为M1,M2,M3,S1,S2,S3。除了主从 Redis 节点之间进行数据复制外,所有 Redis 节点之间采用 Gossip 协议进行通信,交换维护节点元数据信息。【redis-cluster是基于gossip协议实现的无中心化节点的集群,因为去中心化的架构不存在统一的配置中心,各个节点对整个集群状态的认知来自于节点之间的信息交互。在Redis Cluster,这个信息交互是通过Redis Cluster Bus来完成的】
数据分片策略
分布式数据存储方案中最为重要的一点就是数据分片,也就是所谓的
**Sharding**
为了使得集群能够水平扩展,首要解决的问题就是如何将整个数据集按照一定的规则分配到多个节点上,常用的数据分片的方法有:范围分片,哈希分片,一致性哈希算法和虚拟哈希槽
范围分片假设数据集是有序,将顺序相临近的数据放在一起,可以很好的支持遍历操作。范围分片的缺点是面对顺序写时,会存在热点。比如日志类型的写入,一般日志的顺序都是和时间相关的,时间是单调递增的,因此写入的热点永远在最后一个分片。
对于关系型的数据库,因为经常性的需要表扫描或者索引扫描,基本上都会使用范围的分片策略。
Redis Cluster采用哈希分区规则,采用虚拟槽分区,虚拟槽分区巧妙地使用了哈希空间,使用分散度良好的哈希函数把所有的数据映射到一个固定范围内的整数集合,整数定义为槽(slot)。比如Redis Cluster槽的范围是0 ~ 16383。槽是集群内数据管理和迁移的基本单位,采用大范围的槽的主要目的是为了方便数据的拆分和集群的扩展,每个节点负责一定数量的槽。
计算公式:slot = CRC16(key)%16383
每一个节点负责维护一部分槽以及槽所映射的键值数据。
Redis 虚拟槽分区的特点:
- 解耦数据和节点之间的关系,简化了节点扩容和收缩难度。
- 节点自身维护槽的映射关系,不需要客户端或者代理服务维护槽分区元数据
- 支持节点、槽和键之间的映射查询,用于数据路由,在线集群伸缩等场景。
HashTags
- 通过分片手段,可以将数据合理的划分到不同的节点上,这本来是一件好事。但是有的时候,我们希望对相关联的业务以原子方式进行操作
- 举个简单的例子:
我们在单节点上执行MSET , 它是一个原子性的操作,所有给定的key会在同一时间内被设置,不可能出现某些指定的key被更新另一些指定的key没有改变的情况。
但是在集群环境下,我们仍然可以执行MSET命令,但它的操作不在是原子操作,会存在某些指定的key被更新,而另外一些指定的key没有改变,原因是多个key可能会被分配到不同的机器上。
- 举个简单的例子:
这里就会存在一个矛盾点,及要求key尽可能的分散在不同机器,又要求某些相关联的key分配到相同机器。这个也是在面试的时候会容易被问到的内容。怎么解决呢?
从前面的分析中我们了解到,分片其实就是一个hash的过程,对key做hash取模然后划分到不同的机器上。所以为了解决这个问题,我们需要考虑如何让相关联的key得到的hash值都相同呢? 如果key全部相同是不现实的,所以怎么解决呢?
在redis中引入了HashTag的概念,可以使得数据分布算法可以根据key的某一个部分进行计算,然后让相关的key落到同一个数据分片
- 举个简单的例子,加入对于用户的信息进行存储,
user:user1:id、user:user1:name/
那么通过hashtag的方式,
user:{user1}:id、user:{user1}.name; - 表示:当一个key包含 {} 的时候,就不对整个key做hash,而仅对 {} 包括的字符串做hash
- 举个简单的例子,加入对于用户的信息进行存储,
重定向客户端
问题:Redis Cluster
并不会代理查询,那么如果客户端访问了一个key并不存在的节点,这个节点是怎么处理的呢?
比如我想获取key为msg的值,msg计算出来的槽编号为254,当前节点正好不负责编号为254的槽,那么就会返回客户端下面信息:
-MOVED 254 127.0.0.1:6381
表示客户端想要的254槽由运行在IP为127.0.0.1,端口为6381的Master实例服务。如果根据key计算得出的槽恰好由当前节点负责,则当期节点会立即返回结果
ASK 重定向
Ask重定向一般发生于集群伸缩的时候。集群伸缩会导致槽迁移,当我们去源节点访问时,此时数据已经可能已经迁移到了目标节点,使用Ask重定向可以解决此种情况
槽迁移过程
槽迁移的过程中有一个不稳定状态,这个不稳定状态会有一些规则,这些规则定义客户端的行为,从而使得RedisCluster不必宕机的情况下可以执行槽的迁移。
下面这张图描述了我们迁移编号为1、2、3的槽的过程中,他们在MasterA节点和MasterB节点中的状态
迁移工作流程:
- 向MasterB发送状态变更命令,吧Master B对应的slot状态设置为IMPORTING
- 向MasterA发送状态变更命令,将Master对应的slot状态设置为MIGRATING
当MasterA的状态设置为MIGRANTING后,表示对应的slot正在迁移,为了保证slot数据的一致性,MasterA此时对于slot内部数据提供读写服务的行为和通常状态下是有区别的,
- MIGRATING状态
- 如果客户端访问的Key还没有迁移出去,则正常处理这个key
- 如果key已经迁移或者根本就不存在这个key,则回复客户端ASK信息让它跳转到MasterB去执行
- IMPORTING状态
- 当MasterB的状态设置为IMPORTING后,表示对应的slot正在向MasterB迁入,及时Master仍然能对外提供该slot的读写服务,但和通常状态下也是有区别的
- 当来自客户端的正常访问不是从ASK跳转过来的,说明客户端还不知道迁移正在进行,很有可能操作了一个目前还没迁移完成的并且还存在于MasterA上的key,如果此时这个key在A上已经被修改了,那么B和A的修改则会发生冲突。所以对于MasterB上的slot上的所有非ASK跳转过来的操作,MasterB都不会处理,而是通过MOVED命令让客户端跳转到MasterA上去执行
- 当MasterB的状态设置为IMPORTING后,表示对应的slot正在向MasterB迁入,及时Master仍然能对外提供该slot的读写服务,但和通常状态下也是有区别的
迁移后总是在目标节点上执行,防止出现两边同时写导致的冲突问题。而且迁移过程中新增的key一定会在目标节点上执行,源节点也不会新增key,是的整个迁移过程既能对外正常提供服务,又能在一定的时间点完成slot的迁移。
扩容集群
通过 Redis Cluster 的命令来模拟扩容整个过程
当一个 Redis 新节点运行并加入现有集群后,我们需要为其迁移槽和数据。首先要为新节点指定槽的迁移计划,确保迁移后每个节点负责相似数量的槽,从而保证这些节点的数据均匀。
- 首先启动一个 Redis 节点,记为 M4。
- 使用 cluster meet 命令,让新 Redis 节点加入到集群中。新节点刚开始都是主节点状态,由于没有负责的槽,所以不能接受任何读写操作,后续我们就给他迁移槽和填充数据。
- 对 M4 节点发送 cluster setslot { slot } importing { sourceNodeId } 命令,让目标节点准备导入槽的数据。
- 对源节点,也就是 M1,M2,M3 节点发送 cluster setslot { slot } migrating { targetNodeId } 命令,让源节点准备迁出槽的数据。
- 源节点执行 cluster getkeysinslot { slot } { count } 命令,获取 count 个属于槽 { slot } 的键,然后执行步骤六的操作进行迁移键值数据。
- 在源节点上执行 migrate { targetNodeIp} “ “ 0 { timeout } keys { key… } 命令,把获取的键通过 pipeline 机制>批量迁移到目标节点,批量迁移版本的 migrate 命令在 Redis 3.0.6 以上版本提供。
- 重复执行步骤 5 和步骤 6 直到槽下所有的键值数据迁移到目标节点。
- 向集群内所有主节点发送 cluster setslot { slot } node { targetNodeId } 命令,通知槽分配给目标节点。为了>保证槽节点映射变更及时传播,需要遍历发送给所有主节点更新被迁移的槽执行新节点。
收缩集群
收缩节点就是将 Redis 节点下线,整个流程需要如下操作流程。
- 首先需要确认下线节点是否有负责的槽,如果是,需要把槽迁移到其他节点,保证节点下线后整个集群槽节点映射的完整性。
- 当下线节点不再负责槽或者本身是从节点时,就可以通知集群内其他节点忘记下线节点,当所有的节点忘记改节点后可以正常关闭。
下线节点需要将节点自己负责的槽迁移到其他节点,原理与之前节点扩容的迁移槽过程一致。
迁移完槽后,还需要通知集群内所有节点忘记下线的节点,也就是说让其他节点不再与要下线的节点进行 Gossip 消息交换。
Redis 集群使用 cluster forget { downNodeId } 命令来讲指定的节点加入到禁用列表中,在禁用列表内的节点不再发送 Gossip 消息。
通讯协议Gossip
一个Redis集群由多个节点组成,各个节点之间是怎么通信的呢?通过Gossip协议!Gossip是一种谣言传播协议,每个节点周期性地从节点列表中选择 k 个节点,将本节点存储的信息传播出去,直到所有节点信息一致,即算法收敛了。
Gossip协议基本思想:一个节点想要分享一些信息给网络中的其他的一些节点。于是,它周期性的随机选择一些节点,并把信息传递给这些节点。这些收到信息的节点接下来会做同样的事情,即把这些信息传递给其他一些随机选择的节点。一般而言,信息会周期性的传递给N个目标节点,而不只是一个。这个N被称为fanout
Redis Cluster集群通过Gossip协议进行通信,节点之前不断交换信息,交换的信息内容包括节点出现故障、新节点加入、主从节点变更信息、slot信息等等。gossip协议包含多种消息类型,包括ping,pong,meet,fail,等等
- meet消息:通知新节点加入。消息发送者通知接收者加入到当前集群,meet消息通信正常完成后,接收节点会加入到集群中并进行周期性的ping、pong消息交换。
- ping消息:节点每秒会向集群中其他节点发送 ping 消息,消息中带有自己已知的两个节点的地址、槽、状态信息、最后一次通信时间等
- pong消息:当接收到ping、meet消息时,作为响应消息回复给发送方确认消息正常通信。消息中同样带有自己已知的两个节点信息。
- fail消息:当节点判定集群内另一个节点下线时,会向集群内广播一个fail消息,其他节点接收到fail消息之后把对应节点更新为下线状态。
特别的,每个节点是通过集群总线(cluster bus) 与其他的节点进行通信的。通讯时,使用特殊的端口号,即对外服务端口号加10000。例如如果某个node的端口号是6379,那么它与其它nodes通信的端口号是 16379。nodes 之间的通信采用特殊的二进制协议。
故障转移
- Redis集群实现了高可用,当集群内节点出现故障时,通过故障转移,以保证集群正常对外提供服务。
- redis集群通过ping/pong消息,实现故障发现。这个环境包括主观下线和客观下线。
- 主观下线: 某个节点认为另一个节点不可用,即下线状态,这个状态并不是最终的故障判定,只能代表一个节点的意见,可能存在误判情况。
客观下线: 指标记一个节点真正的下线,集群内多个节点都认为该节点不可用,从而达成共识的结果。如果是持有槽的主节点故障,需要为该节点进行故障转移。
- 假如节点A标记节点B为主观下线,一段时间后,节点A通过消息把节点B的状态发到其它节点,当节点C接受到消息并解析出消息体时,如果发现节点B的pfail状态时,会触发客观下线流程;
- 当下线为主节点时,此时Redis Cluster集群为统计持有槽的主节点投票,看投票数是否达到一半,当下线报告统计数大于一半时,被标记为客观下线状态
流程如下:
故障恢复:故障发现后,如果下线节点的是主节点,则需要在它的从节点中选一个替换它,以保证集群的高可用。流程如下:
- 资格检查:检查从节点是否具备替换故障主节点的条件。
- 准备选举时间:资格检查通过后,更新触发故障选举时间。
- 发起选举:到了故障选举时间,进行选举。
- 选举投票:只有持有槽的主节点才有票,从节点收集到足够的选票(大于一半),触发替换主节点操作