《Redis官方教程》Redis集群规范(一)

  1. 云栖社区>
  2. 博客>
  3. 正文

《Redis官方教程》Redis集群规范(一)

青衫无名 2017-05-22 09:37:00 浏览1646
展开阅读全文

Redis集群规范


欢迎来到Redis集群规范。在这里你可以找到有关Redis的算法和设计的基本原理。这篇文章是一项正在进行的工作,因为它是不断地与Redis的实际实现同步。

 

主要属性和设计原理


Redis的集群目标


 

Redis集群是一个分布式的实现,具有以下目标,按设计的重要性排序:

  • 高性能,并且多达1000个节点的线性可扩展性。没有代理,使用异步复制,并且在进行赋值时没有合并操作。
  • 可接受程度的写安全:当客户端与大多数master节点建立连接后,系统努力(使用最优的方式)保持来自客户端的写操作。通常有小窗口,其中确认的写操作可能会丢失。当客户端在一个小的分区中,窗口丢失写操作会更大。
  • 可用性:Redis集群支持网络分区——其中大部分主节点都可访问,并且不可访问的各master节点对应的从至少一个可访问。而且采用副本迁移,有多个从的主会提供一个从给没有从的主。

本文档中描述的在Redis >=3.0的版本中实现。

 

已实现的部分


 

Redis集群实现了所有Redis的非分布式版本中提供的单键命令。命令执行复杂的多键操作, 像set类型的合集或交集的命令,只要键是属于同一个节点就行。

Redis群集实现有一个散列标签的概念,能强制让特定的键存储在相同的节点。但是在手动重新散列期间,多键操作的可能不可用,而单键操作总是可用。

Redis集群不支持这样的Redis的单实例版本的多个数据库。只是有数据库0并且SELECT命令是不允许的。

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

在Redis的群集节点负责保持数据,并持有群集的状态,其中包括映射键到正确的节点。集群节点也能自动发现其他节点,检测非工作节点,当发生失败,必要时把slave切换成master,以便在继续发生故障时,持续运作。

执行任务的所有群集节点使用的是TCP总线和二进制协议连接,称为Redis集群总线。每个节点被连接到使用群集总线的所有其他节点。节点使用Gossip协议传播有关群集的信息,以发现新节点,发送Ping报文,以确保所有其他节点工作正常,并发送必要信息触发特定条件。群集总线也用于以传播跨集群发布/订阅消息,并当用户请求来协调手工故障转移(手动故障转移是未由Redis集群故障检测器发起的故障转移,而是直接由系统管理员)。

因为集群节点不能代理请求,客户端可以使用重定向错误-MOVED和-ASK重定向到其他节点。客户端是在理论上自由将请求发送到集群中的所有节点,如果需要的话得到重定向,因此客户端不需要保持群集的状态。然而,客户端缓存键和节点之间的映射可以明显的改善性能。

 

 

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


在 Redis 集群中,节点负责存储数据、记录集群的状态,包括键值到正确节点的映射。集群节点同样能自动发现其他节点,检测非正常工作节点, 并且在需要的时候升级slave成master来保证故障发生时持续运作。

为了执行这些任务,所有的集群节点都通过TCP总线和二进制协议连接,叫集群总线redis cluster bus。 每一个节点都通过集群总线与集群上的其余每个节点连接。节点们使用一个 gossip 协议来传播集群的信息,这样可以:发现新的节点、 发送ping包用来确保所有节点都在正常工作、在特定情况发生时发送集群消息来触发特定条件。集群总线也用于在集群中传播 发布/订阅 消息、用户请求协调手动故障转移(手动故障转移都并非由Redis的集群故障检测器启动的故障转移,而是直接由系统管理员)。

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

 

写安全


Redis 集群节点间使用异步副本备份,最后一个故障转移取得隐式合并功能,这意味着最后选举出的master数据最终替换其它副本.

通常存在一个时间窗口,可能在分片中丢失写入数据。 但是一个连接到绝大部分master节点的客户端的时间窗口,与一个连接到极小部分master节点的客户端的时间窗口 有很大的区别。

Redis 集群会努力尝试保存所有与大多数master节点连接的客户端执行的写操作,相比于与少数master节点连接的客户端执行的写操作,但以下两种情况除外,会导致失败期间在多数分片丢失写操作:

1) A写入操作能到达一个master节点,但当master节点要回复客户端的时候,这个写入有可能没有通过master-slave异步备份传播到slave节点那里。 如果在某个写入操作没有到达slave节点的时候master节点已经宕机了,那么该写入会永远地丢失掉(如果master长时间周期不可达而它的slave升级成master)。

这通常在所有情况中很难发现,master突然发生故障的情况下,由于master尝试回复客户端(写入的应答)和slave(传播写操作)在大致相同时间。然而,它是一个现实世界的故障模式。

2) 另一个理论上可能会丢失写入操作的模式是:

  • A master因为分区不可达。
  • 它故障转移, 它的一个slave升级成了master。
  • 过一段时间之后这个节点再次变得可达。
  • 一个持有过期路由表的客户端或许会在集群把这个master节点变成一个slave节点(新master节点的slave节点)之前对它进行写入操作。

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

通常所有节点都会尝试通过非阻塞连接尝试(non-blocking connection attempt)尽快去访问一个再次加入到集群里的节点,一旦跟该节点建立一个新的连接就会发送一个ping包过去(这足够升级节点配置信息)。这就使得一个节点很难在恢复可写入状态之前没被告知配置信息更改。

 

写入操作到达少数分片会有更大的丢失窗口。比如:

Redis 集群在拥有少数master节点和至少一个客户端的分片上容易丢失为数不少的写入操作,这是因为,如果master节点被故障转移到集群中多数节点那边, 那么所有发送到这些master节点的写入操作可能会丢失。

特别是一个master节点要被故障转移,必须是大多数master节点在至少 NODE_TIMEOUT 时长里无法访问到,所以如果分区在这段时间之前被修复,就没有写入操作会丢失。当分区故障持续超过 NODE_TIMEOUT,所有在少数节点一边到该时间点执行的写操作可能会丢失

,然而集群的少数节点这边,和大多数节点失联,会在 NODE_TIMEOUT 这个时间内开始拒绝往受损分区进行写入,所以在少数节点这边变得不再可用后,会有一个最大时间窗口. 因此在那时间之后将不会再有写入操作被接收或丢失。

可用性


Redis 集群在分区的少数节点那边不可用。在分区的多数节点这边假设至少有大多数可达的master节点,并且对于每个不可达master节点都至少有一个slave节点可达,在经过了( NODE_TIMEOUT +n秒)时间后,有个slave节点选举出来故障转移成master节点,这时集群又再恢复可用(故障转移通常在1-2秒内)。

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

在该示例,一个由 N 个master节点组成的集群,每个master节点都只有一个slave节点。只要有单个节点被分割出去,集群的多数节点这边仍然是可访问的。当有两个节点被分割出去后集群仍可用的概率是 1-(1/(N*2-1)) (在第一个节点故障出错后总共剩下 N*2-1 个节点,那么失去slave节点只剩master节点的出错的概率是 1/(N*2-1))。

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

感谢redis集群特性 副本迁移,集群在真实环境可用性提升,因为副本升级为孤立的master节点(master节点不再有副本),所以每次成功的故障转移,集群重新配置slave节点来更好地防止下次故障.

性能


在 Redis 集群中节点并不是把命令转发到负责键的节点上,而是把客户端重定向到服务一定范围内的键的节点上。 最终客户端获得一份最新的集群路由表,里面有写着哪些节点服务哪些键,所以在正常操作中客户端是直接联系到对应的节点来发送指令。

由于使用了异步复制,节点不会等待其他节点对写入操作的回复。(除非显式发送WAIT指令)

同样,由于多键指令仅限于相邻的键,如果不是重新分片,那么数据是永远不会在节点间移动的。

普通操作是可以被处理得跟在Redis单机版一样的。这意味着,在一个拥有 N 个master节点的 Redis 集群中,由于线性扩展的设计,你可以认为同样的操作在集群上的性能是Redis单机版的n倍。同时,请求通常在一次来回中被执行,客户端会保持跟节点的长连接,所以延迟指标跟在Reids 单机版情况是一样的。

为什么要避免使用合并操作


Redis 集群的设计是避免在多个节点中存在同个键值对的冲突版本,在这点上 Redis 数据模型不总是满足需要,Redis 中的值通常都是比较大的,经常可以看到列表或者有序集合中有数以百万计的元素。数据类型也是语义复杂的。传输和合并这样的值将会变成一个主要的性能瓶颈, 并且/或者可能需要应用端逻辑的引入,额外的内存来存储元数据,诸如此类。

