(四)Redis集群模式
1.Redis Cluster
1.1 Redis 集群方案的演变
大规模数据存储系统都会面临的一个问题就是如何横向拓展。当你的数据集越来越大,一主多从的模式已经无法支撑这么大量的数据存储,于是你首先考虑将多个主从模式结合在一起对外提供服务,但是这里有两个问题就是如何实现数据分片的逻辑和在哪里实现这部分逻辑?业界常见的解决方案有两种,一是引入 Proxy 层来向应用端屏蔽身后的集群分布,客户端可以借助 Proxy 层来进行请求转发和 Key 值的散列从而进行进行数据分片,这种方案会损失部分性能但是迁移升级等运维操作都很方便,业界 Proxy 方案的代表有 Twitter 的 Twemproxy 和豌豆荚的 Codis;二是 smart client 方案,即将 Proxy 的逻辑放在客户端做,客户端根据维护的映射规则和路由表直接访问特定的 Redis 实例,但是增减 Redis 实例都需要重新调整分片逻辑。
1.2 Redis Cluster 简介
Redis 集群是一个分布式(distributed)、容错(fault-tolerant)的 Redis 实现, 集群可以使用的功能是普通单机 Redis 所能使用的功能的一个子集(subset)。Redis 3.0 版本开始官方正式支持集群模式,Redis 集群模式提供了一种能将数据在多个节点上进行分区存储的方法,采取了和上述两者不同的实现方案——去中心化的集群模式,集群通过分片进行数据共享,分片内采用一主多从的形式进行副本复制,并提供复制和故障恢复功能。
1.3 去中心化的集群模式特点
- 所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽。
- 节点的fail是通过集群中超过半数的节点检测失效时才生效。
- 客户端与redis节点直连,不需要中间proxy层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。
- redis-cluster把所有的物理节点映射到[0-16383]slot上(不一定是平均分配),cluster 负责维护node<->slot<->value。
- Redis集群预分好16384个桶,当需要在 Redis 集群中放置一个 key-value 时,根据 CRC16(key) mod 16384的值,决定将一个key放到哪个桶中。
下图是一个三主三从的 Redis Cluster,三机房部署(其中一主一从构成一个分片,之间通过异步复制同步数据,一旦某个机房掉线,则分片上位于另一个机房的 slave 会被提升为 master 从而可以继续提供服务) ;每个 master 负责一部分 slot,数目尽量均摊;客户端对于某个 Key 操作先通过公式计算(计算方法见下文)出所映射到的 slot,然后直连某个分片,写请求一律走 master,读请求根据路由规则选择连接的分片节点。
1.4 三种集群方案的优缺点
集群模式 |
优点 |
缺点 |
---|---|---|
客户端分片 |
不使用第三方中间件,实现方法和代码可以自己掌控并且可随时调整。这种分片性能比代理式更好(因为少了分发环节),分发压力在客户端,无服务端压力增加 |
不能平滑地水平扩容,扩容/缩容时,必须手动调整分片程序,出现故障不能自动转移,难以运维 |
代理层分片 |
运维成本低。业务方不用关心后端 Redis 实例,跟操作单点 Redis 实例一样。Proxy 的逻辑和存储的逻辑是隔离的 |
代理层多了一次转发,性能有所损耗;进行扩容/缩容时候,部分数据可能会失效,需要手动进行迁移,对运维要求较高,而且难以做到平滑的扩缩容;出现故障,不能自动转移,运维性很差。Codis 做了诸多改进,相比于 Twemproxy 可用性和性能都好得多 |
Redis Cluster |
无中心节点,数据按照 slot 存储分布在多个 Redis 实例上,平滑的进行扩容/缩容节点,自动故障转移(节点之间通过 Gossip 协议交换状态信息,进行投票机制完成 slave 到 master 角色的提升)降低运维成本,提高了系统的可扩展性和高可用性 |
开源版本缺乏监控管理,原生客户端太过简陋,failover 节点的检测过慢,维护 Membership 的 Gossip 消息协议开销大,无法根据统计区分冷热数据 |
2. 哈希槽
2.1 什么是哈希槽
Redis Cluster 中,数据分片借助哈希槽 (下文均称 slot) 来实现,集群预先划分 16384 个 slot,对于每个请求集群的键值对,根据 Key 进行散列生成的值唯一匹配一个 slot。Redis Cluster 中每个分片的 master 负责 16384 个 slot 中的一部分,当且仅当每个 slot 都有对应负责的节点时,集群才进入可用状态。当动态添加或减少节点时,需要将 16384 个 slot 做个再分配,slot 中的键值也要迁移。
2.2 哈希槽的好处
使用哈希槽的好处就在于可以方便的添加或移除节点。
当需要增加节点时,只需要把其他节点的某些哈希槽挪到新节点就可以了;
当需要移除节点时,只需要把移除节点上的哈希槽挪到其他节点就行了;
在这一点上,我们以后新增或移除节点的时候不用先停掉所有的 redis 服务。
3. 故障检测
跟大多数分布式系统一样,Redis Cluster 的节点间通过持续的 heart beat 来保持信息同步,不过 Redis Cluster 节点信息同步是内部实现的,并不依赖第三方组件如 Zookeeper。集群中的节点持续交换 PING、PONG 数据,消息协议使用 Gossip,这两种数据包的数据结构一样,之间通过 type 字段进行区分。
Redis 集群中的每个节点都会定期向集群中的其他节点发送 PING 消息,以此来检测对方是否存活,如果接收 PING 消息的节点在规定时间内(node_timeout)没有回复 PONG 消息,那么之前向其发送 PING 消息的节点就会将其标记为疑似下线状态(PFAIL)。每次当节点对其他节点发送 PING 命令的时候,它都会随机地广播三个它所知道的节点的信息,这些信息里面的其中一项就是说明节点是否已经被标记为 PFAIL 或者 FAIL。当节点接收到其他节点发来的信息时,它会记下那些被集群中其他节点标记为 PFAIL 的节点,这称为失效报告(failure report)。如果节点已经将某个节点标记为 PFAIL ,并且根据自身记录的失效报告显示,集群中的大部分 master 也认为该节点进入了 PFAIL 状态,那么它会进一步将那个失效的 master 的状态标记为 FAIL 。随后它会向集群广播 “该节点进一步被标记为 FAIL ” 的这条消息,所有收到这条消息的节点都会更新自身保存的关于该 master 节点的状态信息为 FAIL。
4. 故障转移(Failover)
4.1 纪元(epoch)
Redis Cluster 使用了类似于 Raft 算法 term(任期)的概念称为 epoch(纪元),用来给事件增加版本号。Redis 集群中的纪元主要是两种:currentEpoch 和 configEpoch。
4.1.1 currentEpoch
这是一个集群状态相关的概念,可以当做记录集群状态变更的递增版本号。每个集群节点,都会通过 server.cluster->currentEpoch 记录当前的 currentEpoch。
集群节点创建时,不管是 master 还是 slave,都置 currentEpoch 为 0。当前节点接收到来自其他节点的包时,如果发送者的 currentEpoch(消息头部会包含发送者的 currentEpoch)大于当前节点的currentEpoch,那么当前节点会更新 currentEpoch 为发送者的 currentEpoch。因此,集群中所有节点的 currentEpoch 最终会达成一致,相当于对集群状态的认知达成了一致。
4.1.2 currentEpoch 作用
currentEpoch 作用在于,当集群的状态发生改变,某个节点为了执行一些动作需要寻求其他节点的同意时,就会增加 currentEpoch 的值。目前 currentEpoch 只用于 slave 的故障转移流程,这就跟哨兵中的sentinel.current_epoch 作用是一模一样的。当 slave A 发现其所属的 master 下线时,就会试图发起故障转移流程。首先就是增加 currentEpoch 的值,这个增加后的 currentEpoch 是所有集群节点中最大的。然后slave A 向所有节点发起拉票请求,请求其他 master 投票给自己,使自己能成为新的 master。其他节点收到包后,发现发送者的 currentEpoch 比自己的 currentEpoch 大,就会更新自己的 currentEpoch,并在尚未投票的情况下,投票给 slave A,表示同意使其成为新的 master。
4.1.3 configEpoch
这是一个集群节点配置相关的概念,每个集群节点都有自己独一无二的 configepoch。所谓的节点配置,实际上是指节点所负责的槽位信息。
每一个 master 在向其他节点发送包时,都会附带其 configEpoch 信息,以及一份表示它所负责的 slots 信息。而 slave 向其他节点发送包时,其包中的 configEpoch 和负责槽位信息,是其 master 的 configEpoch 和负责的 slot 信息。节点收到包之后,就会根据包中的 configEpoch 和负责的 slots 信息,记录到相应节点属性中。
4.1.4 configEpoch 作用
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 的负责节点为自己。
5 集群数据一致性
Redis 集群尽可能保证数据的一致性,但在特定条件下会丢失数据,原因有两点:异步复制机制以及可能出现的网络分区造成脑裂问题。
5.1 异步复制
master 以及对应的 slaves 之间使用异步复制机制,考虑如下场景:
写命令提交到 master,master 执行完毕后向客户端返回 OK,但由于复制的延迟此时数据还没传播给 slave;如果此时 master 不可达的时间超过阀值,此时集群将触发 failover,将对应的 slave 选举为新的master,此时由于该 slave 没有收到复制流,因此没有同步到 slave 的数据将丢失。
5.2 脑裂(split-brain)
在发生网络分区时,有可能出现新旧 master 同时存在的情况,考虑如下场景:
由于网络分区,此时 master 不可达,且客户端与 master 处于一个分区,并且由于网络不可达,此时客户端仍会向 master 写入。由于 failover 机制,将其中一个 slave 提升为新的 master,等待网络分区消除后,老的 master 再次可达,但此时该节点会被降为 slave 清空自身数据然后复制新的 master ,而在这段网络分区期间,客户端仍然将写命令提交到老的 master,但由于被降为 slave 角色这些数据将永远丢失。