redis架构系列——Cluster集群模式详解

设计的主要特点和基本原理

Redis 集群目标

  • 高性能和线性可扩展性,最多可达1000个节点。没有代理,使用异步复制,并且不对值执行合并操作。
  • 可接受的写入安全程度:系统尝试(尽最大努力)保留来自与大多数主节点连接的客户端的所有写入。通常,有一些小窗口可能会丢失确认的写入。当客户端位于少数分区中时,丢失确认写入的窗口会更大。
  • 可用性:Redis集群能够在大多数主节点可访问的分区中继续存在,并且每个主节点至少有一个无法再访问的副本。此外,使用副本迁移时,不再被任何副本复制的主节点将从多个副本覆盖的主节点接收一个副本。

已实现的子集

Redis集群实现了Redis的非分布式版本。执行复杂多键的命令在以下情况下实现设置并集和交集等操作操作中涉及的所有密钥都哈希到同一插槽。

Redis集群实现了一个称为哈希标签的概念,可以使用强制将某些密钥存储在同一个哈希槽中。然而,在手动重新分片、多键操作可能在一段时间内不可用而单键操作始终可用。

Redis集群不支持像单机版那样的多个数据库的Redis。仅支持database 0;不允许select命令

Redis 集群协议中的客户端和服务器角色

在Redis集群中,节点负责保存数据,并获取集群的状态,包括将密钥映射到正确的节点。群集节点还能够自动发现其他节点,检测不工作节点,并在需要时按顺序将副本节点提升为master在发生故障时继续运行。

为了执行其任务,所有群集节点都使用TCP总线和二进制协议,称为Redis集群总线。每个节点都使用集群连接到集群中的所有其他节点总线。节点使用gossip协议来传播有关集群的信息为了发现新节点,发送ping数据包以确保所有其他节点工作正常,并且需要发送集群消息信号特定条件。集群总线也用于在集群中传播Pub/Sub消息并编排手动消息用户请求时的故障转移(手动故障转移是故障转移不是由Redis集群故障检测器启动的,而是由直接系统管理员)。

因为Node并不提供Proxy机制,当Client将请求发给错误的nodes时(此node上不存在此key所属的slot),node将会反馈“MOVED”或者“ASK”错误信息,以便Client重新定向到合适的node。理论上,Client可以将请求发送给任意一个nodes,然后根据在根据错误信息转发给合适的node,客户端可以不用保存集群的状态信息,当然这种情况下性能比较低效,因为Client可能需要2次TCP调用才能获取key的结果,通常客户端会缓存集群中nodes与slots的映射关系,并在遇到“Redirected”错误反馈时,才会更新本地的缓存。

安全写入(write safety)

在Master-slaves之间使用异步replication机制,在failover之后,新的Master将会最终替代其他的replicas(即slave)。在出现网络分区时(network partition),总会有一个窗口期(node timeout)可能会导致数据丢失;不过,Client与多数派的Master、少数派Master处于一个分区(网络分区,因为网络阻断问题,导致Clients与Nodes被隔离成2部分)时,这两种情况下影响并不相同。

1)write提交到master,master执行完毕后向Client反馈“OK”,不过此时可能数据还没有传播给slaves(异步replication);如果此时master不可达的时间超过阀值(node timeout,参见配置参数),那么将触发slave被选举为新的Master(即Failover),这意味着那些没有replication到slaves的writes将永远丢失了!

2)还有一种情况导致数据丢失:
A)因为网络分区,此时master不可达,且Master与Client处于一个分区,且是少数派分区。
B)Failover机制,将其中一个slave提升为新Master。
C)此后网络分区消除,旧的Master再次可达,此时它将被切换成slave。
D)那么在网络分区期间,处于少数派分区的Client仍然将write提交到旧的Master,因为它们觉得Master仍然有效;当旧的Master再次加入集群,切换成slave之后,这些数据将永远丢失。

可用性

处于“小分区”的集群节点是不可用的;“大分区”端必须持有大多数Masters,同时每个不可达的Master至少有一个slave也在“大分区”端,当NODE_TIMEOUT时,触发failover,此后集群才是可用的。Redis Cluster在小部分nodes失效后仍然可以恢复有效性,如果application希望大面积节点失效仍然有效,那么Cluster不适合这种情况。

比如集群有N个Master,且每个Master都有一个slave,那么集群的有效性只能容忍一个节点(master)被分区隔离(即一个master处于小分区端,其他处于大分区端),当第二个节点被分区隔离之前仍保持可用性的概率为1 - (1/(N * 2 - 1))(解释:当第一个节点失效后,剩余N * 2 -1个节点,此时没有slave的Master失效的概率为1/(N * 2 -1))。比如有5个Master,每个Master有一个slave,当2个nodes被隔离出去(或者失效)后,集群可用性的概率只有1/(5 * 2 - 1) = 11.11%,因此集群不再可用。

