Redis 3.0 之后, 节点之间通过去中心化的方式, 提供了完整的 sharding,replication(复制机制仍使用原有机制, 并且具备感知主备的能力),failover 解决方案, 称为 Redis Cluster. 即: 将 proxy/sentinel 的工作融合到了普通 Redis 节点里. 后面将介绍 Redis Cluster 这种模式下, 水平拆分, 故障转移等需求的实现方式.
拓扑结构
一个 Redis Cluster 由多个 Redis 节点组成. 不同的节点组服务的数据无交集, 每个节点对应数据 sharding 的一个分片. 节点组内部分为主备 2 类, 对应前面叙述的 master 和 slave. 两者数据准实时一致, 通过异步化的主备复制机制保证. 一个节点组有且仅有一个 master, 同时有 0 到多个 slave. 只有 master 对外提供写服务, 读服务可由 master/slave 提供. 如下所示:
上图中, key-value 全集被分成了 5 份, 5 个 slot(实际上 Redis Cluster 有 16384 [0-16383] 个 slot, 每个节点服务一段区间的 slot, 这里面仅仅举例).A 和 B 为 master 节点, 对外提供写服务. 分别负责 1/2/3 和 4/5 的 slot.A/A1 和 B/B1/B2 之间通过主备复制的方式同步数据.
上述的 5 个节点, 两两通过 Redis Cluster Bus 交互, 相互交换如下的信息:
数据分片 (slot) 和节点的对应关系;
集群中每个节点可用状态;
集群结构发生变更时, 通过一定的协议对配置信息达成一致. 数据分片的迁移, 主备切换, 单点 master 的发现和其发生主备关系变更等, 都会导致集群结构变化.
publish/subscribe(发布订阅)功能, 在 Cluster 版内部实现所需要交互的信息.
Redis Cluster Bus 通过单独的端口进行连接, 由于 Bus 是节点间的内部通信机制, 交互的是字节序列化信息. 相对 Client 的字符序列化来说, 效率较高.
Redis Cluster 是一个去中心化的分布式实现方案, 客户端和集群中任一节点连接, 然后通过后面的交互流程, 逐渐的得到全局的数据分片映射关系.
配置的一致性
对于去中心化的实现, 集群的拓扑结构并不保存在单独的配置节点上, 后者的引入同样会带来新的一致性问题. 那么孤立的节点间, 如何对集群的拓扑达成一致, 是 Redis Cluster 配置机制要解决的问题. Redis Cluster 通过引入 2 个自增的 Epoch 变量, 来使得集群配置在各个节点间最终达成一致.
1, 配置信息数据结构
Redis Cluster 中的每个节点都保存了集群的配置信息, 并且存储在 clusterState 中, 结构如下:
image
上图的各个变量语义如下:
clusterState 记录了从集群中某个节点视角, 来看集群配置状态;
currentEpoch 表示整个集群中最大的版本号, 集群信息每变更一次, 改版本号都会自增.
nodes 是一个列表, 包含了本节点所感知的, 集群所有节点的信息(clusterNode), 也包含自身的信息.
clusterNode 记录了每个节点的信息, 其中包含了节点本身的版本 Epoch; 自身的信息描述: 节点对应的数据分片范围(slot), 为 master 时的 slave 列表, 为 slave 时的 master 等.
每个节点包含一个全局唯一的 NodeId.
当集群的数据分片信息发生变更(数据在节点间迁移时),Redis Cluster 仍然保持对外服务.
当集群中某个 master 出现宕机时, Redis Cluster 会自动发现, 并触发故障转移的操作. 会将 master 的某个 slave 晋升为新的 master.
由此可见, 每个节点都保存着 Node 视角的集群结构. 它描述了数据的分片方式, 节点主备关系, 并通过 Epoch 作为版本号实现集群结构信息的一致性, 同时也控制着数据迁移和故障转移的过程.
2, 信息交互
去中心化的架构不存在统一的配置中心. 在 Redis Cluster 中, 这个配置信息交互通过 Redis Cluster Bus 来完成(独立端口).Redis Cluster Bus 上交互的信息结构如下:
image
clusterMsg 中的 type 指明了消息的类型, 配置信息的一致性主要依靠 PING/PONG. 每个节点向其他节点频繁的周期性的发送 PING/PONG 消息. 对于消息体中的 Gossip 部分, 包含了 sender/receiver 所感知的其他节点信息, 接受者根据这些 Gossip 跟新对集群的认识.
对于大规模的集群, 如果每次 PING/PONG 都携带着所有节点的信息, 则网络开销会很大. 此时 Redis Cluster 在每次 PING/PONG, 只包含了随机的一部分节点信息. 由于交互比较频繁, 短时间的几次交互之后, 集群的状态也会达成一致.
3, 一致性的达成
当 Cluster 结构不发生变化时, 各个节点通过 gossip 协议在几轮交互之后, 便可以得知 Cluster 的结构信息, 达到一致性的状态. 但是当集群结构发生变化时(故障转移 / 分片迁移等), 优先得知变更的节点通过 Epoch 变量, 将自己的最新信息扩散到 Cluster, 并最终达到一致.
clusterNode 的 Epoch 描述的单个节点的信息版本;
clusterState 的 currentEpoch 描述的是集群信息的版本, 它可以辅助 Epoch 的自增生成. 因为 currentEpoch 是维护在每个节点上的, 在集群结构发生变更时, Cluster 在一定的时间窗口控制更新规则, 来保证每个节点的 currentEpoch 都是最新的.
更新规则如下:
1, 当某个节点率先知道了变更时, 将自身的 currentEpoch 自增, 并使之成为集群中的最大值. 再用自增后的 currentEpoch 作为新的 Epoch 版本;
2, 当某个节点收到了比自己大的 currentEpoch 时, 更新自己的 currentEpoch;
3, 当收到的 Redis Cluster Bus 消息中的某个节点的 Epoch> 自身的时, 将更新自身的内容;
4, 当 Redis Cluster Bus 消息中, 包含了自己没有的节点时, 将其加入到自身的配置中.
上述的规则保证了信息的更新都是单向的, 最终朝着 Epoch 更大的信息收敛. 同时 Epoch 也随着 currentEpoch 的增加而增加, 最终将各节点信息趋于稳定.
sharding
不同节点分组服务于相互无交集的分片(sharding),Redis Cluster 不存在单独的 proxy 或配置服务器, 所以需要将客户端路由到目标的分片.
1, 数据分片(slot)
Redis Cluster 将所有的数据划分为 16384 [0-16383] 个分片, 每个分片负责其中一部分. 每一条数据 (key-value) 根据 key 值通过数据分布算法 (一致性哈希) 映射到 16384 个 slot 中的一个. 数据分布算法为:
slotId = crc16(key) % 16384
image.gif
客户端根据 slotId 决定将请求路由到哪个 Redis 节点. Cluster 不支持跨节点的单命令, 如: sinterstore, 如果涉及的 2 个 key 对应的 slot 在不同的 Node, 则执行失败. 通常 Redis 的 key 都是带有业务意义的, 如: Product:Trade:20180890310921230001,Product:Detail:20180890310921230001. 当在集群中存储时, 上述同一商品的交易和详情可能会存储在不同的节点上, 进而对于这 2 个 key 不能以原子的方式操作. 为此, Redis 引入了 HashTag 的概念, 使得数据分布算法可以根据 key 的某一部分进行计算, 让相关的 2 条记录落到同一个数据分片. 如:
商品交易记录 key:Product:Trade:{20180890310921230001}
商品详情记录 key:Product:Detail:{20180890310921230001}
image.gif
Redis 会根据 {} 之间的字符串作为数据分布式算法的输入.
2, 客户端的路由
Redis Cluster 的客户端相比单机 Redis 需要具备路由语义的识别能力, 且具备一定的路由缓存能力. 当 Client 访问的 key 不在当前 Redis 节点的 slots 中, Redis 会返回给 Client 一个 moved 命令. 并告知其正确的路由信息, 如下所示:
image
当 Client 接收到 moved 后, 再次请求新的 Redis 时, 此时 Cluster 的结构又可能发生了变化. 此时有可能再次返回 moved .Client 会根据 moved 响应, 更新其内部的路由缓存信息, 以便后续的操作直接找到正确的节点, 减少交互次数.
当 Cluster 在数据重新分布过程中时, 可以通过 ask 命令控制客户端的路由, 如下所示:
image
上图中, slot 1 需要迁移到新节点上, 此时如果客户端已经完成迁移的 key, 节点将相应 ask 告知客户端想目标节点重试.
ask 命令和 moved 命令的不同在于, moved 会更新 Client 数据路由, ask 只是重定向新节点, 但是后续的相同 slot 仍会路由到旧节点.
迁移的过程可能会持续一段时间, 这段时间某个 slot 的数据, 同时可能存在于新旧 2 个节点. 由于 move 操作会使 Client 的路由缓存变更, 如果新旧节点对于迁移中的 slot 所有 key 都回应 moved, 客户端的路由缓存会频繁变更. 因此引入 ask 类型消息, 将重定向和路由缓存分离.
3, 分片的迁移
在一个稳定的 Redis Cluster 中, 每个 slot 对应的节点都是确定的. 在某些情况下, 节点和分片需要变更:
新的节点作为 master 加入;
某个节点分组需要下线;
负载不均衡需要调整 slot 分布.
此时需要进行分片的迁移, 迁移的触发和过程控制由外部系统完成. Redis Cluster 只提供迁移过程中需要的原语, 包含下面 2 种:
节点迁移状态设置: 迁移前标记源 / 目标节点.
key 迁移的原子化命令: 迁移的具体步骤.
image.gif
下面的 Demo 会介绍 slot 1 从节点 A 迁移到 B 的过程.
image
1, 向节点 B 发送状态变更命令, 将 B 的对应 slot 状态置为 importing.
2, 向节点 A 发送状态变更命令, 将 A 对应的 slot 状态置为 migrating.
3, 针对 A 上的 slot 的所有 key, 分别向 A 发送 migrate 命令, 告知 A 将对应的 key 迁移到 B.
当 A 节点的状态置为 migrating 后, 表示对应的 slot 正在从 A 迁出, 为保证该 slot 数据的一致性. A 此时提供的写服务和通常状态下有所区别, 对于某个迁移中的 slot:
如果 Client 访问的 key 尚未迁出, 则正常的处理该 key;
如果 key 已经迁出或者 key 不存在, 则回复 Client ASK 信息让其跳转到 B 处理;
image.gif
当节点 B 状态变成 importing 后, 表示对应的 slot 正在向 B 迁入. 即使 B 能对外提供该 slot 的读写服务, 但是和通常情况下有所区别:
当 Client 的访问不是从 ask 跳转的, 说明 Client 还不知道迁移. 有可能操作了尚未迁移完成的, 处在 A 上面的 key, 如果这个 key 在 A 上被修改了, 则后续会产生冲突.
所以对于该 slot 上所有非 ask 跳转的操作, B 不会进行操作, 而是通过 moved 让 Client 跳转至 A 执行.
image.gif
这样的状态控制, 保证了同一个 key 在迁移之前总是在源节点执行. 迁移后总是在目标节点执行, 从而杜绝了双写的冲突. 迁移过程中, 新增加的 key 会在目标节点执行, 源节点不会新增 key. 使得迁移有界限, 可以在某个确定的时刻结束.
单个 key 的迁移过程可以通过原子化的 migrate 命令完成. 对于 A/B 的 slave 节点, 是通过主备复制, 从而达到增删数据.
当所有 key 迁移完成后, Client 通过 cluster setslot 命令设置 B 的分片信息, 从而包含了迁入的 slot. 设置过程中会让 Epoch 自增, 并且是 Cluster 中的最新值. 然后通过相互感知, 传播到 Cluster 中的其他节点.
failover
同 Sentinel 一样, Redis Cluster 也具备一套完整的故障发现, 故障状态一致性保证, 主备切换机制.
1,failover 的状态变迁
1)故障发现: 当某个 master 宕机时, 宕机时间如何被集群其他节点感知.
2)故障确认: 多个节点就某个 master 是否宕机如何达成一致.
3)slave 选举: 集群确认了某个 master 宕机后, 如何将它的 slave 升级成新的 master; 如果有多个 slave, 如何选择升级.
4)集群结构变更: 成功选举成为 master 后, 如何让整个集群知道, 以更新 Cluster 结构信息.
2, 故障发现
Redis Cluster 节点间通过 Redis Cluster Bus 两两周期性的 PING/PONG 交互. 当某个节点宕机时, 其他 Node 发出的 PING 消息没有收到响应, 并且超过一定时间 (NODE_TIMEOUT) 未收到, 则认为该节点故障, 将其置为 PFAIL 状态(Possible Fail). 后续通过 Gossip 发出的 PING/PONG 消息中, 这个节点的 PFAIL 状态会传播到集群的其他节点.
Redis Cluster 的节点两两通过 TCP 保持 Redis Cluster Bus 连接, 当对 PING 无反馈时, 可能是节点故障, 也可能是 TCP 链接断开. 如果是 TCP 断开导致的误报, 虽然误报消息会因为其他节点的正常连接被忽略, 但是也可以通过一定的方式减少误报. Redis Cluster 通过 预重试机制 排除此类误报: 当 NODE_TIMEOUT/2 过去了, 但是还未收到响应, 则重新连接重发 PING 消息, 如果对端正常, 则在很短的时间内就会有响应.
3, 故障确认
对于网络分隔的情况, 某个节点 (B) 并没有故障, 但是和 A 无法连接, 但是和 C/D 等其他节点可以正常联通. 此时只会有 A 将 B 标记为 PFAIL 状态, 其他节点认为 B 正常. 此时 A 和 C/D 等其他节点信息不一致, Redis Cluster 通过故障 确认协议 达成一致.
集群中每个节点都是 Gossip 的接收者, A 也会接收到来自其他节点的 Gossip 消息, 被告知 B 是否处于 PFAIL 状态. 当 A 收到来气其他 master 节点对于 B 的 PFAIL 达到一定数量后, 会将 B 的 PFAIL 状态升级为 FAIL 状态. 表示 B 已经确认为故障态, 后面会发起 slave 选举流程.
A 节点内部的集群信息中, 对于 B 的状态从 PFAIL 到 FAIL 的变迁, 如下图所示:
image
4,slave 选举
上图中, B 是 A 的 master, 并且 B 已经被集群公认是 FAIL 状态了, 那么 A 发起竞选, 期望成为新的 master.
如果 B 有多个 slave (A/E/F)都认知到 B 处于 FAIL 状态了, A/E/F 可能会同时发起竞选. 当 B 的 slave 个数>= 3 时, 很有可能产生多轮竞选失败. 为了减少冲突的出现, 优先级高的 slave 更有可能发起竞选, 从而提升成功的可能性. 这里的优先级是 slave 的数据最新的程度, 数据越新的 (最完整的) 优先级越高.
slave 通过向其他 master 发送 FAILVOER_AUTH_REQUEST 消息发起竞选, master 收到后回复 FAILOVER_AUTH_ACK 消息告知是否同意. slave 发送 FAILOVER_AUTH_REQUEST 前会将 currentEpoch 自增, 并将最新的 Epoch 带入到 FAILOVER_AUTH_REQUEST 消息中, 如果自己未投过票, 则回复同意, 否则回复拒绝.
5, 结构变更通知
当 slave 收到过半的 master 同意时, 会替代 B 成为新的 master. 此时会以最新的 Epoch 通过 PONG 消息广播自己成为 master, 让 Cluster 的其他节点尽快的更新拓扑结构.
当 B 恢复可用之后, 它手续爱你仍然认为自己是 master, 但逐渐的通过 Gossip 协议得知 A 已经替代了自己, 然后降级为 A 的 slave.
可用性和性能
Redis Cluster 还提供了一些方法可以提升性能和可用性.
1,Redis Cluster 的读写分离
对于读写分离的场景, 应用对于某些读请求允许舍弃一定的数据一致性, 以换取更高的吞吐量. 此时希望将读请求交给 slave 处理, 以分担 master 的压力.
通过分片映射关系, 某个 slot 一定对应着一个 master 节点. Client 通过 moved 命令, 也只会路由到各个 master 中. 即使 Client 将请求直接发送到 slave 上, 也会回复 moved 到 master 去处理.
为此, Redis Cluster 引入了 readonly 命令. Client 向 slave 发送该命令后, 不再 moved 到 master 处理, 而是自己处理, 这成为 slave 的 readonly 模式. 通过 readwrite 命令, 可以将 slave 的 readonly 模式重置.
2,master 单点保护
假如 Cluster 的初始状态如下所示:
image
上图中 A,B 两个 master 分别有自己的 slave, 假设 A1 发生宕机, 结构变为如下所示:
image
此时 A 成为了单点, 一旦 A 再次宕机, 将造成不可用. 此时 Redis Cluster 会把 B 的某个 slave (如 B1 )进行副本迁移, 变成 A 的 slave. 如下所示:
image
这样集群中每个 master 至少有一个 slave, 使得 Cluster 具有高可用. 集群中只需要保持 2*master+1 个节点, 就可以保持任一节点宕机时, 故障转移后继续高可用.
来源: http://www.jianshu.com/p/d0ab5a0c1ae3