redis集群

当数据量变大时,主从复制不能满足大数据量的情况,需要对数据进行拆分。

业界常见的解决方案有两种,一是引入 Proxy 层来向应用端屏蔽身后的集群分布,客户端可以借助 Proxy 层来进行请求转发和 Key 值的散列从而进行进行数据分片,这种方案会损失部分性能但是迁移升级等运维操作都很方便,业界 Proxy 方案的代表有 Twitter 的 Twemproxy 和豌豆荚的 Codis;二是 smart client 方案,即将 Proxy 的逻辑放在客户端做,客户端根据维护的映射规则和路由表直接访问特定的 Redis 实例,但是增减 Redis 实例都需要重新调整分片逻辑(客户端需要感知)。

性能:redis采用的不一样,采用去中心化的设计,即p2p。
一致性:主从采用异步复制,保证高可用的情况下牺牲了部分性能。
可用性:集群模式前靠哨兵保证可以性,集群模式引入了新的故障检测机制,不需要启动哨兵集群,redis集群本身进行自动master选举和failover保证可用性。

一个三主三从的 Redis Cluster,三机房部署(其中一主一从构成一个分片,之间通过异步复制同步数据,一旦某个机房掉线,则分片上位于另一个机房的 slave 会被提升为 master 从而可以继续提供服务) ;每个 master 负责一部分 slot,数目尽量均摊;客户端对于某个 Key 操作先通过公式计算(计算方法见下文)出所映射到的 slot,然后直连某个分片,写请求一律走 master,读请求根据路由规则选择连接的分片节点

读策略:

master-only :
所有的读操作都在主节点上操作,适合对于一致性要求非常高的业务。

master-slave:
负载均衡,主从节点均负责一部分的读操作。

slave-only:
只读从节点,适合用于读写分离场景,主节点只负责写操作。

集群模式无中心节点,数据按照slot存储分布于多个redis实例中,平滑的进行扩缩容(复杂但保证高可用),可以自动故障转移,通过gossip协议交换状态信息,投票机制完成salve升主,降低运维成本。

哈希槽:
数据分片借助哈希槽,预先划分16384个slot,在对key操作时都会先散列出值来匹配hash槽,当所有hash槽都被分配时集群才可用,动态添加减少主节点时,hash槽也需要进行迁移和再分配。
其中又有一个hash标签的说法,会选取第一个{和第一个}中的内容,确保可以让某几个键放入同一个槽

hash槽的内部实现,Redis 集群中每个节点都会维护集群中所有节点的 clusterNode 结构体,其中的 slots 属性是个二进制位数组,长度为 2048 bytes,共包含 16384 个 bit 位,节点可以根据某个 bit 的 0/1 值判断对应的 slot 是否由当前节点处理。每个节点通过 clusterStats 结构体来保存从自身视角看去的集群状态,其中 nodes 属性是一个保存节点名称和 clusterNode 指针的字典,而 slots 数组是一个记录哪个 slot 属于哪个 clusterNode 结构体的数组。

可以理解为每个节点都持有n个结构体,里面有slot的二进制位数组(2048bytes),每位表示该slot是否属于某节点,以此判断集群状态

slot 在迁移过程有两个状态,在迁出节点会对该 slot 标记为 MIGRATING,在迁入节点会对该 slot 标记为 IMPORTING。当该 slot 内的 Key 都迁移完毕之后,新的 slot 归属信息都进过消息协议进行传播,最终集群中所有节点都会知道该 slot 已经迁移到了目标节点,并更新自身保存的 slot 和节点间的映射关系。

我们通过 redis-cli 可以发起对集群的读写请求,节点会计算我们请求的 Key 所属的 slot,一旦发现该 slot 并非由自己负责的话,会向客户端返回一个 MOVED 错误(需要注意的是集群模式下 redis-cli 不会打印 MOVED 错误而是会直接显示 Redirected,使用单机版 redis-cli 连接则可以看到 MOVED 错误),指引客户端重定向到正确的节点,并再次发送先前的命令,得到正确的结果。