幸好Redis Cluster提供了“replicas migration”机制,在实际应用方面,可以有效的提高集群的可用性,当每次failover发生后,集群都会重新配置、平衡slaves的分布,以更好的抵御下一次失效情况的发生。

在第二种情况下,如果Master无法与其他大多数Masters通讯的时间超过阀值后,此Master也将不再接收Writes,自动切换为readonly状态。当网络分区消除后,仍然会有一小段时间,客户端的write请求被拒绝,因为此时旧的Master需要更新本地的集群状态、与其他节点建立连接、角色切换为slave等等,同时Client端的路由信息也需要更新。

只有当此master被大多数其他master不可达的时间达到阀值时,才会触发Failover,这个时间称为NODE_TIMEOUT,可以通过配置设定。所以当网络分区在此时间被消除的话,writes不会有任何丢失。反之,如果网络分区持续时间超过此值,处于“小分区”(minority)端的Master将会切换为readonly状态,拒绝客户端继续提交writes请求,那么“大分区”端将会进行failover,这意味着NODE_TIMEOUT期间发生在“小分区”端的writes操作将丢失(因为新的Master上没有同步到那些数据)。

Cluster主要组件

keys分布模型

集群将key分成16384个slots(hash 槽),slot是数据映射的单位,言外之意,Redis Cluster最多支持16384个nodes(每个nodes持有一个slot)。集群中的每个master持有16384个slots中的一部分,处于“stable”状态时,集群中没有任何slots在节点间迁移,即任意一个hash slot只会被单个node所服务(master,当然可以有多个slave用于replicas,slave也可以用来扩展read请求)。keys与slot的映射关系,是按照如下算法计算的:HASH_SLOT = CRC16(key) mod 16384。其中CRC16是一种冗余码校验和,可以将字符串转换成16位的数字。

hash tags

在计算hash slots时有一个意外的情况,用于支持“hash tags”;hash tags用于确保多个keys能够被分配在同一个hash slot中,用于支持multi-key操作。hash tags的实现比较简单,key中“{}”之间的字符串就是当前key的hash tags,如果存在多个“{}”,首个符合规则的字符串作为hash tags,如果“{}”存在多级嵌套,那么最内层首个完整的字符串作为hash tags,比如“{foo}.student”,那么“foo”是hash tags。如果key中存在合法的hash tags,那么在计算hash slots时,将使用hash tags,而不再使用原始的key。即“foo”与“{foo}.student”将得到相同的slot值,不过“{foo}.student”仍作为key来保存数据,即redis中数据的key仍为“{foo}.student”。

集群节点的属性

集群中每个节点都有唯一的名字,称之为node ID,一个160位随机数字的16进制表示,在每个节点首次启动时创建。每个节点都将各自的ID保存在实例的配置文件中,此后将一直使用此ID,或者说只要配置文件不被删除,或者没有使用“CLUSTER RESET”指令重置集群,那么此ID将永不会修改。

集群通过node ID来标识节点,而不是使用IP + port,因为node可以修改它的IP和port,不过如果ID不变,我们仍然认定它是集群中合法一员。集群可以在cluster Bus中通过gossip协议来探测IP、port的变更,并重新配置。

node ID并不是与node相关的唯一信息,不过是唯一一个全局一致的。每个node还持有如下相关的信息,有些信息是关系集群配置的,其他的信息比如最后ping时间等。每个node也保存其他节点的IP、Port、flags(比如flags表示它是master还是slave)、最近ping的时间、最近pong接收时间、当前配置的epoch、链接的状态,最重要的是还包含此node上持有的hash slots。这些信息均可通过“CLUSTER NODES”指令开查看。

Cluster Bus

每个Node都有一个特定的TCP端口,用来接收其他nodes的链接;此端口号为面向Client的端口号 + 10000,比如果客户端端口号为6379,那么次node的BUS端口号为16379,客户端端口号可以在配置文件中声明。由此可见,nodes之间的交互通讯是通过Bus端口进行,使用了特定的二进制协议,此端口通常应该只对nodes可用,可以借助防火墙技术来屏蔽其他非法访问。

集群拓扑

Redis Cluster中每个node都与其他nodes的Bus端口建立TCP链接(full mesh,全网)。比如在由N各节点的集群中,每个node有N-1个向外发出的TCP链接,以及N-1个其他nodes发过来的TCP链接;这些TCP链接总是keepalive,不是按需创建的。如果ping发出之后,node在足够长的时间内仍然没有pong响应,那么次node将会被标记为“不可达”,那么与此node的链接将会被刷新或者重建。Nodes之间通过gossip协议和配置更新的机制,来避免每次都交互大量的消息,最终确保在nodes之间的信息传送量是可控的。

