Redis学习之Redis Cluster规范(四)

Redis集群规范

Redis集群目标

Redis Cluster是Redis的分布式实现,具备如下目标,按设计重要性排序:node

  • 高性能和线性可扩展性,最多1000个节点。没有代理,使用异步复制,而且不对值执行合并操做。
  • 写入安全的可接受程度:系统尝试(以best-effort的方式)保留源自与大多数主节点链接的客户端的全部写入。一般有小窗口能够丢失确认的写入。当客户端处于少数分区时,Windows丢失已确认的写入会更大。
  • 可用性:Redis Cluster可以在大多数主节点可访问的分区中存活,而且每一个主节点在不可访问时至少有一个可访问的从节点。此外,使用副本迁移,任何不能够复制的master中的slave将从多个slave覆盖的masters中接收一个master。

本文档中描述的内容在Redis 3.0或更高版本中实现。redis

实施子集

Redis Cluster实现Redis的非分布式版本中可用的全部单个键命令。执行复杂的多键操做(如Set类型联合或交叉)的命令也能够实现,只要这些键都散列到同一个槽。算法

Redis Cluster实现了一个称为hash tags的概念,可用于强制某些key存储在同一个哈希槽中。可是,在手动从新分片期间,多键操做可能会在一段时间内不可用,而单键操做始终可用。数据库

Redis Cluster不支持多个数据库,例如Redis的独立版本。只有数据库0,不容许使用SELECT命令。数组

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

在Redis集群中,节点负责保存数据并获取集群的状态,包括将键映射到正确的节点。集群节点还可以自动发现其余节点,检测非工做节点,并在须要时促进从节点变成主节点,以便在发生故障时继续运行。缓存

要执行任务,全部集群节点都使用TCP总线和二进制协议链接,称为Redis集群总线。每一个节点都使用集群总线链接到集群中的每一个其余节点。节点使用gossip protocol传播有关集群的信息,以便发现新节点,发送ping数据包以确保全部其余节点正常工做,以及发送发出特定条件信号所需的集群消息。集群总线还用于在集群中传播发布/订阅消息,并在用户请求时协调手动故障转移(手动故障转移是故障转移,不是由Redis集群故障检测程序启动,而是由系统管理员直接启动)。安全

因为集群节点不能代理请求,客户端可能会被重定向到其余节点,可使用-MOVED-ASK命令。理论上,客户端能够自由地向集群中的全部节点发送请求,并在须要时重定向,所以客户端不须要保持集群的状态。可是,可以在key和节点之间缓存映射的客户端能够以合理的方式提升性能。bash

写安全

Redis Cluster使用节点之间的异步复制,上次故障转移获胜者拥有隐式合并功能。这意味着最后选出的主数据集最终将替换全部其余副本。在分区期间总会有一个时间窗口,可能会丢失写入。然而,在链接到大多数主设备的客户端和链接到少数主设备的客户端的状况之间,这些窗口是很是不一样的。服务器

与少数端执行的写操做相比,Redis Cluster更加努力地保留由链接到大多数主服务器的客户端执行的写操做。如下是致使在故障期间在多数分区中收到的已确认写入丢失的状况示例:网络

  1. 写入能够到达主设备,可是当主设备可能可以回复客户端时,写入可能不会经过主节点和从属节点之间使用的异步复制传播到从设备。若是主设备在没有写入到达从设备的状况下死亡,则若是主设备在其提高的一个从设备的足够长的时间段内没法访问,则写入将永久丢失。在主节点彻底忽然发生故障的状况下,这一般很难被观察到,由于主设备几乎同时尝试回复客户端(具备写入的确认)和从设备(传播写入)。然而,这是一个真实的失败模式。
  2. 另外一种理论上可能出现写入丢失的故障模式以下:
  • 因为分区,主服务器没法访问。
  • 它被其中一个slave击败了。
  • 一段时间后,它可能再次可达。
  • 具备过期路由表的客户端能够在集群将其转换为(新主服务器的)从属服务器以前写入旧主服务器。

第二种故障模式不太可能发生,由于主节点没法与大多数其余主设备通讯足够的时间进行故障转移将再也不接受写入,而且当分区被修复时,写入仍然会在少许时间内被拒绝容许其余节点通知配置更改。此故障模式还要求客户端的路由表还没有更新。

针对分区的少数端的写入有一个更大的窗口能够丢失。例如,Redis Cluster在有少数主设备和至少一个或多个客户端的分区上丢失了不少的写入次数,若是多数主设备在故障转移时发送到主设备的全部写入可能都会丢失。

具体来讲,对于要进行故障转移的主服务器,至少NODE_TIMEOUT期间必须由大多数主服务器没法访问,所以若是在该时间以前修复了分区,则不会丢失任何写入。当分区持续时间超过NODE_TIMEOUT时,在少数端执行的全部写操做可能会丢失。然而,Redis集群的少数派一方将在没有与大多数人接触的状况下时间超过NODE_TIMEOUT时开始拒绝写入,所以有一个最大窗口,此后少数群体将再也不可用。所以,在此以后不接受或丢失写入。

可用性

Redis Cluster在分区的少数端不可用。在分区的多数端,假设每一个没法访问的主服务器至少有大多数主服务器和从服务器,则服务器会在NODE_TIMEOUT一段时间后再次可用,再次须要几秒钟以便从服务器得到选举并故障转移其主服务器,一般在1或2秒内执行)。

这意味着Redis Cluster旨在拯救集群中几个节点的故障,但对于须要在大型网络分裂时须要可用性的应用程序而言,它不是合适的解决方案。

在由N个主节点组成的集群的示例中,每一个节点具备单个从节点,只要单个节点被分区,集群的大多数端将保持可用,而且在两个节点被分区时,将保持可用的几率为1-(1/(N*2-1))(在第一个节点失败后,咱们总共留下了N*2-1节点,而且惟一没有副本的主机失败的几率是1/(N*2-1))

例如,在每一个节点具备5个节点和每一个结点都有个slave的集群中,有1/(5*2-1) = 11.11%可能性,在两个节点与多数节点分开后,集群将再也不可用。

因为Redis Cluster功能称为replicas migration,所以复制副本迁移到孤立主服务器(主服务器再也不具备副本)这一事实能够改善许多真实场景中的集群可用性。所以,在每一个成功的故障事件中,集群能够从新配置从设备布局,以便更好地抵抗下一个故障。

性能

在Redis集群中,节点不会将命令代理到负责给定key的正确节点,而是将客户端重定向到服务于key空间的给定部分的正确节点。

最终客户端得到集群的最新表示以及哪一个节点服务于哪一个key子集,所以在正常操做期间,客户端直接联系正确的节点以发送给定命令。

因为使用了异步复制,节点不会等待其余节点的写入确认(若是未使用WAIT命令显式请求)。

此外,因为多键命令仅限于键,所以除了从新分片以外,数据永远不会在节点之间移动。

正常操做的处理方式与单个Redis实例彻底相同。这意味着在具备N个主节点的Redis集群中,您能够指望与单个Redis实例相同的性能乘以N,由于设计会线性扩展。同时,查询一般在单个往返中执行,由于客户端一般保留与节点的持久链接,所以延迟数字也与单个独立Redis节点状况相同。

Redis Cluster的主要目标是提供极高的性能和可扩展性,同时保留弱的但合理的数据安全性和可用性。

为何避免合并操做