MOVED 意为这个 slot 的负责已经永久转交给另一个节点,因此可以直接把请求准发给现在负责该 slot 的节点。但是考虑在 slot 迁移过程中,会出现属于该 slot 的一部分 Key 已经迁移到目的地节点,而另一部分 Key 还在源节点,那如果这时收到了关于这个 slot 的请求,那么源节点会先在自己的数据库里查找是否有这个 Key,查到的话说明还未迁移那么直接返回结果,查询失败的话就说明 Key 已经迁移到目的地节点,那么就向客户端返回一个 ASK 错误,指引客户端转向目的地节点查询该 Key。同样该错误仅在单机版 redis-cli 连接时打印。

这两个错误在实际线上环境中出现频率很高,那么定制化的客户端如何处理这二者呢?如果客户端每次都随机连接一个节点然后利用 MOVED 或者 ASK 来重定向其实是很低效的,所以一般客户端会在启动时通过解析 CLUSTER NODES 或者 CLUSTER SLOTS 命令返回的结果得到 slot 和节点的映射关系缓存在本地,一旦遇到 MOVED 或者 ASK 错误时会再次调用命令刷新本地路由(因为线上集群一旦出现 MOVED 或者是 ASK 往往是因为扩容分片导致数据迁移,涉及到许多 slot 的重新分配而非单个,因此需要整体刷新一次),这样集群稳定时可以直接通过本地路由表迅速找到需要连接的节点。

故障检测

Redis 集群中的每个节点都会定期向集群中的其他节点发送 PING 消息,以此来检测对方是否存活,如果接收 PING 消息的节点在规定时间内(node_timeout)没有回复 PONG 消息,那么之前向其发送 PING 消息的节点就会将其标记为疑似下线状态(PFAIL)。每次当节点对其他节点发送 PING 命令的时候,它都会随机地广播三个它所知道的节点的信息,这些信息里面的其中一项就是说明节点是否已经被标记为 PFAIL 或者 FAIL。当节点接收到其他节点发来的信息时,它会记下那些被集群中其他节点标记为 PFAIL 的节点,这称为失效报告(failure report)。如果节点已经将某个节点标记为 PFAIL ,并且根据自身记录的失效报告显示,集群中的大部分 master 也认为该节点进入了 PFAIL 状态,那么它会进一步将那个失效的 master 的状态标记为 FAIL 。随后它会向集群广播 “该节点进一步被标记为 FAIL ” 的这条消息,所有收到这条消息的节点都会更新自身保存的关于该 master 节点的状态信息为 FAIL。

故障转移:

集群节点创建时,不管是 master 还是 slave,都置 currentEpoch 为 0。当前节点接收到来自其他节点的包时,如果发送者的 currentEpoch(消息头部会包含发送者的 currentEpoch)大于当前节点的currentEpoch,那么当前节点会更新 currentEpoch 为发送者的 currentEpoch。因此,集群中所有节点的 currentEpoch 最终会达成一致,相当于对集群状态的认知达成了一致。

currentEpoch 作用在于,当集群的状态发生改变,某个节点为了执行一些动作需要寻求其他节点的同意时,就会增加 currentEpoch 的值。目前 currentEpoch 只用于 slave 的故障转移流程,这就跟哨兵中的sentinel.current_epoch 作用是一模一样的。当 slave A 发现其所属的 master 下线时,就会试图发起故障转移流程。首先就是增加 currentEpoch 的值,这个增加后的 currentEpoch 是所有集群节点中最大的。然后slave A 向所有节点发起拉票请求,请求其他 master 投票给自己,使自己能成为新的 master。其他节点收到包后,发现发送者的 currentEpoch 比自己的 currentEpoch 大,就会更新自己的 currentEpoch,并在尚未投票的情况下,投票给 slave A,表示同意使其成为新的 master。
结论:currentEpoch相当于一种对集群状态更新级别的认可,主要用于主从切换。

