五、Redis 集群
5.1 什么是Redis集群?
如何提供一个高可用的Redis服务? —— 构建Redis集群
单服务器Redis由于数据都是存储在一台服务器,如果这台服务器出现宕机或者故障,可能会导致服务不可用甚至数据丢失。
要避免这种单点故障,最好的办法是将数据被分到其他服务器上,让这些服务器也能够对外提供服务,这样即使有一台服务器发生了故障,其他服务器依然可以继续提供服务。
数据一致性?
由于多个服务器保存同一份数据,所以**如何保持数据的一致性是关键问题。Redis中有三种方案来保证数据的一致性:主从复制、哨兵机制、切片集群**。
5.2 主从复制模式
5.2.1 什么是主从复制?
主从复制模式中,主从服务器之间采用的是**「读写分离」的方式。主服务器可以进行读写操作,当发生写操作时自动将写操作同步给从服务器,而从服务器一般是只读,并接受主服务器同步过来写操作命令,然后执行这条命令。**
也就是说,所有的数据修改只在主服务器上进行,然后将最新的数据同步给从服务器,这样就使得主从服务器的数据是一致的。主从服务器之间的命令复制是**异步**进行的。
但是,与Raft算法不一致的是,主服务器并不会等到从服务器实际执行完命令后,再把结果返回给客户端,而是**主服务器自己在本地执行完命令后,就会向客户端返回结果了。如果从服务器还没有执行主服务器同步过来的命令,主从服务器间的数据就不一致了。所以,无法实现强一致性保证(主从数据时时刻刻保持一致),数据不一致是难以避免的**。
5.2.2 主从复制是怎么实现的?
-
第一次同步:全量复制
通过
replicaof
命令可以让服务器B成为服务器A的从服务器:# 服务器 B 执行这条命令 replicaof <服务器 A 的 IP 地址> <服务器 A 的 Redis 端口号>
服务器B成为服务器A的从服务器后,会发生与A服务器的第一次同步。第一次同步可以分为三个阶段:
-
第一阶段:建立连接,协商同步;
从服务器会向主服务器申请数据同步,然后主服务器向从服务器作出响应,表明复制方法(全量复制)等。
-
第二阶段:主服务器同步数据给从服务器(全量复制);
主服务器会新开辟一个子进程来生成当前的RDB快照,然后把这个快照文件发送给从服务器;
从服务器收到RDB快照后,清空该服务器上的数据,然后载入这个RDB快照;
在此期间,如果主服务器收到了新的写操作命令,将会写到一个缓冲区中**
replication buffer
**,等待后面同步。 -
第三阶段:主服务器发送新的写操作命令给从服务器。
当从服务器载入RDB文件完成后,会发送一个确认消息给主服务器;
主服务器会将在**
replication buffer
**新增的一些写操作命令同步给从服务器,从服务器执行这些命令,从而实现主从服务器的数据一致性。至此,第一次同步完成。
-
-
提供服务期间:基于长连接的命令传播
第一次同步完成后,主服务器和从服务器之间会维护它们所建立的TCP连接,形成一个长连接。后续主服务器在收到写操作命令时,会通过这个连接将该命令传播给从服务器执行,保持数据的一致性。这个过程被称为**基于长连接的命令传播**,通过这种方式来保证第一次同步后的主从服务器的数据一致性。
-
出现网络断连:增量复制
如果主从服务器之间的网络连接断开了,则无法进行命令传播,导致数据不一致,客户端可能从「从服务器」读到旧的数据。当网络恢复时,该如何保证主从服务器的数据一致性?
Redis通过**增量复制**的方式继续同步,只会把网络断开期间主服务器接收到的写操作命令,同步给从服务器。
❓ 主服务器怎么知道要将哪些增量数据发送给从服务器呢?
主服务器中维护了一个「环形」缓冲区**
repl_backlog_buffer
**,主服务器在进程命令传播时,还会将这些命令写入到这个缓冲区中,因此这个缓冲区里会保存着最近传播的写命令。另外,主服务器和从服务器都维护了一个标记同步进度的偏移量offset(
master_repl_offset
和slave_repl_offset
),当网络重连时,从服务器会将自己的偏移量slave_repl_offset
复制给主服务器,由主服务器决定需要如何同步:- 如果判断出从服务器要读取的数据还在
repl_backlog_buffer
缓冲区里,那么主服务器将采用**增量同步**的方式; - 相反,如果判断出从服务器要读取的数据已经不存在
repl_backlog_buffer
缓冲区里,那么主服务器将采用**全量同步**的方式。
❓ 增量同步具体是怎么做的?
主服务器在
repl_backlog_buffer
中找到主从服务器差异(增量)的数据后,就会将增量的数据写入到replication buffer
缓冲区,然后经过命令传播同步给从服务器。 - 如果判断出从服务器要读取的数据还在
5.2.3 如何分担主服务器的压力?
主服务器在第一次数据同步时会有两个耗时的操作:生成RDB快照和传输RDB文件。
如果从服务器非常多,且每个都需要主服务器进行全量同步的话,会带来一些问题:
- 主服务器忙于fork()创建子进程来生成RDB,导致无法正常处理请求;
- 传输RDB文件占用了大量网络贷款,对主服务器的接收请求造成影响。
Redis为了解决这个问题,让从服务器也可以有自己的从服务器,从服务器当作**“经理”**的角色,与它的从服务器的数据同步都是由这个“经理”完成的,而不需要主服务器参与,缓解主服务器的压力。
通过这种方式,主服务器生成 RDB 和传输 RDB 的压力可以分摊到充当经理角色的从服务器。
5.3 哨兵机制
5.3.1 为什么需要哨兵机制?
在 Redis 的主从架构中,由于主从模式是读写分离的,如果**主节点(master)挂了,那么将没有主节点来服务客户端的写操作请求,也没有主节点给从节点(slave)进行数据同步了,即十分依赖主节点。需要人工介入**将一个「从节点」切换为「主节点」,然后让其他从节点指向新的主节点,同时还需要通知上游那些连接 Redis 主节点的客户端,将其配置中的主节点 IP 地址更新为「新主节点」的 IP 地址。
Redis 2.8版本以后提供了**哨兵(Sentinel)机制,作用是实现主从节点故障转移,会监测主节点是否存活,如果发现主节点挂了,它就会选举一个从节点切换为主节点,并且把新主节点的相关信息通知给从节点和客户端**。
类似Raft算法中领导者选举的方式
5.3.2 哨兵机制是如何工作的?
哨兵本身也是一个(服务器)节点,和主节点、从节点类似,但是它是一种特殊的”观察者节点“,观察的对象就是主从节点。
哨兵节点负责三件事情:监控、选主、通知。
所以,哨兵机制的工作内容主要有:
- 如何**监控**节点?如何判断主节点是否真的故障了?
- 根据什么规则选择一个**从节点切换为主节点**?
- 怎么把新主节点的相关信息**通知**给从节点和客户端?
5.3.3 如何监控节点? —— 心跳机制
哨兵会每隔 1 秒给所有主从节点发送 PING 命令,当主从节点收到 PING 命令后,会发送一个响应命令给哨兵,即可判断它们是否在正常运行。(心跳机制)
如果主节点或者从节点没有在规定的时间内响应哨兵的 PING 命令,哨兵就会将它们标记为「主观下线」。
5.3.4 如何判断主节点是否真的故障了?
当标记了「主观下线」,有可能主节点其实没有故障,而只是因为主节点的系统压力比较大导致网络阻塞,所以才没有在规定时间内响应哨兵的PING命令。因此,针对主节点,哨兵机制设置了「客观下线」状态。
一般来说,为了减少误判,一般会用多个节点部署成**哨兵集群**(最少需要三台机器来部署哨兵集群),通过多个哨兵节点一起判断,就可以就可以避免单个哨兵因为自身网络状况不好,而误判主节点下线的情况。
如何判断主节点为「客观下线」呢?
当一个哨兵判断主节点为「主观下线」后,就会向其他哨兵发起命令。其他哨兵节点收到这个命令,会根据自身和主节点的网络状况,做出赞成投票或拒绝投票的响应。当这个哨兵的**赞同票数达到哨兵配置文件中的 quorum
配置项设定的值后,这时主节点就会被该哨兵标记为「客观下线」**。
PS:quorum 的值一般设置为哨兵个数的二分之一加 1,例如 3 个哨兵就设置 2。(超过半数)
哨兵判断完主节点客观下线后,哨兵就要开始在多个「从节点」中,选出一个从节点来做新主节点。
5.3.5 由哪个哨兵进行主从故障转移? —— 领导者选举
当主节点客观下线后,需要哨兵从从节点中选出一个新的主节点,由于有多个哨兵节点,应该**让哪个哨兵节点负责呢?**
哨兵集群中通过**「领导者选举」来选出一个哨兵节点作为Leader,让Leader进行主从切换。一般来说,哪个哨兵节点判断主节点为「客观下线」,这个哨兵节点就是候选者,所谓的候选者就是想当 Leader 的哨兵。候选者会向其他哨兵发送命令**,表明希望成为 Leader 来执行主从切换,并让所有其他哨兵对它进行投票。
每个哨兵只有一次投票机会,如果用完后就不能参与投票了,可以投给自己或投给别人,但是只有候选者才能把票投给自己。任何一个「候选者」,要满足两个条件则会成为Leader:
- 第一,拿到半数以上的赞成票;
- 第二,拿到的票数同时还需要大于等于哨兵配置文件中的
quorum
值。
所以,为了保证Leader选举顺利,至少需要3个哨兵节点,quorum 的值建议设置为哨兵个数的二分之一加 1,例如 3 个哨兵就设置 2,5 个哨兵设置为 3,而且哨兵节点的数量应该是奇数。
5.3.6 主从故障转移的过程是怎样的?
在哨兵集群中通过投票的方式,选举出了哨兵 leader 后,就可以进行主从故障转移的过程了
主从故障转移操作包含以下四个步骤:
-
第一步:在已下线主节点(旧主节点)属下的所有「从节点」里面,挑选出一个从节点,并将其转换为**「新主节点」**;
(1)将网络状态不好的从节点过滤掉;
(2)第一轮考察:节点的优先级(显式的根据每个节点的性能配置的);
(2)第二轮考察:复制进度最靠前的节点;
(3)第三轮考察:ID号更小的节点;
-
第二步:让已下线主节点属下的所有「从节点」修改复制目标为**「新主节点」**;
哨兵 leader 向所有从节点发送
SLAVEOF
,让它们成为新主节点的从节点: -
第三步:将「新主节点」的IP地址和信息,通过「发布者/订阅者机制」通知给客户端;
**通过 Redis 的发布者/订阅者机制来实现**的。每个哨兵节点提供发布者/订阅者机制,客户端可以从哨兵订阅消息。哨兵提供的消息订阅频道有很多,不同频道包含了主从节点切换过程中的不同关键事件,几个常见的事件如下:
-
第四步:继续监视旧主节点,当其重新上线时,将它设置为新主节点的从节点;
5.3.7 哨兵集群是如果组成的?
前面提到了Redis中的哨兵机制是通过一个哨兵集群(多个哨兵节点)组成的,那是如何组成哨兵集群的,它们之间是如何感知对方的?
哨兵节点之间是通过 Redis 的发布者/订阅者机制来相互发现的,主节点中有一个名为__sentinel__:hello
的频道,不同的哨兵就是通过这个频道来相互发现和互相通信的。如下图所示,哨兵 A 把自己的 IP 地址和端口的信息发布到__sentinel__:hello
频道上,哨兵 B 和 C 订阅了该频道。那么此时,哨兵 B 和 C 就可以从这个频道直接获取哨兵 A 的 IP 地址和端口号。然后,哨兵 B、C 可以和哨兵 A 建立网络连接。
哨兵节点之间是通过向主节点发送INFO
命令来获取所有「从节点」的信息,哨兵节点需要监视所有的节点,但是它并不知道从节点的信息,所以哨兵会**每隔10秒**向主节点来获取从节点的信息。
5.4 切片集群模式
5.4.1 为什么需要切片集群模式?
Redis用作缓存时,如果缓存数据量很大,无法在一台服务器上缓存,需要使用**Redis切片集群**(Redis Cluster ),数据可以分布在不同的服务器上,降低系统对单主节点的依赖。
5.4.2 切片集群是如何实现的?
对于切片集群,最重要的问题就是决定**一个缓存数据要放到哪一台服务器上。Redis Cluster 采用哈希槽(Hash Slot),来处理数据和节点之间的映射关系。在 Redis Cluster 方案中,一个切片集群共有 16384 个哈希槽**,每个数据(键值对)都会根据它的key,被映射到一个哈希槽中:
- 根据key,使用**CRC16**算法计算一个16 bit的值;
- 将这个16 bit的值对16384取模,得到一个0~16383之间的数,这个数就代表着哈希槽的编号。
然后,需要将这些哈希槽映射到各个Redis节点中,有两种映射方案:
- 平均分配:使用
cluster create
命令创建 Redis 集群时,Redis会自动把所有哈希槽平均分布到集群节点上。比如集群中有 9 个节点,则每个节点上槽的个数为 16384/9 个。 - 手动分配:使用
cluster meet
命令手动建立节点间的连接,组成集群,再使用cluster addslots
命令,指定每个节点上的哈希槽个数。需要把 16384 个槽都分配完,否则 Redis 集群无法正常工作。
5.5 集群脑裂导致数据丢失怎么办?
5.5.1 集群脑裂是怎么出现的,如何导致数据丢失?
脑裂:是指的一个集群有多个主节点。
Redis的主从架构中,主节点提供写操作,从节点提供读操作。如果主节点由于网络问题与其他从节点失联,但是与客户端之间网络正常。此时,客户端新发送的写请求操作会被主节点存放到缓存区中。
哨兵发现了主节点失联,判断为客观下线后会在集群内布重新选择一个主节点,这时集群就有两个主节点了 — 脑裂出现了。
如果此时网络恢复,由于在集群中,「旧主节点」已经被降级成为了从节点,所以「旧主节点」会向「新主节点」申请数据同步,并清空该节点中的数据。这样就会**导致断联期间客户端的一些写入的数据丢失,这就是集群产生脑裂数据丢失的问题。**
5.5.2 该如何解决?
Redis使用的策略是:当主节点发现从节点的连接数量少于某个阈值或者主从数据复制和同步的延迟超过了某个阈值,就会禁止主节点进行写入操作,直接返回错误给客户端。
即:主库连接的从库中至少有 N 个从库,和主库进行数据复制时的 ACK 消息延迟不能超过 T 秒,否则,主库就不会再接收客户端的写请求了。
资料参考
内容大多参考自:图解Redis介绍 | 小林coding (xiaolincoding.com)