redis集群主要组件概览


键分布模型


键空间被分割为 16384 槽(slot),事实上集群的最大master节点数量是 16384 个。(然而建议最大节点数量设置在1000)所有的master节点都负责 16384 个哈希槽中的一部分。当集群处于稳定状态时,当集群中没有在执行重配置操作(即:hash槽没有从一处移到另一处)。当集群在稳定状态,每个哈希槽都只由一个节点进行支配(不过master节点可以有一个或多个slave节点,可以在网络分区或节点失效时替换掉master节点,并且这样可以用来水平扩展读操作(这些读操作不要求实时数据))。以下是用来把键映射到哈希槽的算法(下一段落,除了哈希标签以外就是按照这个规则):

HASH_SLOT = CRC16(key) mod 16384

CRC16的定义如下:

  • 名称:XMODEM(也可以称为 ZMODEM 或 CRC-16/ACORN)
  • 输出宽度:16 bit
  • 多项数(poly):1021(即x16+ x12 + x5 + 1 )
  • 初始化:0000
  • 反射输入字节(Reflect Input byte):False
  • 反射输出CRC(Reflect Output CRC):False
  • 输出CRC的异或常量(Xor constant to output CRC):0000
  • 输入”123456789″的输出:31C3

CRC16的16位输出中的14位会被使用(这也是为什么上面的式子中有一个对 16384 取余的操作)。

在我们的测试中,CRC16能相当好地把不同的键均匀地分配到 16384 个槽中。

注意:在本文档的附录A中有CRC16算法的实现。

键哈希标签


