本文次要讲Redis集群相干的机制

哨兵机制请参阅专栏:https://segmentfault.com/a/11...

本文参考:

  • redis官网文档-Redis集群标准
  • 《redis设计与实现》

Redis集群定义

Redis集群是以高性能、平安和高可用为指标而设计的一套分布式实现。

高性能:高吞吐,同时具备高达1000个节点的线性扩大能力。没有代理、应用异步复制,同时也不会做值执行合并操作。

平安: 保障肯定水平的写平安,尽最大致力保留连贯大多数主节点的客户端的所有写操作。

高可用:在网络分区的状况,redis可能在领有大多数主节数的分区下失常提供服务,并且保障每个主节点至多领有一个从节点,当没有从节点的主节点产生故障须要转移时,能够从领有多个从节点的其余主节点接管一个从节点来进行转移。

对于写平安

redis集群在主从节点间应用异步复制,而最初博得选举的主节点的数据将会替换所有其余从节点的正本。那么,在分区期间总有可能失落写入的工夫窗口。而这个工夫窗口在客户端连贯的是大多数主节点和多数主节点这两种不状况的状况下有很大的不同。而Redis集群则是进最大致力来保障连贯到大多数主节点的客户端执行的写入。

以下可能产生数据失落的场景

1、redis主节点宕机:redis主节点在解决完写入后将音讯返回给客户,再通过异步复制把写入操作同步到从节点,如果在写入音讯还没有同步到从节点之前主节点宕机了,因为主节点长时间无法访问Redis集群将会把其从节点降级为主节点,而从节点又没有同步宕机前在主节点执行的写入操作,那么这时候这个写入操作就失落了。

2、redis主节点所在的网络产生分区:

呈现以下事件时可能产生写入失落:

  1. 因为分区导致主节点不可达;
  2. 产生故障转移,从节点降级为主节点;
  3. 一段时间后主节点又可达;
  4. 在Redis集群把这个旧主节点转换为新主节点的从节点之前,客户端通过过期的路由表把写操作发送到旧主节点。

第1种状况产生可能写抛弃的工夫窗口实际上十分小,因为把响应写回客户端跟把写入操作流传给从节点,这简直是同时产生的。

第2种状况其实不太可能产生,因为故障的主节点在长时间(NODE_TIMEOUT)无奈与大多数主节点通信时,将会故障转移并且不再承受写入操作,而当分区复原时,在其余节点同步配置给它之前,会有一段时间回绝写入操作。同时,它还须要客户端有一个过期的路由表。

另外,在网络分区的状况下,如果客户端连贯的主节点在在具备多数主节点的分区中,在大数节点主节点分区中Redis集群检测到并产生故障转移之前,会有一个比拟大的失落写入的工夫容器。具体的来说,如果产生分区,在NODE_TIMEOUT达到之前复原了网络,那么不会产生数据库,因为工夫没有故障转移;而如果在NODE_TIMEOUT达到之前网络还没有复原,那么将触发故障转移,在NODE_TIMEOUT这段时间的所有写入操作都将失落了。

对于高性能

  • 在Redis Redis集群中,不会通过代理将申请转发到解决该key集的节点上,而是可能依据槽位的散布状况,间接地将向解决该key汇合的节点发送申请命令。
  • 节点之间通过异步复制,节点不须要期待其余节点的对写入的确认(前提是没有应用wait命令)
  • 防止值合并。容许多键操作,前提是波及多键操作的单个命令(或者事务、lua脚本)的所有键都是指向同一个hash槽。

不存在值合并操作

值合并操作通过产生在查问的键值对在不同节点上有不同的版本,须要获取多个节点的数据后对数据进行一次合并后再返回。

Redis的设计防止在不同节点上雷同的键值存在版本抵触,并且redis的数据模型也不容许这种行为。因为在Redis,值通常十分大,通过会看到蕴含数百万个元素的列表和排序集,另外redis的数据类型也比较复杂。如果须要传输并且合并不同节点上不同版本的值,这可能会是一个性能瓶颈,并且十分十分严格的程序来管制,同时也须要额定调配一些内存来治理它们。

对于高可用

假如在大多数主节点的网络分区,有多个主节点,并且每个不可达主节点都有对应的从节点,在NODE_TIMEOUT后的几秒内就可能主动产生故障,而如果在领有多数节点的网络分区,Redis将是不可用的。实际上Redis集群的高可用须要有一个前提,即只是产生多数节点的故障。如果产生大规模的节点故障,那么Redis集群将不是一个适合的解决方案。

Redis集群部署及相干配置

详见另一个链接:

Redis集群相干机制

key分布式

