Redis3.0 Cluster集群规范

Redis Cluster是Redis分布式实现,目标在于高性能、线性可扩展性及安全写操作。

目标

高性能和线性可扩展性:高达1000个节点、无代理,异步复制,对值不执行合并操作。

可接受的安全写:系统尽可能保证大多主节点的写操作,通常有一些小窗口,在这些小窗口中可能丢失公认的写操作。

可用性:集群大多时候在主节点上工作,当主节点不可访问时,至少有一个从节点可访问。

功能

Redis Cluster中所有单键命令都可用,复杂的多键操作,只能是位于同一插槽下的键能够使用。

Redis Cluster实现了称为哈希标签的概念,以此强制将某些键存储在同一哈希槽中。在手动重新分片期间,多键操作可能会在一段时间内变得不可用,但单键操作始终可用。

Redis Cluster不支持多个数据库,只有数据库0,不允许使用SELECT命令。

群集协议

Redis Cluster集群中,节点负责保存数据,获取集群状态,将键映射到正确的节点。

集群节点能够自动发现其他节点,检测不工作的节点,并在必要时将从节点提升为主节点,将故障进行转移。

集群的所有节点都有TCP总线,节点间使用集群总线的二进制协议进行连接。

使用gossip协议传播集群信息,以便发现新节点。通过发送ping包确认其他节点的状态,并发送集群消息作为通知方式。

集群总线用于跨集群传播发布/订阅消息,并对用户发起的故障转移请求进行编排。

集群没有代理的概念,请求可随意发往任意节点,节点使用重定向命令,将客户端请求端重定向到正确的数据节点。

理论上,客户端可以自由地向集群中的任意节点发送请求,客户端不需要保持集群的状态。但若客户端能够缓存键和节点间的映射关系,能大大提高性能。

安全写

Redis Cluster在节点之间使用异步复制,故障转移时会做隐式合并。这意味着最终选择的主数据集将替换其他副本,在分区期间可能丢失写操作。

以下是丢失数据的场景:

1、写操作到达主节点并确认,写入却不能在主节点与从节点间异步复制,若主节点宕机,而写入没有到达从节点,若从节点被提升为主节点,则写入将永远丢失。这种场景通常很难观察到,因为,主节点尝试在同一时间答复客户端(带有写入确认)和从节点(传播写入)。然而,这是一个真实存在的失败场景。

2、从理论上讲,丢失写操作的另一种可能场景:主节点由于分区而无法访问,被其中一个从节点做了故障转移。一段时间后,原来的主节点又变得可用,持有过期路由表的客户端可能会先写入旧主节点,而群集却认为旧的主节点是为新主节点的从节点。

第二种场景之所以停留在理论上,是因为,进行故障转移的主节点将不再接受写操作,且在分区固定后,仍会在短时间内拒绝写操作,允许其他节点更改配置,且同时满足客户端的路由表尚未作更新的假设。

主节点故障转移的条件:半数以上的主节点无法访问它时,分区的持续时间超过NODE_TIMEOUT时,将被执行故障转移。

可用性

Redis Cluster旨在解决集群少数几节点故障时仍可用。对于有大量网络拆分的高可用性应用而言,它并非一个合适的解决方案。

在由N个主节点组成的集群中,每个主节点都有一个从节点,当一个主节点被分区时,集群的大部分仍然可用,在两个节点被分区时可用概率为1-(1/(N*2-1))。

在第一个节点失败后,共有N*2-1个节点,没有从节点的主节点其失败概率为1/(N*2-1))。

如,在有5个主节点组成的集群中,每个主节点有一个从节点,两个节点与其他节点发生分区后,集群将不再可用,其概率为1/(5*2-1)= 11.11%。

Redis Cluster副本迁移特性,能改善集群的可用性,在每次分区后,集群能重新配置从节点的布局,以便更好地抵御下次失败。

性能

Redis Cluster中,节点不会代理某个键所在的节点,而是将客户端重定向到键所在的节点。

使用异步复制,因此,节点不必等待其他节点的写入确认(若未明确使用WAIT命令)。

由于多键命令仅局限于近键,所以,除了重新分片外,数据不会在节点之间移动。

高性能和可伸缩性,同时保持数据的安全性和可用性。

为何不执行合并操作

Redis Cluster设计避免多个节点中相同键值对的版本冲突。

Redis中的值通常比较大,通常能够看到包含数百万个元素的列表或排序集。

