--- icon: logos:redis title: Redis 集群 cover: https://raw.githubusercontent.com/dunwu/images/master/snap/20230914072642.png date: 2020-06-24 10:45:38 categories: - 数据库 - KV数据库 - redis tags: - 数据库 - KV数据库 - redis - 高可用 - 选主 - 故障转移 - 集群 - 分区 - raft - gossip permalink: /pages/7fdb1c11/ --- # Redis 集群 > **[Redis 集群(Redis Cluster)](https://redis.io/topics/cluster-tutorial) 是 Redis 官方提供的“分布式数据库”方案**。 > > Redis Cluster 既然被设计分布式系统,自然需要具备分布式系统的基本特性:伸缩性、高可用、一致性。 > > - **伸缩性** - Redis Cluster 通过划分虚拟 hash 槽来进行“分区”,以实现集群的伸缩性。 > - **高可用** - Redis Cluster 采用主从架构,支持“复制”和“自动故障转移”,以保证 Redis Cluster 的高可用。 > - **一致性** - 根据 CAP 理论,Consistency、Availability、Partition tolerance 三者不可兼得。而 Redis Cluster 的选择是 AP,即不保证“强一致性”,尽力达到“最终一致性”。 > > Redis Cluster 应用了 [Raft 协议](https://ramcloud.atlassian.net/wiki/download/attachments/6586375/raft.pdf) 协议和 Gossip 协议。 > > 关键词:`高可用`、`监控`、`选主`、`故障转移`、`分区`、`Raft`、`Gossip` ## Redis Cluster 分区 ### 集群节点 Redis Cluster 由多个节点组成,节点刚启动时,彼此是相互独立的。**节点通过握手( [`CLUSTER MEET`](https://redis.io/commands/cluster-meet/) 命令)来将其他节点添加到自己所处的集群中**。 向一个节点发送 `CLUSTER MEET` 命令,可以让当前节点与指定 IP、PORT 的节点进行三次握手,握手成功时,当前节点会将指定节点加入所在集群。 **集群节点保存键值对以及过期时间的方式与单机 Redis 服务完全相同**。 Redis Cluster 节点分为主节点(master)和从节点(slave): - 主节点用于处理槽。 - 从节点用于复制主节点, 并在主节点下线时, 代替主节点继续处理命令请求。 ### 分配 Hash 槽 分布式存储需要解决的首要问题是把整个数据集按照“**分区规则**” 到**多个节点**,即每个节点负责整体数据的一个 **子集**。 **Redis Cluster 将整个数据库规划为 “16384” 个虚拟的哈希槽**,数据库中的每个键都属于其中一个槽。**每个节点都会记录哪些槽指派给了自己, 而哪些槽又被指派给了其他节点**。 **如果数据库中有任何一个槽没有得到分配,那么集群处于“下线”状态**。 通过向节点发送 [`CLUSTER ADDSLOTS`](https://redis.io/commands/cluster-addslots) 命令,可以将一个或多个槽指派给节点负责。 ```shell > CLUSTER ADDSLOTS 1 2 3 OK ``` 集群中的每个节点负责一部分哈希槽,比如集群中有3个节点,则: - 节点A存储的哈希槽范围是:0 – 5500 - 节点B存储的哈希槽范围是:5501 – 11000 - 节点C存储的哈希槽范围是:11001 – 16384 ### 路由 当客户端向节点发送与数据库键有关的命令时,接受命令的节点会**计算出命令要处理的数据库属于哪个槽**,并**检查这个槽是否指派给了自己**: - 如果键所在的槽正好指派给了当前节点,那么当前节点直接执行命令。 - 如果键所在的槽没有指派给当前节点,那么节点会向客户端返回一个 `MOVED` 错误,指引客户端重定向至正确的节点。 #### 计算键属于哪个槽 决定一个 key 应该分配到那个槽的算法是:**计算该 key 的 CRC16 结果再模 16834**。 ``` HASH_SLOT = CRC16(KEY) mod 16384 ``` 当节点计算出 key 所属的槽为 i 之后,节点会根据以下条件判断槽是否由自己负责: ``` clusterState.slots[i] == clusterState.myself ``` #### MOVED 错误 节点在接到一个命令请求时,会先检查这个命令请求要处理的键所在的槽是否由自己负责, 如果不是的话, 节点将向客户端返回一个 `MOVED` 错误, `MOVED` 错误携带的信息可以指引客户端转向至正在负责相关槽的节点。 `MOVED` 错误的格式为: ```shell MOVED : ``` > 提示:`MOVED` 命令的作用有点类似 HTTP 协议中的重定向。 ### 重新分区 对 Redis Cluster 的重新分片工作是由客户端(redis-trib)执行的, **重新分片的关键是将属于某个槽的所有键值对从一个节点转移至另一个节点**。 重新分区操作可以“**在线**”进行,在重新分区的过程中,集群不需要下线,并且源节点和目标节点都可以继续处理命令请求。 重新分区的实现原理如下图所示: ![img](https://raw.githubusercontent.com/dunwu/images/master/cs/database/redis/redis-cluster-trib.png) ### ASK 错误 如果节点 A 正在迁移槽 `i` 至节点 B , 那么当节点 A 没能在自己的数据库中找到命令指定的数据库键时, 节点 A 会向客户端返回一个 `ASK` 错误, 指引客户端到节点 B 继续查找指定的数据库键。 `ASK` 错误与 `MOVED` 的区别在于: - `MOVED` 错误表示槽的负责权已经从一个节点转移到了另一个节点; - 而 `ASK` 错误只是两个节点在迁移槽的过程中使用的一种临时措施。 判断 ASK 错误的过程如下图所示: ![img](https://raw.githubusercontent.com/dunwu/images/master/cs/database/redis/redis-ask.png) ## Redis Cluster 复制 Redis Cluster 中的节点分为主节点和从节点,其中主节点用于处理槽,而从节点则用于复制某个主节点,并在被复制的主节点下线时,代替下线主节点继续处理命令请求。 向一个节点发送命令 `CLUSTER REPLICATE ` 可以让接收命令的节点成为 node_id 所指定节点的从节点,并开始对主节点进行复制。 Redis Cluster 节点间的复制是“异步”的。 ## Redis Cluster 故障转移 ### 故障检测 **集群中每个节点都会定期向集群中的其他节点发送 `PING` 消息,以此来检测对方是否在线**。 节点的状态信息可以分为: - 在线状态; - 疑似下线状态(`PFAIL`) - 即在规定的时间内,没有应答 `PING` 消息 - 已下线状态(`FAIL`) - 半数以上负责处理槽的主节点都将某个主节点视为“疑似下线”,则这个主节点将被标记为“已下线” ### 故障转移 1. 下线主节点的所有从节点中,会有一个从节点被选中。 2. 被选中的从节点会执行 `SLAVEOF no one` 命令,成为新的主节点。 3. 新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己。 4. 新的主节点向集群广播一条 `PONG` 消息,告知其他节点这个从节点已变成主节点。 5. 新的主节点开始接收和自己负责处理的槽有关的命令请求,故障转移完成。 ### 选主 > Redis Sentinel 和 Redis Cluster 的选主流程非常相似,二者都基于[Raft 协议](https://ramcloud.atlassian.net/wiki/download/attachments/6586375/raft.pdf) 实现。 1. 从节点发现自己的主节点状态为 `FAIL`。 2. 从节点将自己记录的纪元(`epoch`)加 1,并广播消息,要求所有收到消息且有投票权的主节点都为自己投票。——这里的纪元(`epoch`),相当于 Raft 协议中的选期(`term`)。因个人习惯,后面统一将纪元描述为选期。 3. 如果某主节点具有投票权(它正在负责处理槽),并且这个主节点尚未投票,那么主节点就返回一条确认消息,表示支持该从节点成为新的主节点。 4. 每个参与选举的从节点都会根据收到的确认消息,统计自己所得的选票。 5. 假设集群中存在 N 个具有投票权的主节点,那么**当某从节点得到“半数以上”(`N / 2 + 1`)的选票,则该从节点当选为新的主节点**。 6. 由于每个选期中,任意具有投票权的主节点“只能投一票”,所以获得“半数以上”选票的从节点只能有一个。 7. 如果在一个选期中,没有从节点能获得“半数以上”投票,则本次选期作废,开始进入下一个选期,直到选出新的主节点为止。 ## Redis Cluster 通信 **集群中的节点通过发送和接收消息来进行通信**。Redis Cluster 各实例之间的通信方式采用 [Gossip 协议](http://publicatio.bibl.u-szeged.hu/1529/1/gossip11.pdf)来实现。 Redis Cluster 采用 Gossip 协议基于两个主要目标:**去中心化**以及**失败检测**。 Redis Cluster 中,每个节点之间都会同步信息,但是每个节点的信息不保证实时的,即无法保证数据强一致性,但是保证“**数据最终一致性**”——当集群中发生节点增减、故障、主从关系变化、槽信息变更等事件时,通过不断的通信,在经过一段时间后,所有的节点都会同步集群全部节点的最新状态。 Redis Cluster 节点发送的消息主要有以下五种: - `MEET` - 请求接收方加入发送方所在的集群。 - `PING` - 集群中每个节点每隔一段时间(默认为一秒)从已知节点列表中随机选出五个节点,然后对这五个节点中最久没联系的节点发送 `PING` 消息,以此检测被选中的节点是否在线。 - `PONG` - 当接收方收到发送方发来的 `MEET` 消息或 `PING` 消息时,会返回一条 `PONG` 消息作为应答。 - `FAIL` - 当一个主节点 A 判断另一个主节点 B 已经进入 `FAIL` 状态时,节点 A 会向集群广播一条关于节点 B 的 `FAIL` 消息,所有收到这条消息的节点都会立即将节点 B 标记为已下线。 - `PUBLISH` - 当节点收到一个 `PUBLISH` 命令时,节点会执行这个命令,并向集群广播一条 `PUBLISH` 消息,所有接受到这条消息的节点都会执行相同的 `PUBLISH` 命令。 ## Redis Cluster 应用 ### 集群功能限制 Redis Cluster 相对 **单机**,存在一些功能限制,需要 **开发人员** 提前了解,在使用时做好规避。 - `key` **批量操作** 支持有限:类似 `mset`、`mget` 操作,目前只支持对具有相同 `slot` 值的 `key` 执行 **批量操作**。对于 **映射为不同** `slot` 值的 `key` 由于执行 `mget`、`mget` 等操作可能存在于多个节点上,因此不被支持。 - `key` **事务操作** 支持有限:只支持 **多** `key` 在 **同一节点上** 的 **事务操作**,当多个 `key` 分布在 **不同** 的节点上时 **无法** 使用事务功能。 - `key` 作为 **数据分区** 的最小粒度,不能将一个 **大的键值** 对象如 `hash`、`list` 等映射到 **不同的节点**。 - 不支持 **多数据库空间**:**单机** 下的 Redis 可以支持 `16` 个数据库(`db0 ~ db15`),**集群模式** 下只能使用 **一个** 数据库空间,即 `db0`。 - **复制结构** 只支持一层:**从节点** 只能复制 **主节点**,不支持 **嵌套树状复制** 结构。 ### 集群规模限制 Redis Cluster 的优点是易于使用。分区、主从复制、弹性扩容这些功能都可以做到自动化,通过简单的部署就可以获得一个大容量、高可靠、高可用的 Redis 集群,并且对于应用来说,近乎于是透明的。 所以,**Redis Cluster 非常适合构建中小规模 Redis 集群**,这里的中小规模指的是,大概几个到几十个节点这样规模的 Redis 集群。 但是 Redis Cluster 不太适合构建超大规模集群,主要原因是,它采用了去中心化的设计。 Redis 的每个节点上,都保存了所有槽和节点的映射关系表,客户端可以访问任意一个节点,再通过重定向命令,找到数据所在的那个节点。那么,这个映射关系表是如何更新的呢?Redis Cluster 采用了一种去中心化的流言 (Gossip) 协议来传播集群配置的变化。 Gossip 协议的优点是去中心化;缺点是传播速度慢,并且是集群规模越大,传播的越慢。 ### 集群配置 我们后面会部署一个 Redis Cluster 作为例子,在那之前,先介绍一下集群在 redis.conf 中的参数。 - **cluster-enabled** `` - 如果配置”yes”则开启集群功能,此 redis 实例作为集群的一个节点,否则,它是一个普通的单一的 redis 实例。 - **cluster-config-file** `` - 注意:虽然此配置的名字叫“集群配置文件”,但是此配置文件不能人工编辑,它是集群节点自动维护的文件,主要用于记录集群中有哪些节点、他们的状态以及一些持久化参数等,方便在重启时恢复这些状态。通常是在收到请求之后这个文件就会被更新。 - **cluster-node-timeout** `` - 这是集群中的节点能够失联的最大时间,超过这个时间,该节点就会被认为故障。如果主节点超过这个时间还是不可达,则用它的从节点将启动故障迁移,升级成主节点。注意,任何一个节点在这个时间之内如果还是没有连上大部分的主节点,则此节点将停止接收任何请求。 - **cluster-slave-validity-factor** `` - 如果设置成0,则无论从节点与主节点失联多久,从节点都会尝试升级成主节点。如果设置成正数,则 cluster-node-timeout 乘以 cluster-slave-validity-factor 得到的时间,是从节点与主节点失联后,此从节点数据有效的最长时间,超过这个时间,从节点不会启动故障迁移。假设 cluster-node-timeout=5,cluster-slave-validity-factor=10,则如果从节点跟主节点失联超过 50 秒,此从节点不能成为主节点。注意,如果此参数配置为非 0,将可能出现由于某主节点失联却没有从节点能顶上的情况,从而导致集群不能正常工作,在这种情况下,只有等到原来的主节点重新回归到集群,集群才恢复运作。 - **cluster-migration-barrier** `` - 主节点需要的最小从节点数,只有达到这个数,主节点失败时,它从节点才会进行迁移。更详细介绍可以看本教程后面关于副本迁移到部分。 - **cluster-require-full-coverage** `` - 在部分 key 所在的节点不可用时,如果此参数设置为”yes”(默认值), 则整个集群停止接受操作;如果此参数设置为”no”,则集群依然为可达节点上的 key 提供读操作。 ## 其他 Redis 集群方案 Redis Cluster 不太适合用于大规模集群,所以,如果要构建超大 Redis 集群,需要选择替代方案。一般有三种方案类型: - 客户端分区方案 - 代理分区方案 - 查询路由方案 ### 客户端分区方案 **客户端** 就已经决定数据会被 **存储** 到哪个 Redis 节点或者从哪个 Redis 节点 **读取数据**。其主要思想是采用 **哈希算法** 将 Redis 数据的 `key` 进行散列,通过 `hash` 函数,特定的 `key`会 **映射** 到特定的 Redis 节点上。 **客户端分区方案** 的代表为 Redis Sharding,Redis Sharding 是 Redis Cluster 出来之前,业界普遍使用的 Redis **多实例集群** 方法。Java 的 Redis 客户端驱动库 [**Jedis**](https://github.com/redis/jedis),支持 Redis Sharding 功能,即 ShardedJedis 以及 **结合缓存池** 的 ShardedJedisPool。 - **优点**:不使用 **第三方中间件**,**分区逻辑** 可控,**配置** 简单,节点之间无关联,容易 **线性扩展**,灵活性强。 - **缺点**:**客户端** 无法 **动态增删** 服务节点,客户端需要自行维护 **分发逻辑**,客户端之间 **无连接共享**,会造成 **连接浪费**。 ### 代理分区方案 **客户端** 发送请求到一个 **代理组件**,**代理** 解析 **客户端** 的数据,并将请求转发至正确的节点,最后将结果回复给客户端。 - **优点**:简化 **客户端** 的分布式逻辑,**客户端** 透明接入,切换成本低,代理的 **转发** 和 **存储** 分离。 - **缺点**:多了一层 **代理层**,加重了 **架构部署复杂度** 和 **性能损耗**。 **代理分区** 主流实现的有方案有 **[Twemproxy](https://github.com/twitter/twemproxy)** 和 [**Codis**](https://github.com/CodisLabs/codis)。 #### Twemproxy **[Twemproxy](https://github.com/twitter/twemproxy)** 也叫 `nutcraker`,是 Twitter 开源的一个 Redis 和 Memcache 的 **中间代理服务器** 程序。 **[Twemproxy](https://github.com/twitter/twemproxy)** 作为 **代理**,可接受来自多个程序的访问,按照 **路由规则**,转发给后台的各个 Redis 服务器,再原路返回。**[Twemproxy](https://github.com/twitter/twemproxy)** 存在 **单点故障** 问题,需要结合 Lvs 和 Keepalived 做 **高可用方案**。 - **优点**:应用范围广,稳定性较高,中间代理层 **高可用**。 - **缺点**:无法平滑地 **水平扩容/缩容**,无 **可视化管理界面**,运维不友好,出现故障,不能 **自动转移**。 #### Codis [**Codis**](https://github.com/CodisLabs/codis) 是一个 **分布式** Redis 解决方案,对于上层应用来说,连接 Codis-Proxy 和直接连接 **原生的** Redis-Server 没有的区别。[**Codis**](https://github.com/CodisLabs/codis) 底层会 **处理请求的转发**,不停机的进行 **数据迁移** 等工作。[**Codis**](https://github.com/CodisLabs/codis) 采用了无状态的 **代理层**,对于 **客户端** 来说,一切都是透明的。 - **优点**:实现了上层 Proxy 和底层 Redis 的 **高可用**,**数据分区** 和 **自动平衡**,提供 **命令行接口** 和 RESTful API,提供 **监控** 和 **管理** 界面,可以动态 **添加** 和 **删除** Redis 节点。 - **缺点**:**部署架构** 和 **配置** 复杂,不支持 **跨机房** 和 **多租户**,不支持 **鉴权管理**。 ### 查询路由方案 **客户端随机地** 请求任意一个 Redis 实例,然后由 Redis 将请求 **转发** 给 **正确** 的 Redis 节点。Redis Cluster 实现了一种 **混合形式** 的 **查询路由**,但并不是 **直接** 将请求从一个 Redis 节点 **转发** 到另一个 Redis 节点,而是在 **客户端** 的帮助下直接 **重定向**( `redirected`)到正确的 Redis 节点。 - **优点**:**去中心化**,数据按照 **槽** 存储分布在多个 Redis 实例上,可以平滑的进行节点 **扩容/缩容**,支持 **高可用** 和 **自动故障转移**,运维成本低。 - **缺点**:重度依赖 Redis-trib 工具,缺乏 **监控管理**,需要依赖 Smart Client (**维护连接**,**缓存路由表**,`MultiOp` 和 `Pipeline` 支持)。Failover 节点的 **检测过慢**,不如有 **中心节点** 的集群及时(如 ZooKeeper)。Gossip 消息采用广播方式,集群规模越大,开销越大。无法根据统计区分 **冷热数据**。 ## 参考资料 - [《Redis 设计与实现》](https://item.jd.com/11486101.html) - [深入剖析 Redis 系列(三) - Redis 集群模式搭建与原理详解](https://juejin.im/post/5b8fc5536fb9a05d2d01fb11)