聊一聊 Redis Cluster的实现


本文摘自Redis官方文档。
Redis 集群教程
Redis 集群规范

1.Redis集群介绍

Redis 集群是一个提供在多个Redis间节点间共享数据的程序集。

Redis集群并不支持处理多个keys的命令,因为这需要在不同的节点间移动数据,从而达不到像Redis那样的性能,在高负载的情况下可能会导致不可预料的错误.

Redis 集群通过分区来提供一定程度的可用性,在实际环境中当某个节点宕机或者不可达的情况下继续处理命令. Redis 集群的优势:

  • 自动分割数据到不同的节点上。
  • 整个集群的部分节点失败或者不可达的情况下能够继续处理命令。

1.2 Redis 集群的数据分片

Redis 集群没有使用一致性hash, 而是引入了 哈希槽 的概念。

Redis 集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽.集群的每个节点负责一部分hash槽,举个例子,比如当前集群有3个节点,那么:

  1. 节点 A 包含 0 到 5500号哈希槽.
  2. 节点 B 包含5501 到 11000 号哈希槽.
  3. 节点 C 包含11001 到 16384号哈希槽.
    这种结构很容易添加或者删除节点. 比如如果我想新添加个节点D, 我需要从节点 A, B, C中得部分槽到D上. 如果我想移除节点A,需要将A中的槽移到B和C节点上,然后将没有任何槽的A节点从集群中移除即可. 由于从一个节点将哈希槽移动到另一个节点并不会停止服务,所以无论添加删除或者改变某个节点的哈希槽的数量都不会造成集群不可用的状态.

1.3 Redis 集群的主从复制模型

为了使在部分节点失败或者大部分节点无法通信的情况下集群仍然可用,所以集群使用了主从复制模型,每个节点都会有N-1个复制品.

在我们例子中具有A,B,C三个节点的集群,在没有复制模型的情况下,如果节点B失败了,那么整个集群就会以为缺少5501-11000这个范围的槽而不可用.

然而,如果在集群创建的时候(或者过一段时间)我们为每个节点添加一个从节点A1,B1,C1,那么整个集群便有三个master节点和三个slave节点组成,这样在节点B失败后,集群便会选举B1为新的主节点继续服务,整个集群便不会因为槽找不到而不可用了

不过当B和B1 都失败后,集群是不可用的.

扫描二维码关注公众号,回复: 12683373 查看本文章

1.4 Redis 一致性保证

Redis 并不能保证数据的强一致性. 这意味这在实际中集群在特定的条件下可能会丢失写操作.

第一个原因是因为集群是用了异步复制. 写操作过程:

  1. 客户端向主节点B写入一条命令.
  2. 主节点B向客户端回复命令状态.
  3. 主节点将写操作复制给他得从节点 B1, B2 和 B3.

主节点对命令的复制工作发生在返回命令回复之后, 因为如果每次处理命令请求都需要等待复制操作完成的话, 那么主节点处理命令请求的速度将极大地降低 —— 我们必须在性能和一致性之间做出权衡。 注意:Redis 集群可能会在将来提供同步写的方法。 Redis 集群另外一种可能会丢失命令的情况是集群出现了网络分区, 并且一个客户端与至少包括一个主节点在内的少数实例被孤立。

举个例子 假设集群包含 A 、 B 、 C 、 A1 、 B1 、 C1 六个节点, 其中 A 、B 、C 为主节点, A1 、B1 、C1 为A,B,C的从节点, 还有一个客户端 Z1 假设集群中发生网络分区,那么集群可能会分为两方,大部分的一方包含节点 A 、C 、A1 、B1 和 C1 ,小部分的一方则包含节点 B 和客户端 Z1 .


Z1仍然能够向主节点B中写入, 如果网络分区发生时间较短,那么集群将会继续正常运作,如果分区的时间足够让大部分的一方将B1选举为新的master,那么Z1写入B中得数据便丢失了.

注意, 在网络分区出现期间, 客户端 Z1 可以向主节点 B 发送写命令的最大时间是有限制的, 这一时间限制称为节点超时时间(node timeout), 是 Redis 集群的一个重要的配置选项。

如何搭建Redis集群,也见 官方文档:
Redis 集群教程
Redis 集群规范

1.5 Redis Cluster 如何故障转移?

Redis cluster集群 故障转移

1.6 Redis 集群的目标