Reids集群的key散布不同于单机,单机上所有key都散布在一个主节点上,而在Redis集群中,key会通过一致性哈希算法散布到不同槽位上,再把这些槽位调配给各个主节点。在Redis集群中,键空间(key space)会被划分为16384个槽位,实际上是redis设计了一致性哈希算法,通过这一算法可能将不同的key散列到这13684个槽位上。对于集群中的每一个主节点,它将解决这16384个槽位的一个子集,并且不同主节点的子集之间的元素没有交加,也就是说,一个槽位只会调配到一个主节点上,而一个主节点容许同时负责多个槽位的解决。

HASH_SLOT = CRC16(key) mod 16384

key散列标签

除了通过一致性哈希来确认key所属的槽位,还能够通过哈希标签将一些雷同标签的key强制散列到同一个槽位上。比方,key的名字组成为aaa.{xxx}.ccc,那么将仅对{}内的xxx做哈希散列来计算出槽位。

散列标签的规定为,取第一呈现的{和第一次进去}两头的非空字符来做散列,以下列举几种取散列key的例子

key理论进行哈希散列的字符
{user1}.nameUser1
{{user1}}.name{user1
{}.{user1}.name{}.{user1}.name (第一呈现的{和第一次进去}两头的字符为空字符串)
{class}.{user1}.nameclass

集群拓朴

redis集群由多个节点组成,每个节点与其它节点都会建设一个TCP连贯,整个redis集群的网络拓朴出现一个网状结构。redis集群中节间之间的连贯为长连贯,节点之间通过ping-pong的心跳来检测节点之间的状态,ping-pong的报文其中蕴含了gossip协定的报文,这能够用来做配置更新以及节点发现。

尽管节点之间有心跳,但通过gossip协定和配置更新机制能够防止失常运行状况下指数级的音讯替换。这个具体下文进行阐明。

节点间的握手

一个集群通常由多个节点组成,在刚开始的时候,每个节点都是独立的,它们都只处于一个蕴含本人的集群中。那么如何将多个节点组成成一个大的集群?

实际上redis除了数据同步端口(比例默认状况下是6379)外,还须要监听一个集群总线端口(syncPort+10000,默认即16379)。集群间的握手通过总线端口来连贯,如果发送方与接受方同属一个集群,那么通过集群总线端口连贯后,接受方会立刻返回ping命令,否则将抛弃所有其余的数据包。

在redis集群,只有两种形式可能承受节点作为集群中的一部分:

  • 第一种状况是通过Meet命令。节点node1向指定ip和port的节点node2发送meet命令,如CLUSTER MEET ip port,那么node2将被纳入到node1所在的集群,作为集群的一部分。
  • 第二种状况下,redis集群节点间的心跳蕴含gossip协定的报文,这里边蕴含除其本身节点外,多数随机的其余节点的信息。如果一个被信赖的节点通过gossip协定把其余节点的信息流传过去,那么以后节点也会认为它是可信赖的,最终也会建设到该节点的连贯。即A->B,B->C,那么A->C。

槽位分派

Redis集群反对通过CLUSTER的子命令去批改槽位的分派,具体的命令如下所示

CLUSTER ADDSLOTS slot1 [slot2] ... [slotN]CLUSTER DELSLOTS slot1 [slot2] ... [slotN]CLUSTER SETSLOT slot NODE nodeCLUSTER SETSLOT slot MIGRATING nodeCLUSTER SETSLOT slot IMPORTING node
  • ADDSLOTS和DELSLOTS仅用于节点调配槽,调配槽位意味着通知给定的主节点它将负责为指定的哈希槽提供存储和服务。ADDSLOTS通常用于从头创立集群,为每个主节点调配所有16384个可用你哈希槽的子集;DELSLOTS用于被动批改集群配置或者调试,实际上很有实在场景须要应用到。
  • CLUSTER SETSLOT slot NODE node用于将槽调配给特定ID的节点。与ADDSLOTS的区别,集体认为,ADDSLOTS须要将命令发送到指定的主节点,而CLUSTER SETSLOT slot NODE node能够发送到集群中任一个主节点。
  • MIGRATING和IMPORTING用来示意节点以后槽位的状态,MIGRATING示意以后槽位正在迁徙,IMPORTING示意以后槽位正在导入。

    • 主节点在收到CLUSTER SETSLOT slot MIGRATING node命令后,会批改指定槽位的状态,并将槽位的键逐渐地迁徙到指定ID的节点中,如果这时候收到这个槽位的查问命令,会先判断key是否存在于以后节点的键空间,如果存在将会解决该命令,如果不存在,则会返回ASK谬误,告诉客户端转向正在导入该槽位的节点,并在发送key的申请命令之前发送ASKING命令。
    • 主节点在收到CLUSTER SETSLOT slot IMPORTING node命令后,会开始筹备接管指定ID节点的指定槽位,节点也将承受对于这个槽位的所有查问,当然前提必须是客户端提前发送了ASKING命令。否则将会返回一个MOVED谬误。

    留神:在槽位从新分派未完结前,槽位调配配置还是不变,即某一个槽位正在进行迁徙,负责该槽位的节点还是指向迁徙前的结点,这时候还是由它来解决该槽位的申请命令,当槽位内的所有key都迁徙到另一个节点时,槽位才会分派到迁徙后的节点,槽位调配配置才会发生变化。

槽位重新分配

在集群中执行命令

在redis集群中,因为key的分布式,key常常哈希后被指定到不同的槽位,而不同的槽位又不同的节点负责,并且在上文也提到,redis cluster并没有代理,客户端到负责该key的主节点,采纳的是直连的形式,如果redis是如何将key的申请发送到精确的主节点上呢?

客户端能够自在地向每个节点发送与数据库无关的命令,接管命令的节点会计算出命令要解决的数据库键key属于哪个槽,并且查看这个槽是否指派给了本人(每个节点都会记录本人和其余节点别离负责的槽位)。如果发现以后槽位并没有分派给本人,那么将返回MOVED谬误。

MOVED谬误

如果发现这个数据库键key所属的槽并没有指派给本人,那么则会响应一个MOVED响应,格局为MOVED <slot> <ip>:<port>。通过MOVED响应来告诉客户端负责解决槽位节点ip和端口,客户端在承受到MOVED响应后,将之间的申请命令转发到正确的节点上。

ASK谬误

如果在节点处于从新分片的期间,源节点向指标节点迁徙一个槽时,这时候槽的一部分数据可能还在源节点上,一部分数据曾经迁徙到指标节点上。这时如果源节点收到一个该槽的申请命令,

  • 源节点先在本人保留的数据中查找指定的键,如果找到的话,就间接执行客户端的命令。
  • 如果源节点没能在本人的键空间中找到指定的键,那么这个键有空间曾经被迁徙到了指标节点,源节点就会向客户端返回一个ASK(ASK <slot> <ip>:<port>),指引客户端转向正在导入槽的指标节点,并再次发送之前想要 申请的命令。

故障转移

心跳

在Redis集群中,节点之间通过ping包和pong包一直地进行数据交换,这两种报文构造雷同,都携带重要的配置信息,惟一的区别是音讯类型字段,ping和pong统称为心跳数据包。

个别状况下,节点发送ping包,接收者回复pong包。但也有例外的状况,在须要尽快进行配置更新时,节点会间接发送一个pong给其余节点。

在一个redis集群中,可能存在十分多的节点,一个节点并不会向全副其余的节点都发送ping心跳包,而是每秒只会随机的抉择其中几个节点进行发送。另外,节点保留有其余节点最初的心跳刷新工夫,如果某个或某些节点的最终心跳工夫超过了NODE_TIMEOUT的一半,那么也会向这个或这些节点发送一个ping心跳包,并且在NODE_TIMEOUT工夫达到之前,还会尝试从新建设一个TCP连贯,以确保不会因为以后的TCP连贯存在问题而认为节点不可达。

那么如果NODE_TIMEOUT设置为十分短并且节点数十分多,则可能导致集群中的心跳特地频繁。

心跳包的数据结构

上文咱们提到ping包和pong包都属于心跳包。这两者蕴含一个对所有包类型公共的包头header,以及一个特定于ping和pong的非凡gossip局部。

公共包头蕴含以下信息:

  • Node ID:节点id,一个120bit的伪随机字符,在第一个创立节点时调配的,并在redis集群中整个生命周期放弃不变(除非调用reset 命令)。
  • configEpoch和currentEpoch:配置纪元。(todo)
  • node Flag: 节点身份标识,示意是master还是slave,或者是其余sing-bit节点
  • myslots:一个用bitmap来示意的以后节点负责的槽位,如果是从节点,则发送的是其主节点的槽位bitmap。
  • port:发送方的tcp端口(加10000即可取得redis集群总线端口)
  • state:节点状态(ok或者down)
  • slaveof:正在复制的主节点ID,如果以后是主节点,则是一个40字节长的00000字符数组。
Gossip

除此之外 ,ping包和pong包还蕴含一个gossip局部。gossip局部向接管方提供了发送方对集群中其余节点的认知,这部分并不蕴含所有发送所有已知的其余节点信息,而是只蕴含其中随机的几个节点信息,具体的数量与集群的大小成正比。gossip局部中每个节点的构造如下所示:

  • Node ID
  • IP and Port:地址和端口
  • Node Flags:节点标识

Gossip局部容许接受者通过从发送者获取其余节点的状态信息,这对于故障检测和发现集群中的其余节点是十分有用的。

故障探测

Redis故障探测用于辨认大多数节点不再能够拜访主节点或从节点,并做出将一个从节点晋升为主节点的响应。另外,当集群中的从节点无奈降级时,集群将处理错误状态以进行接管来自客户端的查问。

正如咱们上方提到的,每个节点都存储了已知其余节点的相关联的状态标识,其中有两个标识用于故障检测,即PFAIL和FAIL。

  • PFAIL示意可能的故障,是一种未确认的故障类型,与Sentinel模式下的主观下线相似。
  • FAIL示意节点曾经故障,并且是通过大多数节点在固定工夫内的确认。

PFAIL

当节点A发现节点B在超过NODE_TIMEOUT工夫不可拜访,节点A会应用PFAIL标识节点B。上文心跳章节咱们提过节点之间通过心跳进行探测,每个节点保留有其余节点的最初心跳刷新工夫,如果除了随机探测,还会在最初心跳刷新工夫+NODE_TIMEOUT/2时,次要发动一次探测,同时会还从新创立TCP连贯确保没有因为连贯问题而误判。

FAIL

在节点被标识为PFAIL后还不足以触发从节点降级机制,因为PFAIL只是每个节点对于其余节点的一个主观认知。要 将一个节点断定为不可用,PFAIL须要降级为FAIL.

在心跳大节早提及过,节点间会通过心跳中的gossip局部来传递对其余节点信息的视图,如果在gossip局部中,一个节点的标识为PFAIL,那么则会记录这个节点的PFAIL报告,这个FPAIL报告具备时效性(默认NODE_TIMEOUT*2),如果过期,那么它会被移除。当一个节点的PFAIL报告足够多时(满足需要的票数),那么PFAIL将会降级为FAIL,发送FAIL包给所有其余节点。

FAIL状态是单向的,它不会再进行降级,只能在满足条件下进行移除

  • 节点复原可达并且该节点的角色是从节点。在这种状况下FLAG能够革除,因为从节点没有故障转移。
  • 节点复原可达,且该节点的角色是主节点,但这个主节点没有被分派任何槽位。在这种状况下,FAIL状态能够革除,因为没有调配槽位的节点并不是集群中真正的一部分,并且在期待集群配置来退出集群。
  • 节点复原可达,且该节点的角色是主节点,但很长一段时间(n次NODE_TIMEOUT)后依然没有任何的从节点降级(可能起因:网络分区)。它最好重新加入集群并且放弃现状。

故障转移

在上一节中咱们理解到,主节点是如何被断定为下线状态的。那么主节点下线后,从节点降级为主节点这个流程是如何进行的呢?实际上选取哪个从节点来进行降级,这个不是由主节点来抉择的,而是靠从节点自行发动招标,获取得大多数票的从节点将降级主节点,接管旧主节点负责的槽位。具体流程如下所示:

  • 从节点收到FAIL包
  • 从节点与下线主节点的其余从节点进行通信,依据从节点的数据复制的offset来计算本身的等级,offset越高的从节点等级越高,0示意最高级。
  • 依据等级提早肯定工夫,目标是为了让等级高的优先开始投票选举(计算公式如下)

    DELAY = 500 milliseconds + random delay between 0 and 500 milliseconds +        SLAVE_RANK * 1000 milliseconds.
  • 向各个主节点发送FAILOVER_AUTH_REQUEST,期待响应获取批准;FAILOVER_AUTH_REQUEST蕴含epoch和须要负责的槽位bitmap
  • 主节点在以下条件都满足的条件下,返回批准投票;

    • FAILOVER_AUTH_REQUEST的currentEpoch大于主节点的lastVoteEpoch,且未对雷同的epoch投过票。
    • 发动投票的从节点的主节点被以后主节点标识为FAIL
    • FAILOVER_AUTH_REQUEST的currentEpoch不能小于主节点的currentEpoch
  • 从节点在失去超过半数的投票后,它开始通过ping和ping包宣示本人成为主节点,蕴含本人的ConfigEpoch和接管的hash槽。同时为了尽快地其余节点的配置更新,它还会立刻播送pong到所有其余节点。如果以后存在其余节点不可达,那么当这些节点复原后将同其余节点的ping和pong包来更新更新,或者在其余节点发现故障复原的节点发送的心跳包曾经过期后,将会给后者发现更新包。