共计 7373 个字符,预计需要花费 19 分钟才能阅读完成。
Redis3.0 以后,节点之间通过去中心化的方式提供了完整的 sharding(数据分片)、replication(复制机制、Cluster 具备感知准备的能力)、failover 解决方案。
拓扑结构
Redis Cluster 由多个 Redis 节点组构成。不同节点组服务的数据无交集,每一个节点组对应数据 sharding 的一个分片。
节点组内分为主备两类节点,两者数据准实时一致,通过异步化的主备复制机制。
master 节点对用户提供读写服务,slave 节点对用户提供读服务。
Redis Cluster 总共有 16384 个 slot,每一个节点负责一部分 slot。
Redis Cluster 中所有的几点之间两两通过 Redis Cluster Bus 交互,主要交互以下关键信息:
数据分片 (slot) 和节点的对应关系
集群中每个节点可用状态
集群结构发生变更时,通过一定的协议对配置信息达成一致。数据分片的迁移、故障发生时的主备切换决策、单点 master 的发现和其发生主备关系的变更等场景均会导致集群结构变化
publish 和 subscribe(发布 / 订阅)功能在 cluster 版的内容实现所需要交互的信息。
Redis Cluster Bus 通过单独的端口进行连接,bus 是节点间的内部通信机制,交互的是字节序列化信息,而不是 client 到 Redis 服务器的字符序列化以提升交互效率。
Redis Cluster 是去中心化的分布式实现方案,客户端可以和集群中的任一节点连接。
配置一致性
去中心化意味着集群的拓扑结构并不保存在单独的配置节点上,Redis Cluster 通过引入两个自增的 epoch 变量来使得集群配置在各个节点间达成最终一致。
配置信息的数据结构
Redis Cluster 中的每一个节点都保存了集群的配置信息,这些信息存储在 clusterState 中。
clusterState 记录了从集群中某个节点的视角看来的集群配置状态
currentEpoch 表示整个集群中的最大版本号,集群信息每变更一次,该版本号都会自增以保证每个信息的版本号唯一
nodes 是一个列表,包含了本节点所知的集群所有节点的信息(clusterNode),其中也包含本节点自身
clusterNode 记录了每个节点的信息,比较关键的信息包括该信息的版本 epoch,该版本信息的描述:该节点对应的数据分片(slot),当该节点为 master 节点时对应的 slave 节点列表、当该节点为 slave 时对应的 master 节点
每个 clusterNode 还包含了一个全局唯一的 nodeId
当集群的数据分片信息发生变更时,Redis Cluster 仍然保持对外服务,在迁移过程中,通过分片迁移相关状态的一组变量来管控迁移过程
当集群中的某个 master 出现宕机时,Redis Cluster 会自动发现并触发故障转移的操作,将宕机 master 的某个 slave 升级为 master,这个过程同样需要一组 failover 相关状态的变量来管控故障转移。
Redis Cluster 通过 epoch 作为版本号来实现集群配置的一致性。
信息交互
去中心化的架构不存在统一的配置中心,各个节点对集群状态的认知来自于节点间的信息交互。在 Redis Cluster 中,该信息的交互通过 Redis Cluster Bus 来完成。
clusterMsg 的 type 字段指明了消息的类型。配置信息的一致性主要依靠 PING 和 PONG,两者除了 type 不同,其余字段语义均相同,消息体为 Gossip 数据。
每一个节点向其他节点较为频繁的周期性发送 PING 消息和接受 PONG 响应。在这些下拍戏的 Gossip 部分,包含了发送者节点 (或者响应者节点) 所知的集群其他节点信息,接收节点可以根据这些 Gossip 信息更新自己对于集群的认知。
规模较大的集群可能存在上千个节点,但是这些节点在正常情况下都是稳定的,因此每次都发送全量数据并不必要,而且还会造成网络负担。
作为优化,Redis Cluster 在每次的 PING 和 PONG 包中,只包含全集群部分节点信息,节点随机选取,以此控制网络流量。由于交互频繁,短时间的几次交互之后,集群状态就会以 Gossip 协议的方式被扩散到了集群中的所有节点。
一致性达成
集群结构稳定不发生变化时,各个节点通过 Gossip 协议在几轮交互之后便可得知全集群的信息并且达到一致的状态。
但是,当发生故障转移、分片迁移等情况将会造成集群结构变更,变更的信息需要各个节点之间自行协调,优先得知变更信息的节点利用 epoch 变量将自己的最新信息扩散到整个集群,达到最终一致。
配置信息 clusterNode 的 epoch 属性描述的粒度是单个节点
配置信息 clusterState 的 currentEpoch 属性的粒度是整个集群,它的存在用来辅助 epoch 自增的生成。由于 currentEpoch 信息也是维护在各个几点自身的,Redis Cluster 结构在发生变更时,通过一定时间窗口控制和更新规则保证每个节点看到的 currentEpoch 都是最新的。
集群信息的更新规则:
当某个节点率先知道了信息变更时,这个节点将 currentEpoch 自增使之成为集群中的最大值,再用自增后的 currentEpoch 作为新的 epoch 版本
当某个节点收到了比自己大的 currentEpoch 时,更新自己的 currentEpoch 值使之保持最新
当收到的 Redis Cluster Bus 消息中某个节点信息的 epoch 值大于接收者自己内部的配置信息存储的值时,意味着自己的信息太旧,此时接收者直接将自己的映射信息更新为消息的内容
当收到的 Redis Cluster Bus 消息中某个节点信息未包含在接收节点的内部配置信息中时,意味着接受者尚未意识到该节点的存在,此时接收者直接将消息的信息添加到自己的内部配置信息中。
sharding
不同的节点组服务于相互无交互的数据子集(sharding,分片)。
数据分片(slot)
Redis Cluster 将所有的数据划分为 16384 个分片(slot),每个分片负责其中一部分。每一条数据根据 key 值通过数据分布算法映射到 16384 个 slot 中的一个。
数据分布算法:slotId=crc(key)%16384
客户端根据 slotId 决定将请求路由到哪个 Redis 节点。Cluster 不支持跨节点的单命令。
为此,Redis 引入 HashTag 的概念,使得数据分布算法可以根据 key 的某一部分进行计算,让相关的两条记录落到同一个数据分片,例如:
某条商品交易记录的 key 值为:product_trade_{prod123}
这个商品的详情记录的 key 值为:product_detail_{prod123}
Redis 会根据 {} 之间的子字符串作为数据分布算法的输入。
客户端路由
Redis Cluster 的客户端需要具备一定的路由能力。当一个 Client 访问的 key 不在对应 Redis 节点的 slot 中,Redis 返回给 Client 一个 moved 命令,告知其正确的路由信息。
从 Client 收到 moved 响应,到再次向 moved 响应中指向的节点发送请求期间,Redis Cluster 的数据分布可能又发生了变更,此时,指向的节点会继续响应 moved。Client 根据 moved 响应更新其内部的路由缓存信息,以便下一次请求时直接路由到正确的节点,降低交互次数。
当 Cluster 处在数据重分布 (目前由人工触发) 过程中时,可以通过 ask 命令控制客户端路由。
ask 命令和 moved 命令的不同语义在于,后者会更新路由缓存,前者只是本条操作重定向到新节点,后续的相同 slot 操作仍路由到旧节点。ask 类型将重定向和路由缓存更新分离,避免客户端的路由缓存信息频繁更新。
分片迁移
在稳定的 Redis Cluster 下,每一个 slot 对应的节点是确定的。但是在某些情况下,节点和分片的对应关系要发生变更:
新的节点作为 master 加入
某个节点分组需要下线
负载不均需要调整 slot 分布
此时需要进行分片的迁移。分片迁移的触发和过程由外部系统完成,Redis Cluster 只提供迁移过程中需要的原语供外部系统调用。这些原语主要有两种:
节点迁移状态设置:迁移前标记源 / 目标节点
key 迁移的原子化命令:迁移的具体步骤
向节点 B 发送状态变更命令,将 B 的对应 slot 状态置为 IMPORTING
向节点 A 发送状态变更命令,将 A 的对应 slot 状态置为 MIGRATING
针对 A 的 slot 上的所有的 key,分别向 A 发送 MIGRATE 命令,告知 A 将对应 key 的数据迁移到 B。
当节点 A 的状态被设置为了 MIGRATING 后,表示对应的 slot 正在从 A 迁出,为保证该 slot 数据的一致性,A 此时对 slot 内部数据提供读写服务的行为和通常状态下有所区别,对于某个迁移中的 slot:
如果客户端访问的 key 尚未迁移出,则正常地处理 key
如果 key 已经被迁移出或者根本不存在该 key,则回复客户端 ASK 信息让其跳转到 B 执行
当节点 B 的状态被设置为了 IMPORTING 之后,表示对应的 slot 正在向 B 迁入中,即使 B 仍能对外提供该 slot 的读写服务,但行为和通常状态下也有所区别:
当来自客户端的正常访问不是从 ASK 跳转而来时,说明客户端尚不知道迁移正在进行,很有可能操作了一个目前尚未迁移完成的正处在 A 上的 key,如果此时 key 已经在 A 上被修改了,那么 B 和 A 的修改值将在未来发生冲突。
对于该 slot 上的所有非 ASK 跳转而来的操作,B 不会进行处理,而是通过 MOVED 命令让客户端跳转至 A 执行
这样的状态控制可以保证同一个 key 在迁移之前总是在源节点执行,迁移后总是在目标节点执行,杜绝了两边同时写导致值冲突的可能性。且迁移过程中新增的 key 总是在目标节点执行,源节点不会再有新增的 key,使得迁移过程时间有界。
Redis 单机对于命令的处理是单线程的,同一个 key 在 MIGRATE 的过程中不会处理对该 key 的其他操作,从而保证了迁移的原子性。
当 slot 的所有 key 从 A 迁移至 B 上之后,客户端通过 CLUSTER SETSLOT 命令设置 B 的分片信息,使之包含迁移的 slot。设置的过程中会自增一个 epoch,它大于当前集群中的所有 epoch 值,这个新的配置信息会传播到集群中的其他每一个节点,完成分片节点映射关系的更新。
failover
Redis Cluster 同 Sentinel 一样,具备完整的节点故障发现、故障状态一致性保证、主备切换机制。
failover 状态变迁
failover 的过程如下:
故障发现:当某个 master 宕机时,宕机事件如何被集群其他节点感知
故障确认:多个节点就某个 master 是否宕机如何达成一致
slave 选举:集群确认了某个 master 确实宕机后,如何将它的 slave 升级成新的 master;如果原 master 有多个 slave,选择谁升级
集群结构变更:选举成功的 slave 升级成新的 master 后如何让全集群的其他节点知道以更新他们的集群结构信息
故障发现
Redis Cluster 节点间通过 Redis Cluster Bus 两两周期性地进行 PING/PONG 交互,当某个节点宕机时,其他发向它的 PING 消息将无法及时响应,当 PONG 的响应超过一定时间 (NODE_TIMEOUT) 未收到,则发送者认为接受节点故障,将其置为 PFAIL 状态,后续通过 Gossip 发出的 PING/PONG 消息中,这个节点的 PFAIL 状态将会被转播到集群的其他节点。
Redis Cluster 的节点间通过 TCP 保持 Redis Cluster Bus 连接,当对端无 PONG 回复时,除了节点故障外,还有可能是 TCP 连接断开。对于 TCP 连接断开导致的响应超时,将会产生节点状态误报。因此 Redis Cluster 通过预重试机制排除此类误报:当 NODE_TIMEOUT/ 2 过去了却还未收到 PING 对应的 PONG 消息,则重建连接重发 PING 消息,如果对端正常,PONG 会在很短时间内抵达。
故障确认
对于网络分割的节点,某个节点 (假设叫 B 节点) 并没有故障,但可能和 A 无法连接,但是和 C / D 等其他节点可以正常联通,此时只有 A 会将 B 标记为 PFAIL,其他节点扔人认为 B 是正常的。此时 A 和 C / D 等其他节点信息不一致。Redis Cluster 通过故障确认协议达成一致。
A 会受到来自其他节点的 Gossip 消息,被告知节点 B 是否处于 PFAIL 状态,当 A 受到的来自其他 master 节点的 B 的 PFAIL 达到一定数量后,会将 B 的 PFAIL 升级为 FAIL 状态,表示 B 已确认为故障,后续将会发起 slave 选举流程
slave 选举
上例中,如果 B 是 A 的 master,且 B 已经被集群公认是 FAIL 状态,那么 A 将发起竞选,期望替代 B 成为新的 master。
如果 B 有多个 slave A/E/ F 都意识到 B 处于 FAIL 状态了,A/E/ F 可能会同时发起竞选,当 B 的 slave 数量 >= 3 个时,很有可能因为票数均匀无法选出胜者,延长 B 上的 slot 不可用时间。为此,slave 间会在选举前协商优先级,优先级高的 slave 更有可能早地发起选举,优先级较低的 slave 发起选举的时间越靠后,避免和高优先级的 slave 竞争,提升一轮完成选举的可能性。
优先级最重要的决定因素是 slave 最后一次同步 master 信息的时间,越新标识这个 slave 的数据越新,竞选优先级越高。
slave 通过向其他 master 节点发送 FAILOVER_AUTH_REQUEST 消息发起竞选,master 收到之后回复 FAILOVER_AUTH_ACK 消息告知自己是否同意改 slave 成为新的 master。slave 发送 FAILOVER_AUTH_REQUEST 前会将 currentEpoch 自增并将最新的 epoch 带入到 AILOVER_AUTH_REQUEST 消息中,master 收到 FAILOVER_AUTH_REQUEST 消息后,如果发现对于本轮 (本 epoch) 自己尚未投过票,则回复同意,否则回复拒绝。
集群结构变更通知
当 slave 收到超过半数的 master 的同意回复时,该 slave 顺利的替代 B 成为新 master,此时它会以最新的 epoch 通过 PONG 消息广播自己成为 master 的信息,让集群中的其他节点更快地更新拓扑信息。
当 B 恢复可用之后,它首先仍然认为自己是 master,但逐渐得通过 Gossip 协议得知 A 已经替代自己的事实之后降级为 A 的 slave。
主备复制
Redis 采用主备复制的方式保持一致性,即所有节点中,有一个节点为 master,对外提供写入服务,所有的数据变更由外界对 master 的写入触发,之后 Redis 内部异步地将数据从主节点复制到其他节点上。
主备复制流程
Redis 包含 master 和 slave 节点:master 节点对外提供读写服务;slave 节点作为 master 的数据备份,拥有 master 的全量数据,对外不提供写服务。主备复制由 slave 主动触发。
slave 向 master 发起 SYNC 命令。这一步在 slave 启动后触发,master 被动地将新进 slave 节点加入自己的主备复制集群
master 收到 SYNC 后,开启 BGSAVE 操作。BGSAVE 是 Redis 的一种全量模式的持久化机制
BGSAVE 完成后,master 会将快照信息发送给 slave
发送期间,master 收到的来自客户端的新的写命令,除了正常响应外,都再存入一份到 backlog 队列
快照信息发送完成后,master 继续发送 backlog 队列信息
backlog 发送完成后,后续的写操作同时发送给 slave,保持实时地异步复制
slave 侧的处理逻辑:
发送完 SYNC 后,继续对外提供服务
开始接收 master 的快照信息,此时,将 slave 现有数据清空,并将 master 快照写入自身内存
接收 backlog 内容并执行它,即回放,期间对外提供读请求
继续接收后续来自 master 的命令副本并继续回放,以保证数据和 master 一致
如果有多个 slave 节点并发发送 SYNC 命令给 master,只要第二个 slave 的 SYNC 命令发生在 master 完成 BGSAVE 之前,第二个 slave 将受到和第一个 slave 相同的快照和后续的 backlog;否则,第二个 slave 的 SYNC 将触发 master 的第二次 BGSAVE。
断点续传
slave 通过 SYNC 命令和 master 进行数据同步时,master 都会 dump 全量数据。假设 master 和 slave 断开很短的时间,数据只有很少的差异,重连后也会发送这些全量数据导致大量的无效开销。最好的方式就是,master-slave 只同步断开期间的少量数据。
Redis 的 PSYNC 可用于替代 SYNC,做到 master-slave 基于断点续传的主备同步协议。master-slave 两端通过维护一个 offset 记录当前已经同步过的命令,slave 断开期间,master 的客户端命令会保持在缓存中,在 slave 命令重连后,告知 master 断开时的最新 offset,master 则将缓存中大于 offset 的数据发送给 slave,而断开前已经同步过的数据,则不再重新同步,这样减少了数据传输开销。
可用性和性能
Redis Cluster 读写分离
对于有读写分离需求的场景,应用对于某些读的请求允许舍弃一定的数据一致性,以换取更高的读吞吐量,此时希望将读的请求交由 slave 处理以分担 master 的压力。
默认情况下,数据分片映射关系中,某个 slot 对应的节点一定是一个 master 节点,客户端通过 MOVED 消息得知的集群拓扑结构也只会将请求路由到各个 master 中,即便客户将读请求直接发送到 slave 上,后者也会回复 MOVED 到 master 的响应。
Redis Cluster 引入了 READONLY 命令。客户端向 slave 发送该命令后,slave 对于读操作,将不再 MOVED 回 master 而不是直接处理,这被称为 slave 的 READONLY 模式。通过 READWRITE 命令,可将 slave 的 READONLY 模式重置。
master 单点保护
集群只需要保持 2 *master+ 1 个节点,就可以在任一节点宕机后仍然自动地维持,称为 master 的单点保护。