Redis集群

Published: 07 Jan 2019 Category: redis

一、Redis集群的目标

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

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

二、集群规范

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

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

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

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

2.2 安全写入

Redis 集群节点间使用异步冗余备份(asynchronous replication),所以在分区过程中总是存在一些时间段(windows?),在这些时间段里容易丢失写入数据。

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

1) 一个写入操作能到达一个主节点,但当主节点要回复客户端的时候,这个写入有可能没有通过主从节点间的异步冗余备份传播到从节点那里。 如果在某个写入操作没有到达从节点的时候主节点已经宕机了,那么该写入会永远地丢失掉,以防主节点长时间不可达而它的一个从节点已经被提升为主节点。

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

  • 因为分区使一个主节点变得不可达。
  • 故障转移(fail over)到主节点的一个从节点。(即从节点被提升为主节点)
  • 过一段时间之后主节点再次变得可达。
  • 一个没有更新路由表(routing table)的客户端或许会在集群把这个主节点变成一个从节点(新主节点的从节点)之前对它进行写入操作。

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

2.3 可用性

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

2.4 集群拓扑结构

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

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

节点握手

  • 当一个节点使用 MEET 消息介绍自己。一个 meet 消息跟一个 PING 消息完全一样,但它会强制让接收者接受发送者为集群中的一部分。 只有在系统管理员使用以下命令要求的时候,节点才会发送 MEET 消息给其他节点:
  • 一个已被信任的节点能通过传播gossip消息让另一个节点被注册为集群中的一部分。也就是说,如果 A 知道 B,B 知道 C,那么 B 会向 A 发送 C 的gossip消息。A 收到后就会把 C 当作是网络中的一部分,并且尝试连接 C。 这意味着,只要我们往任何连接图中加入节点,它们最终会自动形成一个完全连接图。从根本上来说,这表示集群能自动发现其他节点,但前提是有一个由系统管理员强制创建的信任关系。 这个机制能防止不同的 Redis 集群因为 IP 地址变更或者其他网络事件而意外混合起来,从而使集群更具健壮性。 当节点的网络连接断掉时,它会积极尝试连接所有其他已知节点。

2.5 MOVED 重定向

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

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

一个 MOVED 错误如下:

GET x
-MOVED 3999 127.0.0.1:6381

这个错误包括键(3999)的哈希槽和能处理这个查询的节点的 ip:端口号(127.0.0.1:6381)。客户端需要重新发送查询到给定 ip 地址和端口号的节点。

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

2.6 失效检测(Failure detection)

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

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

PFAIL 标识:

当一个节点在超过 NODE_TIMEOUT 时间后仍无法访问某个节点,那么它会用 PFAIL 来标识这个不可达的节点。无论节点类型是什么,主节点和从节点都能标识其他的节点为 PFAIL。

Redis 集群节点的不可达性(non reachability)是指,发送给某个节点的一个活跃的 ping 包(active ping)(一个我们发送后要等待其回复的 ping 包)已经等待了超过 NODE_TIMEOUT 时间,那么我们认为这个节点具有不可达性。为了让这个机制能正常工作,NODE_TIMEOUT 必须比网络往返时间(network round trip time)大。节点为了在普通操作中增加可达性,当在经过一半 NODE_TIMEOUT 时间还没收到目标节点对于 ping 包的回复的时候,就会马上尝试重连接该节点。这个机制能保证连接都保持有效,所以节点间的失效连接通常都不会导致错误的失效报告。

FAIL 标识:

单独一个 PFAIL 标识只是每个节点的一些关于其他节点的本地信息,它不是为了起作用而使用的,也不足够触发从节点的提升。要让一个节点真正被认为失效了,那需要让 PFAIL 状态上升为 FAIL 状态。 在本文的节点心跳章节有提到的,每个节点向其他每个节点发送的 gossip 消息中有包含一些随机的已知节点的状态。最终每个节点都能收到一份其他每个节点的节点标识。使用这种方法,每个节点都有一套机制去标记他们检查到的关于其他节点的失效状态。