节点间handshake

Nodes通过Bus端口发送ping、pong;如果一个节点不属于集群,那么它的消息将会被其他nodes全部丢弃。一个节点被认为是集群成员的方式有2种:
1)如果此node在“Cluster meet”指令中引入,此命令的主要意义就是将指定node加入集群。那么对于当前节点,将认为指定的node为“可信任的”。(此后将会通过gossip协议传播给其他nodes)
2)当其他nodes通过gossip引入了新的nodes,这些nodes也是被认为是“可信任的”。

只要我们将一个节点加入集群,最终此节点将会与其他节点建立链接,即cluster可以通过信息交换来自动发现新的节点,链接拓扑仍然是full mesh。

集群currentEpoch

Redis Cluster使用了类似于Raft算法“term”(任期)的概念,那么在redis Cluster中term称为epoch,用来给events增量版本号。当多个nodes提供了信息有冲突时,它可以作为node来知道哪个状态是最新的。currentEpoch为一个64位无签名数字。

在集群node创建时,master和slave都会将各自的currentEpoch设置为0,每次从其他node接收到数据包时,如果发现发送者的epoch值比自己的大,那么当前node将自己的currentEpoch设置为发送者的epoch。由此,最终所有的nodes都会认同集群中最大的epoch值;当集群的状态变更,或者node为了执行某个行为需求agreement时,都将需要epoch(传递或者比较)。

当前来说,只有在slave提升期间发生;currentEpoch为集群的逻辑时钟(logical clock),指使持有较大值的获胜。(currentEpoch,当前集群已达成认同的epoch值,通常所有的nodes应该一样)

configEpoch

每个master总会在ping、pong数据包中携带自己的configEpoch以及它持有的slots列表。新创建的node,其configEpoch为0,slaves通过递增它们的configEpoch来替代失效的master,并尝试获得其他大多数master的授权(认同)。当slave被授权,一个新的configEpoch被生成,slave提升为master且使用此configEpoch。

接下来介绍configEpoch帮助解决冲突,当不同的nodes宣称有分歧的配置时。

slaves在ping、pong数据包中也会携带自己的configEpoch信息,不过这个epoch为它与master在最近一次数据交换时,master的configEpoch。

每当节点发现configEpoch值变更时,都会将新值写入nodes.conf文件,当然currentEpoch也也是如此。这两个变量在写入文件后会伴随磁盘的fsync,持久写入。严格来说,集群中所有的master都持有唯一的configEpoch值。同一组master-slaves持有相同的configEpoch。

slave选举与提升

在slaves节点中进行选举,在其他masters的帮助下进行投票,选举出一个slave并提升为master。当master处于FAIL状态时,将会触发slave的选举。slaves都希望将自己提升为master,此master的所有slaves都可以开启选举,不过最终只有一个slave获胜。当如下情况满足时,slave将会开始选举:

1)当此slave的master处于FAIL状态

2)此master持有非零个slots

3)此slave的replication链接与master断开时间没有超过设定值,为了确保此被提升的slave的数据是新鲜的,这个时间用户可以配置。

为了选举,第一步,就是slave自增它的currentEpoch值,然后向其他masters请求投票(需求支持,votes)。slave通过向其他masters传播“FAILOVER_AUTH_REQUEST”数据包,然后最长等待2倍的NODE_TIMEOUT时间,来接收反馈。一旦一个master向此slave投票,将会响应“FAILOVER_AUTH_ACK”,此后在2 * NODE_TIMOUT时间内,它将不会向同一个master的slaves投票;虽然这对保证安全上没有必要,但是对避免多个slaves同时选举时有帮助的。slave将会丢弃那些epoch值小于自己的currentEpoch的AUTH_ACK反馈,即不会对上一次选举的投票计数(只对当前轮次的投票计数)。一旦此slave获取了大多数master的ACKs,它将在此次选举中获胜;否则如果大多数master不可达(在2 * NODE_TIMEOUT)或者投票额不足,那么它的选举将会被中断,那么其他的slave将会继续尝试。

slave rank(次序)

当master处于FAIL状态时,slave将会随机等待一段时间,然后才尝试选举,等待的时间:

DELAY = 500ms + random(0 ~ 500ms) + SLAVE_RANK * 1000ms

一定的延迟确保我们等待FAIL状态在集群中传播,否则slave立即尝试选举(不进行等待的话),不过此时其他masters或许尚未意识到FAIL状态,可能会拒绝投票。

