上一篇介绍了 Redis 的哨兵机制。这节开始介绍 Redis 的切片集群。
当单个 Redis 实例存储的数据越来越多时,其所需的内存空间大小、磁盘空间大小、CPU 处理能力也会越来越高。此时有两种扩展方案:
主从复制和哨兵机制保障了高可用,就读写分离而言虽然从节点扩展了主从的读并发能力,但是写能力和存储能力无法进行扩展。
切片集群,就是指启动多个 Redis 实例组成一个集群,然后按照一定的规则,把收到的数据划分成多份,每一份用一个实例来保存。从而提高整个集群的写和存储能力,实现高可扩展。
对于一个分布式集群来说,它的良好运行离不开集群节点信息和节点状态的正常维护。
通常我们可以选择两种方案进行信息的维护:
Redis Cluster 是 Redis 官方推出的一种分布式解决方案。它通过将数据划分为多个槽,并将这些槽分配到不同的实例上,实现了数据的分布式存储和高可用性。
除了这个方案外,还有很多集群方案,比如在 Redis Cluster 推出前,是使用 Codis+Zookeeper 实现集群的。
为了保证高可用性,集群一般会采用主从架构,最少需要三个主节点,每个主节点建议至少配一个从节点。不同于主从架构,切片集群中的从节点主要实现数据的热备、主备切换,默认不采用读写分离的方式,读写请求都由主节点完成。
如果要提升高并发读的性能,可以通过增加主节点来提升读吞吐量,当然也可以通过修改配置将从库配置为可读,做读写分离模式,不过这种可能会复杂一点,同时客户端也需要支持读写分离。
切片集群采用去中心化方式来维护集群状态,集群中不需要哨兵来保证高可用,各个节点实现了故障节点探测、主从切换、发现节点等任务。各个节点之间通过 Gossip 协议和同步更新机制来保证数据的一致性。
每个节点在集群中有一个唯一的 ID,由 160 位随机十六进制数表示,保存在配置文件中。
每个节点维护的集群内其他节点的信息如下:
Redis Cluster 没有使用一致性哈希,而是引入了**哈希槽(hash slot)**的概念。Redis Cluster 中有 16384(2^14)个哈希槽,每个 key 通过 CRC16 校验后对 16383 取模来决定放置哪个槽。Redis Cluster 中的每个节点负责一部分哈希槽。
Redis 实例会把自己的哈希槽信息发给和它相连接的其它实例,来完成哈希槽分配信息的扩散。当实例之间相互连接后,每个实例就有所有哈希槽的映射关系了。那么,客户端就可以在访问任何一个实例时,都能获得所有的哈希槽信息了。客户端收到哈希槽信息后,会把哈希槽信息缓存在本地。当客户端请求键值对时,会先计算键所对应的哈希槽,然后就可以给相应的实例发送请求了。
Hash Tags 是指加在键值对 key 中的一对花括号,能够将多个相关的 key 分配到相同的哈希槽中。例如:{user1000}.order 和 {user1000}.info 这两个 key 会被 hash 到相同的哈希槽中,因为只有 user1000 会被用来计算哈希槽的值。
在集群中,实例和哈希槽的对应关系并不是一成不变的,最常见的变化有两个:
实例之间还可以通过相互传递消息,获得最新的哈希槽分配信息,但是客户端是无法主动感知这些变化的。这就会导致,客户端缓存的分配信息和最新的分配信息不一致。
Redis Cluster 方案提供了一种重定向机制来解决这个问题:客户端给一个实例发送数据读写操作时,这个实例上并没有相应的数据,客户端要再给一个新实例发送操作命令。
当客户端把一个键值对的操作请求发给一个实例时,如果这个实例上并没有这个键值对映射的哈希槽,客户端会收到 MOVED 报错,其中包含哈希槽所在的新实例信息。
如果数据迁移只是部分完成,客户端会收到 ASK 报错,表示哈希槽正在迁移。此时,客户端需要先给新的实例发送一个 ASKING 命令。这个命令的意思是,让这个实例允许执行客户端接下来发送的命令。然后,客户端再向这个实例发送 GET 命令,以读取数据。
和 MOVED 命令不同,ASK 命令并不会更新客户端缓存的哈希槽分配信息。所以,如果客户端再次请求哈希槽中的数据,它还是会旧的实例发送请求,再触发一次重定向完成缓存更新。
集群总线,每个 Redis Cluster 节点有一个额外的 TCP 端口用来接受其他节点的连接。这个端口与用来接收客户端命令的 TCP 端口有一个固定的 offset(值为 10000,不可修改)。例如,一个节点在端口 6379 监听客户端连接,那么它的集群总线端口 16379 也会被打开。另外,如果加上 10000 后溢出(超过 65535),则会对 65536 取模。
节点到节点的通讯只使用集群总线,同时使用集群总线协议:有不同的类型和大小的帧组成的二进制协议。
通过集群总线,可以实现集群的节点自动发现、故障节点探测、主从切换等任务。
Gossip 协议又称 epidemic 协议(epidemic protocol,翻译成中文就是流言协议、传染病协议),是基于流行病传播方式的节点或者进程之间信息交换的协议,在 P2P 网络和分布式系统中应用广泛,它的方法论也特别简单:在一个处于有界网络的集群里,如果每个节点都随机与其他节点交换特定信息,经过足够长的时间后,集群各个节点对该份信息的认知终将收敛到一致。
Gossip 的消息有以下四种常见的类型:
除了这四种之外,还有 PUBLISH、UPDATE 等,源码中总共定义了九种类型。
Gossip 协议通过定时 PING/PONG 实现节点之间的心跳检测和状态同步:
因为是随机选取,所以可能会出现,有些实例一直没有被发送 PING 消息,导致它们维护的集群状态已经过期了。
为了避免这种情况,实例还会按照每 100ms 一次的频率,扫描本地的实例列表,如果发现有实例最近一次接收 PONG 消息的时间大于 cluster-node-timeout/2,就会立刻给该实例发送 PING 消息,更新这个实例上的集群状态信息。
每个实例在发送一个 Gossip 消息时,除了会传递自身的状态信息,默认还会传递集群十分之一实例的状态信息,以此加快集群的同步。
集群状态的同步效率越高就意味通信造成的开销越大,只能在两者间进行取舍。
配置项 cluster-node-timeout 定义了集群实例被判断为故障的心跳超时时间,默认是 15 秒。调大该值,可以减少心跳检测的次数,但是如果真的发生了故障,被检测出的时间也会变长,可能影响到集群的正常使用。
Gossip 中各个节点之间的状态可能存在一定的延迟,导致节点之间的数据不一致。为了解决这个问题,Redis 采用了基于时间戳的数据冲突检测机制。当一个节点收到另一个节点的写请求时,它会先检查该节点最近一次的写操作时间戳,如果该时间戳比自己的时间戳早,则说明该节点保存的数据已经过期,需要进行更新。
节点有三种状态:在线状态、疑似下线状态 PFAIL(Possible Failure)、已下线状态 FAIL。
集群中每个节点都会定期地向其他节点发送 PING 消息,以此检测对方是否在线。如果接收到 PING 消息的节点没有在规定的时间内(cluster-node-timeout)返回 PONG 消息,就会被标记为 PFAIL 状态。
故障下线的具体过程如下:
FAIL 消息会强制每个接收到这消息的节点把某标记为 FAIL 状态。
PFAIL 是有时效性的,如果超过了 cluster-node-time*2 的时间,就会恢复到正常状态。
主节点故障下线后,需要在下属的从节点中选举出一个,来成为新的主节点。
从节点的选举由从节点发起,由其他主节点投票要提升哪个从节点。
一个从节点的选举是在主节点被至少一个具有成为主节点必备条件的从节点标记为 FAIL 的状态的时候发生的。
从节点并不是在标记主节点为 FAIL 时立刻发起选举的,而是延迟一个随机的时间,这样是为了避免多个从节点同时发起选举。也可以确保主节点的 FAIL 状态在整个集群内传开。
整个选举的流程大致如下:
若如果无法选举出从节点,那么整个集群就置为错误状态并停止接收客户端的查询,此时需要手动恢复。
选举机制大致如下:
首先将不适合作为主节点的从节点过滤掉:把已经下线的从节点、网络连接状态不好的从节点、5 秒内没有回复过 INFO 命令的从节点、与主节点断开连接的时间超过 cluster-node-timeout * cluster-replica-validity-factor
的从节点都给过滤掉;
接下来要对剩余的所有从节点进行三轮考察:优先级、复制进度、ID 号。在进行每一轮考察的时候,哪个从节点优先胜出,就选择其作为新主节点;如果有多个节点情况相同,则进入下一轮考察:
被选举出来的从节点会执行故障转移,大致过程如下:
SLAVEOF no one
命令,成为新的主节点;Redis Cluster 添加新节点需要在某个节点上执行 CLUSTER MEET ip port 命令,之后会进行如下的握手操作:
加入集群后,新节点就可以定期和其他节点进行 PING/PONG 通信了,新节点加入集群的消息会通过节点间的 PING/PONG 通信主键传播开来,最后整个集群都知道了新节点的加入。
如果新增的是一个主节点,则会进行重新分片操作;如果是一个从节点,则会进行主从复制。
在切片集群中,数据会按照一定的分布规则分散到不同的实例上保存,可能会导致数据倾斜的问题。数据倾斜分为两类:数据量倾斜和数据访问倾斜。
在某些情况下,实例上的数据分布不均衡,某个实例上的数据量特别大。可能导致数据量倾斜的原因主要有以下三个:
虽然每个集群实例上的数据量相差不大,但是某个实例上的数据是热点数据,被访问得非常频繁。
可以将热点数据复制多份,并在每一个副本的 key 中加一个随机前缀,从而映射到不同的哈希槽这种。但是这种方法只能应用于只读的热点数据,如果是针对可读写的热点数据,只能考虑实例的横向和纵向扩展了。
本文介绍了 Redis 的切片集群,主要是官方提供的 Redis Cluster 方案。下一节将介绍 Redis 用于缓存方面的问题。