当下面的条件满足的时候,会使用这个机制来让 PFAIL 状态升级为 FAIL 状态:

  • 某个节点,我们称为节点 A,标记另一个节点 B 为 PFAIL。
  • 节点 A 通过 gossip 字段收集到集群中大部分主节点标识的节点 B 的状态信息。
  • 大部分主节点标记节点 B 为 PFAIL 状态,或者在 NODE_TIMEOUT * FAIL_REPORT_VALIDITY_MULT 这个时间内是处于 PFAIL 状态。 如果以上所有条件都满足了,那么节点 A 会:

  • 标记节点 B 为 FAIL。
  • 向所有可达节点发送一个 FAIL 消息。

FAIL 消息会强制每个接收到这消息的节点把节点 B 标记为 FAIL 状态。

注意,FAIL 标识基本都是单向的,也就是说,一个节点能从 PFAIL 状态升级到 FAIL 状态,但要清除 FAIL 标识只有以下两种可能方法:

  • 节点已经恢复可达的,并且它是一个从节点。在这种情况下,FAIL 标识可以清除掉,因为从节点并没有被故障转移。
  • 节点已经恢复可达的,而且它是一个主节点,但经过了很长时间(N * NODE_TIMEOUT)后也没有检测到任何从节点被提升了。

PFAIL -> FAIL 的转变使用一种弱协议(agreement)

1) 节点是在一段时间内收集其他节点的信息,所以即使大多数主节点要去”同意”标记某节点为 FAIL,实际上这只是表明说我们在不同时间里从不同节点收集了信息,得出当前的状态不一定是稳定的结论。

2) 当每个节点检测到 FAIL 节点的时候会强迫集群里的其他节点把各自对该节点的记录更新为 FAIL,但没有一种方式能保证这个消息能到达所有节点。比如有个节点可能检测到了 FAIL 的节点,但是因为分区,这个节点无法到达其他任何一个节点。

然而 Redis 集群的失效检测有一个要求:最终所有节点都应该同意给定节点的状态是 FAIL,哪怕它处于分区。有两种情况是来源于脑裂情况,或者是小部分节点相信该节点处于 FAIL 状态,或者是相信节点不处于 FAIL 状态。在这两种情况中,最后集群都会认为给定的节点只有一个状态:

第 1 种情况:如果大多数节点都标记了某个节点为 FAIL,由于链条反应,这个主节点最终会被标记为 FAIL。

第 2 种情况: 当只有小部分的主节点标记某个节点为 FAIL 的时候,从节点的提升并不会发生(它是使用一个更正式的算法来保证每个节点最终都会知道节点的提升。),并且每个节点都会根据上面的清除规则(在经过了一段时间 > N * NODE_TIMEOUT 后仍没有从节点提升操作)来清除 FAIL 状态。

本质上来说,FAIL 标识只是用来触发从节点提升(slave promotion)算法的安全部分。理论上一个从节点会在它的主节点不可达的时候独立起作用并且启动从节点提升程序,然后等待主节点来拒绝认可该提升(如果主节点对大部分节点恢复连接)。PFAIL -> FAIL 的状态变化、弱协议、强制在集群的可达部分用最短的时间传播状态变更的 FAIL 消息,这些东西增加的复杂性有实际的好处。由于这种机制,如果集群处于错误状态的时候,所有节点都会在同一时间停止接收写入操作,这从使用 Redis 集群的应用的角度来看是个很好的特性。还有非必要的选举,是从节点在无法访问主节点的时候发起的,若该主节点能被其他大多数主节点访问的话,这个选举会被拒绝掉。

2.7 集群阶段(Cluster epoch)

Redis 集群使用一个类似于木筏算法(Raft algorithm)”术语”的概念。在 Redis 集群中这个术语叫做 阶段(epoch),它是用来记录事件的版本号,所以当有多个节点提供了冲突的信息的时候,另外的节点就可以通过这个状态来了解哪个是最新的。 currentEpoch 是一个 64bit 的 unsigned 数。

Redis 集群中的每个节点,包括主节点和从节点,都在创建的时候设置了 currentEpoch 为0。

当节点接收到来自其他节点的 ping 包或 pong 包的时候,如果发送者的 epoch(集群连接消息头部的一部分)大于该节点的 epoch,那么更新发送者的 epoch 为 currentEpoch。

由于这个语义,最终所有节点都会支持集群中较大的 epoch。