延迟的时间是随机的,这用来“去同步”(desynchronize),避免slaves同时开始选举。SLAVE_RANK表示此slave已经从master复制数据的总量的rank。当master失效时,slaves之间交换消息以尽可能的构建rank,持有replication offset最新的rank为0,第二最新的为1,依次轮推。这种方式下,持有最新数据的slave将会首先发起选举(理论上)。当然rank顺序也不是严格执行的,如果一个持有较小rank的slave选举失败,其他slaves将会稍后继续。

一旦,slave选举成功,它将获取一个新的、唯一的、自增的configEpoch值,此值比集群中任何masters持有的都要大,它开始宣称自己是master,并通过ping、pong数据包传播,并提供自己的新的configEpoch以及持有的slots列表。为了加快其他nodes的重新配置,pong数据包将会在集群中广播。当前node不可达的那些节点,它们可以从其他节点的ping或者pong中获知信息(gossip),并重新配置。

其他节点也会检测到这个新的master和旧master持有相同的slots,且持有更高的configEpoch,此时也会更新自己的配置(epoch,以及master);旧master的slaves不仅仅更新配置信息,也会重新配置并与新的master跟进(slave of)。

Masters响应slave的投票请求

当Master接收到slave的“FAILOVER_AUTH_REQUEST”请求后,开始投票,不过需要满足如下条件:

1)此master只会对指定的epoch投票一次,并且拒绝对旧的epoch投票:每个master都持有一个lastVoteEpoch,将会拒绝AUTH_REQUEST中currentEpoch比lastVoteEpoch小的请求。当master响应投票时,将会把lastVoteEpoch保存在磁盘中。

2)此slave的master处于FAIL状态时,master才会投票。

3)如果slave的currentEpoch比此master的currentEpoch小,那么AUTH_REQUEST将会被忽略。因为master只会响应那些与自己的currentEpoch相等的请求。如果同一个slave再此请求投票,持有已经增加的currentEpoch,它(slave)将保证旧的投票响应不能参与计票。

比如master的currentEpoch为5,lastVoteEpoch为1:

1)slave的currentEpoch为3

2)slave在选举开始时,使用epoch为4(先自增),因为小于master的epoch,所以投票响应被延缓。

3)slave在一段时间后将重新选举,使用epoch为5(4 + 1,再次自增),此时master上延缓的响应发给slave,接收后视为有效。

1)master在2 * NODE_TIMEOUT超时之前,不会对同一个master的slave再次投票。这并不是严格需要,因为也不太可能两个slave在相同的epoch下同时赢得选举。不过,它确保当一个slave选举成功后,它(slave)有一段缓冲时间来通知其他的slaves,避免另一个slave赢得了新的一轮的选择,避免不必要的二次failover。

2)master并不会尽力选举最合适的slave。当slave的master处于FAIL状态,此master在当前任期(term)内并不投票,只是批准主动投票者(即master不发起选举,只批准别人的投票)。最合适的slave应该在其他slaves之前,首先发起选举。

3)当master拒绝一个slave投票,并不会发出一个“否决”响应,而是简单的忽略。

4)slave发送的configEpoch是其master的,还包括其master持有的slots;master不会向持有相同slots、但configEpoch只较低的slave投票。

Hash Slots配置传播

Redis Cluster中重要的一部分就是传播集群中哪些节点上持有的哪些hash slots信息;无论是启动一个新的集群,还是当master失效其slave提升后更新配置,这对它们都至关重要。有2种方式用于hash slot配置的传播:

1)heartbeat 消息:发送者的ping、pong消息中,总是携带自己目前持有的slots信息,不管自己是master还是slave。

2)UPDATE 消息:因为每个心跳消息中会包含发送者的configEpoch和其持有的slots,如果接收者发现发送者的信息已经stale(比如发送者的configEpoch值小于持有相同slots的master的值),它会向发送者反馈新的配置信息(UPDATE),强制stale节点更新它。

当一个新的节点加入集群,其本地的hash slots映射表将初始为NULL,即每个hash slot都没有与任何节点绑定。

Rule 1:如果此node本地视图中一个hash slot尚未分配(设置为NULL),并且有一个已知的node声明持有它,那么此node将会修改本地hash slot的映射表,将此slot与那个node关联。slave的failover操作、reshard操作都会导致hash slots映射的变更,新的配置信息将会通过心跳在集群中传播。

Rule 2:如果此node的本地视图中一个hash slot已经分配,并且一个已知的node也声明持有它,且此node的configEpoch比当前slot关联的master的configEpoch值更大,那么此node将会把slot重新绑定到新的node上。根据此规则,最终集群中所有的nodes都赞同那个持有声明持有slot、且configEpoch最大值的nodes为slot的持有者。
  • 29
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吴代庄

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值