Redis集群设计避免了多个节点中相同键值对的冲突版本,就像Redis数据模型的状况同样,这并不老是使人满意的。Redis中的值一般很是大; 一般会看到包含数百万个元素的列表或排序集。数据类型在语义上也很复杂。转移和合并这些值多是主要瓶颈,and/or可能须要应用程序端逻辑的non-trivial参与,存储元数据的附加存储器等等。

这里没有严格的技术限制。CRDT或同步复制的状态机能够模拟相似于Redis的复杂数据类型。可是,此类系统的实际运行时行为与Redis Cluster不一样。Redis Cluster的设计旨在涵盖非集群Redis版本的确切用例。

key分发模型

key空间分为16384个槽,有效地设置了16384个主节点的簇大小的上限(但建议的最大节点大小约为1000个节点)。

集群中的每一个主节点处理16384个散列槽的子集。当没有正在进行的集群从新配置时(即散列插槽从一个节点移动到另外一个节点),集群是稳定的。当集群稳定时,单个节点将提供单个散列槽(可是,在网络分裂或故障的状况下,服务节点能够有一个或多个将替换它的从属,而且能够用于扩展读取过期数据的读取操做)。

用于将键映射到散列槽的基本算法以下(读取此规则的散列标记异常的下一段):

HASH_SLOT = CRC16(key) mod 16384
复制代码

CRC16规定以下:

  • Name:XMODEM(也称为ZMODEM或CRC-16 / ACORN)
  • Width:16位
  • Poly:1021(其实是x^16 + x^12 + x^5 + 1)
  • Initialization:0000
  • Reflect Input byte:False
  • Reflect Output CRC:错误
  • Xor constant to output CRC:0000
  • Output for "123456789":31C3

使用16个CRC16输出位中的14个(这就是为何在上面的公式中存在模16384运算的缘由)。

在咱们的测试中,CRC16在16384个插槽中均匀分配不一样类型的key时表现很是出色。

:所用CRC16算法的参考实现可在本文档的附录A中找到。

键哈希标签

计算用于实现散列标记的散列槽有一个例外。散列标记是一种确保在同一散列槽中分配多个key的方法。这用于在Redis集群中实现多键操做。