数据类型在语义上也很复杂。

传输和合并这些类型的值,可能会引发性能瓶颈,且可能需要应用程序配合。

CRDT或同步复制的状态机,能够对类似于Redis的复杂数据类型进行建模,然而,此类系统的实际行为与Redis Cluster并不相同。

组件

键分配模型

键空间被划分为16384个插槽,平均分配在现有的集群主节点上,推荐的最大节点数约为1000个。

HASH_SLOT = CRC16(key) mod 16384

节点属性

集群中节点的名称都是唯一的。节点名称由160位随机数构成的十六进制,在节点首次启动时获得(通常使用/dev/urandom)。

节点将ID保存在节点配置文件中,在系统管理员未删除节点配置文件前永久有效,可通过CLUSTER RESET命令重置ID 。

节点ID用于标识集群中的节点。节点可以更改IP,无需更改节点ID。

群集能够检测IP/端口的变化,使用gossip协议在群集总线上重新配置。

节点ID不是与节点关联的惟一信息,却是惟一始终一致的信息。

每个节点都维护着有关群集其他节点的信息:

节点ID、节点IP和端口,一组标志

$ redis-cli cluster nodes

将CLUSTER NODES命令发送到群集中的任何节点,查询群集状态及每个节点的信息。

集群总线

Redis Cluster节点有一个额外的TCP端口,用于接收其他节点的传入连接。此端口处于客户端TCP端口的固定偏移量上。如:客户端TCP端口为6379,则当前实例的总线端口为10000+6379。

节点间的通信使用群集总线和群集总线协议,群集协议由不同类型和大小的帧构成的二进制协议。

注:可通过阅读Redis Cluster源代码中的cluster.h和cluster.c文件获取集群总线协议的详细信息 。

集群拓扑

Redis Cluster是一个完整的网格,节点间使用TCP连接。在N个节点的群集中,每个节点都有N-1个传出\入连接。这些TCP连接始终保持活动状态,且不会按需创建。当节点希望对集群总线中的ping做出回应时,会等待足够长的时间,在节点被标记为不可访前进行Pong响应,并使用重连的方式刷新节点连接。

虽然Redis Cluster节点是一个完整的网格,但是节点使用gossip协议和配置更新机制,避免正常情况下节点间大量交换消息,因此,消息数量并非指数级。

节点握手

节点总是在集群总线端口上接受连接,在接收到ping信号时对其进行应答,即使ping节点不受信任。如果发送节点不是集群的一部分,接收节点将丢弃所有数据包。

一个节点仅以两种方式将另一个节点识为群集的一部分:

节点是否向其显示MEET消息。Meet消息与PING消息完全一样,但会强制接收者接受该节点作为群集的一部分。只有系统管理员通过以下命令请求节点时,节点才会将MEET消息发送给其他节点:

CLUSTER MEET ip port

若受信任的节点使用gossip协议与某个节点通信,该节点还将另一个节点识为群集的一部分。

如:若A知道B,B知道C,则最终B将向A发送有关C的gossip消息。这种情况下,A会将C视为网络的一部分,并尝试与C连接。

这意味着只要将节点加入任何连通图中,最终会自动形成一个完整的连通图。

集群能够自动发现其他节点,前提是系统管理员强制建立信任关系。

重定向

Redis客户端能够向集群中的任意节点(包括从节点)发送查询。

节点将分析查询,若可接受(即,查询中仅提及单个键,或提及的多个键全部位于同一哈希槽中),它将查找负责该哈希槽的节点。

若哈希槽由当前节点提供服务,则直接处理,否则,节点将检查哈希槽到节点的映射,通过MOVED回复客户端,如以下所示:

GET x
-MOVED 3999 127.0.0.1:6381

回复中包含键的哈希槽(3999)及可提供查询服务的实例ip和端口。

客户端将查询重新发到指定节点的IP地址和端口。

注:即使客户端在重新发出查询之前等待了很长时间,且与此同时,若群集配置已发生变化,哈希槽3999由另一个节点接替,则目标节点将再次发出MOVED答复。

客户端可以尝试记住哈希槽和对应的服务,但不是必需的。

一种替代方法:在收到MOVED重定向后,使用CLUSTER NODES或CLUSTER SLOTS命令,刷新整个客户端群集布局。遇到重定向时,很可能重新配置了多个插槽,尽快更新客户端配置通常是最佳做法。

