详解redis群集模式

Redis集群介绍

Redis集群一般有四种方式,分别为:主从复制、哨兵模式、Cluster以及各大厂的集群方案

。在3.0版本之前只支持单实例模式,3.0之后支持了集群方式。在3.0之前各大厂为了解决单实例Redis的存储瓶颈问题各自推出了自己的集群方案,其核心思想就是数据分片,主要有客户端分片、代理分片、服务端分片。这里咱们只介绍前三种方式:主从、哨兵、Cluster。

1、主从复制

Redis单节点的数据是存储在一台服务器上的,如果服务器出现故障,会导致数据不可用,而且读写都是在同一台服务器上,请求量大时会出现I/O瓶颈。为了避免单点故障和读写不分离,Redis提供了复制功能来实现Master中的数据向Slave数据库的同步。Master可以有多个Slave节点,Slave节点也可以有Slave节点,从节点是级联结构,如下图所示:



主从复制工作原理

一般情况下为了让数据读写分离

,Master节点

用来执行写操作,Slave节点提供读操作,Master执行写操作时将变化的数据同步到Slave,其工作原理如下图所示:



Redis主从复制基本原理有三种:全量复制、基于长连接的命令传播、增量复制。

首先介绍一下全量复制,当主从服务器刚建立连接的时候,会按照三个阶段完成数据的第一次同步。假设现在有实例1(192.168.1.1)和实例2(192.168.1.2),当我们在实例2上执行“replicaof 192.168.1.1 6379”命令后,实例2就变成了实例1的从库,并开始从实例1上复制数据,有如下三个阶段:



第一个阶段,是主从库之间建立连接、协商同步的过程,为全量复制做准备。具体来说,从库给主库发送psync命令,表示要进行数据同步,主库根据这个命令的参数来启动复制。psync命令包含了主库的runID和复制进度offset两个参数。

•runID:是每个Redis实例启动时自动生成的一个随机ID,用来唯一标记这个实例。当从库和主库第一次复制时,因为不知道主库的runID,所以将runID设置为“?”。

•offset

:设置为-1,表示第一次复制。

主库收到psync命令后,会用FULLRESYNC响应命令带上两个参数:主库runID和主库目前的复制进度offset,返回给从库,从库收到响应后会记录下这两个参数。FULLRESYNC响应表示第一次复制采用的全量复制,也就是说,主库会把当前所有的数据都复制给从库。

第二个阶段,主库将所有数据同步给从库,从库收到数据后,首先清空现有数据,然后在本地完成数据加载。这个过程依赖于内存快照生成的RDB文件。具体来说,主库执行bgsave命令,生成RDB文件,接着将文件发给从库。

第三个阶段,主库会把第二阶段执行过程中新接收到的写命令,再发送给从库。具体来说,当主库完成RDB文件发送后,就会把此时replication buffer

中的修改和新增操作发给从库,从库再重新执行这些操作。这样一来,主从库就实现同步了。

以上是全量复制的基本流程,一旦主从库完成了全量复制,它们之间就会一直维护一个网络连接,主库会通过这个连接将后续陆续收到的命令操作再同步给从库,这个过程也称为基于长连接的命令传播,可以避免频繁建立连接的开销。

长连接是基于网络的,那么它就存在网络断开的风险,在Redis2.8

之前,如果主从库在命令传播时出现了网络闪断,那么从库会和主库重新进行一次全量复制,开销非常大。在Redis2.8开始,网络闪断之后,主从库会采用增量复制的方式继续同步,就只会把主从网络断连期间主库收到的命令同步给从库。

增量复制核心在于repl_backlog_buffer这个缓冲区。当主从库断连后,主库会把断连期间收到的写操作命令写入replication buffer,同时也会写入repl_backlog_buffer