为了实现散列标签,在某些条件下以稍微不一样的方式计算key的散列槽。若是key包含一个“{...}”模式仅是{and}之间的子串 ,以得到散列slot被散列。可是,因为可能存在屡次出现{or},如下规则很好地指定了算法:

  • 若是key包含一个{字符。
  • 若是{右边有一个字符}
  • 若是第一次出现{和第一次出现}之间有一个或多个字符。

若是知足条件三,不是对key进行散列,而是仅对第一次出现{和第一次出现}之间的内容进行散列。

例子:

  • 两个key{user1000}.following{user1000}.followers将散列到相同的散列slot,由于只有在子串user1000会计算散列slot。
  • 对于键foo{}{bar},一般将整个键进行哈希处理,由于第一次出现{右侧是},而中间没有字符。
  • 对于键foo{{bar}}zap,子串{bar将被散列,由于它是第一次出现{和右边第一次出现}之间的子串。
  • 对于keyfoo{bar}{zap}的子串bar将被散列,算法在第一个有效或无效(无内部字节)匹配{and}后中止匹配。
  • 该算法的结果是,若是key开头{},则保证整个散列。当使用二进制数据做为键名时,这颇有用。

添加哈希标记异常,如下是Ruby和C语言中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
复制代码

C示例代码:

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位随机数的十六进制表示,在第一次启动节点时得到(一般使用/dev/urandom)。节点将其ID保存在节点配置文件中,并将永久使用相同的ID,或者至少只要系统管理员未删除节点配置文件,或经过CLUSTER RESET命令请求硬重置

节点ID用于标识整个集群中的每一个节点。给定节点能够更改其IP地址,而无需也更改节点ID。集群还可以检测IP /端口的变化,并使用在集群总线上运行的gossip protocol进行从新配置。

节点ID不是与每一个节点关联的惟一信息,而是惟一始终全局一致的信息。每一个节点还具备如下相关信息集。某些信息是关于此特定节点的集群配置详细信息,而且最终在集群中保持一致。其余一些信息,例如上次节点被ping时,对每一个节点来讲都是本地的。

每一个节点都维护有关集群中知道的其余节点的如下信息:节点ID,节点的IP和端口,一组标志,标记为节点的主节点slave,上次节点被ping后的时间戳,最近接收到pong的节点的时间戳, configuration epoch(在本说明书后面解释),链路状态以及最后服务的散列slots集合。

CLUSTER NODES文档中描述了全部节点字段的详细说明

集群节点命令可在簇中被发送到任何节点,并提供该集群的状态,并根据本地视图所查询的节点具备集群的每一个节点的信息。

如下是发送到三个节点的小型集群中的主节点的CLUSTER NODES命令的示例输出。

$ 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,地址:端口,标志,最后ping发送,最后接收到的pong,configuration epoch,链路状态,slots。一旦咱们谈到Redis Cluster的特定部分,咱们将详细介绍上述领域的详细信息。

集群总线

每一个Redis集群节点都有一个额外的TCP端口,用于接收来自其余Redis集群节点的传入链接。此端口与用于接收来自客户端的传入链接的普通TCP端口相距固定偏移量。要获取Redis集群端口,应将10000添加到普通命令端口。例如,若是Redis节点正在侦听端口6379上的客户端链接,则还将打开集群总线端口16379。

节点到节点的通讯仅使用集群总线和集群总线协议进行:由不一样类型和大小的帧组成的二进制协议。集群总线二进制协议未公开记录,由于它不是用于外部软件设备使用此协议与Redis集群节点通讯。可是,您能够经过读取Redis集群源代码中的cluster.hcluster.c文件来获取有关集群总线协议的更多详细信息 。

集群拓扑

Redis Cluster是一个完整的网格,其中每一个节点使用TCP链接与每一个其余节点链接。

在N个节点的集群中,每一个节点具备N-1个传出TCP链接和N-1个传入链接。

这些TCP链接始终保持活动状态,不按需建立。当节点指望在响应集群总线中的ping时发出pong应答时,在等待足够长的时间以将节点标记为不可达以前,它将尝试经过从头开始从新链接来刷新与节点的链接。

当Redis Cluster节点造成一个完整的网格时,节点使用gossip protocol和配置更新机制,以免在正常状况下在节点之间交换太多消息,所以交换的消息数量不是指数级的。

节点握手

节点始终接受集群总线端口上的链接,甚至在收到ping时也会回复ping,即便ping节点不受信任也是如此。可是,若是发送节点不被视为集群的一部分,则接收节点将丢弃全部其余分组。

节点将仅以两种方式接受另外一个节点做为集群的一部分:

  • 若是节点为本身显示MEET消息。MEET消息与PING消息几乎彻底相同,但强制接收者接受节点做为集群的一部分。仅当系统管理员经过如下命令请求时,节点才会将MEET消息发送到其余节点:

    CLUSTER MEET ip port

  • 若是已经信任的节点谈及另外一个节点,则节点还将另外一个节点注册为集群的一部分。所以,若是A知道B,而且B知道C,则最终B将向A发送关于C的gossip消息。当发生这种状况时,A将注册C做为网络的一部分,并将尝试与C链接。

这意味着只要咱们链接任何链接图中的节点,它们最终将自动造成彻底链接的图。这意味着集群可以自动发现其余节点,但前提是系统管理员强制创建了信任关系。

此机制使集群更加健壮,但可防止不一样的Redis集群在更改IP地址或其余网络相关事件后意外混合。

MOVED重定向

Redis客户端能够自由地向集群中的每一个节点发送查询,包括从节点。节点将分析查询,若是它是可接受的(即,查询中只提到一个key,或者提到的多个key都是相同的哈希槽),它将查找哪一个节点负责哈希槽key或key所属的地方。

若是节点为哈希槽提供服务,则只处理查询,不然节点将检查其内部哈希槽到节点映射,并将回复具备MOVED错误的客户端,以下例所示:

GET x
-MOVED 3999 127.0.0.1:6381
复制代码

该错误包括key的哈希槽(3999)和能够为查询提供服务的实例的ip:端口。客户端须要将查询从新发出到指定节点的IP地址和端口。请注意,即便客户端在从新发出查询以前等待很长时间,而且同时集群配置发生更改,若是散列槽3999如今由另外一个节点提供服务,则目标节点将再次回复MOVED错误。若是联系的节点没有更新的信息,则会发生相同的状况

所以,从集群节点的角度来看,咱们尝试经过ID来简化咱们与客户端的接口,只是在哈希槽和由IP:端口对识别的Redis节点之间公开映射。

客户端不是必需的,但应该尝试记住127.0.0.1:6381提供的哈希槽3999。这样,一旦须要发出新命令,它就能够计算目标key的散列槽而且更有可能选择正确的节点。

另外一种方法是在收到MOVED重定向时使用CLUSTER NODESCLUSTER SLOTS命令刷新整个客户端集群布局。遇到重定向时,可能会从新配置多个插槽而不是一个,所以尽快更新客户端配置一般是最佳策略。

请注意,当集群稳定(配置中没有持续更改)时,最终全部客户端都将得到散列插槽映射 - >节点,从而使集群高效,客户端直接寻址正确的节点而无需重定向,代理或其余单个节点失败点实体。

客户端还必须可以处理本文档后面描述的**-ASK重定向**,不然它不是完整的Redis集群客户端。

集群实时重配置

Redis Cluster支持在集群运行时添加和删除节点的功能。添加或删除节点被抽象为相同的操做:将哈希槽从一个节点移动到另外一个节点。这意味着可使用相同的基本机制来从新平衡集群,添加或删除节点等。

  • 要向集群添加新节点,会向集群添加空节点,并将一些散列插槽集从现有节点移动到新节点。
  • 要从集群中删除节点,分配给该节点的哈希槽将移动到其余现有节点。
  • 为了从新平衡集群,在节点之间移动一组给定的散列槽。

实现的核心是移动哈希槽的能力。从实际的角度来看,哈希槽只是一组key,所以Redis Cluster在从新分片期间的确实作的是将key从一个实例移动到另外一个实例。移动哈希槽意味着将哈希的合适的全部key移动到此哈希槽中。

要了解其工做原理,咱们须要显示CLUSTER用于操做Redis集群节点中的插槽转换表的子命令。

可使用如下子命令(在这种状况下,其余子命令无用):

前两个命令ADDSLOTSDELSLOTS,仅用于将插槽分配(或删除)到Redis节点。分配时隙意味着告诉给定主节点它将负责存储和提供指定散列槽的内容。

在分配散列槽以后,它们将使用gossip协议在集群中传播,如稍后在配置传播部分中所指定的 。

ADDSLOTS当从头开始建立新集群时,一般会使用该命令,以便为每一个主节点分配全部可用的16384个散列插槽的子集。

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

SETSLOT若是使用SETSLOT <slot> NODE表单,子命令用于将槽分配给特定节点ID 。不然,插槽能够在两种特殊状态进行设置MIGRATINGIMPORTING。使用这两个特殊状态是为了将散列槽从一个节点迁移到另外一个节点。

  • 当插槽设置为MIGRATING时,节点将接受与此散列插槽有关的全部查询,但仅当存在有问题的key时,不然使用-ASK重定向将查询转发到做为迁移目标的节点。
  • 当一个插槽设置为IMPORTING时,该节点将接受与该散列插槽有关的全部查询,但前提是该请求前面有一个ASKING命令。若是ASKING客户端未给出该命令,则查询将经过重定向错误-MOVED,重定向到真正的哈希槽全部者。

让咱们经过哈希槽迁移的例子来讲明这一点。假设咱们有两个Redis主节点,称为A和B。咱们想将散列槽8从A移动到B,因此咱们发出以下命令:

  • 咱们发送B:CLUSTER SETSLOT 8 IMPORTING A
  • 咱们发送A:CLUSTER SETSLOT 8 MIGRATING B

每次使用属于散列槽8的key查询客户端时,全部其余节点将继续将客户端指向节点“A”,所以会发生如下状况:

  • 有关现有key的全部查询都由“A”处理。
  • 关于A中不存在的key的全部查询都由“B”处理,由于“A”将客户端重定向到“B”。

这样咱们就再也不在“A”中建立新key了。与此同时,redis-trib在从新分片和Redis集群配置期间使用的特殊程序将把散列槽8中的现有key从A迁移到B.这是使用如下命令执行的:

CLUSTER GETKEYSINSLOT slot count
复制代码

上面的命令将返回count指定哈希槽中的键。对于返回的每一个key,redis-trib向节点“A”发送一个MIGRATE命令,该命令将以原子方式将指定的key从A迁移到B(两个实例都被锁定了迁移key所需的时间(一般是很是小的时间),所以存在没有竞争条件)。这就是MIGRATE的工做原理:

MIGRATE target_host target_port key target_database id timeout
复制代码

MIGRATE将链接到目标实例,发送key的序列化版本,一旦收到OK代码,将删除其本身的数据集中的旧key。从外部客户端的角度来看,key在任何给定时间存在于A或B中。

在Redis集群中,不须要指定0之外的数据库,但 MIGRATE是一个通用命令,可用于不涉及Redis集群的其余任务。 即便在移动复杂key(如长列表)时,MIGRATE也会尽量快地进行优化,但在Redis集群中,若是使用数据库的应用程序存在延迟限制,则从新配置存在bigkey的集群不被视为明智的过程。

当迁移过程最终完成时,该SETSLOT <slot> NODE <node-id>命令被发送到迁移中涉及的两个节点,以便再次将槽设置为其正常状态。一般会将相同的命令发送到全部其余节点,以免等待新配置在集群中的天然传播。

ASK重定向

在上一节中,咱们简要介绍了ASK重定向。为何咱们不能简单地使用MOVED重定向?由于虽然MOVED意味着咱们认为哈希槽是由不一样节点永久服务的,而且应该针对指定节点尝试下一个查询,但ASK意味着仅将下一个查询发送到指定节点。

这是必需的,由于关于散列槽8的下一个查询能够是关于仍在A中的key,所以咱们老是但愿客户端尝试A,而后在须要时尝试B。因为这仅发生在16384可用的一个散列槽中,所以集群上的性能能够接受。

咱们须要强制该客户端行为,所以为了确保客户端在A尝试以后只尝试节点B,若是客户端在发送查询以前发送ASKING命令,则节点B将仅接受设置为IMPORTING的插槽的查询。

基本上,ASKING命令在客户端上设置一次性标志,强制节点提供有关IMPORTING槽的查询。

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

  • 若是收到ASK重定向,则仅发送重定向到指定节点的查询,但继续向旧节点发送后续查询。
  • 使用ASKING命令启动重定向查询。
  • 尚未更新本地客户端表以将哈希插槽8映射到B。

一旦散列槽8迁移完成,A将发送MOVED消息,而且客户端能够将散列槽8永久映射到新的IP和端口对。请注意,若是有错误的客户端先前执行了映射,这不是问题,由于它在发出查询以前不会发送ASKING命令,所以B将使用MOVED重定向错误将客户端重定向到A.

插槽迁移以相似的术语解释,但在CLUSTER SETSLOT 命令文档中使用不一样的措辞(为了文档中的冗余)。

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

虽然有可能让Redis集群客户端实现不记得内存中的插槽配置(插槽号和节点的地址之间的映射),而且只能经过联系等待重定向的随机节点来工做,这样的客户端将是很是低效的。

Redis集群客户端应该尝试足够智能以记住插槽配置。可是,此配置不须要是最新的。因为联系错误的节点只会致使重定向,所以应该触发客户端视图的更新。

客户端一般须要在两种不一样的状况下获取完整的插槽列表和映射的节点地址:

  • 在启动时,为了填充初始插槽配置。
  • MOVED接收到重定向。

请注意,客户端能够MOVED经过仅更新其表中移动的插槽来处理重定向,但这一般效率不高,由于一般会当即修改多个插槽的配置(例如,若是将从属设备提高为主服务器,则全部服务旧master的插槽将从新映射)。MOVED经过从头开始向节点提取完整的插槽映射来对重定向作出反应要简单得多。

为了检索插槽配置,Redis Cluster提供了不须要解析的CLUSTER NODES命令的替代方法,而且仅提供客户端严格须要的信息。

新命令称为CLUSTER SLOTS,它提供一个插槽范围数组,以及服务于指定范围的关联主节点和从属节点。

如下是CLUSTER SLOTS输出的示例:

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
复制代码

返回数组的每一个元素的前两个子元素是范围的起始端插槽。附加元素表示地址 - 端口对。第一个地址 - 端口对是服务于插槽的主设备,附加的地址端口对是服务于相同插槽的全部从设备,它们不处于错误状态(即未设置FAIL标志)。

例如,输出的第一个元素表示从5461到10922(包括起始和结束)的插槽由127.0.0.1:7001提供服务,而且能够看到只读slave的信息:127.0.0.1:7004。

若是集群配置错误,则没法保证CLUSTER SLOTS返回覆盖完整16384个插槽的范围,所以客户端应使用NULL对象初始化填充目标节点的插槽配置映射,并在用户尝试执行有关key的命令时报告错误属于未分配的插槽。

在发现未分配插槽时将错误返回给调用方以前,客户端应尝试再次获取插槽配置以检查集群是否已正确配置。

多键操做

使用哈希标记,客户端能够自由使用多键操做。例如,如下操做有效:

MSET {user:1000}.name Angela {user:1000}.surname White
复制代码

当key所属的散列槽的从新分片正在进行时,多键操做可能变得不可用。

更具体地说,即便在从新分片期间,仍然能够得到全部存在且仍然散列到相同slot(源节点或目的地节点)的多键操做。

对于不存在或在从新分片期间在源节点和目标节点之间拆分的键的操做将生成-TRYAGAIN错误。客户端能够在一段时间后尝试操做,或报告错误。

一旦指定的散列槽的迁移终止,全部多键操做再次可用于该散列槽。

使用从节点缩放读取

一般,从节点会将客户端重定向到给定命令中涉及的散列槽的权威主节点,可是客户端可使用从属节点来使用READONLY命令扩展读取。

READONLY告诉Redis集群从属节点客户端能够读取可能过期的数据,而且对运行写入查询不感兴趣。

当链接处于只读模式时,仅当操做涉及未由从属主节点提供的key时,集群才会向客户端发送重定向。这多是由于:

  1. 客户端发送了一个关于从未由该从属服务器的主服务器提供服务的散列槽的命令。
  2. 集群被从新配置(例如从新配置),而且从属设备再也不可以为给定的哈希槽提供命令。

发生这种状况时,客户端应更新其散列图映射,如前面部分所述。

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

心跳和gossip消息

Redis集群节点不断交换ping和pong数据包。这两种数据包具备相同的结构,而且都携带重要的配置信息。惟一的实际区别是消息类型字段。咱们将ping和pong包的总和称为心跳包

一般节点发送ping数据包,触发接收器回复pong数据包。然而,这不必定是真的。节点能够仅发送pong数据包以向其余节点发送有关其配置的信息,而不会触发回复。例如,这是有用的,以便尽快广播新配置。

一般,节点将每秒ping几个随机节点,以便每一个节点发送的ping数据包总数(以及接收到的pong数据包)是一个恒定的数量,而无论集群中的节点数量。

可是,每一个节点都会确保ping全部其余节点不会让发送ping或接收pong的节点的时间超过一半的NODE_TIMEOUT。在NODE_TIMEOUT通过以前,节点还尝试将TCP链路与另外一个节点从新链接,以确保不会仅由于当前TCP链接存在问题而不相信节点不可达。

若是NODE_TIMEOUT设置为一个小数字而且节点数(N)很是大,则全局交换的消息数量能够是至关大的,由于每一个节点将尝试每隔一半NODE_TIMEOUT时间ping它们没有获取到新信息的每一个其余节点。

例如,在节点超时设置为60秒的100节点集群中,每一个节点将尝试每30秒发送99个ping,总ping数为3.3 /秒。乘以100个节点,在整个集群中每秒330次ping。

有一些方法能够下降消息数量,但Redis Cluster故障检测当前使用的带宽没有报告问题,所以目前使用了明显且直接的设计。注意,即便在上面的例子中,每秒交换的330个数据包在100个不一样的节点之间均匀分配,所以每一个节点接收的流量是可接受的。

心跳包内容

Ping和pong数据包包含全部类型数据包通用的标头(例如,请求故障转移投票的数据包),以及特定于Ping和Pong数据包的特殊Gossip部分。

公共标头具备如下信息:

  • 节点ID,一个160位伪随机字符串,在第一次建立节点时分配,并在Redis集群节点的全部生命周期内保持不变。
  • 发送节点的currentEpochconfigEpoch字段,用于挂载Redis Cluster使用的分布式算法(这将在下一节中详细介绍)。若是节点是从属节点,则它configEpochconfigEpoch其主节点的最后一个节点。
  • 节点标志,指示节点是不是从设备,主设备和其余单比特节点信息。
  • 由发送节点服务的散列槽的位图,或者若是节点是从属节点,则是其主节点服务的槽的位图。
  • 发送方TCP基本端口(即Redis用于接受客户端命令的端口;向此添加10000以获取集群总线端口)。
  • 从发送方的角度来看集群的状态(down或ok)。
  • 发送节点的主节点ID(若是它是从属节点)。

Ping和pong包也包含gossip部分。本节向接收方提供发送方节点对集群中其余节点的见解。gossip部分仅包含关于发送者已知的节点集中的几个随机节点的信息。gossip部分中提到的节点数量与集群大小成比例。

对于在gossip部分中添加的每一个节点,将报告如下字段:

  • 节点ID。
  • 节点的IP和端口。
  • 节点标志。

gossip部分容许接收节点从发送者的角度得到关于其余节点的状态的信息。这对于故障检测和发现集群中的其余节点都颇有用。

故障检测

Redis集群故障检测用于识别大多数节点没法再访问主节点或从节点,而后经过将从属设备提高为主节点来进行响应。当没法进行从属提高时,集群将处于错误状态以中止接收来自客户端的查询。

如前所述,每一个节点都采用与其余已知节点相关联的标志列表。有两个标志用于故障检测,被称为PFAILFAILPFAIL表示可能的故障,而且是未确认的故障类型。FAIL意味着节点出现故障,而且大多数master在固定的时间内确认了这一状况。

PFAIL标志:

PFAIL当节点不可访问超过NODE_TIMEOUT时间时,节点使用该标志标记另外一个节点。主节点和从节点均可以标记另外一个节点PFAIL,不管其类型如何。

Redis集群节点的不可达性概念是咱们有一个活动的ping(咱们发送的ping,咱们尚未获得回复)等待的时间超过NODE_TIMEOUT。对于这种工做机制,NODE_TIMEOUT与网络往返时间相比必须很大。为了在正常操做期间增长可靠性,节点将尝试在NODE_TIMEOUT已通过去一半的状况下与集群中的其余节点从新链接,而不会回复ping。此机制可确保链接保持活动状态,所以断开的链接一般不会形成节点之间错误的故障报告。

Fail标志:

PFAIL标志就是本地信息对其余节点信息的见解,但它不足以触发一个slave节点称为主节点。对于要被视为关闭的节点,须要将PFAIL条件升级到FAIL条件。

如本文档的节点心跳部分所述,每一个节点都向每一个其余节点发送gossip消息,包括一些随机已知节点的状态。每一个节点最终都会为每一个其余节点接收一组节点标志。这样,每一个节点都有一个机制来向其余节点发出有关它们检测到的故障状况的信号

假如PFAIL条件升级为FAIL条件时,下面的一组条件将知足:

  • 咱们称之为A的某个节点将另外一个节点B标记为PFAIL
  • 节点A经过gossip部分从集群中的大多数master的角度收集关于B状态的信息。
  • 大多数master在NODE_TIMEOUT * FAIL_REPORT_VALIDITY_MULT(在当前实现中,有效性因子设置为2,所以这只是时间NODE_TIMEOUT的两倍)时间内发出信号PFAILFAIL状态。

若是知足以上全部条件,则节点A将:

  • 将节点标记为FAIL
  • FAIL向全部可访问节点发送消息。

FAIL消息将强制每一个接收节点将此节点状态标记为Fail,不管它是否已经标记了PFAIL状态中的节点。

请注意,FAIL标志主要是单向的。也就是说,一个节点能够去从PFAILFAIL,但FAIL标志只能在下列状况下被清除:

  • 该节点已经能够访问而且是从属节点。在这种状况下,FAIL能够清除标志,由于slave未进行故障转移。
  • 该节点已经能够访问,而且是不为任何插槽提供服务的主节点。在这种状况下,FAIL能够清除标志,由于没有插槽的主服务器不会真正参与集群,而且正在等待配置以加入集群。
  • 该节点已经能够访问而且是主节点,可是很长时间(N次NODE_TIMEOUT)已通过去而没有任何可检测的从属促销。它最好从新加入集群并在这种状况下继续。

值得注意的是,虽然PFAIL- > FAIL过渡使用了一种协议形式,但使用的协议很弱:

  1. 节点在一段时间内收集其余节点的视图,所以即便大多数master须要“赞成”,实际上这只是咱们在不一样时间从不一样节点收集的状态,咱们不肯定,也不须要,在必定时刻,大多数master都赞成了。然而,咱们丢弃旧的故障报告,所以大多数master在一个时间窗口内发出故障信号。
  2. 虽然检测到该FAIL条件的每一个节点都将使用该FAIL消息强制该集群中的其余节点上的该条件,可是没法确保该消息将到达全部节点。例如,节点能够检测到该FAIL条件,而且因为分区将没法到达任何其余节点。

可是,Redis集群故障检测具备活跃度要求:最终全部节点都应该就给定节点的状态达成一致。有两种状况可能源于裂脑状况。一些少数节点认为节点处于FAIL状态,或者少数节点认为节点不处于FAIL状态。在这两种状况下,最终集群将具备给定节点状态的单个视图:

状况1:若是大多数主机已将节点标记为FAIL因为故障检测及其产生的链效应,则每一个其余节点最终将标记主机FAIL,由于在指定的时间窗口中将报告足够的故障。

状况2:当只有少数master标记了一个节点时FAIL,slave升级为master将不会发生(由于它使用更正式的算法,确保每一个人最终都知道slave升级),而且每一个节点将根据FAIL状态清除FAIL状态清除上述规则(即通过N次NODE_TIMEOUT后没有变化)。

该FAIL标志仅用做触发器来运行slave变成master的算法的安所有分。理论上,从属设备能够在其主设备没法访问时独立启动slave promotion,并等待主设备拒绝提供确认(若是主设备实际上可由多数人访问)。然而,因为PFAIL -> FAIL状态复杂性的增长,薄弱的协议,以及FAIL强制在集群的可到达部分中以最短的时间传播状态的消息具备实际优势。因为这些机制,若是集群处于错误状态,一般全部节点将几乎同时中止接受写入。从使用Redis Cluster的应用程序的角度来看,这是一个理想的功能。还避免了因为本地问题而没法到达其主设备的从设备发起的错误选举尝试(主设备可由大多数其余主节点到达)。

集群Current epoch

Redis Cluster使用相似于Raft算法“term”的概念。在Redis Cluster中,该术语称为epoch,它用于为事件提供增量版本控制。当多个节点提供冲突信息时,另外一个节点能够了解哪一个状态是最新的。

currentEpoch是一个64位无符号数。

在建立节点时,每一个Redis Cluster节点(从属节点和主节点)都将其currentEpoch设置为0。

每当从另外一节点接收到分组时,若是发送方的epoch(集群总线消息报头的一部分)大于本地节点epoch,currentEpoch则更新为发送方时期。

因为这些语义,最终全部节点都将采用集群中的最好节点的configEpoch

当集群的状态发生变化且节点寻求协议以执行某些操做时,将使用此信息。

目前,这只发生在slave promotion期间,以下一节所述。基本上,epoch是集群的逻辑时钟,而且要求给定的信息将具备较小epoch的集群统一。

Configuration epoch

每一个master老是在ping和pong数据包中公布configEpoch,以及一个位图公布其服务的插槽集。

建立新节点时,在master中将configEpoch设置为零。

在slave选举期间建立了一个新的configEpoch。slave试图增长了它们的epoch来取代失败的master,并试图得到大多数master的受权。当slave被受权时,将建立一个新的惟一的configEpoch,而且slave变成master后将使用新的configEpoch

以下一节所述,当不一样节点声明不一样的配置(因为网络分区和节点故障而可能发生的状况)时,configEpoch有助于解决冲突。

从节点还在ping和pong数据包中通告该configEpoch字段,可是在slave的状况下,该configEpoch字段表示它们最后一次交换数据包时的主节点的配置。这容许其余实例检测从属设备什么时候具备须要更新的旧配置(主节点不会授予具备旧配置的slave的投票权限)。

每次更改某个已知节点的configEpoch时,它都会被接收到此信息的全部节点永久存储在nodes.conf文件中。currentEpoch值也会发生一样的状况。保证fsync-ed在节点继续运行以前更新这两个变量并保存到磁盘。

在故障转移期间使用简单算法生成的configEpoch值保证是新的,增量的和惟一的。

slave选举和晋升

slave节点选举和晋升由slave节点处理,在主节点的投票支持下实现slave promotion。当主机FAIL处于从至少一个具备先决条件以成为主设备的从设备的角度处于状态时,发生从设备选举。

为了让slave可以自我提高,它须要开始选举并赢得选举。若是master处于FAIL状态,那么给定master的全部slave均可以开始选举,可是只有一个slave会赢得选举并促使本身掌握节点。

当知足如下条件时,slave开始选举:

  • slave的master处于FAIL状态。
  • 主机正在提供非零数量的插槽。
  • 从复制连接与主服务器断开链接的时间不超过给定的时间,以确保提高的从属数据是最新的。此时间是用户可配置的。

为了被选举,slave的第一步是增长其currentEpoch计数器,并从主实例请求投票。

经过将FAILOVER_AUTH_REQUEST分组广播到集群的每一个主节点,slave请求投票。而后它等待两倍的回复最大时间NODE_TIMEOUT到达(但老是至少2秒)。

一旦master投票给一个给定的slave,回答FAILOVER_AUTH_ACKNODE_TIMEOUT * 2段时间内它就不能再投票给同一个master的另外一个slave。在此期间,它将没法回复同一主机的其余受权请求。这不是保证安全所必需的,但对于防止多个slaveconfigEpoch在大约同一时间(一般是不一样的)被选中(一般不须要)是有用的。

从属服务器在收到epoch时间小于发送投票请求时的currentEpoch时丢弃任何AUTH_ACK回复。这确保它不计算用于先前选举的投票。

一旦slave接收到来自大多数master的ACK,它就赢得了选举。不然,若是在两倍NODE_TIMEOUT(但老是至少2秒)的时间内未得到多数投票,则选举停止,而且在NODE_TIMEOUT * 4(而且老是至少4秒)以后将再次尝试新的选举。

slave等级

一旦master处于FAIL状态,slave会在尝试晋升以前等待一小段时间。该延迟计算以下:

DELAY = 500 milliseconds + random delay between 0 and 500 milliseconds +
        SLAVE_RANK * 1000 milliseconds.
复制代码

固定延迟确保咱们等待FAIL状态在集群中传播,不然slave可能会在其余masters仍然不知道此masterFAIL状态时尝试当选,拒绝投票给它们

随机延迟用于使slaves去同步,所以它们不太可能同时开始选举。

SLAVE_RANK是slave关于它从master处理的复制数据量的级别。当主设备发生故障时,从设备交换消息以创建(best effort)排名:具备最新复制偏移的从设备为等级0,第二高的等级为1,依此类推。经过这种方式,最新的slave试图在其余人以前当选。

排名顺序没有严格执行; 若是更高级别的slave未能当选,其余人将很快尝试。

一旦slave赢得选举,它将得到一个新的惟一和增量的configEpoch,高于任何其余现有master。它开始在ping和pong数据包中宣传本身做为master的角色,提供一组服务的插槽与configEpoch

为了加速其余节点的从新配置,将pong分组广播到集群的全部节点。目前没法访问的节点在从另外一个节点接收到ping或pong数据包时,最终将被从新配置,或者若是检测到它经过心跳包发布的信息已过时,则将从另外一个节点接收UPDATE数据包。

其余节点将检测到有一个新主服务器为旧主服务器提供服务但具备更好的configEpoch以及具备相同插槽服务,并将升级其配置。旧主服务器的从服务器(若是它从新加入集群,则是故障转移主服务器)不只会升级配置,还会从新配置以重新主服务器进行复制。如何配置从新加入集群的节点将在下一节中介绍。

master回复slave投票请求

在上一节中,讨论了slave如何试图当选。本节解释了从请求为给定从属者投票的master的角度发生的事情。

master们以FAILOVER_AUTH_REQUEST要求形式收到slave的投票请求。

要得到投票,须要知足如下条件:

  1. 主设备只对给定epoch投票一次,并拒绝给旧的epoch投票:每一个主设备都有一个lastVoteEpoch字段,只要auth请求包中的currentEpoch值不大于lastVoteEpoch,它就会拒绝再次投票。当master对投票请求做出确定回复时,lastVoteEpoch会相应更新,并安全地存储在磁盘上。
  2. 只有当slave的master被标记为FAIL时,masters才会投票给slave。
  3. auth请求的currentEpoch小于master的currentEpoch将被忽略。所以,master回复的currentEpoch将始终与auth请求相同。若是同一个slave再次要求投票,增长currentEpoch,能够保证不能接受来自master的旧延迟回复用于新投票。

不使用规则3致使的问题示例:

mastercurrentEpoch是5,lastVoteEpoch是1(这可能发生在选举失败后)

  • slavecurrentEpoch是3。
  • slave试图用epoch 4(3 + 1)当选,master用currentEpoch5 回答肯定,但回复延迟了。
  • slave将尝试再次当选,在晚些时候,使用epoch 5(4 + 1),延迟回复到达slavecurrentEpoch5,并被接受为有效。
  1. 若是该master的slave已经被投票,则master不会在NODE_TIMEOUT * 2过去以前投票给同一master的slave。这不是严格要求的,由于两个slave不可能在同一时期赢得选举。可是,实际上它确保当一个slave被选中时,它有足够的时间通知其余slave,并避免另外一个slave赢得新选举的可能性,执行没必要要的第二次故障转移。
  2. master们不会以任何方式选择最好的slave。若是slave的master处于FAIL,而且master没有在当前任期内投票,则给予正面投票。最好的slave是最有可能开始选举并在其余slave选举以前赢得它,由于它一般可以提早开始投票过程,由于它的*排名更高,*如上一节所述。
  3. 当master拒绝为给定的slave投票时没有否认回应,该请求就会被忽略。
  4. 对于slave声称的插槽,master不投票给slave发送的configEpoch数量小于master表中的任何一个configEpoch。请记住,从属设备发送其主设备configEpoch,以及主设备提供的插槽位图。这意味着请求投票的slave必须具备其想要故障转移的插槽的配置,该配置新于或等于授予投票的主设备。

分区期间配置epoch有用性的实际示例

本节说明了如何使用epoch概念使slave promotion过程对分区更具抵抗力。

  • master再也不无限期到达。master有三个slaveA,B,C。
  • slaveA赢得选举并晋升为master。
  • 网络分区使A在大多数集群中不可用。
  • slaveB赢得选举并被提高为master。
  • 分区使B在大多数集群中不可用。
  • 先前的分区是固定的,A再次可用。

此时B已关闭且A再次具备master的角色(实际上UPDATE消息会当即从新配置它,但在这里咱们假设全部UPDATE消息都丢失了)。与此同时,slaveC将尝试当选,以便将B故障转移。这就是:

  1. C将尝试当选并将成功,由于对于大多数master来讲,它的master实际上已经失败了。它将得到一个新的增量configEpoch
  2. A将由于其散列槽失去master身份,由于与A发布的节点相比,其余节点已经具备与更高配置epoch(B是其中的一个)相关联的相同散列槽。
  3. 所以,全部节点都将升级其表以将散列槽分配给C,而且集群将继续其操做。

正如您将在下一节中看到的,从新加入集群的陈旧节点一般会尽快收到有关配置更改的通知,由于只要它ping任何其余节点,接收方就会检测到它有陈旧信息并将发送一个UPDATE信息。

散列槽配置传播

Redis Cluster的一个重要部分是用于传播有关哪一个集群节点为一组给定哈希槽服务的信息的机制。这对于新集群的启动和在从属服务器提高为其故障主服务器的插槽提供服务后升级配置的能力相当重要。

相同的机制容许以无限长的时间划分的节点以合理的方式从新加入集群。

哈希槽配置有两种传播方式:

  1. 心跳消息。ping或pong数据包的发送方老是添加有关它(或其主节点,若是它是从节点)服务的散列插槽集的信息。
  2. UPDATE消息。因为在每一个心跳包中都有关于所服务的发送方configEpoch和一组哈希slot的信息,若是心跳包的接收方发现发送方信息是陈旧的,它将发送包含新信息的包,迫使过期节点更新其信息。

心跳或UPDATE消息的接收器使用某些简单规则以便将其表映射散列槽更新到节点。建立新的Redis集群节点时,其本地哈希槽表将简单地初始化为NULL条目,以便每一个哈希槽不绑定或连接到任何节点。这看起来相似于如下内容:

0 -> NULL
1 -> NULL
2 -> NULL
...
16383 -> NULL
复制代码

为了更新其哈希槽表,第一个遵循节点的规则以下:

规则1:若是散列槽未分配(设置为NULL),而且已知节点声明它,我将修改个人散列槽表并将声明的散列槽与其关联。

所以,若是咱们从节点A接收到声称为configEpoch值为3的散列插槽1和2提供服务的心跳,则该表将被修改成:

0 -> NULL
1 -> A [3]
2 -> A [3]
...
16383 -> NULL
复制代码

建立新集群时,系统管理员须要手动分配(使用CLUSTER ADDSLOTS命令,经过redis-trib命令行工具或经过任何其余方式)每一个主节点服务的插槽仅用于节点自己,以及信息将快速传播到集群中。

可是这条规则还不够。咱们知道散列槽映射能够在两个事件期间发生变化:

  1. 在故障转移期间,slave会替换其master。
  2. 插槽从节点从新分段到另外一个节点。

如今让咱们关注故障转移。当从设备故障转移其主设备时,它得到configEpoch,该configEpoch保证大于其主设备之一(而且一般大于先前生成的任何其余配置时期)。例如,做为A的从属节点B,可使用configEpoch 4来故障转移B。它将开始发送心跳包(第一次在集群范围内进行大规模广播),而且因为如下第二规则,接收器将更新他们的哈希槽表:

规则2:若是已经分配了一个散列槽,而且一个已知节点正在使用configEpoch大于当前与该槽相关联的主节点的configEpoch通告它,我将把散列槽从新绑定到新节点。

所以,在接收到来自B的消息声称服务于配置时期为4的散列插槽1和2以后,接收器将按如下方式更新其表:

0 -> NULL
1 -> B [4]
2 -> B [4]
...
16383 -> NULL
复制代码

活动属性:因为第二个规则,最终集群中的全部节点都会赞成插槽的全部者是公布它的节点中具备最大configEpoch的插槽的全部者。

Redis集群中的此机制称为last failover wins

在从新分析期间也会发生一样的状况。当导入散列槽的节点完成导入操做时,其configEpoch会增长,以确保更改将在整个集群中传播。

更新消息,仔细看看

考虑到上一节,更容易了解更新消息的工做原理。节点A可能在一段时间后从新加入集群。它将发送心跳包,声称它服务于散列槽1和2,配置时期为3.全部具备更新信息的接收器将看到相同的散列槽与具备更高配置时期的节点B相关联。所以,他们将使用插槽的新配置向A 发送UPDATE消息。因为上面的规则2,A将更新其配置 。

节点如何从新加入集群

当节点从新加入集群时,将使用相同的基本机制。继续上面的例子,节点A将被通知哈希槽1和2如今由B服务。假设这两个是A服务的惟一哈希槽,A服务的哈希槽的数量将降低到0!因此A将从新配置成新master的slave

遵循的实际规则比这复杂一点。一般,可能会发生A在不少时间以后从新加入,同时可能发生最初由A服务的哈希时隙由多个节点服务,例如,哈希slot 1能够由B服务,而哈希时隙2由C提供。

所以,实际的Redis集群节点角色切换规则是:主节点将更改其配置以复制(做为其从属)其最后一个散列槽的节点

在从新配置期间,最终服务的散列槽的数量将降低到零,而且节点将相应地从新配置。请注意,在基本状况下,这只意味着旧主服务器将成为在故障转移后替换它的从服务器的从服务器。可是,在通常形式中,规则涵盖全部可能的状况。

从属设备彻底相同:它们从新配置以复制其前主设备的最后一个哈希槽的节点。

副本迁移

Redis Cluster实现了一个名为副本迁移的概念,以提升系统的可用性。咱们的想法是,在具备主从设置的集群中,若是从设备和主设备之间的映射是固定的,则若是发生单个节点的多个独立故障,则可用性随时间受到限制。

例如,在每一个主服务器都有一个从服务器的集群中,只要主服务器或从服务器发生故障,集群就能够继续运行,但若是二者都失败,则集群能够继续运行。然而,存在一类故障,这些故障是因为可能随时间累积的硬件或软件问题引发的单个节点的独立故障。例如:

  • Master A有一个slaveA 1。
  • A master失败了。A1被提高为新的master。
  • 三小时后,A1以独立的方式失败(与A的失败无关)。因为节点A仍处于关闭状态,所以没有其余从站可用于升级。集群没法继续正常运行。

若是主服务器和从服务器之间的映射是固定的,那么使集群更能抵抗上述状况的惟一方法是向每一个主服务器添加从服务器,但这样作成本很高,由于它须要执行更多Redis实例,更多内存和等等。

另外一种方法是在集群中建立不对称,让集群布局随着时间的推移自动更改。例如,集群能够具备三个主设备A,B,C。A和B各自具备单个从设备A1和B1。然而,主设备C是不一样的而且具备两个从设备:C1和C2。

副本迁移是自动从新配置从站以便迁移到再也不覆盖的主站(无工做从站)的过程。使用副本迁移,上面提到的场景变为:

  • Amaster失败了。A1升级。
  • C2做为A1的从属进行迁移,不然不会被任何从属服务器支持。
  • 三小时后,A1也失败了。
  • C2被提高为新的master以取代A1。
  • 集群能够继续操做。

副本迁移算法

迁移算法不使用任何形式的协议,由于Redis集群中的从属布局不是须要与configEpoch一致和/或版本化的集群配置的一部分。相反,当没有支持主服务器时,它使用算法来避免从服务器的大规模迁移。该算法最终确保(一旦集群配置稳定),每一个主设备将由至少一个从设备支持。

这就是算法的工做原理。首先,咱们须要在此上下文中定义什么是 好的从属:从给定节点的角度来看,良好的从属是没有FAIL从属状态的从属。

在每一个从设备中触发算法的执行,该从设备检测到至少有一个没有良好从设备的主设备。然而,在检测到这种状况的全部slave中,只有一个子集应该起做用。该子集实际上一般是单个从设备,除非不一样的从设备在给定时刻具备其余节点的故障状态的略微不一样的视图。

活跃slave是具备最大可达master链接的从站数,不是在FAIL状态,并具备最小的ID节点的slave。

所以,例如,若是有10个主服务器,每一个服务器有1个从服务器,2个主服务器各有5个从服务器,那么将尝试迁移的从服务器是 - 在具备5个从服务器的2个主服务器中 - 具备最低节点ID的从服务器。鉴于没有使用协议,当集群配置不稳定时,可能会出现竞争条件,其中多个从属设备认为本身是具备较低节点ID的非故障从设备(在实践中不太可能发生这种状况) )。若是发生这种状况,结果是多个从属服务器迁移到同一个主服务器,这是无害的。若是比赛发生的方式会使分出的master没有slave,但在最终每一个master都将获得至少一个slave的支持。可是,正常行为是单个从设备从具备多个从设备的主设备迁移到孤立主设备。

该算法由用户可配置的参数控制,该参数称为 cluster-migration-barrier:在从属设备迁移以前必须留下主设备的良好从设备的数量。例如,若是此参数设置为2,则只有在其主服务器保留两个工做从服务器时,服务器才能尝试迁移。

configEpoch冲突解决算法

在故障转移期间经过slave promotion建立configEpoch新值时,它们将保证是惟一的。

可是,有两个不一样的事件,其中新的configEpoch值以不安全的方式建立,只是递增本地节点的本地currentEpoch并但愿同时没有冲突。这两个事件都是系统管理员触发的:

  1. 具备TAKEOVER选项的CLUSTER FAILOVER命令可以手动将从节点提高为主节点,而无需大多数主节点可用。例如,这在多数据中心设置中颇有用。
  2. 迁移用于集群从新平衡的插槽还会在本地节点内生成新的configEpoch,而不会出于性能缘由而达成协议。

具体来讲,在手动从新分片期间,当散列槽从节点A迁移到节点B时,从新分片程序将强制B将其配置升级到集群中发现的最大的时期加1(除非节点是已是具备最大configEpoch的那个),而不须要来自其余节点的协议。一般,真实世界的从新分片涉及移动数百个散列槽(特别是在小簇中)。对于每一个移动的散列槽,要求每次在从新分片期间生成新configEpoch的协议是低效的。此外,每次在resharding期间,都要让每一个集群节点中用fsync来存储新配置。由于它的执行方式,咱们能够用更好的方法代替,咱们只须要在第一个hash slot被移动时,使用新的configEpoch ,这更有效。

然而,因为上述两种状况,有可能(尽管不太可能)以具备相同configEpoch的多个节点结束。系统管理员执行的从新分片操做以及同时发生的故障转移(加上不少坏运气)currentEpoch若是传播速度不够快,可能会致使冲突。

此外,软件错误和文件系统损坏也可能致使具备相同配置时期的多个节点。

当服务于不一样散列槽的主服务器具备相同的configEpoch时,没有问题。更重要的是,slave failing over master具备惟一的configEpoch。

也就是说,手动干预或从新分配可能会以不一样方式更改集群配置。Redis Cluster主要活动属性要求插槽配置始终收敛,所以在任何状况下咱们都但愿全部主节点都有不一样的configEpoch

为了强制执行此操做,在两个节点以相同的configEpoch方式结束时使用冲突解决算法

  • 若是主节点检测到另外一个主节点正在使用相同的configEpoch
  • 若是与声称相同的configEpoch节点ID相比,节点具备按字典顺序更小的节点ID。
  • 而后它将其增长currentEpoch1,并将其用做新的configEpoch

若是有任何一组节点具备相同configEpoch的节点,那么除了具备最大节点ID的节点以外的全部节点都将向前移动,从而保证每一个节点最终将选择惟一的configEpoch而无论发生了什么。

此机制还保证在建立新集群后,全部节点都以不一样的方式启动configEpoch(即便实际上并未使用),由于redis-trib确保CONFIG SET-CONFIG-EPOCH在启动时使用。可是,若是因为某种缘由致使节点配置错误,它将自动将其配置更新为不一样的configEpoch。

节点重置

节点能够经过软件重置(无需从新启动),以便在不一样的角色或不一样的集群中重复使用。这在正常操做,测试和云环境中很是有用,在这些环境中,能够从新配置给定节点以加入不一样的节点集以放大或建立新集群。

在Redis集群中,使用CLUSTER RESET命令重置节点。该命令有两种变体:

  • CLUSTER RESET SOFT
  • CLUSTER RESET HARD

必须将命令直接发送到节点才能重置。若是未提供复位类型,则执行软复位。

如下是重置执行的操做列表:

  1. 软复位和硬复位:若是节点是从属节点,则将其转换为主节点,并丢弃其数据集。若是节点是主节点并包含键,则重置操做将停止。
  2. 软复位和硬复位:释放全部插槽,重置手动故障切换状态。
  3. 软复位和硬复位:节点表中的全部其余节点都被删除,所以节点再也不知道任何其余节点。
  4. 仅硬重置:currentEpochconfigEpochlastVoteEpoch设置为0。
  5. 仅硬重置:节点ID更改成新的随机ID。

没法重置具备非空数据集的主节点(由于一般您但愿将数据从新硬化到其余节点)。可是,在适当的特殊条件下(例如,当为了建立新集群而彻底销毁集群时),必须在继续复位以前执行FLUSHALL

从集群中删除节点

经过将其全部数据从新分配给其余节点(若是它是主节点)并将其关闭,实际上能够从现有集群中删除节点。可是,其余节点仍将记住其节点ID和地址,并将尝试与其链接。

所以,当删除节点时,咱们还但愿从全部其余节点表中删除其条目。这是经过使用该CLUSTER FORGET <node-id>命令完成的 。

该命令有两个做用:

  1. 它从节点表中删除具备指定节点ID的节点。
  2. 它设置了60秒禁止,以防止从新添加具备相同节点ID的节点。

第二个操做是必需的,由于Redis Cluster使用gossip来自动发现节点,所以从节点A移除节点X可能致使节点B再次将节点X gossip 到A节点。因为禁止60秒,Redis集群管理工具备60秒,以便从全部节点中删除节点,从而防止因为自动发现而从新添加节点。

有关详细信息,请参阅CLUSTER FORGET文档。

Redis Cluster开发运维常见问题

1)Pub/Sub广播