注:当群集稳定时(配置没有更改),所有客户端可获得哈希槽到节点的映射,客户端直接寻址正确的节点,提高集群效率。

客户端还必须能够处理-ASK重定向,否则它不是完整的Redis Cluster客户端。

实时更新配置

Redis Cluster支持在集群运行时添加和删除节点的功能。

添加和删除被抽象为同种操作:将哈希槽从一个节点移动到另一个节点,意味着可用相同的机制重新平衡群集。

将新节点添加到群集:将一个空节点添加到群集,并将某些哈希槽集从现有节点移至新节点。

从群集中删除节点:将分配给该节点的哈希槽移至其他节点。

为重新平衡群集,在节点间移动一组指定的哈希槽。该实现的核心是移动哈希槽的能力。

实际上,哈希槽只是一组键,因此,Redis Cluster在重新分片过程中,要做的只是将键从一个实例移至另一个实例。

要了解其工作原理,需清楚CLUSTER命令在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用于向Redis节点分配或删除插槽。

注:详情可参考CLUSTER ADDSLOTSCLUSTER DELSLOTSCLUSTER SETSLOT

分配插槽意味着:告诉指定的主节点,负责为指定的哈希插槽提供内容存储和查询。

分配哈希槽后,使用gossip协议在群集中传播。

ADDSLOTS从头创建新集群时,此命令将16384个哈希槽分配给各个主节点。

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

SETSLOT <slot> NODE用于将插槽分配给指定的节点 。也可在两种特殊状态(MIGRATING和IMPORTING)间设置插槽。这两种状态为了将哈希槽从一个节点迁移到另一个节点。

将插槽设置为MIGRATING时,节点将接受与该哈希插槽有关的所有查询,当键不存在时,将使用-ASK重定向到作为迁移目标的节点。

将插槽设置为IMPORTING,节点将接受与该哈希插槽有关的所有查询,前提是请求之前带有ASKING命令。

如果客户端未提供ASKING命令,则通过-MOVED重定向到实际的哈希槽所有者。

假设有两个redis主节点,分别为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”中创建新键。

同时,在reshardings和redis集群配置期间,使用的一个名为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将连接到目标实例,发送键的序列化版本,一旦接收到OK代码,就会删除自己数据集中的旧键。从客户端的角度看,键始终存在于a或B中。

迁移完成时,将如下命令发送给迁移所涉及到的两个节点,以便将插槽设置为正常状态。

SETSLOT <slot> NODE <node-id>

通常将相同的命令发送给所有其他节点,以避免集群自然传播造成的等待。

ASK redirection

为什么不能简单地使用MOVED重定向?

MOVED意味着哈希槽是由另一个节点提供的,查询应针对指定的节点,ASK意味着下一次查询应发送到指定的节点。

之所以这样做,是因为下一个哈希槽8的查询可能仍在A中,因此,希望客户端尝试A,在需要时再尝试B。

强制客户端行为,可确保客户端是先尝试A无果后才尝试节点B,若客户端在发送查询前发送ASKING命令,则节点B仅接受,针对设置成IMPORTING插槽的查询。

ASKING会在客户端上设置一个一次性标志,强制节点为IMPORTING插槽的查询提供服务。

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

若接收到ASK重定向,则只发送重定向到指定节点的查询,但继续向旧节点发送后续查询。

使用ask命令启动重定向查询,不要更新本地客户端表。

一旦哈希槽8迁移完成,A将发送MOVED消息,客户端可将哈希槽8永久映射到新的IP和端口。

注:若一个有bug的客户端更早地执行映射,是没问题的,因为,它不会在发出查询之前发送请求命令,所以,B将使用MOVED重定向将客户端重定向到A。

从节点读取

通常,从节点会将客户端重定向到主节点,但客户端也可使用READONLY从节点获取数据。

READONLY告诉Redis Cluster从节点,客户端可以接受可能已过时的数据,且对写查询不感兴趣。

当连接处于只读模式时,仅当操作涉及节点不提供的键时,才会向客户端发送重定向。

这是因为:

1、客户端发送了与节点无关的哈希槽。

2、群集已重新配置(如重新分片),且从节点不再为指定的哈希槽提供服务。

发生这种情况时,客户端应更新哈希映射。可使用READWRITE命令清除连接的只读状态。

容错能力

Redis Cluster节点不断交换ping和pong数据包。

两种报文具有相同的结构,且都携带重要的配置信息。唯一的区别是消息类型。ping和pong数据包总和称为心跳数据包。