这个缓冲区。repl_backlog_buffer是一个环形缓冲区,主库记录自己写到的位置,从库也记录自己读到的位置。主从连接恢复之后,从库首先给主库发送psync命令,并把自己当前的slave_repl_offset发给主库,主库会判断自己的master_repl_offset和slave_repl_offset之间的差距,一般来说master_repl_offset

会大于slave_repl_offset。此时,主库只用把master_repl_offset和slave_repl_offset之间的命令操作同步给从库就行。

2、哨兵模式

sentinel,中文名哨兵。Redis的sentinel

系统用于管理多个Redis实例,该系统主要执行以下四个任务:

1.监控(Monitoring):Sentinel会不断的检查主服务器和从服务器是否正常运作。

2.自动故障转移(Automatic failover):当主节点不能正常工作时,哨兵会开始自动故障转移操作,它会将失效主节点的其中一个从节点升级为新的主节点,并让其他从节点改为复制新的主节点。

3.通知(Notification):哨兵可以将故障转移的结果发送给客户端

4.配置提供者(Configuration provider):客户端在初始化时,通过连接哨兵来获得当前Redis服务的主节点地址。

其中,监控和自动故障转移功能,使得哨兵可以及时发现主节点故障并完成转移;而配置提供者和通知功能,则需要在与客户端的交互中才能体现。

哨兵用于实现Redis集群

的高可用性,本身也是分布式的,作为一个哨兵集群去运行。Sentinel的进程之间使用流言协议(gossip protocols)来接收关于主服务器是否下线的信息, 并使用投票协议(agreement protocols)来决定是否执行自动故障迁移, 以及选择哪个从服务器作为新的主服务器。下面分别介绍一下监控和自动故障转移的基本原理:

Sentinel集群监控原理



1.每个 Sentinel 以每秒一次的频率向它所知的主从服务器以及其它 Sentinel 实例发送一个 PING 命令。

2.如果一个实例距离最后一次有效回复 PING 命令的时间超过指定的值, 那么这个实例会被 Sentinel 标记为主观下线。

3.正在监视这个主服务器的所有 Sentinel 要以每秒一次的频率确认主服务器的确进入了主观下线状态。

4.有足够数量的 Sentinel 在指定的时间范围内同意这一判断, 那么这个主服务器被标记为客观下线。

5.每个 Sentinel 会以每 10 秒一次的频率向它已知的所有主从服务器发送 INFO 命令。当一个主服务器被 Sentinel 标记为客观下线时, Sentinel 向下线主服务器的所有从服务器发送 INFO 命令的频率会从 10 秒一次改为每秒一次。

6.Sentinel 和其它 Sentinel 协商主节点的状态,如果主节点处于 ODOWN(客观下线) 状态,则投票自动选出新的主节点,将剩余的从节点指向新的主节点进行数据复制。

7.当没有足够数量的 Sentinel 同意主服务器 下线时, 主服务器的客观下线状态就会被移除。主服务器重新向 Sentinel 的 PING 命令返回有效回复时,主服务器主观下线状态就会被移除 。

哨兵是如何对Slave进行监控的呢?当然是通过Master来实现的,哨兵向Master发送INFO命令,Master收到命令后便将Slave列表告诉哨兵。然后哨兵根据Slave列表信息与每一个Slave建立连接,并且根据这个连接持续监控Slave。

Sentinel集群故障自动转移

故障转移简单来说有以下三个流程:

1.Sentinel系统挑选出现故障的主服务器属下的其中一个从服务器,并将选中的从服务器升级为新的主服务器。

2.Sentinel系统向出现故障的主服务器属下的所有从服务器发送新的复制命令

,让他们成为新的主服务器的从服务器,当所有从服务器都开始复制新的主服务器时,故障转移操作执行完毕。

3.Sentinel系统还会继续监听已下线的故障服务器,如果它重新上线时,会将它设置为新的主服务器的从服务器。

示意图



如上图所示,Server1为Master节点,Server2、Server3、Server4为主服务器Server1的从节点,而Sentinel系统正在监视所有4个服务器

故障转移