每一个 master 在向其他节点发送包时,都会附带其 configEpoch 信息,以及一份表示它所负责的 slots 信息。而 slave 向其他节点发送包时,其包中的 configEpoch 和负责槽位信息,是其 master 的 configEpoch 和负责的 slot 信息。节点收到包之后,就会根据包中的 configEpoch 和负责的 slots 信息,记录到相应节点属性中。

configEpoch 主要用于解决不同的节点的配置发生冲突的情况。举个例子就明白了:节点A 宣称负责 slot 1,其向外发送的包中,包含了自己的 configEpoch 和负责的 slots 信息。节点 C 收到 A 发来的包后,发现自己当前没有记录 slot 1 的负责节点(也就是 server.cluster->slots[1] 为 NULL),就会将 A 置为 slot 1 的负责节点(server.cluster->slots[1] = A),并记录节点 A 的 configEpoch。后来,节点 C 又收到了 B 发来的包,它也宣称负责 slot 1,此时,如何判断 slot 1 到底由谁负责呢?

这就是 configEpoch 起作用的时候了,C 在 B 发来的包中,发现它的 configEpoch,要比 A 的大,说明 B 是更新的配置。因此,就将 slot 1 的负责节点设置为 B(server.cluster->slots[1] = B)。在 slave 发起选举,获得足够多的选票之后,成功当选时,也就是 slave 试图替代其已经下线的旧 master,成为新的 master 时,会增加它自己的 configEpoch,使其成为当前所有集群节点的 configEpoch 中的最大值。这样,该 slave 成为 master 后,就会向所有节点发送广播包,强制其他节点更新相关 slots 的负责节点为自己。

结论:configEpoch同样用于对集群状态更新级别的认可,不同的是用于对其他节点对slot归属的认可。

自动 Failover(故障转移)

当一个 slave 发现自己正在复制的 master 进入了已下线(FAIL)状态时,slave 将开始对已下线状态的 master 进行故障转移,以下是故障转移执行的步骤

该下线的 master 下所有 slave 中,会有一个 slave 被选中。具体的选举流程为:slave 自增它的 currentEpoch 值,然后向其他 masters 请求投票,每个 slave 都向集群其他节点广播一条 CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST 消息用于拉票,集群中具有投票权的 master 收到消息后,如果在当前选举纪元中没有投过票,就会向第一个发送来消息的 slave 返回 CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK 消息,表示投票给该 slave。某个 slave 如果在一段时间内收到了大部分 master 的投票,则表示选举成功。

被选中的 slave 会执行 SLAVEOF no one 命令,成为新的 master

新的 master 会撤销所有对已下线 master 的 slot 指派,并将这些 slot 全部指派给自己

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

新的 master 开始接收和自己负责处理的 slot 有关的命令请求,故障转移完成

手动 Failover

Redis 集群支持手动故障转移,也就是向 slave 发送 CLUSTER FAILOVER 命令,使其在 master 未下线的情况下,发起故障转移流程,升级为新的 master ,而原来的 master 降级为 slave。

为了不丢失数据,向 slave 发送 CLUSTER FAILOVER 命令后,流程如下:

slave 收到命令后,向 master 发送 CLUSTERMSG_TYPE_MFSTART 命令

master 收到该命令后,会将其所有客户端置于阻塞状态,也就是在 10s 的时间内,不再处理客户端发来的命令,并且在其发送的心跳包中,会带有 CLUSTERMSG_FLAG0_PAUSED 标记

slave 收到 master 发来的,带 CLUSTERMSG_FLAG0_PAUSED 标记的心跳包后,从中获取 master 当前的复制偏移量,slave 等到自己的复制偏移量达到该值后,才会开始执行故障转移流程:发起选举、统计选票、赢得选举、升级为 master 并更新配置

CLUSTER FAILOVER 命令支持两个选项:FORCE 和 TAKEOVER。使用这两个选项,可以改变上述的流程。

如果有 FORCE 选项,则 slave 不会与 master 进行交互,master 也不会阻塞其客户端,而是 slave 立即开始故障转移流程:发起选举、统计选票、赢得选举、升级为 master 并更新配置。