Redis 集群是 Redis 的一个分布式实现,主要是为了实现以下这些目标(按在设计中的重要性排序):

  • 在1000个节点的时候仍能表现得很好并且可扩展性(scalability)是线性的。
  • 没有合并操作,这样在 Redis 的数据模型中最典型的大数据值中也能有很好的表现。
  • 写入安全(Write safety):那些与大多数节点相连的客户端所做的写入操作,系统尝试全部都保存下来。不过公认的,还是会有小部分(small windows?)写入会丢失。
  • 可用性(Availability):在绝大多数的主节点(master node)是可达的,并且对于每一个不可达的主节点都至少有一个它的从节点(slave)可达的情况下,Redis 集群仍能进行分区(partitions)操作。

1.7 实现子集

Redis 集群实现了所有在非分布式 Redis 版本中的处理单一键值(key)的命令。那些使用多个键值的复杂操作, 比如 set 里的并集(unions)和交集(intersections)操作,只有在这些key是在同一哈希槽中才可用。

Redis Cluster中有hash tags 的概念,它用于将某些特定key始终存在相同的哈希槽中。然而,在手动重新分片期间,多键操作是不可用的,单键操作始终可用。

Redis 集群不像单机版本的 Redis 那样支持多个数据库,集群只有数据库 0,而且也不支持 SELECT 命令。

注意:Redis支持多个数据库,并且每个数据库的数据是隔离的不能共享,并且基于单机才有,如果是集群就没有数据库的概念。
Redis是一个字典结构的存储服务器,而实际上一个Redis实例提供了多个用来存储数据的字典,客户端可以指定将数据存储在哪个字典中。这与我们熟知的在一个关系数据库实例中可以创建多个数据库类似,所以可以将其中的每个字典都理解成一个独立的数据库。

每个数据库对外都是一个从0开始的递增数字命名,Redis默认支持16个数据库(可以通过配置文件支持更多,无上限),可以通过配置databases来修改这一数字。客户端与Redis建立连接后会自动选择0号数据库,不过可以随时使用SELECT命令更换数据库,如要选择1号数据库:

redis> SELECT 1
OK
redis [1] > GET foo
(nil)

1.8 在Redis Cluster 协议中的客户端和服务端

在 Redis 集群中,节点负责存储数据、记录集群的状态(包括键值到正确节点的映射)。集群节点同样能自动发现其他节点,检测出没正常工作的节点, 并且在需要的时候在从节点中推选出主节点。

为了完成这些任务,集群中所有的节点都使用TCP bus 和 二进制协议,被称作Redis Cluster Bus,进行连接。 每一个节点都通过集群连接(cluster bus)与集群上的其余每个节点连接起来。节点们使用一个 gossip 协议来传播集群的信息,这样可以:发现新的节点、 发送ping包(用来确保所有节点都在正常工作中)、在特定情况发生时发送集群消息。集群连接也用于在集群中发布或订阅消息,手动故障转移等。

由于集群节点不能代理(proxy)请求,所以客户端在接收到重定向错误(redirections errors) -MOVED 和 -ASK 的时候, 将命令重定向到其他节点。理论上来说,客户端是可以自由地向集群中的所有节点发送请求,在需要的时候把请求重定向到其他节点,所以客户端是不需要保存集群状态。 不过客户端可以缓存键值和节点之间的映射关系,这样能明显提高命令执行的效率。

1.9 安全写入

Redis Cluster在节点之间使用异步复制,最后一次故障转移将获得隐式合并功能。这意味着最后选择的主的数据集最终将替换所有其他从的副本。在分区期间总会存在一定的时间窗口会丢失写数据。一个连接到绝大部分主节点的客户端的时间窗口,与一个连接到极小部分主节点的客户端的时间窗口是十分不同的。

Redis 集群会努力尝试保存所有与大多数主节点连接的客户端执行的写入,但以下两种情况除外:

  1. 一个写入操作能到达一个主节点,但当主节点要回复客户端的时候,这个写入有可能没有异步复制到从节点。 如果在某个写入操作没有到达从节点的时候主节点已经宕机了,那么该写入会永远地丢失掉,以防主节点长时间不可达而它的一个从节点已经被提升为主节点。
  2. 另一个理论上可能会丢失写入操作的模式是:
    • 由于网络分区导致的主节点不可达。
    • 故障转移(fail over)到主节点的一个从节点。(即从节点被提升为主节点)
    • 过一段时间之后主节点再次变得可达。
    • 一个没有更新路由表(routing table)的客户端或许会在集群把这个主节点变成一个从节点(新主节点的从节点)之前对它进行写入操作。