为了实现哈希标签(hash tags,计算hash槽有一个特殊处理,哈希标签是确保两个键都在同一个哈希槽里的一种方式。用来实现集群中多键多键操作。

 

为了实现哈希标签,哈希槽在一定条件下是用另一种不同的方式计算的。基本来说,如果一个键包含一个 “{…}”模式,只有 { 和 } 之间的部分字符串会用做哈希计算以获取哈希槽。但是由于可能出现多个 { 或 },计算的算法详细说明如下:

  • 当键包含一个{ 字符。
  • 并且当在{ 的右边有一个 }。
  • 并且当第一次出现{和第一次出现 } 之间有一个或多个字符。

然后不是直接计算键的哈希,只有在第一个 { 和它右边第一个 } 之间的内容会被用来计算哈希值。

例子:

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

加上哈希标签的特殊处理,下面是用 Ruby 和 C 语言实现的 HASH_SLOT 函数。

Ruby example code:

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

C example code:

unsigned int HASH_SLOT(char *key, int keylen) {
    int s, e; /* start-end indexes of { and } */

    /* Search the first occurrence of '{'. */
    for (s = 0; s < keylen; s++)
        if (key[s] == '{') break;

    /* No '{' ? Hash the whole key. This is the base case. */
    if (s == keylen) return crc16(key,keylen) & 16383;

    /* '{' found? Check if we have the corresponding '}'. */
    for (e = s+1; e < keylen; e++)
        if (key[e] == '}') break;

    /* No '}' or nothing between {} ? Hash the whole key. */
    if (e == keylen || e == s+1) return crc16(key,keylen) & 16383;

    /* If we are here there is both a { and a } on its right. Hash
     * what is in the middle between { and }. */
    return crc16(key+s+1,e-s-1) & 16383;
}

集群节点属性


在集群中,每个节点都有一个唯一的名字。节点名字是一个十六进制表示的160 bit 随机数,这个随机数是节点第一次启动时生成的(通常是用 /dev/urandom)。 节点会把它的ID保存在配置文件里,以后永远使用这个ID,或者至少只要这个节点配置文件没有被系统管理员删除掉,或者通过指令CLUSTER RESET强制请求硬重置(hard reset)

节点ID是用于在整个集群中标识每个节点。节点改变IP地址,没有任何必要改变节点ID。集群能检测到 IP /端口的变化,然后使用在集群总线(cluster bus)上的 gossip 协议来重新配置。

节点ID不仅是关联节点的信息,也是全局始终唯一的。 每个节点也有下面的关联信息。一些信息是具体集群节点的配置详情,并且在集群中最终一致。一些其它信息,比如节点最后ping的时间,是每个节点和其它都不同的。

 

每个节维护了感知集群其它节点如下信息: 每个节点的节点ID,IP和端口,一系列标识,当标识为slave对应的master节点,最后节点ping包的时间和最后收到pong回复的时间,当前节点配额(后文会解释),连接状态和最后服务的一堆哈希槽。

 

详细的描述在CLUSTER NODES文档中:http://redis.io/commands/cluster-nodes

在任意节点执行 CLUSTER NODES 命令可以获得上述信息。

下面的例子是在一个只有三个节点的小集群中发送 CLUSTER NODES 命令到一个master节点得到的输出。

$ redis-cli cluster nodes
d1861060fe6a534d42d8a19aeb36600e18785e04 127.0.0.1:6379 myself - 0 1318428930 1 connected 0-1364
3886e65cc906bfd9b1f7e7bde468726a052d1dae 127.0.0.1:6380 master - 1318428930 1318428931 2 connected 1365-2729
d289c575dcbc4bdd2931585fd4339089e461a27d 127.0.0.1:6381 master - 1318428931 1318428931 3 connected 2730-4095

在上面列出来的信息中,各个字段依次表示的是:节点ID,IP地址:端口号,标识,上一次发送 ping 指令的时间,上一次收到 pong 回复的时间,配额,连接状态,节点使用的哈希槽,上述字段的详情很快会在redis集群规范部分中全部讲清楚

 

集群总线


每个Redis的集群节点都有一个额外的TCP端口,用于接收来自其他Redis的集群节点连接。此端口号(普通TCP端口号的的固定偏移值)用于接收来自客户端的连接。为了Redis群集端口,加了10000到普通的命令端口。例如,如果一个Redis的节点监听端口为6379,会打开集群总线端口16379(=10000+6379)。节点到节点的通信独自使用群集总线和群集总线协议:不同类型和大小的帧组成一个二进制协议。群集总线二进制协议不公开发布,因为它的设计目的不是用这个协议和外部软件的设备交互。然而,你可以通过阅读cluster.h和cluster.c文件Redis的集群源代码获取有关群集总线协议的更多细节。

集群拓扑结构


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

在一个有 N 个节点的集群中,每个节点都有 N-1 个对外的 TCP 连接,和 N-1 个对内的连接。

这些 TCP 连接会永久保持,并不是按需创建的。当一个节点在集群总线中预计回复一个ping,等待足够长的时间,以标识节点不可达,它会尝试从头开始连接来刷新与节点的连接。

而Redis的群集节点形成全网状,节点使用gossip协议和一个配置更新机制,以避免在正常条件下交换节点之间太多消息,因此,交换的消息的数量不是指数。

 

节点握手


节点总是通过集群总线端口接受连接,甚至会回复接收到的 ping 请求,即使发送 ping 请求的节点是不可信的。 然而如果某个节点不被认为是在集群中,那么所有它发出的数据包都会被丢弃掉。

只有在两种方式下,一个节点才会认为另一个节点是集群中的一部分:

  • 当一个节点使用MEET 消息介绍自己。一个 meet 消息跟一个 PING 消息完全一样,但它会强制让接收者接受自己为集群中的一部分。 只有在系统管理员使用以下命令请求的时候,节点才会发送MEET 消息给其他节点:

CLUSTER MEET ip port

  • 一个已被信任的节点能通过传播gossip消息让另一个节点被注册为集群中的一部分。也就是说,如果 A 知道 B,B 知道 C,最终B会发知道C 的gossip消息给A。A 收到后就会把 C 当作是网络中的一部分,并且尝试连接 C。

这意味着,只要我们往任何连接图中加入节点,它们最终会自动形成一个完全连接图。这表示集群能自动发现其他节点,但前提是有一个由系统管理员强制创建的信任关系。

这个机制能防止不同的 Redis 集群因为 IP 地址变更或者其他网络事件而意外混合起来,从而使集群更健壮。

重定向和重分片


MOVED 重定向

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

如果哈希槽在这个节点上,那么这个请求就简单的执行了。否则这个节点会查看它内部的 哈希槽-节点 映射,然后给客户端返回一个 MOVED 错误,如下:

GET x
-MOVED 3999 127.0.0.1:6381

这个错误包括键的哈希槽和能处理这个查询的节点的 IP:端口。客户端需要重新发送请求到给定 ip 地址和端口号的节点。 注意,即使客户端在重发请求之前等待了很长一段时间,与此同时集群的配置信息发生改变,如果哈希槽 3999 现在是归属其它节点,那么目标节点会再向客户端回复一个 MOVED 错误。如果连接的节点没有信息变更,会重复这样。

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

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

另一种方法是使用CLUSTER NODES 或CLUSTER SLOTS命令刷新整个客户端集群布局。当遇到一个MOVED,当遇到重定向,很可能多个插槽进行重新配置,而不是只有一个,所以尽快更新客户端的配置往往是最好的策略。

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

一个客户端也应该能处理后文提到的 -ASK 重定向错误,否则不是一个完整的redis集群客户端

集群在线重新配置


Redis 集群支持在集群运行过程中添加或移除节点。实际上,添加或移除节点都被抽象为同一个操作,那就是把哈希槽从一个节点移到另一个节点。这意味着相同的原理能用来重新平衡集群,增/删节点,等等.

  • 向集群添加一个新节点,就是把一个空节点加入到集群中并把某些哈希槽从已存在的节点移到新节点上。
  • 从集群中移除一个节点,就是把该节点上的哈希槽移到其他已存在的节点上。
  • 重新平衡,就是指向一堆哈希槽在节点间迁移

 

所以实现这个的核心是能把哈希槽移来移去。从实际角度看,哈希槽就只是一堆键,所以 Redis 集群在重组分片时做的就是把键从一个节点移到另一个节点。移动一个哈希槽就是移动属于这个槽的所有键。为了理解这是怎么工作的,我们需要介绍 CLUSTER 的子命令,这些命令是用来操作 Redis 集群节点上的哈希槽转换表。

有以下子命令(在这个案例中有的没用到):

  • 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,就是简单地用来给Redis 节点指派或移除哈希槽。指派哈希槽就是告诉一个master节点它会负责存储和服务指定的哈希槽。

在哈希槽被指派后会将这个消息通过 gossip 协议向整个集群传播(协议在后文配置传播章节说明)。

 

ADDSLOTS 命令通常是用于在一个集群刚建立的时候从新给所有master节点指派哈希槽,总共有16384个。

DELSLOTS主要用于群集配置的手工修改或用于调试任务:在实践中很少使用。

SETSLOT用于将哈希槽指定给特定节点的ID ,如果SETSLOT <插槽>节点的形式被使用。否则,哈希槽可以在两种特殊状态MIGRATING 和 IMPORTING。这两个特殊状态用于哈希槽从一个节点迁移到另一个。

  • 当一个槽被设置为 MIGRATING,持有该哈希槽的节点仍会接受所有跟这个哈希槽有关的请求,但只有当查询的键还存在原节点时,原节点会处理该请求,否则这个查询会通过一个-ASK 重定向转发到迁移的目标节点。
  • 当一个槽被设置为 IMPORTING,只有在接受到 ASKING 命令之后节点才会接受所有查询这个哈希槽的请求。如果客户端没有发送 ASKING 命令,那么查询都会通过-MOVED 重定向错误转发到真正的哈希槽归属节点那里,这通常会发生。

这我们用实例让它哈希槽迁移更清晰些。假设我们有两个 Redis master节点,称为 A 和 B。我们想要把哈希槽 8 从 节点A 移到 节点B,所以我们发送了这样的命令:

  • 我们向 节点B 发送:CLUSTER SETSLOT 8 IMPORTING A
  • 我们向 节点A 发送:CLUSTER SETSLOT 8 MIGRATING B

其他所有节点在每次请求的一个键是属于哈希槽 8 的时候,都会把客户端引向节点”A”。具体如下:

  • 所有关于已存在的键的查询都由节点”A”处理。
  • 所有关于不存在于节点 A 的键都由节点”B”处理,因为”A”将重定向客户端请求到”B”。

这种方式让我们可以不用在节点 A 中创建新的键。同时,一个叫做 redis-trib 的特殊脚本,用于重新分片和集群配置,把已存在的属于哈希槽 8的键从节点 A 移到节点 B。这通过以下命令实现:

CLUSTER GETKEYSINSLOT slot count

上面这个命令会返回指定的哈希槽中 count 个键。对于每个返回的键,redis-trib 向节点 A 发送一个MIGRATE 命令,这样会以原子性的方式从A到B迁移指定的键(在移动键的过程中两个节点都被锁住,通常时间很短,所以不会出现竞争状况)。以下是 MIGRATE 的工作原理:

MIGRATE target_host target_port key target_database id timeout

执行 MIGRATE 命令的节点会连接到目标节点,把序列化后的 key 发送过去,一旦收到 OK 回复就会从它自己的数据集中删除老的 key。所以从一个外部客户端看来,在某个时间点,一个 key 要不就存在于节点 A 中要不就存在于节点 B 中。

在 Redis 集群中,不需要指定一个除了 0 号之外的数据库,但 MIGRATE 命令能用于其他跟 Redis 集群无关的的任务,所以它是一个通用的命令。MIGRATE 命令被优化了,使得即使在移动像长列表这样的复杂键仍然能做到快速。 不过当在重配置一个拥有很多键且键的数据量都很大的集群的时候,如果使用它的应用程序来说就会有延时这个限制,这个过程就不是那么好了。

ASK 重定向


在前面的章节中,我们简短地提到了 ASK 重定向(ASK redirection),为什么我们不能单纯地使用 MOVED 重定向呢?因为收到 MOVED,意味着我们认为哈希槽永久地归属到了另一个节点,并且接下来的所有请求都尝试发到目标节点上去。而 ASK 意味着我们只要下一个请求发送到目标节点上去。

这个命令是必要的,因为下一个关于哈希槽 8 的请求需要的键或许还在节点 A 中,所以我们希望客户端尝试在节点 A 中查找,如果需要的话然后在节点 B 中查找。 由于这是发生在 16384 个槽的其中一个槽,所以对于集群的性能影响是在可接受的范围。

然而我们需要强制客户端的行为,以确保客户端会在尝试 A 中查找后去尝试在 B 中查找,如果客户端在发送查询前发送了 ASKING 命令,那么节点 B 只会接受被设为 IMPORTING 的槽的查询。

基本上ASKING 命令在客户端设置了一个一次性标识(one-time flag),强制一个节点可以执行一次关于带有 IMPORTING 状态的槽的查询。

所以从客户端看来,ASK 重定向的完整语义如下:

  • 如果接受到 ASK 重定向,发送单次请求重定向到目标节点,接着发送后续的请求到老的节点。
  • 先发送 ASKING 命令,再开始发送请求。
  • 现在不要更新本地客户端的映射表,把哈希槽 8 映射到B。

一旦完成了哈希槽 8 的转移,节点 A 会发送一个 MOVED 消息,客户端也许会永久地把哈希槽 8 映射到新的 ip:端口号 上。 注意,即使客户端出现bug,过早地执行这个映射更新,也是没有问题的,因为它不会在查询前发送 ASKING 命令,节点 B 会用 MOVED 重定向错误把客户端重定向到节点 A 上。

 

客户端首次连接和处理重定向


虽然可以有一个Redis的群集客户端不在内存中记住哈希槽配置(哈希槽与节点的映射),并只能通过联系随机节点等待被重定向,这样的客户端将是效率非常低。

Redis的集群客户应尽量足够聪明,记忆哈希槽配置。然而这种配置不必是最新的。因为联系错误的节点只会导致一个重定向,应当触发客户视图的更新。

客户通常需要获取哈希槽与节点映射的完整列表:

  • 启动时保存初始的哈希槽配置.
  • 当收到 MOVED重定向.

请注意,客户可能根据MOVED重定向更新变动的哈希槽,但是这通常不是有效的,因为通常配置中多个哈希槽一起修改(例如,如果一个slave升为master,所有归属老master的哈希槽会重新映射) 。更简单对MOVED重定向做出回应是,重新获取哈希槽节点映射表。

为了获取哈希槽配置Redis的群集提供了另一种不需要的解析的命令CLUSTER NODES,并只仅提供客户端严格需要的信息。

新的命令被称为CLUSTER SLOTS并槽提供了一组哈希槽范围,关联了主从节点服务于指定的哈希槽范围。

下面是CLUSTER插槽输出的例子:

127.0.0.1:7000> cluster slots
1) 1) (integer) 5461
   2) (integer) 10922
   3) 1) "127.0.0.1"
      2) (integer) 7001
   4) 1) "127.0.0.1"
      2) (integer) 7004