如上图所示,主服务server1挂掉了,处于下线状态,那么server2

、server3、server4对主服务器的复制操作将被终止,Server2被Sentinel系统升级为新的Master,然后将Server2和Server3转为新Master的从服务器,完成故障转移。同时继续监听已下线的Server1。



如上图所示当Server1恢复后,Sentinel系统将它设置为新的主服务器Server2的从服务器,集群恢复原有状态。

3、Cluster集群

Redis的哨兵模式基本已经可以实现高可用,读写分离 ,但是在这种模式下每台Redis服务器都存储相同的数据,很浪费内存。所以在redis3.0

上加入了Cluster集群模式,实现了Redis的分布式存储,也就是说每台 Redis 节点上存储不同的内容。Redis集群是由多个主从节点群组成的分布式服务集群,具有复制、高可用和分片特性。这种集群模式

没有中心节点,可水平扩展,主要是针对海量数据、高并发、高可用的场景。

Cluster集群模式主要有以下三个特性:

1.分片存储:Redis3.0加入了 Redis 的集群模式,实现了数据的分布式存储,对数据进行分片,将不同的数据存储在不同的master节点上面,从而解决了海量数据的存储问题。

2.指令转换:Redis集群采用去中心化的思想,没有中心节点的说法,对于客户端来说,整个集群可以看成一个整体,可以连接任意一个节点进行操作,就像操作单一Redis实例一样,不需要任何代理中间件

,当客户端操作的key没有分配到该node上时,Redis会返回转向指令,指向正确的Redis节点。

3.主从和哨兵:Redis也内置了高可用机制,支持N个master节点,每个master节点都可以挂载多个slave节点,当master节点挂掉时,集群会提升它的某个slave节点作为新的master节点。



如上图所示,Redis集群可以看成多个主从架构组合起来的,每一个主从架构可以看成一个节点。

Redis集群数据分片原理

集群的整个数据库被分为 16384 个槽(slot),数据库中的每个键都属于这 16384 个槽的其中一个,集群中的每个节点可以处理 0 个或最多 16384 个槽。

Key 与哈希槽映射过程可以分为两大步骤:

1.根据键值对的 key,使用 CRC16 算法,计算出一个 16 bit 的值。

2.将 16 bit 的值对 16384 执行取模,得到 0 ~ 16383 的数表示 key 对应的哈希槽。

另外,Cluster 还允许用户强制某个 key 挂在特定槽位上,通过在 key 字符串里面嵌入 tag 标记,这就可以强制 key 所挂在的槽位等于 tag 所在的槽位。



Cluster集群请求路由方式

客户端直连 Redis 服务,进行读写操作时,若Key 对应的 Slot在当前直连的节点上,则可直接读写,但也有可能并不在当前直连的节点上,则经过“重定向”才能转发到正确的节点,如下图所示



和普通的查询路由相比,Redis Cluster 借助客户端实现的请求路由是一种混合形式的查询路由,它并非从一个 Redis 节点到另外一个 Redis,而是借助客户端转发到正确的节点。实际应用中,可以在客户端缓存 Slot 与 Redis 节点的映射关系,当接收到 MOVED 响应时修改缓存中的映射关系。如此,基于保存的映射关系,请求时会直接发送到正确的节点上,从而减少一次交互,提升效率。

那么客户端具体是怎么确定访问的数据到底分布在哪个实例上呢?

Redis 实例会将自己的哈希槽信息通过 Gossip 协议发送给集群中其他的实例,实现了哈希槽分配信息的扩散。这样,集群中的每个实例都有所有哈希槽与实例之间的映射关系信息。在切片数据的时候是将 key 通过 CRC16 计算出一个值再对 16384 取模得到对应的 Slot,这个计算任务可以在客户端上执行发送请求的时候执行。但是,定位到槽以后还需要进一步定位到该 Slot 所在 Redis 实例。当客户端连接任何一个实例,实例就将哈希槽与实例的映射关系响应给客户端,客户端就会将哈希槽与实例映射信息缓存在本地。当客户端请求时,会计算出键所对应的哈希槽,在通过本地缓存的哈希槽实例映射信息定位到数据所在实例上,再将请求发送给对应的实例。