实际上这是极小概率事件,这是因为,那些由于长时间无法被大多数主节点访问到的节点会被故障转移掉,不再接受任何写入操作,当其分区修复好以后仍然会在一小段时间内拒绝写入操作好让其他节点有时间被告知配置信息的变更。

一个主节点要被故障转移,必须是大多数主节点在至少 NODE_TIMEOUT 这么长时间里无法访问该节点,所以如果分区在这段时间之前修复好了,就没有写入操作会丢失。当分区故障持续超过 NODE_TIMEOUT,集群的多数节点这边会在一超过 NODE_TIMEOUT 这个时间段后开始拒绝往受损分区进行写入,所以在少数节点这边(指分区)变得不再可用后,会有一个写入操作最大损失范围(因为在指定时间段后将不会再有写入操作被接收或丢失)。

1.10可用性

Redis 集群在分区的少数节点那边不可用。集群假设在分区的多数节点这边至少有大多数可达的主节点,并且对于每个不可达主节点都至少有一个从节点可达,在经过了差不多 NODE_TIMEOUT 这么长时间后,有个从节点被推选出来并故障转移掉它的主节点,这时集群又再恢复可用。

这意味着 Redis 集群的设计是能容忍集群中少数节点的出错,但对于要求大量网络分块(large net splits)的可用性的应用来说,这并不是一个合适的解决方案。

举个例子,一个由 N 个主节点组成的集群,每个主节点都只有一个从节点。当有一个节点(因为故障)被分割出去后,集群的多数节点这边仍然是可访问的。当有两个节点(因故障)被分割出去后集群仍可用的概率是 1-(1/(N2-1))(在第一个节点故障出错后总共剩下 N2-1 个节点,那么失去冗余备份(即失去从节点)的那个主节点也故障出错的概率是 1/(N*2-1)))。

比如一个拥有5个节点的集群,每个节点都只有一个从节点,那么在两个节点从多数节点这边分割出去后集群不再可用的概率是 1/(5*2-1) = 0.1111,即有大约 11% 的概率。

1.11 性能

Redis Cluster 的节点并不会把客户端对某key的请求转发到该key所在的正确节点上,而是重定向客户端到正确的节点上。最终客户端获得一份最新的集群表示,里面有写着哪些节点服务哪些键值子集,所以在正常操作中客户端是直接联系到对应的节点并把给定的命令发过去。

由于使用了异步复制,因此节点不必等待其他节点的写入确认(如果未使用WAIT命令明确请求)。

同样,由于一些命令不支持操作多个键值,如果不是重新分片(resharding),那么数据是永远不会在节点间移动的。所以普通操作是可以被处理得跟在单一 Redis 上一样的。这意味着,在一个拥有 N 个主节点的 Redis 集群中,由于 Redis 的设计是支持线性扩展的,所以你可以认为同样的操作在集群上的表现会跟在单一 Redis 上的表现乘以 N 一样。同时,询问(query)通常在一次循环中被执行,客户端会保持跟节点持续不断的连接,所以延迟数据跟在单一 Reids 上是一样的。

为什么要避免使用合并操作
Redis 集群的设计是避免在多个节点中存在同个键值对的冲突版本,这是因为 Redis 数据模型并不提倡这么做:Redis 中的值通常都是比较大的,经常可以看到列表或者排序好的集合中有数以百万计的元素。数据类型也是语义复杂的。传输和合并这样的值将会变成一个主要的性能瓶颈。

2. Redis Cluster中的主要概念

2.1 键分布模型(key distributed model)

键空间被分割为 16384 槽(slot),事实上集群的最大节点数量是 16384 个。(然而建议最大节点数量设置在 ~1000这个数量级上)

所有的主节点都负责 16384 个哈希槽中的一部分。当集群处于稳定状态时,集群中没有在执行重配置(reconfiguration)操作,每个哈希槽都只由一个节点进行处理(不过主节点可以有一个或多个从节点,可以在网络断线或节点失效时替换掉主节点)。

HASH_SLOT = CRC16(key) mod 16384

2.2 键哈希标签(Keys hash tags)