通常,节点发送ping数据包,触发接收者以pong数据包进行回复。

节点可仅发送pong数据包,向其他节点发送有关其配置的信息,而不会触发答复。这对于尽快广播新配置很有用。

通常,一个节点每秒会随机挑选几个节点执行ping操作,以便使各个节点发送的ping数据包(和接收到的pong数据包)的总数恒定。

每个节点都会对未发送ping的节点,或接收pong响应的时间超过NODE_TIMEOUT一半的节点,执行TCP重连操作,确保不会因为当前TCP连接问题,误认为节点不可达。

若NODE_TIMEOUT设置较小,且节点数(N)非常大,则全局交换的消息数可能会很大。因为,每个节点都会在NODE_TIMEOUT一半时间内尝试ping其他没有新消息的节点。

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

有一些方法可以减少消息的数量,但目前还没有关于redis集群带宽问题的报告。

注:在上面的例子中,每秒交换的330个数据包,被平均分配给100个节点,每个节点接收到的流量是可以接受的。

选举

从节点的选举和升级由从节点处理,主节点则对从节点进行投票。

满足以下条件时,从节点开始选举:

1、主节点处于FAIL状态。

2、主节点服务的插槽数量不为零。

3、主从节点断开连接的时间未超过指定时间,确保升级后的从节点数据是合理的,时间由用户配置。

为了选举,从节点首先增加currentEpoch计数器,并向主节点请求投票。

从节点将FAILOVER_AUTH_REQUEST数据包,广播到群集各个主节点请求投票。最多等待NODE_TIMEOUT*2的时间(至少2秒钟)。

主节点使用FAILOVER_AUTH_ACK给从节点投票,在NODE_TIMEOUT * 2的时间内,同一个主节点不能再给另一个从节点投票。在此期间,也无法回复同一主节点的其他授权请求。

从节点会丢弃小于currentEpoch的投票(AUTH_ACK),确保不将之前选举的票数计入。

一旦从节点收到半数以上主节点的ACK,便赢得选举。

若在NODE_TIMEOUT*2时间内未超过半数,则该选举将终止,且新的选举将在NODE_TIMEOUT * 4后再次尝试。

从节点会在选举前等待一段时间,延迟计算如下:

DELAY = 500 milliseconds + random delay between 0 and 500 milliseconds +SLAVE_RANK * 1000 milliseconds.

固定的延迟确保FAIL状态在整个群集中传播,否则,从节点可能会在主节点不知道FAIL状态的情况下进行选举,从而拒绝投票。

随机延迟用于使从节点不同步,因此,从节点们不太可能同时开始选举。

SLAVE_RANK是从节点的秩,当主节点失败时,从节点交换消息以建立一个级别:复制偏移量最新最快的从节点在第0级,第2个在第1级,以此类推。以此方式,最新的从节点试图在其节点前被选上。

等级次序并不严格,若一个等级高的从节点没有被选上,其他的从节点很快就会尝试。一旦一个从节点赢得了选举,将获得一个新的且唯一的增量configEpoch。该增量高于现有其他主节点的增量,并在ping和pong数据包中以主节点的身份进行宣传。

为了加快其他节点的重新配置,将pong数据包广播给群集所有的节点。

无法到达的节点,将从另一个节点接收到ping或pong数据包,或者检测到由心跳包发布的消息过期,则将从另一个节点接收更新包。

其他节点检测到有一个新主节点提供与旧主节点相同的插槽,但具有更大的configEpoch,将会升级它们的配置,重新配置并从新主节点复制数据。

投票要求

收来自从节点的FAILOVER_AUTH_REQUEST投票请求。

要获得投票,须满足以下条件:

1、一个master只对指定的epoch进行一次投票,每个master都有一个lastVoteEpoch字段。

master会拒绝对epoch小于lastVoteEpoch的请求进行投票,且请求包中的currentEpoch需大于lastVoteEpoch。

当主节点对投票请求做出肯定的响应时,lastVoteEpoch将会更新,并存储在磁盘上。

2、只有主节点被标记为FAIL时,其他主节点才会投票给从节点。

3、Auth请求中的currentEpoch,若小于主节点currentEpoch,请求将被忽略。

若相同的从节点再次要求投票,增加currentEpoch,保证一个从主节点返回的过期答复,不会被当作新投票的结果。

注:投票机制可参见一致性算法Paxos