这个时候大家可能会有个疑问:哈希槽与实例之间的映射关系由于新增实例或者负载均衡重新分配导致改变了咋办?

集群中的实例通过 Gossip 协议互相传递消息获取最新的哈希槽分配信息,但是,客户端无法感知。Redis Cluster 提供了重定向机制:客户端将请求发送到实例上,这个实例没有相应的数据,该 Redis 实例会告诉客户端将请求发送到其他的实例上。

Redis 如何告知客户端重定向访问新实例呢?分为两种情况:MOVED 错误、ASK 错误。

**MOVED 错误(负载均衡,数据已经迁移到其他实例上):**当客户端将一个键值对操作请求发送给某个实例,而这个键所在的槽并非由自己负责的时候,该实例会返回一个 MOVED 错误指引转向正在负责该槽的节点。

(error) MOVED 16330 172.17.18.2:6379

该响应表示客户端请求的键值对所在的哈希槽 16330 迁移到了 172.17.18.2 这个实例上,端口是 6379。这样客户端就与 172.17.18.2:6379 建立连接,并发送 GET 请求。同时,客户端还会更新本地缓存,将该 slot 与 Redis 实例对应关系更新正确。



ASK 错误:如果某个 slot 的数据比较多,部分迁移到新实例,还有一部分没有迁移咋办?

如果请求的 key 在当前节点找到就直接执行命令,否则就需要 ASK 错误响应了,槽部分迁移未完成的情况下,如果需要访问的 key 所在 Slot 正在从从 实例 1 迁移到 实例 2,实例 1 会返回客户端一条 ASK 报错信息:客户端请求的 key 所在的哈希槽正在迁移到实例 2 上,你先给实例 2 发送一个 ASKING 命令,接着发送操作命令。

(error) ASK 16330 172.17.18.2:6379

比如客户端请求定位到 key的槽16330 在实例 172.17.18.1 上,节点1如果找得到就直接执行命令,否则响应 ASK 错误信息,并指引客户端转向正在迁移的目标节点 172.17.18.2:6379



注意:ASK 错误指令并不会更新客户端缓存的哈希槽分配信息。所以客户端再次请求 Slot 16330 的数据,还是会先给 172.17.18.1 实例发送请求,只不过节点会响应 ASK 命令让客户端给新实例发送一次请求。MOVED指令则更新客户端本地缓存,让后续指令都发往新实例。

Cluster集群选举算法

1.集群的配置纪元 +1,是一个自曾计数器,初始值 0 ,每次执行故障转移都会 +1。

2.检测到主节点下线的从节点向集群广播一条
CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,要求所有收到这条消息、并且具有投票权的主节点向这个从节点投票。

3.这个主节点尚未投票给其他从节点,那么主节点将向要求投票的从节点返回一条
CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,表示这个主节点支持从节点成为新的主节点。

4.参与选举的从节点都会接收
CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,如果收集到的票 >= (N/2) + 1 支持,那么这个从节点就被选举为新主节点。

5.如果在一个配置纪元里面没有从节点能收集到足够多的支持票,那么集群进入一个新的配置纪元,并再次进行选举,直到选出新的主节点为止。

流程如下图所示:



Cluster集群故障转移

Redis集群的故障转移主要有三个流程:故障检测

、选主流程、故障转移,下面分别简单介绍一下。

故障检查

一个节点认为某个节点失联了并不代表所有的节点都认为它失联了。只有当大多数负责处理 slot 的节点都认定了某个节点下线了,集群才认为该节点需要进行主从切换。Redis 集群节点采用 Gossip协议来广播自己的状态以及自己对整个集群认知的改变。比如一个节点发现某个节点失联了 (PFail),它会将这条信息向整个集群广播,其它节点也就可以收到这个节点的失联信息。