hash tags 用来确保多个key被分配到同样的哈希槽中。它是为了在Redis Cluster中实现多键操作。为了实现哈希标签,哈希槽是用另一种不同的方式计算的。基本来说,如果一个键包含一个 “{…}” 这样的模式,只有 { 和 } 之间的字符串会被用来做哈希以获取哈希槽。但是由于可能出现多个 { 或 },计算的算法如下:

  • 比如这两个键 {user1000}.following 和 {user1000}.followers 会被哈希到同一个哈希槽里,因为只有 user1000 这个子串会被用来计算哈希值。
  • 对于 foo{}{bar} 这个键,整个键都会被用来计算哈希值,因为第一个出现的 { 和它右边第一个出现的 } 之间没有任何字符。
  • 对于 foo{ {bar}}zap 这个键,用来计算哈希值的是 {bar 这个子串,因为它是第一个 { 及其右边第一个 } 之间的内容。
  • 对于foo{bar}{zap} 这个键,用来计算哈希值的是 bar 这个子串,因为算法会在第一次有效或无效(比如中间没有任何字节)地匹配到 { 和 } 的时候停止。
  • 按照这个算法,如果一个键是以 {} 开头的话,那么就当作整个键会被用来计算哈希值。当使用二进制数据做为键名称的时候,这是非常有用的。

HASH_SLOT函数的ruby实现代码大致如下:

def HASH_SLOT(key)
    s = key.index "{"
    if s
        e = key.index "}",s+1
        if e && e != s+1
            key = key[s+1..e-1]
        end
    end
    crc16(key) % 16384
end

2.3 集群节点属性

在集群中,每个节点都有一个唯一的名字。节点名字是一个十六进制表示的160 bit 随机数,这个随机数是节点第一次启动时获得的(通常是用 /dev/urandom)。 节点会把它的ID保存在配置文件里,以后永远使用这个ID,只要这个节点配置文件没有被系统管理员删除掉。

节点ID是用于在整个集群中标识每个节点。一个给定的节点可以在不改变节点ID的情况下改变 IP 和地址。集群能检测到 IP 或端口的变化,然后使用在集群连接(cluster bus)上的 gossip 协议来发布广播消息,通知配置变更。

2.4 集群拓扑(Cluster topology)

Redis 集群是一个网状结构,每个节点都通过 TCP 连接跟其他每个节点连接。

在一个有 N 个节点的集群中,每个节点都有 N-1 个流出的 TCP 连接,和 N-1 个流入的连接。 这些 TCP 连接会永久保持,并不是按需创建的。

2.5 集群总线(Cluster bus)

每个Redis Cluster节点都有一个额外的TCP端口,用于接收其他Redis Cluster节点连接。 要获得Redis Cluster端口,应在常规命令端口中添加10000。 例如,如果Redis节点正在端口6379上侦听客户端连接,则集群总线端口16379也将打开。

2.6 节点握手

节点始终接受集群总线端口上的连接。接收节点将丢弃所有不是集群中节点发送的数据包。

Redis集群搭建中,数据如何在节点分布的原理,下面来介绍一下节点之间是如何进行通信(节点握手)

在分布式存储中需要提供维护节点元数据信息的机制,所谓元数据是指:节点负责哪些数据,是否出现故障等状态信息。常见的元数据维护方式分为:集中式和P2P方式。Redis集群采用P2P的Gossip(流言)协议,Gossip协议工作原理就是节点彼此不断通信交换信息,一段时间后所有的节点都会知道集群完整的信息,这种方式类似流言传播

节点之间通信示意图
在这里插入图片描述

在这里插入图片描述
Redis集群的节点通信原理

2.7 重定向和重新分片

2.7.1 MOVED 重定向

一个 Redis 客户端可以自由地向集群中的任意节点(包括从节点)发送查询。接收的节点会分析查询,如果这个命令是集群可以执行的(就是查询中只涉及一个键),那么节点会找这个键所属的哈希槽对应的节点。

如果刚好这个节点就是对应这个哈希槽,那么这个查询就直接被节点处理掉。否则这个节点会查看它内部的 哈希槽 -> 节点ID 映射,然后给客户端返回一个 MOVED 错误。

一个 MOVED 错误如下:

GET x
-MOVED 3999 127.0.0.1:6381
# 键(3999)的哈希槽和能处理这个查询的节点的 ip:端口号(127.0.0.1:6381)

从集群的角度看,节点是以 ID 来标识的。我们尝试简化接口,所以只向客户端暴露哈希槽和用“ip:端口号”标识的 Redis 节点之间的映射。

客户端最好缓存哈希槽和节点之间的映射,可以提高性能。

虽然并没有要求,但是客户端应该尝试记住哈希槽 3999 是服务于 127.0.0.1:6381。这样的话一旦有一个新的命令需要发送,它能计算出目标键的哈希槽,提高找到正确节点的机率。

注意,当集群是稳定的时候,所有客户端最终都会得到一份哈希槽 -> 节点的映射表,这样能使得集群效率非常高:客户端直接定位目标节点,不用重定向、或代理或发生其他单点故障(single point of failure entities)。

客户端必须能够处理 -ASK 重定向,否则不是一个完整的Redis集群客户端

2.7.2 集群在线重配置(live reconfiguration)

Redis 集群支持在集群运行过程中添加或移除节点。实际上,添加或移除节点都被抽象为同一个操作,那就是把哈希槽从一个节点移到另一个节点。

  • 向集群添加一个新节点,就是把一个空节点加入到集群中并把某些哈希槽从已存在的节点移到新节点上。
  • 从集群中移除一个节点,就是把该节点上的哈希槽移到其他已存在的节点上。
  • 所以实现这个的核心是能把哈希槽移来移去。从实际角度看,哈希槽就只是一堆键,所以 Redis 集群在重新分片(reshard)时做的就是把键从一个节点移到另一个节点。

相关命令如下:

以下是可用的子命令:

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

2.8 ASK 重定向(ASK redirection)

在前面的章节中,我们简短地提到了 ASK 重定向,为什么我们不能单纯地使用 MOVED 重定向呢?因为当我们使用 MOVED 的时候,意味着我们认为哈希槽永久地被另一个不同的节点处理,并且希望接下来的所有查询都尝试发到这个指定的节点上去。而 ASK 意味着我们只要将查询发送到下一个指定节点上去。

2.9 失效检测(Failure detection)

Redis 集群失效检测是用来识别出大多数节点何时无法访问某一个主节点或从节点。当这个事件发生时,就提升一个从节点来做主节点;若如果无法提升从节点来做主节点的话,那么整个集群就置为错误状态并停止接收客户端的查询。

每个节点都有一份跟其他已知节点相关的标识列表。其中有两个标识是用于失效检测,分别是 PFAILFAILPFAIL 表示可能失效(Possible failure),这是一个非公认的(non acknowledged)失效类型。FAIL 表示一个节点已经失效,而且这个情况已经被大多数主节点在某段固定时间内确认过的了。即主观失效客观失效

2.10 丛节点的选举和提升

从节点的选举和提升都是由从节点处理的,主节点会投票要提升哪个从节点。一个从节点的选举是在主节点被至少一个具有成为主节点必备条件的从节点标记为 FAIL 的状态的时候发生的。

当以下条件满足时,一个从节点可以发起选举:

  • 该从节点的主节点处于 FAIL 状态。
  • 这个主节点负责的哈希槽数目不为零。
  • 从节点和主节点之间的重复连接(replication link)断线不超过一段给定的时间,这是为了确保从节点的数据是可靠的。
  • 一个从节点想要被推选出来,那么第一步应该是提高它的 currentEpoch 计数,并且向主节点们请求投票。

从节点通过广播一个 FAILOVER_AUTH_REQUEST 数据包给集群里的每个主节点来请求选票。然后等待回复(最多等 NODE_TIMEOUT 这么长时间)。一旦一个主节点给这个从节点投票,会回复一个 FAILOVER_AUTH_ACK,并且在 NODE_TIMEOUT * 2 这段时间内不能再给同个主节点的其他从节点投票。在这段时间内它完全不能回复其他授权请求。

从节点会忽视所有带有的时期(epoch)参数比 currentEpoch 小的回应(ACKs),这样能避免把之前的投票的算为当前的合理投票。

2.11 Gossip协议

Redis 集群是去中心化的,彼此之间状态同步靠 gossip 协议通信,集群的消息有以下几种类型:

  • Meet 通过「cluster meet ip port」命令,已有集群的节点会向新的节点发送邀请,加入现有集群。
  • Ping 节点每秒会向集群中其他节点发送 ping 消息,消息中带有自己已知的两个节点的地址、槽、状态信息、最后一次通信时间等。
  • Pong 节点收到 ping 消息后会回复 pong 消息,消息中同样带有自己已知的两个节点信息。
  • Fail 节点 ping 不通某节点后,会向集群所有节点广播该节点挂掉的消息。其他节点收到消息后标记已下线。

由于去中心化和通信机制,Redis Cluster 选择了最终一致性和基本可用。

还有很多没有讲到,直接看官方文档吧。

参考文献

[1] Redis 集群教程
[2] Redis 集群规范
[3] Redis集群的节点通信原理

猜你喜欢

转载自blog.csdn.net/besmarterbestronger/article/details/109959324