2) 1) (integer) 0
   2) (integer) 5460
   3) 1) "127.0.0.1"
      2) (integer) 7000
   4) 1) "127.0.0.1"
      2) (integer) 7003
3) 1) (integer) 10923
   2) (integer) 16383
   3) 1) "127.0.0.1"
      2) (integer) 7002
   4) 1) "127.0.0.1"
      2) (integer) 7005

 

返回的数组中的每个元素的前两个子元素是该范围的始未哈希槽。附加元素表示地址端口对。第一个地址端口对是服务于哈希槽的master,和附加的地址端口对服务于相同槽slave,它不存在错误条件(即故障标志没有被设置)。

例如,输出的第一个元素表示,槽从5461至10922(开始和结束包括)由127.0.0.1:7001服务,并且可以通过127.0.0.1:7004水平扩展读负载。

CLUSTER SLOTS是不能保证返回覆盖整个16384插槽,如果群集配置不正确范围,所以客户初始化哈希槽配置时应当用NULL填充空节点,并报告一个错误,如果用户试图执行有关键的命令属于未分配的插槽。

当一个哈希槽被发现是未分配之前,返回一个错误给调用者之前,客户应该再次尝试读取哈希槽配置,检查群集现在配置是否正确。

 

多键操作


使用哈希标签,用户可以自由地使用多键操作。例如下面的操作是有效的:

