共计 7533 个字符,预计需要花费 19 分钟才能阅读完成。
本文次要讲 Redis 集群相干的机制
哨兵机制请参阅专栏:https://segmentfault.com/a/11…
本文参考:
- redis 官网文档 -Redis 集群标准
- 《redis 设计与实现》
Redis 集群定义
Redis 集群是以高性能、平安和高可用为指标而设计的一套分布式实现。
高性能:高吞吐,同时具备高达 1000 个节点的线性扩大能力。没有代理、应用异步复制,同时也不会做值执行合并操作。
平安: 保障肯定水平的写平安,尽最大致力保留连贯大多数主节点的客户端的所有写操作。
高可用:在网络分区的状况,redis 可能在领有大多数主节数的分区下失常提供服务,并且保障每个主节点至多领有一个从节点,当没有从节点的主节点产生故障须要转移时,能够从领有多个从节点的其余主节点接管一个从节点来进行转移。
对于写平安
redis 集群在主从节点间应用异步复制,而最初博得选举的主节点的数据将会替换所有其余从节点的正本。那么,在分区期间总有可能失落写入的工夫窗口。而这个工夫窗口在客户端连贯的是大多数主节点和多数主节点这两种不状况的状况下有很大的不同。而 Redis 集群则是进最大致力来保障连贯到大多数主节点的客户端执行的写入。
以下可能产生数据失落的场景
1、redis 主节点宕机:redis 主节点在解决完写入后将音讯返回给客户,再通过异步复制把写入操作同步到从节点,如果在写入音讯还没有同步到从节点之前主节点宕机了,因为主节点长时间无法访问 Redis 集群将会把其从节点降级为主节点,而从节点又没有同步宕机前在主节点执行的写入操作,那么这时候这个写入操作就失落了。
2、redis 主节点所在的网络产生分区:
呈现以下事件时可能产生写入失落:
- 因为分区导致主节点不可达;
- 产生故障转移,从节点降级为主节点;
- 一段时间后主节点又可达;
- 在 Redis 集群把这个旧主节点转换为新主节点的从节点之前,客户端通过过期的路由表把写操作发送到旧主节点。
第 1 种状况产生可能写抛弃的工夫窗口实际上十分小,因为把响应写回客户端跟把写入操作流传给从节点,这简直是同时产生的。
第 2 种状况其实不太可能产生,因为故障的主节点在长时间 (NODE_TIMEOUT) 无奈与大多数主节点通信时,将会故障转移并且不再承受写入操作,而当分区复原时,在其余节点同步配置给它之前,会有一段时间回绝写入操作。同时,它还须要客户端有一个过期的路由表。
另外,在网络分区的状况下,如果客户端连贯的主节点在在具备多数主节点的分区中,在大数节点主节点分区中 Redis 集群检测到并产生故障转移之前,会有一个比拟大的失落写入的工夫容器。具体的来说,如果产生分区,在 NODE_TIMEOUT 达到之前复原了网络,那么不会产生数据库,因为工夫没有故障转移;而如果在 NODE_TIMEOUT 达到之前网络还没有复原,那么将触发故障转移,在 NODE_TIMEOUT 这段时间的所有写入操作都将失落了。
对于高性能
- 在 Redis Redis 集群中,不会通过代理将申请转发到解决该 key 集的节点上,而是可能依据槽位的散布状况,间接地将向解决该 key 汇合的节点发送申请命令。
- 节点之间通过异步复制,节点不须要期待其余节点的对写入的确认(前提是没有应用 wait 命令)
- 防止值合并。容许多键操作,前提是波及多键操作的单个命令 (或者事务、lua 脚本) 的所有键都是指向同一个 hash 槽。
不存在值合并操作
值合并操作通过产生在查问的键值对在不同节点上有不同的版本,须要获取多个节点的数据后对数据进行一次合并后再返回。
Redis 的设计防止在不同节点上雷同的键值存在版本抵触,并且 redis 的数据模型也不容许这种行为。因为在 Redis,值通常十分大,通过会看到蕴含数百万个元素的列表和排序集,另外 redis 的数据类型也比较复杂。如果须要传输并且合并不同节点上不同版本的值,这可能会是一个性能瓶颈,并且十分十分严格的程序来管制,同时也须要额定调配一些内存来治理它们。
对于高可用
假如在大多数主节点的网络分区,有多个主节点,并且每个不可达主节点都有对应的从节点,在 NODE_TIMEOUT 后的几秒内就可能主动产生故障,而如果在领有多数节点的网络分区,Redis 将是不可用的。实际上 Redis 集群的高可用须要有一个前提,即只是产生多数节点的故障。如果产生大规模的节点故障,那么 Redis 集群将不是一个适合的解决方案。
Redis 集群部署及相干配置
详见另一个链接:
Redis 集群相干机制
key 分布式
Reids 集群的 key 散布不同于单机,单机上所有 key 都散布在一个主节点上,而在 Redis 集群中,key 会通过一致性哈希算法散布到不同槽位上,再把这些槽位调配给各个主节点。在 Redis 集群中,键空间 (key space) 会被划分为 16384 个槽位,实际上是 redis 设计了一致性哈希算法,通过这一算法可能将不同的 key 散列到这 13684 个槽位上。对于集群中的每一个主节点,它将解决这 16384 个槽位的一个子集,并且不同主节点的子集之间的元素没有交加,也就是说,一个槽位只会调配到一个主节点上,而一个主节点容许同时负责多个槽位的解决。
HASH_SLOT = CRC16(key) mod 16384
key 散列标签
除了通过一致性哈希来确认 key 所属的槽位,还能够通过哈希标签将一些雷同标签的 key 强制散列到同一个槽位上。比方,key 的名字组成为 aaa.{xxx}.ccc,那么将仅对 {} 内的 xxx 做哈希散列来计算出槽位。
散列标签的规定为,取第一呈现的 {和第一次进去} 两头的非空字符来做散列,以下列举几种取散列 key 的例子
key | 理论进行哈希散列的字符 |
---|---|
{user1}.name | User1 |
{{user1}}.name | {user1 |
{}.{user1}.name | {}.{user1}.name (第一呈现的 {和第一次进去} 两头的字符为空字符串) |
{class}.{user1}.name | class |
集群拓朴
redis 集群由多个节点组成,每个节点与其它节点都会建设一个 TCP 连贯,整个 redis 集群的网络拓朴出现一个网状结构。redis 集群中节间之间的连贯为长连贯,节点之间通过 ping-pong 的心跳来检测节点之间的状态,ping-pong 的报文其中蕴含了 gossip 协定的报文,这能够用来做配置更新以及节点发现。
尽管节点之间有心跳,但通过 gossip 协定和配置更新机制能够防止失常运行状况下指数级的音讯替换。这个具体下文进行阐明。
节点间的握手
一个集群通常由多个节点组成,在刚开始的时候,每个节点都是独立的,它们都只处于一个蕴含本人的集群中。那么如何将多个节点组成成一个大的集群?
实际上 redis 除了数据同步端口 (比例默认状况下是 6379) 外,还须要监听一个集群总线端口(syncPort+10000, 默认即 16379)。集群间的握手通过总线端口来连贯,如果发送方与接受方同属一个集群,那么通过集群总线端口连贯后,接受方会立刻返回 ping 命令,否则将抛弃所有其余的数据包。
在 redis 集群,只有两种形式可能承受节点作为集群中的一部分:
- 第一种状况是通过 Meet 命令。节点 node1 向指定 ip 和 port 的节点 node2 发送 meet 命令,如 CLUSTER MEET ip port,那么 node2 将被纳入到 node1 所在的集群,作为集群的一部分。
- 第二种状况下,redis 集群节点间的心跳蕴含 gossip 协定的报文,这里边蕴含除其本身节点外,多数随机的其余节点的信息。如果一个被信赖的节点通过 gossip 协定把其余节点的信息流传过去,那么以后节点也会认为它是可信赖的,最终也会建设到该节点的连贯。即 A ->B,B->C,那么 A ->C。
槽位分派
Redis 集群反对通过 CLUSTER 的子命令去批改槽位的分派,具体的命令如下所示
CLUSTER ADDSLOTS slot1 [slot2] ... [slotN]
CLUSTER DELSLOTS slot1 [slot2] ... [slotN]
CLUSTER SETSLOT slot NODE node
CLUSTER SETSLOT slot MIGRATING node
CLUSTER SETSLOT slot IMPORTING node
- ADDSLOTS 和 DELSLOTS 仅用于节点调配槽,调配槽位意味着通知给定的主节点它将负责为指定的哈希槽提供存储和服务。ADDSLOTS 通常用于从头创立集群,为每个主节点调配所有 16384 个可用你哈希槽的子集;DELSLOTS 用于被动批改集群配置或者调试,实际上很有实在场景须要应用到。
- CLUSTER SETSLOT slot NODE node 用于将槽调配给特定 ID 的节点。与 ADDSLOTS 的区别,集体认为,ADDSLOTS 须要将命令发送到指定的主节点,而 CLUSTER SETSLOT slot NODE node 能够发送到集群中任一个主节点。
-
MIGRATING 和 IMPORTING 用来示意节点以后槽位的状态,MIGRATING 示意以后槽位正在迁徙,IMPORTING 示意以后槽位正在导入。
- 主节点在收到
CLUSTER SETSLOT slot MIGRATING node
命令后,会批改指定槽位的状态,并将槽位的键逐渐地迁徙到指定 ID 的节点中,如果这时候收到这个槽位的查问命令,会先判断 key 是否存在于以后节点的键空间,如果存在将会解决该命令,如果不存在,则会返回 ASK 谬误,告诉客户端转向正在导入该槽位的节点,并在发送 key 的申请命令之前发送 ASKING 命令。 - 主节点在收到 CLUSTER SETSLOT slot IMPORTING node 命令后,会开始筹备接管指定 ID 节点的指定槽位,节点也将承受对于这个槽位的所有查问,当然前提必须是客户端提前发送了 ASKING 命令。否则将会返回一个 MOVED 谬误。
留神:在槽位从新分派未完结前,槽位调配配置还是不变,即某一个槽位正在进行迁徙,负责该槽位的节点还是指向迁徙前的结点,这时候还是由它来解决该槽位的申请命令,当槽位内的所有 key 都迁徙到另一个节点时,槽位才会分派到迁徙后的节点,槽位调配配置才会发生变化。
- 主节点在收到
槽位重新分配
在集群中执行命令
在 redis 集群中,因为 key 的分布式,key 常常哈希后被指定到不同的槽位,而不同的槽位又不同的节点负责,并且在上文也提到,redis cluster 并没有代理,客户端到负责该 key 的主节点,采纳的是直连的形式,如果 redis 是如何将 key 的申请发送到精确的主节点上呢?
客户端能够自在地向每个节点发送与数据库无关的命令,接管命令的节点会计算出命令要解决的数据库键 key 属于哪个槽,并且查看这个槽是否指派给了本人(每个节点都会记录本人和其余节点别离负责的槽位)。如果发现以后槽位并没有分派给本人,那么将返回 MOVED 谬误。
MOVED 谬误
如果发现这个数据库键 key 所属的槽并没有指派给本人,那么则会响应一个 MOVED 响应,格局为MOVED <slot> <ip>:<port>
。通过 MOVED 响应来告诉客户端负责解决槽位节点 ip 和端口,客户端在承受到 MOVED 响应后,将之间的申请命令转发到正确的节点上。
ASK 谬误
如果在节点处于从新分片的期间,源节点向指标节点迁徙一个槽时,这时候槽的一部分数据可能还在源节点上,一部分数据曾经迁徙到指标节点上。这时如果源节点收到一个该槽的申请命令,
- 源节点先在本人保留的数据中查找指定的键,如果找到的话,就间接执行客户端的命令。
- 如果源节点没能在本人的键空间中找到指定的键,那么这个键有空间曾经被迁徙到了指标节点,源节点就会向客户端返回一个 ASK(
ASK <slot> <ip>:<port>
),指引客户端转向正在导入槽的指标节点,并再次发送之前想要 申请的命令。
故障转移
心跳
在 Redis 集群中,节点之间通过 ping 包和 pong 包一直地进行数据交换,这两种报文构造雷同,都携带重要的配置信息,惟一的区别是音讯类型字段,ping 和 pong 统称为心跳数据包。
个别状况下,节点发送 ping 包,接收者回复 pong 包。但也有例外的状况,在须要尽快进行配置更新时,节点会间接发送一个 pong 给其余节点。
在一个 redis 集群中,可能存在十分多的节点,一个节点并不会向全副其余的节点都发送 ping 心跳包,而是每秒只会随机的抉择其中几个节点进行发送。另外,节点保留有其余节点最初的心跳刷新工夫,如果某个或某些节点的最终心跳工夫超过了 NODE_TIMEOUT 的一半,那么也会向这个或这些节点发送一个 ping 心跳包,并且在 NODE_TIMEOUT 工夫达到之前,还会尝试从新建设一个 TCP 连贯,以确保不会因为以后的 TCP 连贯存在问题而认为节点不可达。
那么如果 NODE_TIMEOUT 设置为十分短并且节点数十分多,则可能导致集群中的心跳特地频繁。
心跳包的数据结构
上文咱们提到 ping 包和 pong 包都属于心跳包。这两者蕴含一个对所有包类型公共的包头 header,以及一个特定于 ping 和 pong 的非凡 gossip 局部。
公共包头蕴含以下信息:
- Node ID:节点 id,一个 120bit 的伪随机字符,在第一个创立节点时调配的,并在 redis 集群中整个生命周期放弃不变(除非调用 reset 命令)。
- configEpoch 和 currentEpoch:配置纪元。(todo)
- node Flag:节点身份标识,示意是 master 还是 slave,或者是其余 sing-bit 节点
- myslots:一个用 bitmap 来示意的以后节点负责的槽位,如果是从节点,则发送的是其主节点的槽位 bitmap。
- port:发送方的 tcp 端口(加 10000 即可取得 redis 集群总线端口)
- state:节点状态(ok 或者 down)
- slaveof:正在复制的主节点 ID,如果以后是主节点,则是一个 40 字节长的 00000 字符数组。
Gossip
除此之外,ping 包和 pong 包还蕴含一个 gossip 局部。gossip 局部向接管方提供了发送方对集群中其余节点的认知,这部分并不蕴含所有发送所有已知的其余节点信息,而是只蕴含其中随机的几个节点信息,具体的数量与集群的大小成正比。gossip 局部中每个节点的构造如下所示:
- Node ID
- IP and Port:地址和端口
- Node Flags:节点标识
Gossip 局部容许接受者通过从发送者获取其余节点的状态信息,这对于故障检测和发现集群中的其余节点是十分有用的。
故障探测
Redis 故障探测用于辨认大多数节点不再能够拜访主节点或从节点,并做出将一个从节点晋升为主节点的响应。另外,当集群中的从节点无奈降级时,集群将处理错误状态以进行接管来自客户端的查问。
正如咱们上方提到的,每个节点都存储了已知其余节点的相关联的状态标识,其中有两个标识用于故障检测,即 PFAIL 和 FAIL。
- PFAIL 示意可能的故障,是一种未确认的故障类型,与 Sentinel 模式下的主观下线相似。
- FAIL 示意节点曾经故障,并且是通过大多数节点在固定工夫内的确认。
PFAIL
当节点 A 发现节点 B 在超过 NODE_TIMEOUT 工夫不可拜访,节点 A 会应用 PFAIL 标识节点 B。上文 心跳 章节咱们提过节点之间通过心跳进行探测,每个节点保留有其余节点的最初心跳刷新工夫,如果除了随机探测,还会在最初心跳刷新工夫 +NODE_TIMEOUT/ 2 时,次要发动一次探测,同时会还从新创立 TCP 连贯确保没有因为连贯问题而误判。
FAIL
在节点被标识为 PFAIL 后还不足以触发从节点降级机制,因为 PFAIL 只是每个节点对于其余节点的一个主观认知。要 将一个节点断定为不可用,PFAIL 须要降级为 FAIL.
在心跳大节早提及过,节点间会通过心跳中的 gossip 局部来传递对其余节点信息的视图,如果在 gossip 局部中,一个节点的标识为 PFAIL,那么则会记录这个节点的 PFAIL 报告,这个 FPAIL 报告具备时效性(默认 NODE_TIMEOUT*2),如果过期,那么它会被移除。当一个节点的 PFAIL 报告足够多时(满足需要的票数),那么 PFAIL 将会降级为 FAIL,发送 FAIL 包给所有其余节点。
FAIL 状态是单向的,它不会再进行降级,只能在满足条件下进行移除
- 节点复原可达并且该节点的角色是从节点。在这种状况下 FLAG 能够革除,因为从节点没有故障转移。
- 节点复原可达,且该节点的角色是主节点,但这个主节点没有被分派任何槽位。在这种状况下,FAIL 状态能够革除,因为没有调配槽位的节点并不是集群中真正的一部分,并且在期待集群配置来退出集群。
- 节点复原可达,且该节点的角色是主节点,但很长一段时间 (n 次 NODE_TIMEOUT) 后依然没有任何的从节点降级(可能起因:网络分区)。它最好重新加入集群并且放弃现状。
故障转移
在上一节中咱们理解到,主节点是如何被断定为下线状态的。那么主节点下线后,从节点降级为主节点这个流程是如何进行的呢?实际上选取哪个从节点来进行降级,这个不是由主节点来抉择的,而是靠从节点自行发动招标,获取得大多数票的从节点将降级主节点,接管旧主节点负责的槽位。具体流程如下所示:
- 从节点收到 FAIL 包
- 从节点与下线主节点的其余从节点进行通信,依据从节点的数据复制的 offset 来计算本身的等级,offset 越高的从节点等级越高,0 示意最高级。
-
依据等级提早肯定工夫,目标是为了让等级高的优先开始投票选举(计算公式如下)
DELAY = 500 milliseconds + random delay between 0 and 500 milliseconds + SLAVE_RANK * 1000 milliseconds.
- 向各个主节点发送 FAILOVER_AUTH_REQUEST,期待响应获取批准;FAILOVER_AUTH_REQUEST 蕴含 epoch 和须要负责的槽位 bitmap
-
主节点在以下条件都满足的条件下,返回批准投票;
- FAILOVER_AUTH_REQUEST 的 currentEpoch 大于主节点的 lastVoteEpoch,且未对雷同的 epoch 投过票。
- 发动投票的从节点的主节点被以后主节点标识为 FAIL
- FAILOVER_AUTH_REQUEST 的 currentEpoch 不能小于主节点的 currentEpoch
- 从节点在失去超过半数的投票后,它开始通过 ping 和 ping 包宣示本人成为主节点,蕴含本人的 ConfigEpoch 和接管的 hash 槽。同时为了尽快地其余节点的配置更新,它还会立刻播送 pong 到所有其余节点。如果以后存在其余节点不可达,那么当这些节点复原后将同其余节点的 ping 和 pong 包来更新更新,或者在其余节点发现故障复原的节点发送的心跳包曾经过期后,将会给后者发现更新包。