心跳数据包内容
Ping 和 Pong 数据包都包含着一个头部(header),这在这类数据包(比如请求投票的数据包)中是很常见的。一个特殊的 报文片段就是 Ping 包和 Pong 包里一个特殊部分。
常见头部会包含以下这些信息:
- 节点 ID,在节点第一次创建的时候赋值的一个 160 bit 的伪随机字符串,在Redis 集群节点永远都保持不变。
- currentEpoch和 configEpoch 两个字段,用来挂载 Redis 集群使用的分布式算法(这会在下一节中详细解释)。如果节点是slave,configEpoch是上一个已知master的configEpoch。
- 节点标识,标识一个节点是slave/slave,还有其他只占用一个 bit 的节点信息。
- 给定节点负责的哈希槽的位图(bitmap),如果该节点是slave,就是一个其master节点负责的哈希槽位图。
- 发送端的 TCP 基本端口号(也就是,这个端口号是 Redis 用来接收客户端命令的,加上 10000 就是集群总线端口)。
- 从发送者的角度看来的集群状态(down还是ok)。
- 如果这是个slave节点,那么会有master节点 ID。
ping 包和 pong 包都包含着一个 gossip 字段。这个字段是用来让接收者知道发送者是怎么看待集群中的其他节点。gossip 报文片段只包含在发送者已知节点集合里随机取的一些节点的信息。
提到的gossip报文片段的节点数据与集群大小成比例。
每个添加到gossip 报文片段的节点有如下几个字段
- 节点 ID。
- 节点的 IP 和端口号。
- 节点标识。
从发送者的角度看来,gossip 报文片段是让接收的节点能获得其他节点的状态信息。这对于失效检测或者发现集群中的其他节点都是非常有用的。
失效检测
Redis 集群失效检测是用来识别出大多数节点何时无法访问某一个master节点或slave节点。然后,就提升级一个slave成master。若如果无法升级slave成master,那么整个集群就置为错误状态并停止接收客户端的请求。
正如已经提到的,每个节点都有一份跟其他已知节点相关的标识列表。其中有两个标识是用于失效检测,分别是 PFAIL 和FAIL。PFAIL 表示可能失效(Possible failure),这是一个非承认失效类型。FAIL 表示一个节点已经失效,而且这个情况已经被大多数master节点在固定时间内确认过。
PFAIL 标识:
当一个节点在超过 NODE_TIMEOUT 时间后仍无法访问另一个节点,那么它会用 PFAIL 来标识另一个节点。master节点和slave节点都能标识其他的节点为 PFAIL,不论节点类型。
Redis 集群节点的不可达性是指,发送给某个节点的一个活跃的 ping 包 (一个我们发送后要等待其回复)已经等待了超过 NODE_TIMEOUT 时间迟迟不回复。为了让这个机制能正常工作,NODE_TIMEOUT 必须比网络往返时间大。节点为了在普通操作中增加可靠性,当在经过一半 NODE_TIMEOUT时间还没收到目标节点对于 ping 包的回复,就会马上尝试重连接该节点。这个机制能保证连接都保持有效,所以节点间的失效连接通常都不会导致错误的失效报告。
FAIL 标识:
单独一个 PFAIL 标识仅仅是每个节点的一些关于其他节点的本地信息,但是不足够触发slave节点的升级。要让一个节点真正被认为down需要让 PFAIL 状态上升为 FAIL 状态。
在本文的节点心跳章节有提到的,每个节点向其他每个节点发送的 gossip 消息中有包含一些随机的已知节点的状态。最终每个节点都能收到一份其他每个节点的节点标识。通过这种方法,每个节点都有一套机制去标记检测到的其他节点的失效状态。
当下面的条件满足的时候,会使用这个机制来让 PFAIL 状态升级为 FAIL 状态:
- 某个节点,我们称为节点 A,标记另一个节点 B 为PFAIL。
- 节点 A 通过 gossip 报文片段收集到集群中大部分master节点标识的节点 B 的状态信息。
- 大部分master节点标记节点 B 为PFAIL 状态,或者在 NODE_TIMEOUT * FAIL_REPORT_VALIDITY_MULT 这个时间内是处于 PFAIL 状态(在当前的代码,有效因子FAIL_REPORT_VALIDITY_MULT值为2,所以这个时间是2倍的NODE_TIMEOUT时间)。
如果以上所有条件都满足了,那么节点 A 会:
- 标记节点 B 为FAIL。
- 向所有可达节点发送一个FAIL 消息。
FAIL 消息会强制每个接收到这消息的节点把节点 B 标记为 FAIL 状态,不论是否标记过节点PFAIL。
注意,FAIL 标识基本都是单向的,也就是说,一个节点能从 PFAIL 状态升级到 FAIL 状态,但要清除FAIL 标识只有以下两种可能方法:
- 节点已经恢复可达的,并且它是一个slave节点。在这种情况下,FAIL标识可以清除掉,当slave节点并没有被故障转移。
- 节点已经恢复可达的,并且它是不负责任何哈希槽,在这种情况下FAIL可以清除,因为master没有任何哈希槽处在集群中,就等待配置加入到集群中.
- 节点已经恢复可达的,而且它是一个master节点,但经过了很长时间(N *NODE_TIMEOUT)后也没有检测到任何slave节点被提升了,最好重新加入到集群,并继续这个方法。
值得注意,PFAIL -> FAIL 的转变使用了一种弱协议:
1) 节点是在一段时间内收集其他节点的信息,所以即使大多数master节点要去”同意”标记某节点为 FAIL,实际上这只是表明说我们在不同时间里从不同节点收集了信息,并且我们不确定或不需要,给定的时刻大多数master同意,然后我们舍弃旧的失效报告,所以失效的通知是大多数master在时间窗口内的。
2) 当每个节点检测到 FAIL 节点的时候会强迫集群里的其他节点把各自对该节点的记录更新为 FAIL,但没有一种方式能保证这个消息能到达所有节点。比如有个节点可能检测到了 FAIL 的节点,但是因为网络分区,这个节点无法到达其他任何一个节点。
然而 Redis 集群的失效检测有一个实时要求:最终所有节点都应该同意给定节点的状态。有两种情况是来源于脑裂情况,或者是小部分节点相信该节点处于 FAIL 状态,或者少数节点相信节点不处于 FAIL 状态。在这两种情况中,最后集群都会给节点一个状态:
第 1 种情况: 如果大多数节点都标记了某个节点为 FAIL,由于失效检测和产生的链条反应,这个master节点最终会被标记为FAIL,因为在指定时间窗口内失效将被报告。
第 2 种情况: 当只有小部分的master节点标记某个节点为 FAIL 的时候,slave节点的提升并不会发生(它是使用一个更正式的算法来保证每个节点最终都会知道节点的提升)并且每个节点都会根据上面的清除规则来清除 FAIL 状态(即:在经过了一段时间 > N * NODE_TIMEOUT 后仍没有slave节点升级操作)。
FAIL 标识只是用来触发slave节点升级算法的安全部分。理论上一个slave节点会在它的master节点不可达的时候独立起作用并且启动slave节点提升程序,如果master节点对大部分节点恢复连接,然后等待master节点来拒绝认可该提升。然后增加复杂性的PFAIL -> FAIL 状态变化、弱协议、强制在集群的可达部分用最短的时间传播状态变更的 FAIL 消息,这些东西增加的复杂性有实际的好处。由于这种机制,如果集群处于错误状态的时候,所有节点都会在同一时间停止接收写入操作,这从使用 Redis 集群的应用的角度来看是个很好的特性。还错误的选举,是slave节点本地原因无法访问master节点(该master节点能被其他大多数master节点访问的话),这个选举会被拒绝掉。
配置处理,传播,和失效转移
集群当前epoch
Redis 集群使用一个类似于木筏算法”术语”的概念。在 Redis 集群中这个术语叫做 epoch,它是用来记录事件的递增版本号,所以当有多个节点提供了冲突的信息的时候,另外的节点就可以通过这个状态来了解哪个是最新的。
currentEpoch 是一个 64bit 的 unsigned 数。
Redis 集群中的每个节点,包括master节点和slave节点,都在创建的时候设置了 currentEpoch 为0。
每次当节点接收到来自其他节点的 ping 包或 pong 包的时候,如果发送者的 epoch(集群总线消息头部的一部分)大于该节点的 epoch,那么更新currentEpoch成发送者的 epoch 。
由于这个语义,最终所有节点都会支持集群中最大的 epoch。
这个信息在此处是用于,当一个节点的状态发生改变的时候为了执行一些动作寻求其他节点的同意(agreement)。
目前这个只发生在slave节点的升级期间,随后在下一节中详述。本质上说,epoch 是一个集群里的逻辑时钟,并决定一个给定的消息覆盖另一个带着更小 epoch 的消息。
epoch配置
每一个master节点总是通过发送 ping 包和 pong 包向别人宣传它的 configEpoch 和一份表示它负责的哈希槽的位图。
当一个新节点被创建的时候,master节点中的 configEpoch 设为零。
slave升级的时候创建一个新的configEpoch. slave试图取代失败的主人增加他们的epoch,并尝试从大多数主人获得授权。当slave被授权,新的唯一configEpoch创建并且alave变成master使用新的configEpoch。
将在下一节解释,configEpoch 用于在不同节点提出不同的配置信息的时候解决冲突(这种情况或许会在网络分区和节点失效)。
slave节点也会在 ping 包和 pong 包中向别人宣传它的 configEpoch 字段,不过slave节点的这字段表示的是上一次跟它的master节点交换数据的时候master节点的 configEpoch 值。这能让其他节点检测出slave节点的配置信息是不是需要更新了(master节点不会给一个配置信息过期的slave节点投票)。
所有节点收到信息,每次由于一些已知节点的值比自己的值大而更新 configEpoch 值,都会永久性地存储在 nodes.conf 文件中。currentEpoch也一样。这两人个变量更新时,节点在继续动作前,保证同步保存到磁盘
configEpoch值使用简单算法, 在失效转移生成保证是最新/自增/唯一。
slave节点的选举和升级
slave节点的选举和提升都是由slave节点处理的,master节点会投票要提升哪个slave节点。一个slave节点的选举发生条件:master处理FAIL状态(至少一个slave有条件成为master)。
要让一个slave升级成master,需要发起一次选举并取胜。当master处于FAIL状态,所有给定master的slave都能发起一次选举。然后只有一个slave能赢得选举升级成为master。一个slave发起选举,并要满足如下条件:
- 该slave节点的master节点处于FAIL 状态。
- 这个master节点负责的哈希槽数目不为零。
- slave节点和master节点之间的连接断线不超过一段给定的时间,这是为了确保slave节点的数据是可靠的。
一个slave节点想要被推选出来,那么第一步应该是提高它的 currentEpoch 计数,并且向master节点们请求投票。
slave节点通过广播一个 FAILOVER_AUTH_REQUEST 数据包给集群里的每个master节点来请求选票。然后等待回复,最多等 NODE_TIMEOUT 这么长时间(通常至少2秒)。
一旦一个master节点给这个slave节点投票,会回复一个FAILOVER_AUTH_ACK,并且在 NODE_TIMEOUT * 2 这段时间内不能再给同个master节点的其他slave节点投票。在这段时间内它完全不能回复其他授权请求。这不必保证安全,但用于同时在一轮防止多slave选举(使用不同的configEpoch),多选举是不需要的。
slave节点会忽视所有带有的epoch参数比 currentEpoch 小的回应(ACKs),这样能避免把之前的投票的算为当前的合理投票。
一旦某个slave节点收到了大多数master节点的回应,那么它就赢得了选举。否则,如果无法在 NODE_TIMEOUT 时间内访问到大多数master节点(通常至少2s),那么当前选举会被放弃并在 NODE_TIMEOUT * 4 这段时间后由另一个slave节点尝试发起选举(通常至少4s)。
slave排名
slave是在master节点一进入 FAIL 状态,就等一小段时间尝试发起选举,这段延迟是这么计算的:
DELAY = 500 milliseconds + 0-500随机数 milliseconds +
SLAVE_RANK * 1000 milliseconds.
固定延时确保我们会等到 FAIL 状态在集群内广播后,否则若slave节点尝试发起选举,master节点们仍然不知道那个master节点已经 FAIL,就会拒绝投票。
随机延迟用于破坏同步slave,所以他们不太可能在同一时间开始选举。
该SLAVE_RANK是slave从master复制过来的数据处理数量的排名。slave交换消息时,当master失败,以(尽力而为)建立一个排名:最新的复制数据偏移量slave排名0,第二最新排名1 ,依此类推。如果靠前排名的slave选举失败,其它很快会接着做。
排名顺序没有严格执行;如果排名较高的奴隶落选,其他人会尝试很快。
一旦有slave节点赢得选举,它就会有一个比其它存在的master更大的 唯一自增的configEpoch 。 它开始通过ping 和 pong 数据包向其他节点宣布自己已经是master节点,并提供它负责的哈希槽覆盖老的,这个数据包带有configEpoch。
为了加速其他节点的重新配置,该节点会广播一个 pong 包 给集群里的所有节点. 那些现在访问不到的节点,如果它们通过心跳包发布的信息已经过期,最终也会收到一个 ping 包或 pong 包,并且进行重新配置。
其他节点会检测到有一个更大configEpoch的新master节点在负责处理之前一个旧的master节点负责的哈希槽,然后就升级自己的配置信息。 旧master节点的slave节点(或者是经过故障转移后重新加入集群的该旧master节点)不仅会升级配置信息,还会配置新master节点的备份。节点怎么重新加入集群的配置会在下一节解释。
master节点回复slave节点的投票请求
在上一节中我们讨论了slave节点是如何被选举上的,这一节我们将从master节点的角度解释在为给定slave节点投票的时候发生了什么。
master节点接收到来自于slave节点、要求以 FAILOVER_AUTH_REQUEST 请求的格式投票的请求。
要授予一个投票,必须要满足以下条件:
- 1)一个master节点只能投一次票给指定的epoch,并且拒绝更早的epoch:每个master节点都有一个 lastVoteEpoch 域,一旦认证请求数据包里的currentEpoch小于 lastVoteEpoch,那么master节点就会拒绝再次投票。当一个master节点积极响应一个投票请求,那么 lastVoteEpoch 会相应地进行更新,同时安全地存储到磁盘。
- 2) 一个master节点投票给某个slave节点只有当该slave节点的master节点被标记为FAIL。
- 3) 如果认证请求里的currentEpoch 小于master节点里的 currentEpoch 的话,那么该请求会被忽视掉。因此,master节点的回应总是带着和认证请求一致的 currentEpoch。如果同一个slave节点在增加currentEpoch 后再次请求投票,那么保证一个来自于master节点的、旧的延迟回复不会被新一轮选举接受。
下面的例子是没有依据规则3引发的问题:
master节点的 currentEpoch 是 5, lastVoteEpoch 是 1(在几次失败的选举后这也许会发生的)
- slave节点的currentEpoch 是 3。
- slave节点尝试用 epoch 值为 4(3+1)来赢得选票,master节点回复 ok,里面的currentEpoch 是 5,可是这个回复延迟了。
- slave节点尝试用 epoch 值为 5(4+1)来再次赢得选票,收到的是带着currentEpoch 值为 5 的延迟回复,这个回复会被当作有效的来接收。
- master节点若已经为某个失效master节点的一个slave节点投票后,在经过NODE_TIMEOUT * 2 时间之前不会为同个失效master节点的另一个slave节点投票。这并不是严格要求的,因为两个slave节点用同个 epoch 来赢得选举的可能性很低,不过在实际中,系统确保正常情况当一个slave节点被选举上,那么它有足够的时间来通知其他slave节点,以避免另一个slave节点发起另一个新的选举。
- master节点不会用任何方式来尝试选出最好的slave节点,只要slave节点的master节点处于FAIL 状态并且投票master节点在这一轮中还没投票,master节点就能积极投票。
最佳的slave最可能在其它slave前发起一次选举并赢得它 ,因为它通常能更早发起选举过程,因为它更高排名(前面章节提到的slave排名)。
- 当master拒绝投票给没有积极回应的slave,请求简单的被忽略.
- master节点不会投票给那些configEpoch 值比master节点哈希槽表里的 configEpoch 更小的slave节点。记住,slave节点发送了它的master节点的 configEpoch 值,还有它的master节点负责的哈希槽对应的位图。这意味着,请求投票的slave节点必须拥有它想要进行故障转移的哈希槽的配置信息,而且信息应该比它请求投票的master节点的配置信息更新或者一致。
在网络分区下epoch配置的可用性实例
这一节解释如何使用 epoch 概念来使得slave节点提升过程对网络分区更有抵抗力。
- master节点不是无限期地可达。它拥有三个slave节点 A,B,C。
- slave节点 A 赢得了选举并且被推选为master节点。
- 一个网络分区操作使得集群中的大多数节点无法访问节点 A。
- 节点 B 赢得了选举并且被推选为master节点。
- 一个网络分区操作使得集群中大多数节点无法访问节点 B。
- 之前分区操作的问题被修复了,节点 A 又恢复可访问状态。
此刻,节点 B 已经down,节点 A 当上master又可访问了(事实上UPDATE消息可以快速重新配置它,介此处我们假设所有UPDATE消息已经丢失),同时slave C 竞选对节点 B 进行故障转移。
这两个有同样的哈希槽的slave节点最终都会请求被提升,然而由于它们发布的 configEpoch 是不一样的,而且节点 C 的 epoch 比较大,所以所有的节点都会把它们的配置更新为节点 C 的。
过程如下。
1, B就尽量当选而且一定会成功,因为对于大多数master它的master已经down。它会得到一个自增的configEpoch 。
- A将无法声称自己是master负责它的哈希槽,因为相比A,其他的节点已经具有更高的epoch,关联了相同的哈希插槽
3。因此,所有的节点都将升级他们的哈希槽分配给C,集群将继续运作。
正如你下面章节看到的,一个失联的节点重新加入集群将尽快通知到配置变更,因为一量它ping任何其它节点,接收者会检测它已经失效的上并发送UPDATE消息.
哈希槽信息的传播
Redis 集群很重要的一个部分是用来传播关于集群节点负责哪些哈希槽的信息的机制。这对于新集群的启动和提升配置(slave升级前master处理的槽)的能力来说是必不可少的。
同样的机制让节点划分,因无限长的时间通过合理的方式来加入集群。
哈希槽配置传播有两种方法:
1.心跳消息。 ping或pong包的发送者总是带有负责的哈希槽信息(如果是slave,就是对应master的哈希槽信息)。
2.UPDATE消息。因为每一次心跳数据包中有一个关于发送者configEpoch信息和负责的哈希槽,如果心跳包的接收方发现发送者信息是过时的,它会发送新信息的数据包,迫使过时的节点更新其信息。
心跳或UPDATE消息的接收方使用某些简单的规则,以更新其映射的哈希插槽节点。当创建一个新的Redis集群,其本地哈希槽映射表只是初始化为NULL的条目,使每个哈希位置没有绑定或链接到任何节点。这看起来类似于以下内容:
0 -> NULL
1 -> NULL
2 -> NULL
...
16383 -> NULL
第一个规则允许节点来更新它的哈希槽映射表如下:
规则一:如果一个哈希槽没有赋值,(设置为NULL),并且一个已经节点申明它,我们修改哈希槽映射表并关联申明的哈希槽,因此如果我们收到节点A的心跳包申明负责槽1和2,配置epoch是3,映射表修改如下:
0 -> NULL
1 -> A [3]
2 -> A [3]
...
16383 -> NULL
当创建一个新的集群,一个系统管理员需要手动分配(使用CLUSTER ADDSLOTS命令,通过redis – trib命令行工具,或通过任何其他方式)各主节点对节点本身负责的槽,这些信息将迅速在群集中传播。
但是这条规则是不够的。我们知道,哈希槽映射可以因两个事件改变:
- 一个slave在故障转移中替换它的master
- 一个槽从一个节点重新分片到不同节点
到此为止,我们专注在故障转移。当一个slave故障转移掉它的master, 它获得被保证是大于它的主人的配置epoch(更通常大于任何其他先前生成的配置epoch)。例如节点B,是一个A的slave,可以用配置epoch4故障转移B,它将开始发送心跳数据包(第一次大规模广播集群范围内),也因为以下第二个规则,接收者将更新他们的哈希槽映射表:规则2: 如果一个哈希槽已经分配,和已知的节点首播它使用的configEpoch大于目前master关联槽的configEpoch,我会重新绑定哈希槽到新节点。所以从B接收消息,用配置epoch申明负责槽1和2,接收者将用如下方式更新它们的映射表:
0 -> NULL
1 -> B [4]
2 -> B [4]
...
16383 -> NULL
实时属性:因为第二个规则,最终所有集群中的节点同意槽的归属节点在节点首播时拥有最大configEpoch
这个原理在redis集群中叫做 最后故障转移取胜
这同样发生在重新分片。当一个节点导入哈希槽完成导入操作,它的配置epoch会增加来保证这个改变会在集群中传播。
细看UPDATE 消息
带着上一节,是比较容易看到更新消息是如何工作的。节点A可能一段时间后重新加入集群。它用配置epoch 3申明负责哈希槽1和2,所有的更新信息接收者将转而看到相同的哈希插槽关联着具有较高配置epoch的节点B,将发送心跳包。正因为如此,他们会发送带有最新哈希槽配置的UPDATE信息到A。 A将更新其配置,因为上面的规则2。
节点怎么重新加入集群
当一个节点重新加入的集群,应用了相同的原理。继续上面的例子,节点A将通知哈希槽1和2现在由B负责.假设这两个哈希槽以前都由A负责。假设A负责的槽数量只有两人个,A负责的槽数降为0!因此A将重新配置成为新master的slave。
事实上后面的规则比这复杂一点。通常可能A会过很长时间再加入集群,与此同时可能开始由A负责的槽由多个节点负责,比如B负责槽1,C负责槽2.
所以真实的redis集群节点角色切换规则是:master节点会改变配置来变成slave, 它的master是拿走它最后一个槽的master.
在重新配置期间,最终负责的槽数会变成0, 节点会相应的重新配置。注意,在这基本条件下意味着老master会成为它slave失效转移后的slave. 然而通用规则会覆盖所有可能情况。
slave 做完全一样的事:它们重新配置来做slave , 它们的master是拿走老master最后一个槽的master.
备份迁移
Redis 集群实现了一个叫做备份迁移(replica migration)的概念,以提高系统的可用性。在集群中有master-slave的设定,如果主slave节点间的映射关系是固定的,master/slave一定的可用性受限于故障时间,这个时间发生多个单一节点独立故障。
例如在一个集群中,每个master节点都只有一个slave节点,任何一对master-slave的一个出故障的时候集群能让操作继续执行下去,一对同都出故障就不行。然而这样长期会积累很多由硬件或软件问题引起的单一节点独立故障。例如:
- master A只有一个slave A1。
- master A 失效了。A1 升级成新master。
- 三个小时后,A1 因为一个独立事件(跟节点 A 的失效无关)失效了。由于没有其他slave节点可以升级为master节点,因为节点 A 仍然down着,集群没法继续进行正常操作。
如果master-slave的映射关系是固定的,那要让集群对上述情况更具容灾能力 , 唯一方法就是为每个master多加slave。然而这要付出的代价也更昂贵,因为要求 Redis 部署更多的实例、更多的内存等等。
一个候选方案就是在集群中创建不对称性,然后让集群布局时随着时间自动变化。例如,假设集群有三个master A,B,C。节点 A 和 B 都各有一个slave节点,A1 和 B1。节点 C 有两个slave:C1 和 C2。
备份迁移是slave节点自动重新配置的过程,为了迁移到一个没有可工作slave节点的master节点上。按上面提到的方案,备份迁移过程如下:
- master A 失效。A1 升级成master。
- 节点 C2 迁移成为节点 A1 的slave,要不然 A1 就没有任何slave备份。
- 三个小时后节点 A1 也失效了。
- C2 被提升为取代 A1 的新master。
- 集群仍然能继续正常工作。
备份迁移算法
迁移算法不用任何形式的协议,因为 Redis 集群中的slave节点布局不是集群配置信息的一部分,配置信息要求前后一致并且者用 config epochs 来标记版本号。 当一个master没有备份时,它使用一个算法避免slave节点大批迁移。这个算法保证,一旦集群配置信息稳定下来,最终每个master节点都至少会有一个slave节点作为备份。
这个算法是如何工作的。在开始之前我们需要定义清楚在这个上下文中什么才算是一个好的slave节点:一个健康的slave节点是指节点不处于 FAIL 状态。
若检测出有一个master没有健康slave,那么就会触发这个算法的执行。然而在所有检测出这种情况的slave节点中,只有一部分slave节点会采取行动。 通常这部分slave只有一个,除非有不同的slave在给定时刻对其他节点的失效状态有稍微不同的视角。
采取行动的slave节点是属于那些绑定了最多slave的master节点,并且不处于 FAIL 状态及拥有最小的节点 ID。
例如,如果有 10 个master节点,它们各有 1 个slave节点,另外还有 2 个master节点,它们各有 5 个slave节点。会发生迁移的slave是-那 2 个拥有 5 个slave的master中的所有slave里-节点 ID 最小的那个。鉴于没用到任何协议,在集群配置信息不稳定的情况下,有可能发生一种竞争情况,多个slave节点都认为自己是不处于 FAIL 状态并且拥有较小节点 ID(实际上这是一种比较难出现的状况)。如果这种情况发生的话,结果是多个slave节点都会迁移到同个master节点下,不过这种结局是无害的。这种竞争发生的话,有时候会使得割让出slave的master变成没有任何备份节点,当集群再次达到稳定状态的时候,本算法会再次执行,然后把slave节点迁移回它原来的master节点。
最终每个master节点都会至少有一个slave备份。通常表现象是,一个slave从一个拥有多slave的master节点迁移到一个孤立的master节点。
这个算法能通过一个用户可配置的参数控制 cluster-migration-barrier : 一个master节点在拥有多少个健康slave节点的时候就要割让一个slave节点出来。例如这个参数设为 2,那么只有当一个master节点拥有 2 个可工作的slave节点时,它的一个slave节点会尝试迁移。
configEpoch冲突解决算法
当slave通过故障转移中升级,会生成新的configEpoch值,它可以保证是唯一的。
但也有两个不同的事件, 新configEpoch值以不安全的方式创建,只是递增本地节点的本地currentEpoch ,希望同一时间不存在冲突。这两个事件是系统管理员触发:
- 当在多数master不可达,CLUSTER FAILOVER 和TAKEOVER选项是用来手工升级slave成master 。 这很管用,比如,多数据中心的设置 .
- 当没有协议性能问题,集群重新平衡迁移哈希槽时在本地节点也产生新的配置epoch
具体来说,在手工重新分片中,当一个槽从A迁移到B,重新分片的程序会迫使B升级它的配置epoch,它是集群中最大的加1(除非节点已经是配置epoch中最大),节点相互之没有必需的协议。通常实际的重新分片涉及几百个哈希槽(特别是在小集群中)。 每个哈希槽迁移, 需要一个协议在重新分片过程中来产生新的配置epoch,这是低效的. 另外,它需要集群节点实时同步来存储最新的配置,因为它的这种运行方式,当第一个槽移动只需要一个新的配置epoch,使得在生产环境更加高效.
然而,因为上述两种情况的,它是可能的(虽然不太可能) ,最终多个节点有相同的配置epoch。一个重新分片操作由系统管理员执行,并在故障转移发生在同一时间(加了很多的坏运气),如果他们不传播速度不够快可能会导致currentEpoch碰撞。此外,软件错误和文件系统损坏也可能导致多节点有相同的配置epoch。
当负责不同的槽的master有相同的configEpoch, 这没有问题. 可能slave故障转移成master有一个唯一的配置epoch. 也就是说,手工干预或重新分片可能用不同的方式改变集群配置。redis集群主要的活越性要求槽配置经常汇总,所以在每种环境下,我们想要所有master有不同的configEpoch.
为了增强这个,使用一个冲突解决算法,来保证两个节点最终有相同configEpoch.
- 如果一个master发现其它master正在宣传跟它相同的configEpoch.
- 并且如果节点有按字典顺序比其它节点有更小的节点ID,申明相同的configEpoch.
- 然后它把currentEpoch加1作为新的configEpoch.
如果有一些节点有相同的configEpoch,除了最大节点 ID的节点都将前进,保无论发生什么证最终有唯一configEpoch。
这个原理也保证在新集群创建后,节点开始有不同的configEpoch(即使这没有使用)因为redis-trib保证使用CONFIG SET-CONFIG-EPOCH来起动。然而,如果节点由于某些原因误配置了,它会自动更新配置成另外一个。
节点重置
节点可以软重置(不重启的情况下)来来重新使用不同的角色或加入到不同的集群。 这在普通操作、测试、云环境(给定的节点可以另行配置加入到一堆集群来扩大或创建新集群)中非常有用。
在集群节点中使用CLUSTER RESET 来重置,命令提供两个选项:
- CLUSTER RESET SOFT
- CLUSTER RESET HARD
命令必需直接发送到节点重置。如果没有加上重置类型,默认是软重置.
下面是reset执行的一系列操作:
- 软重置和硬重置:如果节点是slave,变成了master,数据集被丢弃。如果节点是master,包含了键会放弃执行重置
- 软重置和硬重置:所有槽释放,手工故障转移也重置
- 软重置和硬重置:节点列表会移除其它节点,节点不再知道其它节点
- 单独硬重置:currentEpoch,configEpoch, 和lastVoteEpoch 设置成 0.
- 单独硬重置:节点ID变成新的随机ID.
非空数据集的master不能重置(因为通常你想重新分布数据到其它节点)。然后,在特殊条件下,这是合适的(比如当一个集群完全销毁来创建一个新的),FLUSHALL必需在重置前执行.
集群删除节点
实际中可能从一个集群中删除一个节点,把数据重新分布到其它节点(如果它是master),然后关掉它。然而,其它节点仍然记录了它的节点ID和地址,并且尝试重连.
因为这个原因,当删除一个节点,我们想把条目从所有其它节点列表中删除。这可以使用CLUSTER FORGET <node-id> 命令来实现,这个命令做两件事:
1.从节点列表中删除指定的节点ID.
2.在60s间隔内,阻止相同节点ID的节点重新加入.
第二项有必要是因为redis集群使用gossip来自动发现节点,因为从节点A移除节点X,会导致B 把节点X又告诉A。 因为这60s间隔,redis集群管理工具有60s在所有节点移除节点,阻止由于重新发现重新加入节点。
CLUSTER FORGET 文档中可以得到更多信息.
发布/订阅(Publish/Subscribe)
在一个 Redis 集群中,客户端能订阅任何一个节点,也能发布消息给任何一个节点。集群会确保发布的消息都会按需进行转发。
目前的实现方式是单纯地向所有节点广播所有的发布消息,在某些点,将来会用 bloom filters 或其他算法来优化。
附录 A:CRC16算法的 ANSI C 版本的参考实现
/*
* Copyright 2001-2010 Georges Menie (www.menie.org)
* Copyright 2010 Salvatore Sanfilippo (adapted to Redis coding style)
* All rights reserved.
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of the University of California, Berkeley nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/* CRC16 implementation according to CCITT standards.
*
* Note by @antirez: this is actually the XMODEM CRC 16 algorithm, using the
* following parameters:
*
* Name : "XMODEM", also known as "ZMODEM", "CRC-16/ACORN"
* Width : 16 bit
* Poly : 1021 (That is actually x^16 + x^12 + x^5 + 1)
* Initialization : 0000
* Reflect Input byte : False
* Reflect Output CRC : False
* Xor constant to output CRC : 0000
* Output for "123456789" : 31C3
*/
static const uint16_t crc16tab[256]= {
0x0000,0x1021,0x2042,0x3063,0x4084,0x50a5,0x60c6,0x70e7,
0x8108,0x9129,0xa14a,0xb16b,0xc18c,0xd1ad,0xe1ce,0xf1ef,
0x1231,0x0210,0x3273,0x2252,0x52b5,0x4294,0x72f7,0x62d6,
0x9339,0x8318,0xb37b,0xa35a,0xd3bd,0xc39c,0xf3ff,0xe3de,
0x2462,0x3443,0x0420,0x1401,0x64e6,0x74c7,0x44a4,0x5485,
0xa56a,0xb54b,0x8528,0x9509,0xe5ee,0xf5cf,0xc5ac,0xd58d,
0x3653,0x2672,0x1611,0x0630,0x76d7,0x66f6,0x5695,0x46b4,
0xb75b,0xa77a,0x9719,0x8738,0xf7df,0xe7fe,0xd79d,0xc7bc,
0x48c4,0x58e5,0x6886,0x78a7,0x0840,0x1861,0x2802,0x3823,
0xc9cc,0xd9ed,0xe98e,0xf9af,0x8948,0x9969,0xa90a,0xb92b,
0x5af5,0x4ad4,0x7ab7,0x6a96,0x1a71,0x0a50,0x3a33,0x2a12,
0xdbfd,0xcbdc,0xfbbf,0xeb9e,0x9b79,0x8b58,0xbb3b,0xab1a,
0x6ca6,0x7c87,0x4ce4,0x5cc5,0x2c22,0x3c03,0x0c60,0x1c41,
0xedae,0xfd8f,0xcdec,0xddcd,0xad2a,0xbd0b,0x8d68,0x9d49,
0x7e97,0x6eb6,0x5ed5,0x4ef4,0x3e13,0x2e32,0x1e51,0x0e70,
0xff9f,0xefbe,0xdfdd,0xcffc,0xbf1b,0xaf3a,0x9f59,0x8f78,
0x9188,0x81a9,0xb1ca,0xa1eb,0xd10c,0xc12d,0xf14e,0xe16f,
0x1080,0x00a1,0x30c2,0x20e3,0x5004,0x4025,0x7046,0x6067,
0x83b9,0x9398,0xa3fb,0xb3da,0xc33d,0xd31c,0xe37f,0xf35e,
0x02b1,0x1290,0x22f3,0x32d2,0x4235,0x5214,0x6277,0x7256,
0xb5ea,0xa5cb,0x95a8,0x8589,0xf56e,0xe54f,0xd52c,0xc50d,
0x34e2,0x24c3,0x14a0,0x0481,0x7466,0x6447,0x5424,0x4405,
0xa7db,0xb7fa,0x8799,0x97b8,0xe75f,0xf77e,0xc71d,0xd73c,
0x26d3,0x36f2,0x0691,0x16b0,0x6657,0x7676,0x4615,0x5634,
0xd94c,0xc96d,0xf90e,0xe92f,0x99c8,0x89e9,0xb98a,0xa9ab,
0x5844,0x4865,0x7806,0x6827,0x18c0,0x08e1,0x3882,0x28a3,
0xcb7d,0xdb5c,0xeb3f,0xfb1e,0x8bf9,0x9bd8,0xabbb,0xbb9a,
0x4a75,0x5a54,0x6a37,0x7a16,0x0af1,0x1ad0,0x2ab3,0x3a92,
0xfd2e,0xed0f,0xdd6c,0xcd4d,0xbdaa,0xad8b,0x9de8,0x8dc9,
0x7c26,0x6c07,0x5c64,0x4c45,0x3ca2,0x2c83,0x1ce0,0x0cc1,
0xef1f,0xff3e,0xcf5d,0xdf7c,0xaf9b,0xbfba,0x8fd9,0x9ff8,
0x6e17,0x7e36,0x4e55,0x5e74,0x2e93,0x3eb2,0x0ed1,0x1ef0
};
uint16_t crc16(const char *buf, int len) {
int counter;
uint16_t crc = 0;
for (counter = 0; counter < len; counter++)
crc = (crc<<8) ^ crc16tab[((crc>>8) ^ *buf++)&0x00FF];
return crc;
}