这个信息在此处是用于,当一个节点的状态发生改变的时候为了执行一些动作寻求其他节点的同意(agreement)。

目前这个只发生在从节点的提升过程,这个将在下一节中详述。本质上说,epoch 是一个集群里的逻辑时钟,并决定一个给定的消息赢了另一个带着更小 epoch 的消息。

2.8 服务器哈希槽信息的传播规则

Redis 集群很重要的一个部分是用来传播关于集群节点负责哪些哈希槽的信息的机制。这对于新集群的启动和提升从节点来负责处理哈希槽(它那失效的主节点本该处理的槽)的能力来说是必不可少的。

个体持续交流使用的 ping 包和 pong 包都包含着一个头部,这个头部是给发送者使用的,为了向别的节点宣传它负责的哈希槽。这是主要用来传播变更的机制,不过集群管理员手动进行重新配置是例外(比如为了在主节点间移动哈希槽,通过 redis-trib 来进行手动碎片整理)。

当一个新的 Redis 集群节点创建的时候,它的本地哈希槽表(表示给定哈希槽和给定节点 ID 的映射关系表)被初始化,每个哈希槽被置为 nil,也就是,每个哈希槽都是没赋值的。

一个节点要更新它的哈希槽表所要遵守的第一个规则如下:

规则 1(初始化):如果一个哈希槽是没有赋值的,然后有个已知节点认领它,那么我就会修改我的哈希槽表,把这个哈希槽和这个节点关联起来。

由于这个规则,当一个新集群被创建的时候,只需要手动给哈希槽赋值上(通常是通过 redis-trib 命令行工具使用 CLUSTER 命令来实现)负责它的主节点,然后这些信息就会迅速在集群中传播开来。

然而,当一个配置更新的发生是因为一个从节点在其主节点失效后被提升为主节点的时候,这个规则显然还不足够。新的主节点会宣传之前它做从节点的时候负责的哈希槽,但从其他节点看来这些哈希槽并没有被重新赋值,所以如果它们只遵守第一个规则的话就不会升级配置信息。

由于这个原因就有第二个规则,是用来把一个已赋值给以前节点的哈希槽重新绑定到一个新的认领它的节点上。规则如下:

规则 2(更新):如果一个哈希槽已经被赋值了,有个节点它的 configEpoch 比哈希槽当前拥有者的值更大,并且该节点宣称正在负责该哈希槽,那么我们会把这个哈希槽重新绑定到这个新节点上。

因为有这第二个规则,所以集群中的所有节点最终都会同意哈希槽的拥有者是所有声称拥有它的节点中 configEpoch 值最大的那个。

当一个节点检测到其他节点在宣传它的哈希槽的时候是用一份过时的配置信息,那么它就会向这个节点发送一个 UPDATE 消息,这个消息包含新节点的 ID 和它负责的哈希槽(以 bitmap 形式发送)。

注意:目前更新配置信息可以用 ping 包/ pong 包,也可以用 UPDATE 消息,这两种方法是共享同一个代码路径(code path)。

2.9 备份迁移算法

迁移算法不用任何形式的协议,因为 Redis 集群中的从节点布局不是集群配置信息(配置信息要求前后一致并且/或者用 config epochs 来标记版本号)的一部分。 它使用的是一个避免在主节点没有备份时从节点大批迁移的算法。这个算法保证,一旦集群配置信息稳定下来,最终每个主节点都至少会有一个从节点作为备份。

假设集群有三个主节点 A,B,C。节点 A 和 B 都各有一个从节点,A1 和 B1。节点 C 有两个从节点:C1 和 C2。

备份迁移是从节点自动重构的过程,为了迁移到一个没有可工作从节点的主节点上。在上面提到的例子中,备份迁移过程如下:

  • 主节点 A 失效。A1 被提升为主节点。
  • 节点 C2 迁移成为节点 A1 的从节点,要不然 A1 就没有任何从节点。
  • 三个小时后节点 A1 也失效了。
  • 节点 C2 被提升为取代 A1 的新主节点。

集群仍然能继续正常工作。

REF

http://www.redis.cn/topics/cluster-spec.html
http://www.redis.cn/topics/cluster-tutorial.html