去中心Redis-Cluster规范(五)
本文翻译自官方文档
配置处理,传播,故障转移
集群当前时代(epoch)
Redis-Cluser使用一个概念类似Raft算法中的”term”,在Redis-Cluster中被称为时代(epoch),用来标记事件的递增版本.当多个节点提供了冲突的信息时,其他节点可以懂得哪个状态才是最新的.
currentEpoch是个64位的无符号数.
在每个Redis-Cluster节点创建时,主从节点都会涉及currentEpoch为0.
每当从另外一个节点接收到一个数据包时,如果发送者的epoch(集群总线消息头的一部分)比本地节点的epoch大,则本地的currentEpoch为发送者的epoch.
由于这些语义,最终所有节点都会接收集群中最大的configEpoch.
当集群状态发生变化,并且节点为了执行某种操作而开始寻求意见时,这个信息将被使用.
现在只会在从节点升级时使用这个信息,下一个段落会进行说明.简单来说epoch就是集群的逻辑时钟,令指定的信息可以覆盖epoch比较小的信息.
配置epoch
每个主节点总是使用ping和pong来通告他的configEpoch和它提供服务的slot集合的位图(bitmap).
新节点被创建时,configEpoch被设置为0.
从节点选取过程中会创建一个新的configEpoch.从节点会尝试替代失效的主节点,增加他们自己的epoch并且尝试从集群主节点的多数派获得授权.往一个从节点获得授权,一个新的唯一的configEpoch被建立并且这个从节点使用这个新的configEpoch变成主节点.
网络分区或者节点失效会导致不同的节点间的配置发生冲突.在接下来的章节里会解释如何使用configEpoch来帮助解决冲突.
从节点也会通过ping和pong数据表通告configEpoch字段,但是仅用来表示其与主节点交互数据的最后时间.这运行其他的节点实例判断什么时候一个从节点的配置是旧的,需要更新.(主节点不会投票给使用旧配置的从节点)
每当某个已知节点的configEpoch发生变化,所有收到这个变更信息的节点会将之用就存储在nodes.conf文件中.对currentEpoch也会进行相同的处理.在节点继续其他操作前,这两个变量会被确保保存并同步刷新(fsync)到磁盘.
在故障转移过程中生成的configEpoch值使用一个简单的算法来保证是新的,增量的,唯一的.
从节点选举和升级
主节点会为从节点的升级进行投票,进而选举并升级从节点.当至少一个从节点发现它的主节点进入FAIL状态,准备变为主节点时,需要进行从节点选举.
当满足如下条件时,从节点发起一次选举:
- 从节点的主节点处于FAIL状态.
- 主节点提供非0数量的slots服务.
- 为了确保被升级的从节点的数据足够新,这个从节点与主节点之间的复制连接断开必须不超过指定的时间,这个时间由用户设置.
为了被选举,从节点首先要增加自己的currentEpoch计数,然后请求主节点的投票.
从节点向急群众的每个主节点广播FAILOVER_AUTH_REQUEST
数据包来请求投票.接下来它等待回复到达,这个时间最大为两倍的NODE_TIMEOUT时间(但通常至少2秒).
一旦一个主节点积极回复FAILOVER_AUTH_ACK
给指定的从节点以为其投票,那么在NODE_TIMEOUT*2的时间内就不能再为同一个主节点的其他从节点进行投票.
在这个周期内,它不会再回复同一个主节点的其他从节点的授权请求.这虽然不需要保证安全性,但是对阻止多个从节点在近乎相同时间被选举是有用的,这个情况通常不想看到的.
AUTH_ACK回复的epoch必须比发送的投票请求的currentEpoch大,否则会被从节点丢弃掉.这确保了不会统计为之前的选举进行的投票.
一旦从节点接收到大多数主节点的ACK,它就赢得了此次选举.如果在两倍的NODE_TIMEOUT时间内(通常至少2秒)没有达成大多数确认,本次选举将被终止,然后在NODE_TIMEOUT*4时间(至少4秒)后再次发起一次新的选举.
主节点进入FAIL状态后,从节点在尝试选举之前会等待一个很短的时间周期.这个延迟时间的计算方式如下:
DELAY = 500 milliseconds + random delay between 0 and 500 milliseconds +
SLAVE_RANK * 1000 milliseconds.
固定的延迟时间确保FAIL状态跨越了整个集群传播,否则从节点尝试选举时,其他的主节点可能还不知道这个FAIL状态,导致拒绝为其投票.
随机的延迟是为了避免所有从节点在同一时刻发起选举.
SLAVE_RANK是从节点从主节点已经复制的数据量的评估等级.当主节点失效时,从节点间会交换信息,尽最大努力建立评分:具有最新副本的从节点rank=0,具有次新副本的从节点rank=1,等等.通过这种方式具有最新数据的从节点会先于其他从节点尝试发起投票.
评级排序并不是强制的;如果具有高优先级的从节点选举失败,其他的从节点将会在短时间内尝试选举.
一旦一个从节点赢取了选举,它会获得一个新的,唯一的,增量的configEpoch,这个值大于任何已存在的主节点.它会通过ping和pong通告自己成为主节点,还有它现在服务的slots集合(因为configEpoch更大,所以会覆盖先前的).
为了加速其他节点的重配置,pong数据包会广播到集群中的所有节点.当前不可达的节点最终会被重新配置在一下两种情况下:
- 当它们收到另一个节点的ping活pong数据包时.
它通过心跳数据包发布的信息被检测为过期时,其他的节点会发送UPDATE数据包给它.
其他节点检测到一个新的主节点与旧的主节点提供了相同的slots服务,但是configEpoch更大,则会升级他们自己的配置.旧主节点(或被故障转移的主节点重新加入了集群)的从节点不仅要升级配置还要重新配置自己为新的主节点的副本.节点重新加入集群后如何处理将在后续章节解释.
主节点回复从节前的投票请求
在先前的章节讨论了从节点如何尝试选举.本章从主节点的视角来解释如何处理指定从节点的投票请求.
主节点收到的投票请求是从节点通过FAILOVER_AUTH_REQUEST数据包发出的.
对于一次投票必须确保以下条件:
- 对于指定的epoch,一个主节点只能投票一次,并且拒绝为更老的epoch投票:每个主节点有个lastVoteEpoch字段,当授权请求包中的currentEpoch不大于lastVoteEpoch时会拒绝投票.当主节点积极回复一个投票请求时,lastVoteEpoch会按请求中的currentEpoch进行更新,并安全的保存到磁盘上.
- 一个主节点只会为主节点被标记为FAIL的从节点投票.
- 如果授权请求的currentEpoch小于主节点的currentEpoch,将会被忽略.因此这个主节点从是会回复与授权请求想通的currentEpoch.如果相同的从节点再次请求投票,增加currentEpoch,可以确保就的延迟收到的主节点旧回复不会被接受为新的投票.
如果不使用规则3会导致的问题示例如下:
主节点currentEpoch=5,lastVoteEpoch=1(在几个失败的选举后可能会发生这样的情况)
- 从节点currentEpoch=3
- 从节点尝试选举使用epoch=4(3+1),主节点使用currente=5回复ok,然而这个回复延迟到达了.
- 过了一会儿,从节点再次尝试选举,使用epoch=5(4+1),此时收到延迟到达的消息currentEpoch=5,并且接受了.
- 在NODE_TIMEOUT*2时间内主节点不会给已经投票过的相同从节点投票.由于两个从节点不可能在相同的epoch内赢得选举,所以这个限制不是严格需要的.然而在实际情况下,它确保了当一个从节点被选举后,有足够的时间告知其他从节点避免了另一个从节点赢得一次新的选举,执行了一次不必要的故障转移.
- 主节点不会以任何方式努力选出最好的从节点.如果从节点的主节点处于FAIL状态并且其它主节点在期限内还没有投票,则会积极确保一次积极投票.先于其它从节点发起并赢得选举的从节点大多情况下就是最好的从节点,因为像先前的章节解释的,具有更高的评估等级(higher rank)的从节点通常会先于其它节点进入投票进程.
- 主节点拒绝为指定的从节点投票时不会做出任何负面响应,只是简单的忽略掉请求.
- 在主节点的slots表中,如果从节点负责的slots的configEpoch大于该从节点发送的的configEpoch,则主节点不会为其投票.记住,从节点会发送它的主节点的configEpoch和其服务的slots位图.这意味着从节点请求投票时它要进行故障转移的slots配置必须比要进行投票的主节点上保存的更新或相同.
在分区期间配置epoch作用的实际例子
这个章节阐明了epoch概念如何使从节点升级过程更能容忍分区.
- 一个主节点可能无限期的不可达.它有三个从节点A,B,C.
- 从节点A赢得了选举并被升级为主节点.
- 网络分区导致A对集群中的大多数节点不可达.
- 从节点B赢得了选举并被升级为主节点.
- 又一次分区导致B对集群中大多数节点不可达.
- 前一次的分区恢复了,A又重新可达.
在这个时间点上B下线且A作为主节点重新可用(实际上UPDATE消息会迅速的重新配置它,但是我们现在假设所有的UPDATE消息都丢失了).同时,从节点C将会尝试选举以对B进行故障转移.以下为实际发生的情况:
- C将会尝试选举并成功,因为对大多数主节点来说它的主节点实际上是下线的.它将获得一个增量的configEpoch.
- A将不能够成为负责它的slots的主节点,因为其他的节点已经有了相同的slots配置但配置epoch(来自B)比A发布的更大.
- 所以所有的节点将会升级他们的表将这些hash slots分配给C,然后集群继续它的操作.
在接下来的章节中你讲会看到,一个重新加入集群的旧节点尽可能快的获得配置变更的通知,因为只要它ping了其他的节点,接受者会检测到它有过时信息并且发送一个UPDATE消息.
hash slots配置传播
Redis-Cluser一个重要部分就是用于传播哪个集群节点服务指定hash slots集合的信息的机制.这在一个新集群启动还有一个从节点升级来服务它失效的主节点的slots时是至关重要的.
同样这个机制还允许分区出去不确定时间的的节点以一种合理的方式重新加入集群.
hash slots通过两种方式被广播:
- 心跳消息.ping或pong数据包的发送者总是将它或它的主节点(如果它是从节点)服务的hash slots信息添加进去.
- UPDATE消息.因为每个心跳数据表中都有发送者的configEpoch和服务的hash slots集合,如果心跳数据包的接受者发现发送者的信息过时了,它会发送一个含有新信息的数据包,强制过时的节点更新它的信息.
心跳或UPDATE消息的接收者使用一定的简单规则来更新hash slots到节点的映射关系表.当创建一个新的Redis-Cluster节点,它的本地hash slot表简单的初始化为NULL,所有的hash slot都没有绑定或链接到任何节点.看起来类似下面这样:
0 -> NULL
1 -> NULL
2 -> NULL
...
16383 -> NULL
节点为了更新他的hash slot表的需要遵循的第一条规则如下:
规则1: 如果一个hash slot没有被分配(设置为NULL),并且有一个已知节点声称提供该slot服务,那么就更新自己的hash slot表将这些hash slots指向这个已知节点.
所以当我们从负责hash slot 1和2且配置epoch=3的节点收到心跳数据包,则映射表会变更为如下的样子:
0 -> NULL
1 -> A [3]
2 -> A [3]
...
16383 -> NULL
当创建新集群时,系统管理员需要人工分配(通过redis-trib命令行工具或其他类似工具,使用 CLUSTER ADDSLOTS命令)每个节点自己服务端是slots,然后这个信息会迅速夸集群传播.
然而这个规则并不足够.我们知道在一下两种事件发生时,hash slot映射可能变更:
- 一个从节点执行故障转移替换了它的主节点.
- 节点上的slot被重哈希到了另一个不同节点上.
现在让我们聚焦在故障转移上.当一个从节点对其主节点进行故障转移,它获得了确保比它的主节点更大的配置epoch(且比之前生成的任何配置epoch都大).例如节点B是节点A的从节点,在故障转移后B的配置epoch=4.它开始发送心跳数据包,并且由于如下的第二条规则,接受者会更新他们自己的hash slot表.
规则2: 如果一个hash slot已经被分配,且一个已知节点声称他使用了比当前主节点保存的与该slot关联的更大的configEpoch,需要重新绑定这个hash slot到新节点.
所以在从负责服务hash slot 1和2,且关联epoch=4的节点B收到消息后,接收者会更新他们的映射表如下:
0 -> NULL
1 -> B [4]
2 -> B [4]
...
16383 -> NULL
活跃属性:由于第二条规则,最终集群中的所有节点会一致同意一个slot的所有者为声称服务这个slot的且具有最大configEpoch的节点.
相同的情况也会发生在重哈希期间.当节点导入hash slot的节点完成导入操作后,他的配置epoch会自增来确保变更会广播到整个集群.
近看UPDATE消息
从上一章的内容中,很容易看到UPDATE消息是如何工作的.节点A在一段时间后重新加入集群.会发送心跳数据包通告它服务hash slot 1和2,配置epoch=3.所有具有更新信息的接收者会看到相同的hash slot被关联到节点B且具有更大的配置epoch.因此他们会发送一个携带新配置的UPDATE消息给A.A会依据上面提到的规则2更新自己的配置.
节点如何重新加入集群
往节点重新加入集群时使用同样基本的机制.继续上面的例子,节点A会被通知hash slot 1和2现在由B提供服务.假设节点A仅提供这两个hash slot服务,那么A提供服务的hash slot数量将变为0!所以A讲会被重新配置为新的主节点的从节点.
实际遵循的规则比这个要复杂一点儿.通常A要在一段时间之后才能重新加入集群,同时原本A服务的hash slots现在被多个节点服务,例如hash slot 1被B服务,hash slot 2被C服务.
所以实际的Redis-Cluster节点角色切换规则为:一个主节点会配置自己为为窃取了它最后的hash slot的节点的副本(从节点).
在重配置期间,最终服务的hash slot数量将下降到0,然后节点根据情况进行重配置.注意,基本上意思是说旧的主节点将变为在故障转移中替换它的从节点(现在是主节点)的从节点.然而,通用的规则覆盖了所有的可能性.
从节点执行完全相同的动作:他们重配置自己为窃取了它从前的主节点的节点的副本.
副本迁移
Redis-Cluster实现了成为副本迁移的概念,以改善系统的可用性.想法是这样的,在一个具有主从配置的集群中,如果主从间的映射关系是固定的,那么在多个独立节点故障发生时可用性是有限的.
例如在一个集群中有每个主节点有一个从节点,集群可以在主节点或从节点故障时继续提供服务,但主从节点同时故障服务则不能继续.然而由软硬件引起的独立节点故障会随时间累加.例如:
- 主节点A有个从节点A1
- 主节点A故障,A1升级为新的主节点.
- 三小时后A1独立故障(与A的故障无关).没有其他的从节点可用来升级,因为A仍然不在线.集群不能正常进行操作.
如果主从节点间的映射关系固定,那么令集群更能容忍上述场景的方式只能是为每个主节点增加更多的从节点,然而这需要非常高的成本,执行更多的Redis实例,更多的内存,等等.
另一个可选的方式是使用不对称的集群,让集群的布局随时间自动变更.例如集群可能有三个主节点A,B,C.A和B每个有一个从节点A1,B1.然而主节点C不同,有两个从节点C1,C2.
副本迁移是为了让从节点迁移给没有从节点的主节点的自动重配置过程.有了副本迁移,上述场景变为如下:
- 节点A故障,A1被升级.
- C2迁移为A1的从节点,因为A1没有任何从节点.
- 三小时后A1也故障了.
- C2被升级为新的主节点替换A1
- 集群可以继续操作.
副本迁移算法
迁移算法无须任何协商,因为Redis-Cluster的从节点布局不是集群配置的一部分,无须保持一致或使用配置epoch版本化.取而代之的是当一个主节点没有备份时使用一个算法避免从节点大量迁移.这个算法确保最终(一旦集群稳定)每个主节点会有至少一个从节点进行备份.
现在开始说明算法如何工作的.开始前我们需要定义什么是一个好的从节点:从给定的节点的视角看如果一个从节点不处于FAIL状态就是一个好的从节点.
每个从界面发现至少一个单独的没有好的从节点的主节点时触发算法的执行.然而在所有检测到这个情况的从节点中,只有一个子集应该行动.这个子集通常是一个独立的从节目,除非在指定的时刻不同的不同的从节点看到了不同的其他节点故障状态视图.
行动的节点是拥有最多的从节点的主节点的一个不处于FAIL状态的从节点,并且节点ID最小.
所以例如有10个主节点每个拥有1个从节点,另外两个主节点分别有5个从节点,则后者的从节点中具有最小节点ID的从节点将会尝试迁移.因为没有协商,当集群不稳定时,可能多个从节点相信他们自己不在FAIL状态且具有更小的节点ID(实际上这不太可能发生).如果这种情况发生了,多个从节点会迁移给同一个主节点,这是无害的.如果竞争条件导致割让从节点的主节点没有了从节点,一旦集群重新稳定,算法会再次执行,并迁移一个从节点回到原始主节点.
最终每个主节点会有至少一个备份.正常行为是具有多个从节点的主节点的一单独的从节点迁移给一个孤立的主节点.
这个算法由一个用户配置的参数`cluster-migration-barrier
控制.这个参数表示在从节点迁移走之前,一个主节点必须保留的好从节点数量.例如如果该参数设置为2,只有在主节点依然保留两个工作的从节点的情况下,多余的从节点才能尝试迁移.
configEpoch冲突解决算法
在故障转移过程中通过从节点升级,新的configEpoch将被创建,并且会保证是唯一的.
然而在两个不同的事件中,新的configEpoch将以不安全的方式创建,仅仅增加节点本地的currentEpoch值,并且希望同时没有冲突.两个事件都是系统管理员触发的:
- 使用TAKEOVER选项的CLUSTER FAILOVER命令可以人工升级一个从节点为主节点,无须大多数主节点可用.例如在多数据中心配置时,这很有用.
- 由于性能原因在集群重平衡时的slot迁移仅会在本地节点生成新的配置epoch.
具体来说,在人工重哈希时,hash slot从节点A迁移到节点B,重哈希程序会强制B升级它的配置epoch到它在集群中发现的最大值,并加1,不会征求其他节点的意见.通常在真是世界中,重哈希会牵扯到数百个hash slot(特别是在小集群中).在重哈希过程中每个hash slot移动都要征求意见生成新的配置epoch,是低效的.此外为了存储新配置每次配置变化还需要每个节点执行fsync.因此,取而代之,我们进在第一个hash slot移动时生成新的配置epoch,使其在生产环境中更加高效.
此外软件bug和文件系统错误也可能导致多个节点具有相同的配置epoch.
当主节点服务具有相同configEpoch的hash slots时,这没有问题.从节点对主节点进行失效转移时,主节点有唯一的配置epoch是更加重要的.
那就是说,人工干预或重哈希会以不同方式改变集群配置.Redis-Cluster的主要活跃属性需要slot配置是全覆盖的,所以在任何情况下我们真的想要所有的主节点具有不同的configEpoch.
为了强制这个,一个冲突解决算法用来终止两个节点具有相同的configEpoch.
- 如果一个主节点检测到另外一个主节点宣称自己具有相同的configEpoch
- 且如果节点的ID比另外一个宣称相同configEpoch的节点在逻辑上更小.
- 那么它会对自己的currentEpoch+1,然后用其结果作为新的configEpoch.
无论发生什么,只要有一个具有相同configEpoch的节点集合,除了具有最大节点ID的节点外,所有节点都会对自己的currentEpoch+1直到最终每个节点都去的了唯一的configEpoch.
这个机制同时确保了在新集群创建后,所有的节点起初都是设置了不同的configEpoch(即使实际上没有用)因为redis-trib工具会在启动时确保使用CONFIG SET-CONFIG-EPOCH.然而如果处于某种原因,节点没有被设置,它也会自动使用一个不同的配置epoch更新自己的配置.
节点重置
节点可以通过软件重置(无须重启它们)以其他角色重用或者加入到一个不同的集群中.在正常操作,测试,或者云环境中这都是有用的,这样可以将给定的节点加入到不同的节点集合中以扩容或者创建新集群.
在Redis-Cluster中可以使用CLUSTER RESET命令重置节点.变量提供两种变体:
- CLUSTER RESET SOFT
- CLUSTER RESET HARD
命令必须直接发送给要重置的节点.如果没有设置重置类型,将使用软重置.
重置命令执行的操作列表如下:
- 软和硬重置: 如果节点是从节点,它会被转换为主节点,丢弃掉它的数据.如果节点是主节点且有key存储在上面,操作将被终止.
- 软和硬重置: 所有的slots将被释放,并且人工故障转移状态重置.
- 软和硬重置: 移除节点配置表中的所有其他节点,令节点不再知道其他节点.
- 硬重置: currentEpoch,configEpoch,和lastVoteEpoch设置为0.
- 硬重置: 节点ID变更为一个新的随机ID
含有非空数据的主节点不能被重置(因为正常情况下你应该重哈希数据到其他的节点).然而,在特定条件下这是运行的(例如要创建一个新集群时完全销毁一个旧集群), 必须在进行重置前执行FLUSHALL命令.
从集群中移除节点
通过将节点上的所有数据重哈希到其他节点(如果该节点是主节点),是可以将节点从已存在的集群中移除并关闭的.然而,其他节点依然会记得这个节点和它的地址,仍然会尝试连接它.
因此,当一个节点被移除时我们也想从其他所有节点的配置表中移除他的条目.这可以通过使用CLUSTER FORGET \
发布/订阅
在Redis-Cluster中客户端可以向任何一个节点进行订阅,也可以想任何其他节点发布.集群会确保被发布的消息按需转发.
当前实现只是简单的将每个发布的消息广播给其它所有节点,但是在未来某个时间点将使用Bloom过滤器或其它算法进行优化.