如果一个节点收到了某个节点失联的数量 (PFail Count) 已经达到了集群的大多数,就可以标记该节点为确定下线状态 (Fail),然后向整个集群广播,强迫其它节点也接收该节点已经下线的事实,并立即对该失联节点进行主从切换。

选主流程

参考上一节的“Cluster集群选举算法”

故障转移

当一个 Slave 发现自己的主节点进入已下线状态后,从节点将开始对下线的主节点进行故障转移。

1.从下线的 Master 节点的 Slave 节点列表选择一个节点成为新主节点。

2.新主节点会撤销所有对已下线主节点的 slot 指派,并将这些 slots 指派给自己。

3.新的主节点向集群广播一条 PONG 消息,这条 PONG 消息可以让集群中的其他节点立即知道这个节点已经由从节点变成了主节点,并且这个主节点已经接管了原本由已下线节点负责处理的槽。

4.新的主节点开始接收处理槽有关的命令请求,故障转移完成。

Cluster集群扩容与缩容

扩容

Redis集群主要有两种扩容方式:垂直扩容和水平扩容。

垂直扩容:增加内存方式来增加缓存实例的系统容量,比如从2G增加到4G。

水平扩容:通过增加节点的方式来增加整个缓存系统的容量。

垂直扩容比较方便,但是受制于机制内存的限制,一个机器不可能无限增大内存, 所以到了一定阶段肯定要进行水平扩容。下面我们主要讲一下水平扩容。

水平扩容又有两种方式:1、主节点数量不变;2、增加主节点数量

1、主节点数量不变:比如,当前有一台物理机 A,构建了一个包含3个 Redis 实例的集群;扩容时,我们新增一台物理机 B,拉起一个 Redis 实例并加入物理机 A 的集群;B 上 Redis 实例对 A 上的一个主节点进行复制,然后进行主备倒换;如此,Redis 集群还是3个主节点,只不过变成了 A2-B1 的结构,将一部分请求压力分担到了新增的节点上,同时物理容量上限也会增加,主要步骤如下:

1.将新增节点加入集群;

2.将新增节点设置为某个主节点的从节点,进而对其进行复制;

3.进行主备倒换,将新增的节点调整为主。

2、增加主节点数量:不增加主节点数量的方式扩容比较简单,但是,从负载均衡的角度来看,并不是很好的选择。例如,如果主节点数量较少,那么单个节点所负责的 Slot 的数量必然较多,很容易出现大量 Key 的读写集中于少数节点的现象,而增加主节点的数量,可以更有效的分摊访问压力,充分利用资源。主要步骤如下:

1.将新增节点加入集群;

2.将集群中的部分 Slot 迁移至新增的节点。

缩容

•如果下线的是slave,那么通知其他节点忘记下线的节点

•如果下线的是master,那么将此master的slot迁移到其他master之后,通知其他节点忘记此master节点

•其他节点都忘记了下线的节点之后,此节点就可以正常停止服务了

Redis集群测试思路及常见问题

集群搭建好之后,就可以对集群的各种功能和使用进行测试了。一般我们会从两个方面来制定测试计划

:1、集群功能测试;2、集群调优测试

1、集群功能测试

集群功能测试属于最基本的测试,是为了验证集群所提供的各种功能是否能正常使用,主要有以下方面的内容:

•主从节点的数据备份是否正常

•主从节点的切换功能是否正常

•监控及故障转移功能是否正常

•集群扩缩容功能是否正常

集群的功能测试类似于黑盒测试

,内容比较简单,在这里我们就不展开介绍了,下面主要介绍一下集群在使用过程中的调优测试。

2、集群调优测试