MSET {user:1000}.name Angela {user:1000}.surname White

多键操作可能变得不可用,当键所属的哈希槽在进行重新分片。

更具体地,即使重新分片期间,多键操作目标键都存在并且处于相同节点(源或目的地节点)仍然可用。

在重新分片时,操作的键不存在或键在源节点和目的节点之间,将产生 -TRYAGAIN 错误。客户端可以一段时间后再尝试操作,或报错。

只要指定的哈希槽的迁移已经终止,所有多键操作可再次用于该散列槽。

 

通过slave节点水平扩展读


通常情况下从节点将客户端重定向到给定的命令哈希槽对应的master,但是客户端可以使用READONLY命令读来水平扩展读。

READONLY告诉客户端是允许读失效的数据并且不关心写请求。

当连接处于只读模式,当操作涉及到不是slave的主节点提供服务的键,集群将发送一个重定向到客户端。这可能发生的原因是:

1.客户端发送一个命令对应的哈希槽不是由这个slave的master服务。

2.集群重新配置(例如重新分片 )并且slave不再能够服务于给定的哈希槽命令。

当发生这种情况如前面部分中说明的,客户端应该更新其哈希槽映射表。

连接的只读状态可以使用READWRITE命令清除。

 

容错性(Fault Tolerance)


 