问题:publish在集群每一个节点广播:加剧带宽。

解决:单独使用一套Redis Sentinel。

2)集群倾斜

数据倾斜:内存不均。

1:节点和槽分配不均。

2:不一样槽对应键值数量差别较大。

3:包含bigkey。

4:内存相关配置不一致。

请求倾斜:热点。

热点key:重要的key或者bigkey。

优化:

一、避免bigkey

二、热键不要用hash_tag

三、当一致性不高时,能够用本地缓存+MQ

3)读写分离

只读链接:集群模式的从节点不接受任何读写请求。

1:重定向到负责槽的主节点。

2:readonly命令能够读:链接级别的命令。

读写分离:更加复杂。

1:一样的问题:复制延迟、读取过时数据、从节点故障。

2:修改客户端:cluster slaves {nodeId}

4)数据迁移

在线迁移的一些工具:

1:惟品会:redis-migrate-tool。

2:豌豆荚:redis-port。

5)集群限制

1:key批量操做支持有限:例如mget、mset必须在一个slot。

2:key事务和Lua支持有限:操做的key必须在一个节点。

3:key是数据分区的最小粒度:不支持bigkey分区。

4:不支持多个数据库:集群模式下只有一个db 0。

5:复制只支持一层:不支持树形复制结构。

结论

1:Redis Cluster:知足容量和性能的扩展性,不少业务“不须要”。

2:不少场景Redis Sentinel已经足够好。

集群总结

1)Redis cluster数据分区规则采用虚拟槽方式(16384个槽),每一个结点负责一部分槽和相关数据,实现数据和请求的负载均衡。

2)搭建集群划分四个步骤:准备节点、节点握手、分配槽、复制。

3)集群伸缩经过在节点之间移动槽和相关数据实现。

4)使用smart客户端操做集群达到通讯效率最大化,客户端内部负责计算维护键->槽->节点的映射,用于快速定位到目标节点。

5)集群自动故障转移过程分为故障发现和节点恢复。节点下线分为主观下线和客观下线,当超过半数主节点认为故障节点为主观下线时标记它为客观下线状态。从节点负责对客观下线的主节点触发故障恢复流程,保证集群的可用性。

相关文章
相关标签/搜索