集群调优测试,是为了验证集群在提供服务时,如何最大限度避免因各种异常导致数据丢失或者缓存功能失效,提前对配置进行调优或者提前预案,从而保证缓存架构设计是最优的。需要注意的点主要有:集群脑裂、缓存穿透、缓存击穿、缓存雪崩、缓存预热、缓存降级、缓存更新,下面分别介绍一下定义以及解决方案。

集群脑裂

定义

当Redis主从集群环境出现两个主节点为客户端提供服务,这时客户端请求命令可能会发生数据丢失的情况。脑裂产生的场景主要有两个:

1.如果哨兵正在进行选举,故障转移的过程中原主节点恢复和客户端的通信,那么证明原主节点没有真正的故障,这时客户端依旧可以向原主节点正常通信,但是当故障转移结束后,就又产生了一个主节点,这就是脑裂产生的第一个场景,如下图所示:



2. 网络分区,主节点和客户端,哨兵和从库分割为了两个网络,主库和客户端处在一个网络中,从库和哨兵在另外一个网络中,此时哨兵也会发起主从切换,出现两个主节点的情况,如下图所示:



脑裂出现后带来最严重的后果就是数据丢失,为什么会出现数据丢失的问题呢?

主要原因是新主库确定后会向所有的实例发送slave of命令,让所有实例重新进行全量同步,而全量同步首先就会将实例上的数据先清空,所以在主从同步期间在原主库执行的命令将会被清空(上面场景二是同样的道理,在网络分区恢复后原主节点将被降级为从节点,并且执行全量同步导致数据丢失),所以这就是数据丢失的具体原因,如下图所示:



解决方案

应对脑裂的解决办法应该是去限制原主库接收请求,Redis提供了两个配置项:

1.min-replicas-to-write:主库能进行数据同步的最少从库数量,否则主节点拒绝写入。

2.min-replicas-max-lag:主从库间进行数据复制时,从库给主库发送ACK消息的最大延迟(单位s),否则主节点拒绝写入。

这两个配置项必须同时满足,不然主节点拒绝写入。

即使原主假故障,假故障期间也无法响应哨兵心跳,也不能和从库进行同步,自然就无法和从库进行ACK确认。这俩配置项组合要求就无法得到满足,原主库就会被限制接收客户端请求,客户端也就不能在原主库中写新数据。等新主上线,就只有新主能接收和处理客户端请求,此时,新写的数据会被直接写到新主。而原主会被哨兵降为从库,即使它的数据被清空,也不会有新数据的丢失。示例如下:

假设:
min-replicas-to-write=1
min-replicas-max-lag设为12s
哨兵的down-after-milliseconds设为10s

主库因某原因卡住15s,导致哨兵判断主库客观下线,开始进行主从切换。 同时,因原主库卡住15s,没有一个从库能和原主库在12s内进行数据复制,原主库也无法接收客户端请求。主从切换完成后,也只有新主库能接收请求,不会发生脑裂,也就不会发生数据丢失。

**但是,在实际应用中,真的能完全避免数据的丢失吗?**我们看下面的例子:

假设:
min-replicas-to-write 置 1
min-replicas-max-lag 设置为 15s
哨兵的down-after-milliseconds 设置为 10s 哨兵主从切换需要 5s,主库因为某些原因卡住12s,此时,还会发生脑裂吗?主从切换完成后,数据会丢失吗?

主库卡住 12s,达到哨兵设定的切换阈值,所以哨兵会触发主从切换。但哨兵切换时间5s,即哨兵还未切换完成,主库就会从阻塞状态中恢复回来,且没有触发 min-slaves-max-lag 阈值,所以主库在哨兵切换剩下的 3s 内,依旧可以接收客户端的写操作,如果这些写操作还未同步到从库,哨兵就把从库提升为主库了,那么此时也会出现脑裂的情况,之后旧主库降级为从库,重新同步新主库的数据,新主库也会发生数据丢失。

展开阅读全文

本文系作者在时代Java发表,未经许可,不得转载。

如有侵权,请联系nowjava@qq.com删除。

编辑于

关注时代Java

关注时代Java