节点心跳和 gossip 消息


集群里的节点不断地交换 ping / pong 数据包。这两种数据包有相同的数据结构,都传输重要的配置信息 。唯一不同是消息类型字段,我们将提到心跳ping/pong包的总数。

通常节点发送ping包,将触发接收节点回复pong包。然而,这未必都是这样的。可能节点只是发送pong包的配置信息给其他节点,而不会触发应答。这样有用处,例如,为了尽快广播新配置。

 

通常一个节点每秒会随机 ping 几个节点,这样发送的 ping 包(和接收到的 pong 包)的总数会是一个跟集群节点数量无关的常数。

在过去的一半 NODE_TIMEOUT 时间里都没有发送 ping 包过去或接收从那节点发来的 pong 包的节点,会保证去 ping每一个其他节点:。 在 NODE_TIMEOUT 这么长的时间过去之前,若当前的 TCP 连接有问题,节点会尝试去重连接,以确保不会被当作不可达的节点。

如果 NODE_TIMEOUT 被设为一个很小的数而节点数量(N)非常大,那么全局交互的消息会非常多,因为每个节点都会尝试去 ping 每一个在过去一半 NODE_TIMEOUT 时间里都没更新信息的节点。

例如在一个拥有 100 个节点的集群里,节点超时时限设为 60 秒,每个节点在每 30 秒中会尝试发送 99 个 ping 包,也就是每秒发送的 ping 包数量是 3.3 个,乘以 100 个节点就是整个集群每秒有 330 个 ping 包。

 

有一些方法可以降低的消息的数量,但已经出现了与当前使用的Redis集群故障检测的带宽没有报告的问题,所以现在明显的和直接的设计被使用。注意,即使在上例中,每秒交换被均匀地划分在100个不同的节点330的数据包,因此每个节点收到的通信量是可接受的。

 

网友评论

登录后评论
0/500
评论
青衫无名
+ 关注