如果有 TAKEOVER 选项,则更加简单直接,slave 不再发起选举,而是直接将自己升级为 master ,接手原 master 的 slot,增加自己的 configEpoch 后更新配置。

因此,使用 FORCE 和 TAKEOVER 选项,master 可以已经下线;而不使用任何选项,只发送 CLUSTER FAILOVER 命令的话,master 必须在线。

Redis 集群中的消息

每次发送 MEET、PING、PONG 消息时,发送者都从自己的已知节点列表中随机选出两个节点(可以是主节点或者从节点),并将这两个被选中节点的信息分别保存到两个结构中。当接收者收到消息时,接收者会访问消息正文中的两个结构,并根据自己是否认识 clusterMsgDataGossip 结构中记录的被选中节点进行操作:

如果被选中节点不存在于接收者的已知节点列表,那么说明接收者是第一次接触到被选中节点,接收者将根据结构中记录的IP地址和端口号等信息,与被选择节点进行握手。

如果被选中节点已经存在于接收者的已知节点列表,那么说明接收者之前已经与被选中节点进行过接触,接收者将根据 clusterMsgDataGossip 结构记录的信息,对被选中节点对应的 clusterNode 结构进行更新。

有了消息之后,如何选择发送消息的目标节点呢?虽然 PING PONG 发送的频率越高就可以越实时得到其它节点的状态数据,但 Gossip 消息体积较大,高频发送接收会加重网络带宽和消耗 CPU 的计算能力,因此每次 Redis 集群都会有目的性地选择一些节点;但节点选择过少又会影响故障判断的速度

集群数据一致性

异步复制

master 以及对应的 slaves 之间使用异步复制机制,考虑如下场景:

写命令提交到 master,master 执行完毕后向客户端返回 OK,但由于复制的延迟此时数据还没传播给 slave;如果此时 master 不可达的时间超过阀值,此时集群将触发 failover,将对应的 slave 选举为新的master,此时由于该 slave 没有收到复制流,因此没有同步到 slave 的数据将丢失。

脑裂(split-brain)

在发生网络分区时,有可能出现新旧 master 同时存在的情况,考虑如下场景:

由于网络分区,此时 master 不可达,且客户端与 master 处于一个分区,并且由于网络不可达,此时客户端仍会向 master 写入。由于 failover 机制,将其中一个 slave 提升为新的 master,等待网络分区消除后,老的 master 再次可达,但此时该节点会被降为 slave 清空自身数据然后复制新的 master ,而在这段网络分区期间,客户端仍然将写命令提交到老的 master,但由于被降为 slave 角色这些数据将永远丢失。

大多数 master 宕机的情况

如果集群中少数 master 节点宕机,那么 Redis Cluster 依靠自身的机制可以提升相应的 slave 为 master 对外继续提高服务,但是一旦集群中大多数 master 节点都挂掉,那么集群本质上已经不可用了,即使双机房部署下,你的 slave 全部存活也没办法,那么如何解决这个问题呢?redis-cluster的容灾机制是基于gossip协议进行协商,对挂了的主进行切换到从。所以它如果面对部分主节点宕机,能够很快的进行主从切换。但是它的问题是,如果一半的主节点都宕机了,那么它就无法协商出正确的主从节点了,集群此时就不可用了。

方案一
集群三个机房部署,主节点分别部署在三个机房内。

好处:简单,去中心化。

坏处:对于写请求,部分要跨机房

方案二
在这里插入图片描述

部署的时候,必须主节点和从节点分布在两个机房中。监控一旦发现,发生了主从切换了,将会告警告知出来,DBA进行切换。保证主节点和从节点均在一个机房内。

一旦发生主机房全面宕机,或者集群的一半节点发生故障。比如在idc 1里面发生了一半的节点发生了故障,idc 2内部署的Monitor发现这种情况,将主动的将idc 2里面的原来的slave提升至master,组成一个新的集群。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值