redis:redis cluster

什么是redis cluster

redis cluster是redis作者自己提供的集群化方案

redis cluster是redis的分布式解决方案,在3.0版本正式推出,有效的解决了redis分布式方面的需求。当遇到分区内存、并发、流量等瓶颈时,可以采用cluster架构方案达到负载均衡的目的。之前,redis分布式方案一般有两种:

  • 客户端分区方案,优点是分区逻辑可控,缺点是需要自己处理数据路由、高可用、故障转移等问题
  • 代理方案,优点是简化客户端分布式逻辑和升级维护便利,缺点是加重架构部署复杂度和性能损耗

现在官方为我们提供了专有的redis cluster,它非常优雅的解决了redis集群方面的问题,因此理解应用号redis cluster将极大的解放我们使用分布式redis的工作量,同时它也是学习分布式存储的绝佳方案

它提供的能力:

  • 高性能:自动切分数据集到多个节点上
  • 高可用:当部分节点故障或者不可达的情况下继续提供服务
  • 写安全
    • 系统尽最大努力执行从客户端(和绝大多数master节点连接)发过来的所有写操作。
    • 在极端情况下,可能会有一小部分确认写会发生丢失。
    • 如果客户端只是连接了少数节点( are in a minority partition),丢失的确认写数目会更多。

高性能和高可靠性;以及在可接受范围内的数据安全和可靠性的前提下,提供很好的容错性;这些就是Redis 集群的主要目标。Redis 集群设计的目标是让Redis 集群的使用和单机版本一样

redis集群不像单机版的redis那样支持多个数据库,集群只有数据库0,而且也不支持select命令

高性能

redis cluster提供高性能的方案

  • 它实现了数据自动在多个redis节点间分片,这样,从而可以线性扩展至1000个节点
    • redis 集群节点不会将命令发送到负责这个key的真正节点上面,而是引导客户端到负责给定key所在的key空间的正确节点上面。
    • 最终客户端可以获取到最新的集群结构,哪个节点负责哪些key。
    • 后续,客户端可以直接将key转发到对应节点上
  • 没有代理,使用异步复制,在values上面没有合并操作,这样在redis的数据模型中最典型的大数据值也能有很好的表现
    • 由于使用了异步复制,节点不会等待其他节点的确认(如果没有使用wait命令的话)
    • 多个key的命令只适用于“接近”的keys的上面,所以除了resharding,数据不会在节点间迁移
    • 一般的操作都和单机上执行的一样,所以性能一般是N*单机性能。同时一个查询就是一个RT,因为客户端一般都会维持和节点的连接,所以延迟也和单机redis差不多

高可用

redis cluster提供一定程度的高可用

  • 在实际的环境中当某些节点失败或者不能通讯的情况下能够继续提供服务。
  • 大量节点失败的情况下集群会停止服务(比如大多数主节点不可用)
  • 解释:
    • 当每个挂掉的master都至少有一个可达的slave,并且绝大多数master节点都是正常的时候,在NOE_TIME加上几秒钟的切换时间后(一般1、2秒钟),redis cluster又会变得可用
      • 假设有N个Master的Redis集群,每个Master都有一个slave;如果有一个节点挂掉了,那么集群还是可用的;
      • 如果有两个节点挂掉了,那么可用性为:1-(1/(N*2-1)),因为两个节点挂掉了的时候,第一个节点挂掉之后还剩余节点:N*2-1个,而这个节点的slave也挂掉的可能性为:1/(N*2-1).
      • 如果有5个节点(每个节点有一个slave),那么有1/(5*2-1)=11.1%的可能性,有2个节点挂掉之后,集群不可用了。
    • 由于Redis 集群有副本迁移(replica migration)的功能,在现实场景下,Redis Cluster的可靠性更高
      • 通过副本迁移,没有slave的master会从另外一个master(它有多个slave)得到一个slave(Redis Cluster会自动将副本迁移到没有任何副本(slave)的master上面)
      • 因此,每一次从失败中恢复之后,集群会自动调整slave的部署,以便下次更好地从失败中恢复过来。
  • 也就是说: Redis 集群的设计适用于集群中有部分节点失败的时候,还可以自动进行恢复。但是,它不适用于哪些要求在大多数节点都失败的情况下还能恢复集群的应用。

写安全

redis集群的节点之间是异步复制的,上一次故障转移隐含着数据合并的功能。这就意味着上次被选举的master的数据集最终会被所有其他副本所替换。在数据分片之间总是存在一个丢失写的时间窗口。然而这个时间窗口对于连接到少数master客户端和连接到多数master的客户端是很不一样的。redis集群对连接到多数master的client发送的写,比连接到少数master的client发送的写,提供了更好的成功保障。下面是一些在错误发生的时候,导致多数分片丢失确认写的场景例子。

  1. 一个写到达master:maxter在本地写入成功后,返回了client成功消息;但是还没有来得及将本次写发送给slave进行复制就挂掉了;如果master过了一段时间后都还没有来得及恢复(其他slave提升为master了),那么,这个写就永远丢失了。这种情况比较少出现,master同同时回复客户端和slave的时候挂掉了。但这种情况的确是可能存在的。
  2. 另一个理论上可能出现写丢失的情况是:
    • master因为一个分片变得不可访问
    • 它被一个slave替换了
    • 过了一段时间,master变得可以访问了
    • 持有老的路由表的client可能会将数据写入到原来的master上面,直到它的路由表进行了更新。

第2种情况,一般不太可能发送;因为一个master如何在发生切换的这段时间内都无法和其他大多数master进行通信的时候,它是不会接受任何写操作的。当这个master的分片状态恢复写的时候,它也会预留一段不接受写的时间,以便通知其他节点配置发生变化了。而且这个问题发生还需要client的路由表也没有发生更新。

对某个分片的少部分master进行写操作有更大的写丢失窗口。比如:redisCluster会丢失一定数目的只有少部分节点才完成的写操作;客户端发送的写操作,只有少数master进行了更新,而其他大多数节点都没有更新,在这个过程中如果大多数节点中有的节点发生了切换 ,那么这些写就可能被丢失。

总结:一个master挂了,条件是至少在NODE_TIMEOUT时间内他和绝大部分master都无法通信,所以从这个时候开始到分片修复这段时间,写不会丢失;当分片失败持续时间超过NODE_TIMEOUT,那么截止到这个时间点,在少部分Node上执行的写也许会丢失;但是那些少部分节点,如果经过了NODE_TIMEOUT后还无法连接大多数的master,那么它们会启动禁止写操作;所以,对于少部分节点变得不可达有一个最大的时间窗口(就是NODE_TIMEOUT)。经过了这个时间窗口后,没有写回介绍或者丢失

集群功能限制

redis集群相对于单机在功能上存在一些限制,需要开发人员提前了解,在使用时做好规避。限制如下:

  • key批量操作支持有限。如mset、mget,目前只支持具有相同slot值的key执行批量操作。对于映射为不同slot值的key由于执行mget、mset等操作可能存在于多个节点上因此不被支持。
  • key事务操作支持有限。同理只支持多key在同一节点上的事务操作,当多个key分布在不同的节点上时无法使用事务功能。
  • key作为数据分区的最小粒度,因此不能将一个大的键值对象比如hash、list等映射到不同节点
  • 不支持多数据库空间。单机下的Redis可以支持16个数据库,集群模式下只能使用一个数据库空间,即db0。
  • 复制结构只支持一层,从节点只能复制主节点,不支持嵌套树状复制结构。

redis cluster原理

与codis有所不同,redis cluster是去中心化的,如下图

  • 该集群有三个redis节点组成,每个节点负责整个集群的一部分数据,每个节点负责的数据多少可能不一样。
  • 这三个节点相互连接组成一个对等的集群,它们之间通过一种特殊的二进制协议交互集群信息
    • 它们任何两个节点之间都是相互联通的。
    • 客户端可以与任何一个节点向连接,然后就可以访问集群中的任何一个节点,对其进行存取和其他操作

在这里插入图片描述

  • redis cluster将所有数据划分为16384个槽位,它比codis的1024个槽位划分更加精细,每个节点负责其中一部分槽位。
    • 槽位的信息存储在每个节点中,它不像codis,不需要另外的分布式存储空间来存储节点槽位信息
    • 当redis cluster的客户端来连接集群时,也会得到一份集群的槽位配置信息。这样当客户端需要查找某个key时,可以直接定位到目标节点。
    • 这一点不同于codis,codis需要通过proxy来定位目标节点,redis cluster则直接定位
  • 客户端为了可以直接定位某个具体的key所在的节点,需要缓存槽位相关信息,这样才可以准确快速的定位到相应的节点。
  • 同时因为可能会存在客户端与服务端存储槽位的信息不一致的情况,还需要纠正机制来实现槽位信息的校验调整

另外,redis cluster的每个节点会将集群的配置信息持久化到配置文件中,所以必须确保配置文件是可写的,而且尽量不要依靠人工修改配置文件

集群纪元

currentEpoch

currentEpoch是用来记录整个集群共有的版本号信息

  • 当有多个节点提供了冲突的信息的时候,另外的节点就可以通过这个状态值来了解哪个是最新的,并同步更新自己currentEpoch为最新值。

currentEpoch是一个64位的无符号整数

  • 在创建节点时,每个Redis 集群节点(从属节点和主节点)都将其currentEpoch设置为0。
  • 当节点接收到来自其他节点的ping包含着pong包时,如果发送方的epoch(集群总线消息报头的一部分)大于本地节点epoch,currentEpoch则更新为发送方时期。

由于这些语义,最终所有节点都将采用集群中的最好节点的configEpoch(整个集群currentEpoch值是相同的

configEpoch

configEpoch记录节点自身的特征值,主要用于判断槽的归属节点是否需要更新。

  • 当一个丛节点由于其归属的主节点下线而被提升为新的主节点后,它会更新自己的configEpoch值,确保它的版本号是最大的 ,并向其他的节点发送这个信息。
  • 如果原来的主节点故障回复,但是还没有收到自己主节点位置被取代的消息,仍然向其他节点以主节点的身份进行活动,但是其configEpoch已经不是最新的,所以不会被其他节点认可,最终这个节点也会收到来自新主节点发送的消息,知道自己已经被取代,而作为一个从节点从属于新的主节点。

redis cluster中是如何实现数据分布的?这种方式有什么优点

数据分区是分布式存储的核心

hash slot(哈希槽分区,单个key操作)

redis cluster默认会对key值使用crc16算法来进行hash,得到一个整数值,然后用这个整数值对16384进行取模来得到具体槽位

  • key空间被分到16384个槽中,也就决定了redis集群最多有16384个主节点(建议节点数目不要超过1000)。
  • 集群中的每一个主节点处理16384个hash slot(哈希槽)的子集。比如集群中有三个节点,则:
    • 节点A存储的哈希槽范围是:0 – 5500
    • 节点B存储的哈希槽范围是:5501 – 11000
    • 节点C存储的哈希槽范围是:11001 – 16384
  • 一个主节点可以有任意多个从节点, 这些从节点用于在主节点发生网络断线或者节点失效时, 对主节点进行替换。
  • 这样的分布方式方便节点的添加和删除。
    • 比如:
      • 需要新增一个节点D,只需要把A、B、C中的部分哈希槽数据转移到D节点。
      • 同样,如果希望在集群中删除A节点,只需要把A节点的哈希槽的数据移动到B和C节点,当A节点中的数据被全部移走后,A节点就完全可以从集群中删除
      • 因为把哈希槽从一个节点移到另一个节点时不需要停机的,所以,增加或者删除节点,或者更改节点上的哈希槽,也是不需要停机的

决定一个key应该分片到哪个槽的算法是:计算该key的CRC16结果再模16834。

HASH_SLOT =CRC16(key) mod 16384

以下是该算法所使用的参数:

  • 算法的名称: XMODEM (又称 ZMODEM 或者 CRC-16/ACORN)
  • 结果的长度: 16 位
  • 多项数(poly): 1021 (也即是 x16 + x12 + x5 + 1)
  • 初始化值: 0000
  • 反射输入字节(Reflect Input byte): False
  • 发射输出 CRC (Reflect Output CRC): False
  • 用于 CRC 输出值的异或常量(Xor constant to output CRC): 0000
  • 该算法对于输入 “123456789” 的输出: 31C3

CRC16 算法所产生的 16 位输出中的 14 位会被用到(这就是为什么在上面的公式中有一个模数16384运算)。

CRC16 算法可以很好地将各种不同类型的键平稳地分布到 16384 个槽里面。

在这里插入图片描述
redis虚拟槽分区的特点:

  • 解耦数据与节点之间的关系,简化了节点扩容和收缩难度
  • 节点自身维护槽的映射关系,不需要客户端或者代理服务维护槽分区元数据
  • 支持节点、槽、键之间的映射查询,用于数据路由、在线伸缩等场景

注意:

  • 当集群处于稳定状态时,所有客户端最终都会保存有一个哈希槽到节点的映射记录,使得集群非常高效;客户端可以直接向正确的节点发送命令请求,无需转向、代理或者其他任何可能发生单点故障的实体。
  • 当集群没有进行重配置的时候(重配置的时候,槽会从一个节点移动到另外一个节点),我们称集群处于stable状态。当集群处于stable的时候,一个hash slot(哈希槽)只会被一个node负责处理(但是读可以通过slave进行扩充)

Keys Hash tags(多个key操作)

redis cluster还允许用户强制把某个key挂在特定槽位上。通过在key字符串里面嵌入tag标记,这就可以强制key所挂的槽位等于tag所在的槽位

  • 什么叫做哈希标签:

    • 如果key含有大括号“{}”,则只有大括号中的字符串会参与哈希
    • 然而由于key中可能有多个{和},算法将使用下面规则:
      • 如果key包含一个{字符
      • 并且如果有一个}字符和{对应
      • 并且在第一个{和之后第一个}之间有一个或多个字符
    • 这个时候将使用第一个{和之后第一个}之间的字符用来计算hash slot。
    • 这个算法中有一个很有用的地方是,如果key以{}开头,那么整个key都会用来计算hash
  • 比如:

    • this{foo}和another{foo}这两个key会分配到同一哈希槽,所以可以在一个命令同时操作他们
    • {user1000}.following和{user1000}.followers会hash到同一个槽上面,因为只有user1000用来计算hash值
    • foo{}{bar},将使用所有字符串进行hash值计算,因为第一个{和}之间没有字符
    • foo{{bar}}zap,{bar用来计算hash
    • foo{bar}{zap},只有bar用于计算hash

通过hash tag,客户端可以使用多个key的操作,比如:

MSET {user:1000}.name Angela {user:1000}.surname White
  • 多个key的操作在key对应slot正在进行迁移的时候,会变得暂时不可用。
  • 但是,即使当key对应slot在迁移,如果操作的这些key都在同一个节点上面,操作还是可以顺利执行的。
  • 如果操作实在不可用,redis集群会向客户端发送TRYAGAIN错误,客户端可以稍后再试或者直接返回一个错误提示
    在这里插入图片描述

redis cluster中对key操作

redis集群设计中的单一key

  • redis集群实现了所有在单机redis版本中出现的处理单一键值(key)的命令
    • 针对多个数据库键的复杂计算操作, 比如集合的并集操作、合集操作没有被实现, 那些理论上需要使用多个节点的多个数据库键才能完成的命令也没有被实现。
    • 在将来,用户或许可以通过使用migrate copy命令,在集群的计算节点(computation node)中执行针对多个数据库键的只读操作, 但集群本身不会去实现那些需要将多个数据库键在多个节点中移来移去的复杂多键命令。
  • redis集群实现一个hash tags的概念,它主要用于将某些key存放在同一个节点上面;但是在手动reshading的时候,多个key的操作命令,在一段时间内可能会无法使用,不过单个key的操作还是可以正常使用的。

redis集群设计中要避免合并操作

  • Redis 集群的设计避免了相同key-value对在不同节点间的版本冲突,因为Redis数据模型不能够完全满足这个要求。在Redis中存储的数据量常常是很大的,同时数据类型一般都是比较复杂的。传输和合并各类数据值可能成为系统瓶颈,可能需要业务逻辑的参与,需要存储元数据的额外内存等等。
  • 这里不是技术的问题,CRDTs或其他同步复制的机器可以有和Redis类似的复杂数据模型,但是它们的实际运行运行的情况和Redis 集群不一样;Redis 集群设计的目标是让Redis 集群的使用和单机版本一样

redis cluster服务端节点主从模式

为了保证在部分节点故障或者网络不通时集群仍然能够正常工作,集群使用了主从模型,每个哈希槽有一(主节点)到N个副本(N-1g个从节点)

在我们刚才的集群例子中,有A、B、C三个节点,如果B节点故障集群就不能正常工作了,因为B节点中的哈希槽数据5501-11000没法操作。

但是,如果我们给每一个节点都增加一个从节点,就变成了了:A、B、C三个节点是主节点,A1、B1、C1分布是他们的从节点,当B节点宕机时,我们的集群也能正常运作。

B1节点是B节点的副本,如果B节点故障、集群会提升B1为主节点,从而让集群继续正常工作。但是,如果B和B1同时故障,集群就不能继续工作了

redis cluster拓扑

Redis 集群是一个网状结构,每个节点都通过 TCP 连接跟其他任意节点连接。在一个有 N 个节点的集群中,每个节点都与 其他N-1 个节点建立了持续的 TCP 连接,且这个连接是双向的。 这些 TCP 连接会永久保持,并不是按需创建的

  • Redis Cluster是一个完全图,每一个节点都通过tcp和其它所有节点进行连接。在N个节点的集群中,每一个节点都拥有N-1个outgoingTCP连接,N-1个incoming TCP连接。
  • 这些TCP连接始终是保持连接的,而不是按需创建;当一个节点期望它通过Cluster Bus(集群总线)发出的ping有一个pong的响应,如果等待了一段时间还没收到,则认为这个节点不可达;它将会尝试和这个节点进行重连。
  • 虽然Redis Cluster是一个完全图,但是节点间使用gossip协议和一个配置更新机制,从而避免在一般情况下,在节点间交换太多的信息。所以交换的信息不是指数级的。

redis cluster服务端节点属性

redis cluster服务端节点都有自己的属性,redis cluster客户端可以通过CLUSTER NODES 来获得这些属性

  • 每个节点在集群中都有一个独一无二的 ID , 该 ID 是一个十六进制表示的 160 位随机数,在节点第一次启动的时候生成(一般使用/dev/urandom生成)。节点将会把这个名字保存在文件中,节点就会一直沿用这个 ID 。(只要管理员没有删除这个文件或者没有通过cluster reset命令发送给hard reset请求)
  • 节点 ID 用于标识集群中的每个节点。 一个节点可以改变它的 IP 和端口号, 而不改变节点 ID 。 集群可以自动识别出 IP/端口号的变化, 并将这一信息通过 Gossip 协议广播给其他节点知道。

集群中每个master node负责存储数据、集群状态,包括slots与nodes对应关系master nodes能够自动发现其他nodes,检测failure节点,当某个master节点失效时,集群能够将核实的slave提升为master。下图是节点的关联信息,节点定时会将这些信息发送给其他节点:

1fc2412b7429e4ab5d8704fcd39520815ea2727b 10.9.42.37:6103 master - 0 1494082584680 9 connected 10923-13652 
08e70bb3edd7d3cabda7a2ab220f2f3610db38cd 10.9.33.204:6202 slave ad1334bd09ee73fdeb7b8f16194550fc2bf3a038 0 1494082586686 8 connected 
edaafc250f616e9e12c5182f0322445ea9a89085 10.9.33.204:6203 slave 1fc2412b7429e4ab5d8704fcd39520815ea2727b 0 1494082586184 9 connected 
06cd6f24caf98a1c1df0862eadac2b05254f909d 10.9.33.204:6201 slave d458c22ccced2f29358b6e6814a206d08285374e 0 1494082584179 7 connected 
3892b7fb410a4d6339364dbdda2ebc666ffee843 10.9.42.37:6203 slave 73f7d44c03ada58bf5adaeb340359e2c043ecfa0 0 1494082582679 12 connected 
73f7d44c03ada58bf5adaeb340359e2c043ecfa0 10.9.33.204:6103 master - 0 1494082585181 3 connected 13653-16383 
4004a64211bea5050a8f46b8436564d40380cd60 10.9.33.204:6101 master - 0 1494082583678 1 connected 2731-5460 
d458c22ccced2f29358b6e6814a206d08285374e 10.9.42.37:6101 master - 0 1494082588189 7 connected 0-2730 
f8868d59c0f3d935d3dbe35601506039520f7107 10.9.42.37:6201 slave 4004a64211bea5050a8f46b8436564d40380cd60 0 1494082587187 10 connected 
45ba0d6fc3d48a43ff72e10bcc17d2d8b2592cdf 10.9.33.204:6102 master - 0 1494082583179 2 connected 8192-10922 
007d7e17bfd26a3c1e21992bb5b656a92eb65686 10.9.42.37:6202 slave 45ba0d6fc3d48a43ff72e10bcc17d2d8b2592cdf 0 1494082588189 11 connected 
ad1334bd09ee73fdeb7b8f16194550fc2bf3a038 10.9.42.37:6102 myself,master - 0 0 8 connected 5461-8191 

从左至右分别是:

  • 节点ID
  • IP地址和端口
  • 节点角色标志
  • 最后发送ping时间
  • 最后接收到pong时间
  • 连接状态
  • 节点负责处理的hash slot。

集群可以自动识别出ip/port的变化,并通过Gossip(最终一致性,分布式服务数据同步算法)协议广播给其他节点知道。Gossip也称“病毒感染算法”、“谣言传播算法”。

客户端可以通过向集群中的任意节点(主节点或者从节点都可以)发送 CLUSTER NODES 命令来获得。

以下是一个向集群中的主节点发送 CLUSTER NODES 命令的例子, 该集群由三个节点组成:

$ redis-cli cluster nodes
d1861060fe6a534d42d8a19aeb36600e18785e04 :0 myself - 0 1318428930 connected 0-1364
3886e65cc906bfd9b1f7e7bde468726a052d1dae 127.0.0.1:6380 master - 1318428930 1318428931 connected 1365-2729
d289c575dcbc4bdd2931585fd4339089e461a27d 127.0.0.1:6381 master - 1318428931 1318428931 connected 2730-4095

在上面列出的三行信息中, 从左到右的各个域分别是: 节点 ID , IP 地址和端口号, 标志(flag), 最后发送 PING 的时间, 最后接收 PONG 的时间, 连接状态, 节点负责处理的槽。

redis cluster的端口

每个redis集群节点需要打开两个TCP连接。

  • 命令端口:6379

    • 是服务器的监听端口,也是redis cluster客户端需要连接的端口
    • redis cluster客户端只能连接6379端口,不能连接端口16379
  • 总线端口:16379

    • 是提供给集群总线(cluster bus)使用的
    • 需要被所有其他集群节点能访问到
      命令端口和总线端口之间总是相差10000(如果命令端口是6377,那么总线端口就是16377)
  • 防火墙需要确保打开这两个端口,否则集群节点之间不能通讯

redis cluster bus (集群总线)

  • 每一个集群节点都有一个接收其他节点连接的端口,这个端口的大小是接收客户端连接的端口+10000,比如:接收client的端口为6379,那么这个端口的大小为16379
  • 节点和节点之间的通信都是通过集群总线来进行的:
    • 集群总线使用二进制协议(不同于跟客户端通信协议)来进行节点之间数据交换,它由不同类型和大小的frame组成,这个协议更适合节点间使用小的带宽和处理时间来交换数据。
      • 节点之间可以使用gossip协议对当前节点的信息进行泛洪传播,这样有利于发现新节点;
      • 可以发送ping包以确认其他节点都是正常工作的
      • 也可以发送cluster messages来通知一些特殊事件的发生。
      • cluster bus也用来在cluster节点间广播pub/sub消息,当有用户发送了主备切换的指令后,配合完成人工切换
    • 这个二进制协议没有公开文档,因为不想让其他外部软件和redis集群节点通过这个协议进行通信。但是我们可以通过阅读源代码来了解这个协议。
  • 集群总线的作用:失败检测、配置升级、故障转移授权等等。

节点通信

  • 在分布式存储中需要提供维护节点元数据信息的机制。所谓元数据是指:节点负责哪些数据,是否出现故障转移等状态信息。
  • 场景的元数据维护方式分为:集中式和P2P方式。
  • Redis集群采用P2P的gossip协议进行通信:
    • 所有节点持有一份数据,不同的节点间如果出现了元数据的变更,则该节点会把数据不断的发送给其他节点让其他节点进行数据变更。通过节点间不断通信来保持整个集群所有节点的数据都是完整的。
    • 主要交换故障信息、节点的增加和删除、hash slot信息等
    • 这种机制的好处在于,元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续,打到所有节点上去更新,有一定的延迟,降低了压力
    • 缺点在于元数据更新有延迟,可能导致集群的一些操作会有一些滞后

通信过程

  • 集群中的每个节点都会单独开辟一个TCP通道,用于节点之间彼此通信,通信端口号在基础端口上加1000
  • 每个节点在固定过期内通过特定规则选择一个节点发送ping信息。
  • 接收到ping信息的节点用pong信息作为响应。集群中每个节点通过一定规则挑选要通信的节点,每个节点可能知道全部节点,也可能仅知道部分节点,只要这些节点彼此可以正常通信,最终他们会达成一致的状态。
  • 当节点出故障、新节点加入、主从角色变化,槽信息变更等事件发生时,通过不断的ping/pong消息通信,经过一段时间后所有的节点都会直到整个集群全部节点的最新状态,从而达到集群状态同步的目的

gossip消息

gossip协议的主要职责就是信息交换。信息交换的载体就是节点彼此发送的gossip消息,了解这些消息有助于我们理解集群如何完成信息交换。常用的Gossip消息可分为:ping消息、pong消息、meet消息、fail消息等,它们的通信模式如图所示。
在这里插入图片描述

  • meet消息:
    • 用于通知新节点加入
    • 消息发送者通知接受者加入到当前集群
    • meet消息通信正常完成后,接收节点会加入到集群中并进行周期性的ping、pong消息交换
  • ping消息:
    • 集群内交换最频繁的消息
    • 集群中每个节点每秒向多个其他节点发送ping消息,用于检测节点是否在线和交换彼此状态信息
    • ping消息发送封装了自身节点和部分其他节点的状态数据
  • pong消息:
    • 当接收到ping、meet消息时,作为响应消息回复给发送方确认消息正常通信。pong消息内部封装了自身状态数据。
    • 节点也可以向集群内广播自身的pong消息来通知整个集群对自身状态进行更新。
  • fail消息:当节点判定集群内另一个节点下线时,会向集群内广播一个fail消息,其他节点接收到fail消息之后把对应节点更新为下线状态。

节点握手(ping/pong/meet)

  • 节点通过集群总线端口来接收连接,当接收到ping时,使用这个端口进行响应,即使ping的节点不是被信任的。
  • 但是,如果发送报文的节点被认为不是集群的一部分,这个节点发送(除了ping之外)的其它报文都会被接收节点丢弃。

要让一个节点承认另一个节点同属于一个集群, 只有以下两种方法:

  • 一个节点可以通过向另一个节点发送MEET信息,来强制让接收信息的节点承认发送信息的节点称为集群中的一份子。一个节点仅在管理员显式的向它发送cluster meet ip port命令时,才会向另一个节点发送MEET信息
  • 另外,如果一个可信节点向另一个节点传播第三方节点的信息,那么接收信息的那个节点也会将第三方节点识别为集群中的一份子。也即是说, 如果 A 认识 B , B 认识 C , 并且 B 向 A 传播关于 C 的信息, 那么 A 也会将 C 识别为集群中的一份子, 并尝试连接 C 。

这意味着如果我们将一个/一些新节点添加到一个集群中,那么这个/这些新节点最终会和集群中已有的其他所有节点连接起来。

这说明只要管理员使用cluster meet命令显式的指定了可信关系,集群就可以自动发现其他节点。

这种节点识别机制通过防止不同的 Redis 集群因为 IP 地址变更或者其他网络事件的发生而产生意料之外的联合(mix), 从而使得集群更具健壮性。

当节点的网络连接断开时,它会主动连接其他已知的节点。

心跳和八卦消息(ping/pong)

心跳数据包:

  • ping和pong数据包的总和,redis集群节点不断交换ping和pong数据包
  • 这两种报文具有相同的结构,并且都携带重要的配置信息。唯一的实际区别是消息类型字段

特点:

  • 节点发送ping数据包,这将触发接受者以pong数据包进行回复,节点也可能仅发送pong数据包就可以向其他节点发送有关其配置的信息,而不会触发答复
  • 一个节点每秒将对几个随机节点执行ping操作,以便每个节点发送的ping数据包(和接收到的pong数据包)的总数为恒定数量,而与群集中节点的数量无关
  • 每个节点都确保对所有未发送ping或者收到pong时间超过一半的其他节点进行ping NODE_TIMEOUT操作。在NODE_TIMEOUT流逝之前,节点还尝试将TCP链接与另一个节点重新连接,以确保仅由于当前TCP连接中存在问题,才不会认为节点不可达
  • 如果NODE_TIMEOUT设置为较小的数字,并且节点数(N)非常大,则全局交换的消息数可能会很大,因为每个节点都会尝试对每隔一半没有更新信息的其他节点执行ping操作

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

有一些方法可以降低消息的数量,但是目前还没有关于Redis集群故障检测使用的带宽的报告问题,所以目前使用的是明显和直接的设计。请注意,即使在上面的示例中,每秒交换的330个数据包也平均分配给100个不同的节点,因此每个节点接收的流量是可以接受的。

心跳包内容

ping和pong数据包包含所有类型的数据包(比如请求数据转移投票的数据包)共有的标头,以及专用于Ping和Pong数据包的特殊八卦节。
通用标头具有以下信息:

  • 节点ID,这是一个160位的伪随机字符串,在第一次创建节点时被分配,并且在Redis Cluster节点的整个生命周期中都保持不变。
  • 发送节点的currentEpoch和configEpoch字段。如果节点是从属节点,则它configEpoch是configEpoch其主节点中最后一个已知的节点。
  • 节点标志,指示该节点是从节点,主节点还是其他单位节点信息。
  • 发送节点服务的哈希槽的位图,或者如果该节点是从属节点,则为其主节点服务的槽的位图。
  • 发送方TCP基本端口(即Redis用来接受客户端命令的端口;将10000添加到该端口以获得集群总线端口)。
  • 从发件人的角度来看,群集的状态(关闭或正常)。
  • 送节点的主节点ID(如果它是从节点)。

ping和pong数据包还包含一个八卦部分。本节向接收者提供了发送者节点对集群中其他节点的看法。闲话部分仅包含有关发送方已知的节点集中的一些随机节点的信息。闲话部分提到的节点数量与群集大小成正比。

对于八卦部分中添加的每个节点,将报告以下字段:

  • 节点ID。
  • 节点的IP和端口。
  • 节点标志。

单节点状态检测:可能下线(PFail)与确定下线(Fail)

  • 当一个节点无法被集群中大部分的节点访问时,该节点可能已经失效,如果是主节点,需要从它的从节点中提升一个节点作为主节点继续保证节点的写入能力。若如果无法提升从节点来做主节点的话,那么整个集群就置为错误状态并停止接收客户端的查询。
  • 因为redis cluster是去中心化的,一个节点认为某个节点失联了并不代表所有的节点都认为它失联了,所以集群还得经过一次协商的过程,只有当大多数节点都认定某个节点失联了,集群才认为该节点需要进行主从切换来容错

redis集群节点采用gossip协议来广播自己的状态以及改变对整个集群的认知。

  • 如果一个节点发现某个节点失联了(Possibly Fail),它会将这条信息向整个集群广播,其他节点就可以收到这点的失联信息
  • 如果收到了某个节点失联的节点数量(PFail Count)已经达到了集群的大多数,就可以标记该失联节点为确定下线状态(Fail)
  • 然后向整个集群广播,强迫其他节点也接受该节点已经下线的事实,并立即对该失联节点进行主从切换

即:

  • PFAIL表示可能的失败,并且是未确认的失败类型。
  • FAIL表示节点发生故障,并且大多数主机在固定时间内确认了此情况。

PFAIL标识:

  • PFAIL 表示可能失效(Possible failure)的状态,这是一个非公认的(non acknowledged)失效类型。
  • 当一个节点在超过 NODE_TIMEOUT 时间后仍无法访问某个节点,那么它会用 PFAIL 来标识这个不可达的节点。主节点和从节点都可以将另一个节点标记为PFAIL,而不管其类型如何
  • 当节点A主观标识某个节点B的状态为PFAIL后
    • 节点A会从其他节点发送的心跳包中查看节点B的状态(每个节点向其他每个节点发送的 gossip 消息中有包含一些随机的已知节点的状态)
    • 如果节点A通过 gossip 字段收集到集群中大部分主节点标识的节点 B 的状态信息为PFAIL 状态,或者在 NODE_TIMEOUT * FAIL_REPORT_VALIDITY_MULT 这个时间内是处于 PFAIL 状态
    • 节点A标记节点B的状态将会从PFAIL转变为FAIL。
  • redis集群节点不可达的概念是:
    • 我们有一个活动的ping(发送给它的ping尚未得到答复)的等待时间长于NODE_TIMEOUT。为了使得该机制起作用,NODE_TIMEOUT与网络往返时间相比,必须更大。
    • 为了增加正常操作期间的可靠性,节点中的一半将在NODE_TIMEOUT没有回复ping的情况下尝试尽快与集群中的其他节点重新连接。
    • 这种机制可以确保连接保持活动状态,因此,断开的连接通常不会导致节点之间的错误故障报告。

FAIL标志

  • 被标记为FAIL状态的节点基本可以确定为已经失效。
  • 节点A标记节点B失效后,将会利用gossip消息向集群中的节点传播节点B为FAIL状态的消息,接受到该消息的节点会将让自己将节点B标记为FAIL,最终大部分节点将会标记节点B为FAIL状态,如果节点B为主节点,将会进行从节点的节点提升,将其从节点提升为主节点以代替B节点的工作。
  • FAIL 标识基本都是单向的。一个节点能从 PFAIL状态升级到 FAIL 状态,但要清除 FAIL 标识只有以下两种可能方法:
    • 如果被标记为FAIL的是从节点,那么当这个节点重新上线时,FAIL标记就会被移除
      • 保持从节点的FAIL状态时没有意义的,因为它不处理任何槽
      • 一个从节点是否处于FAIL状态,决定了这个从节点在有需要的时候能否被提升为主节点
    • 如果一个主节点被打上FAIL标记之后,经过了节点超时时限的四倍时间, 再加上十秒钟之后, 针对这个主节点的槽的故障转移操作仍未完成,并且这个主节点已经重新上线的话,那么移除对这个节点的FAIL标记

在第二种情况中,如果故障转移仍未能顺利完成,并且主节点重新上线,那么集群就继续使用原来的主节点,从而避免管理员介入的必要。

本质上来说,FAIL 标识只是用来触发从节点提升(slave promotion)算法的安全部分。理论上一个从节点会在它的主节点不可达的时候独立起作用并且启动从节点提升程序,然后等待主节点来拒绝认可该提升(如果主节点对大部分节点恢复连接)。

下面是节点失效检测的实现方法:

  • 当一个节点向另一个节点发送PING命令,但是目标节点未能在给定时间内返回PING命令的回复时,那么发送命令的节点将目标节点标记为PFAIL(可能失效)
    等待PING命令回复的时限叫做“节点超时时限(node timeout)”,是一个节点选项(node wise setting)
  • 每次当节点对其他节点发送 PING命令的时候,它都会随机的广播三个它所知道的节点的信息,这些信息里面的其中一项就是说明节点是否已经被标记为PFAIL或者FAIL
  • 当节点接收到其他节点发来的信息时,它会记下那些被其他节点标记为失效的节点。这叫做“失效报告(failure report)”
  • 如果节点已经将某个节点标记为PFAIL,并且根据节点所收到的失效报告显式,集群中的大部分其他主节点也认为那个节点进入了失效状态,那么节点会将那个失效节点的状态标记为FAIL
  • 一旦某个节点被标记为FAIL,关于这个节点已失效的信息就会被广播到整个集群,所有接收到这条信息的节点都会将失效节点标记为FAIL

简单来说,一个节点要将另一个节点标记为失效,必须先询问其他节点的意见,并且得到大部分主节点的同意才行。

因为过期的失效报告会被移除,所以主节点要将某个节点标记为FAIL的话,必须以最近接收到的失效报告作为依据。

集群状态检测:fail/pfail/ok

每当集群发生配置变化时(可能是哈希槽更新,也可能是某个节点进入失效状态),集群中的每个节点都会对他所知道的节点进行扫描。

一旦配置处理完毕,集群会进入如下两种状态中的其中一种

  • FAIL:集群不能正常工作。当集群中有某个节点进入失效状态时,集群不能处理任何请求,对于每个命令请求,集群节点都返回错误回复
  • OK:集群可以正常工作,负责处理全部16384个槽的几点中,没有一个节点被标记为FAIL状态

这说明即使集群中只有一部分哈希槽不能正常使用,整个集群也会停止处理任何命令。

不过节点从出现问题到标记为FAIL状态的这段时间里,集群仍然能正常运作,所以集群在某些时候,仍然有可能只能处理针对16384个槽中其中一个子集的命令请求。

下面是集群进入FAIL状态的两种情况:

  • 至少有一个哈希槽不可用,因为负责处理这个槽的节点进入了fail状态
  • 集群中的大部分主节点都进入了下线状态。当大部分主节点进入了pfail状态时,集群也会进入fail状态

第二个检测是必须的,因为要将一个节点从PFAIL状态改为FAIL状态,必须要有大部分主节点进行投票表决,但是,当集群中的大部分主节点都进入了失效状态时,单靠一两个节点是没有办法将一个节点标记为FAIL的

因此,有了第二个检测条件,只要集群中的大部分主节点进入了下线状态,那么集群就可以在不请求这些主节点的意见下,将某个节点判断为FAIL状态,从而让这个集群停止处理请求。

有从节点的主节点故障了,会怎么样?从从节点选举出一个作为主节点

一旦某个主节点进入FAIL状态,如果这个主节点有一个或者多个从节点存在,那么其中一个从节点会被升级为新的主节点,而其他从节点则会开始对这个新的主节点进行复制

新的主节点由已下线的主节点属性的所有从节点中自行选举产生,下面是选举的条件:

  • 这个节点是已经下线主节点的从节点
  • 已下线主节点负责处理的槽数量非空
  • 从节点的数据被认为是可靠的,也就是,主从节点之间的复制连接(replication link)的断线时长不能超过节点超时时限(node timeout)乘以 REDIS_CLUSTER_SLAVE_VALIDITY_MULT 常量得出的积。

如果一个从节点满足了以上的所有条件,那么这个从节点将向集群中的其他主节点发送授权请求,询问它们,是否允许自己(从节点)升级为新的主节点。

一个从节点想要被推选出来,那么第一步应该是提高它的 currentEpoch 计数,并且向主节点们请求投票

从节点并不是在主节点一进入 FAIL 状态就马上尝试发起选举,而是有一点点延迟。这段延时主要

  • 保证主节点FAIL的消息被其他主节点知道,否则从节点发起投票后其他主节点并不知道原主节点下线。
  • 让从节点有时间获取与主节点断线期间的最新数据。
  • 通过加入一个随机时间减少多个节点同时发送选举请求的可能性

如果发送授权请求的从节点满足下面属性,那么主节点将向从节点返回FAILOVER_AUTH_GRANTED 授权,统一从节点的升级要求:

  • 发送授权请求的是一个从节点,并且它所属的主节点处于FAIL状态
  • 在已下线主节点的所有从节点中,这个从节点的节点ID在排序中是最小的
  • 这个从节点处于正常运行的状态: 它没有被标记为 FAIL 状态, 也没有被标记为 PFAIL 状态。

一旦某个从节点在给定的时限内得到大部分主节点的授权,它就会开始执行如下故障转移操作:

  • 通过PONG数据包(packet)告知其他节点,这个节点现在是主节点了
  • 通过PONG数据包告知其他节点,这个节点时一个已升级的从节点(promoted slave)
  • 接管所有由已下线主节点负责处理的哈希槽
  • 显式的向所有节点广播一个PONG数据包,加速其他节点识别这个节点的进度,而不是等待定时的 PING / PONG 数据包。

所有其他节点都会根据新的主节点对配置进行响应的相应的更新,特别地:

  • 所有被新的主节点接管的槽会被更新
  • 已下线主节点的所有从节点会察觉到PROMOTED标志, 并开始对新的主节点进行复制。
  • 如果已下线的主节点重新回到上线状态, 那么它会察觉到 PROMOTED 标志, 并将自身调整为现任主节点的从节点。

在集群的生命周期中, 如果一个带有 PROMOTED 标识的主节点因为某些原因转变成了从节点, 那么该节点将丢失它所带有的PROMOTED标识。

从节点提升流程:

  • 从节点通过广播一个 FAILOVER_AUTH_REQUEST 数据包给集群里的每个主节点来请求选票。然后等待回复(最多等 NODE_TIMEOUT 这么长时间)。
  • 一旦一个主节点给这个从节点投票,会回复一个 FAILOVER_AUTH_ACK,并且在 NODE_TIMEOUT * 2 这段时间内不能再给同个主节点的其他从节点投票。确保一个节点只能投一票
  • 被投票的从节点会忽视所有带有的时期(epoch)参数比 currentEpoch 小的回应(ACKs),这样能避免把之前的投票的算为当前的合理投票。
  • 一旦某个从节点收到了大多数主节点的回应,那么它就赢得了选举。否则,如果无法在 NODE_TIMEOUT 时间内访问到大多数主节点,那么当前选举会被中断并在 NODE_TIMEOUT * 4 这段时间后由另一个从节点尝试发起选举。

currentEpoch(选举开始时生成的)。并为了加速其他节点的重新配置,该节点会广播一个 pong 包 给集群里的所有节点,其他节点会检测到有一个新的主节点在负责处理之前一个旧的主节点负责的哈希槽,就会升级自己的配置信息。当原主节点再次上线或者新的从节点上线,都会作为这个主节点的从节点。

主节点投票

主节点接收到来自于从节点、要求以 FAILOVER_AUTH_REQUEST 请求的形式投票的请求,主节点想要发起投票,需要满足以下的条件。

  • 主节点只会在一个epoch时段投票一次,并且拒绝以前时段的投票:lastVoteEpoch记录着最新的一次投票的信息,当该主节点接受投票后,该lastVoteEpoch值将会更新,如果收到的投票请求的currentEpoch比lastVoteEpoch中的值小,说明已经进行过投票或者请求已经过期,直接忽略。
  • 主节点投票必须认为发起投票的主节点的状态为FAIL。
  • 主节点的回应总是带着和投票请求一致的currentEpoch。
  • 主节点如果拒绝为一个从节点进行投票,他只是忽略掉这个消息即可。不会给任何负面的回应。
  • 主节点会查看发起投票的从节点的configEpoch值,该值必须比它的原主节的更大,以保证该节点的数据时足够的新,否则不会给予投票。这个configEpoch值会在请求信息中携带,同时还会附带slots信息。

没有从节点的主节点故障了,会怎么样?

redis cluster可以为每个主节点设置若干个从节点,当主节点发生故障时,集群会自动将其中某个从节点提升为主节点

如果某个主节点没有从节点,那么当它发生故障时,集群将完全处于不可用状态。

不过redis也提供了一个参数cluster-require-full-coverage可以允许部分节点发生故障,其他节点还可以继续提供对外访问。

集群变更感知:ClusterDown/MOVED

当服务器节点变更时,客户端应该立即得到通知以实时刷新自己的节点关系表。那么客户端是如何得到通知的呢?这里分两种情况

  • 目标节点挂了
    • 客户端会抛出一个ConnetionError,紧接着会随机挑一个节点来重试
    • 这时被重试的节点会通过MOVED指令告知目标槽位被分配到新的节点地址
  • 运维手动修改了集群信息,将主节点切换到其他节点,并将旧的主节点移除出集群
    • 这时打在旧的主节点上的指令会收到一个ClusterDown的错误,告知当前节点所在集群不可用(当前节点已经被孤立了,它不再属于之前的集群)
    • 这时客户端会关闭所有的连接,清空槽位映射关系表,然后向上层抛错。
    • 待下一条指令过来时,就会尝试初始化节点信息

槽位迁移感知

如果cluster中某个槽位正在迁移或者已经迁移完毕,那么客户端如何能感知到槽位的变化呢?客户端保存了槽位和节点的映射关系表,它需要及时得到更新,才可以正常的将某条消息发到正确的节点中

cluster有两个特殊的error指令:MOVED和ASKING

MOVED指令:用来纠正槽位,重定向

  • 如果我们将指令发送到来错误的节点,该节点发现对应的指令槽位不归自己管理,就会将目标节点的地址随同MOVED指令回复给客户端通过客户端去目标节点访问,告诉客户端去连接这个节点以获取数据
  • 这个时候客户端会刷新自己的槽位关系表,然后重试指令,后继所有打在该槽位的指令都会转移到目标节点

以下是一个 MOVED 错误的例子:

GET x

-MOVED 3999 127.0.0.1:6381
  • 错误信息包含键x所属的哈希槽3999,以及正确负责处理x的节点的IP和端口号127.0.0.1:6381

在这里插入图片描述

ASKING指令:用来临时纠正槽位

  • ASKING指令:
    • 如果当前槽位正处于迁移中,指令会被先发送到槽位所在的旧节点
    • 如果旧节点存在数据,就直接返回结果
    • 如果旧节点不存在数据,那么数据可能真的不存在,也可能在迁移目标上
      • 所以旧节点会通知客户端去新节点尝试拿数据,看新节点有没有
      • 这时候就会给客户端返回一个asking error携带上目标节点的地址
      • 客户端收到这个asking error之后,就去目标节点尝试。
      • 客户端不会刷新槽位映射关系表,因为它只是临时纠正该指令的槽位信息,不影响后继指令

举个例子:

  • 因为槽8所包含的键分散在节点A和节点B中,所以当客户端在节点A中没有找到某个键时,它应该转向到节点B中去寻找,但是这种转向应该仅仅影响一次命令查询,而不是让客户端每次都直接去查询节点B:在节点A所持有的属于槽8的键没有全部被迁移到节点B之前,客户单应该先访问节点A,然后再返回节点B。
  • 因为这种转向只针对16384个槽中的其中一个槽,所以转向对集群造成的性能损耗属于可接受的范围。
  • 因为上述原因,如果我们要在查找节点A之后,继续查找节点B,那么客户端在向节点B发送命令请求之前,应该先发送一个ASKING命令,否则这个针对带有IMPORTING状态的槽的命令请求将被节点B拒绝执行
  • 接收到客户端ASKING命令的节点将为客户端设置一个一次性的标志(falg),使得客户端可以执行一次针对IMPORTING状态的槽的命令请求

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

  • 如果客户端接收到ASK转向,那么将命令请求的发送对象调整为转向所指定的节点
  • 先发送一个ASKING命令,然后再发送真正的命令请求
  • 不必更新客户端所记录的槽8至节点的映射:槽8应该仍然映射到节点A,而不是节点B

一旦节点A针对槽8的迁移工作完成,节点A在再次收到针对槽8的命令请求时,就会像客户端返回MOVED转向,将关于槽8的命令请求长期的转向节点B

在这里插入图片描述
在这里插入图片描述

对比

ask和moved

  • 两者都属于客户端重定向
  • moved异常代表槽已经确定不再当前节点,确定已经迁移
  • ask异常代表当前正处在slot迁移中,每时每刻槽的位置处于不确定

即:

  • 当节点需要让一个客户端长期的将针对某个槽的命令请求发送到另一个节点时,节点向客户端返回MOVED转向
  • 另一方面,当节点需要让客户端仅仅在下一个命令请求中转向到另一个节点时,节点向客户端返回ASK转向

集群在线迁移(live reconfiguration)

redis集群支持在集群运行的过程中添加或者移除节点。

实际上,节点的添加操作和节点的删除操作可以抽象成同一个操作,那就是,将哈希槽从一个节点移动到另一个节点

  • 添加一个新节点到集群, 等于将其他已存在节点的槽移动到一个空白的新节点里面。
  • 从集群中移除一个节点, 等于将被移除节点的所有槽移动到集群的其他节点上面去。

因此,实现redis集群在线配置的核心就是将槽从一个节点移动到另一个节点的能力。因为一个哈希槽实际上就是一些键的集合,所以redis集群在重哈希(rehash)时真正要做的,就是将一些键从一个节点移动到另一个节点。

和迁移相关的CLUSTER命令有:(用于操作一个集群节点上的slot迁移表)

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 分别用于向节点分配或者移除节点,当槽被指派或者移除之后,节点会将这一信息通过gossip协议传播到整个集群ADDSLOTS 命令通常在新创建集群时,作为一种快速的将各个槽指派给各个节点的手段来使用
  • SETSLOT命令将某个slot分配给某个节点;
  • 至于 CLUSTER SETSLOT slot MIGRATING node 命令和 CLUSTER SETSLOT slot IMPORTING node 命令, 前者用于将给定节点 node 中的槽 slot 迁移出节点, 而后者用于将给定槽 slot 导入到节点 node :
    • 当一个槽被设置为MIGRATING (迁移)状态时,原来持有这个槽的节点仍然会继续接受关于这个槽的命令请求
      • 但只有命令所处理的键仍然存在于节点时,节点才会继续处理这个命令请求
      • 但只有命令所处理的键不存在于节点时,那么节点将向客户端返回一个-ASK转向(redirection)错误,告知客户端,要将命令请求发送到槽的迁移目标节点
  • 当一个槽被设置为IMPORTING 状态时:
    • 节点仅在接收到ASKING命令之后,才会接受关于这个槽的命令请求
    • 如果客户端没有向节点发送ASKING命令,那么节点会使用-MOVED转向错误将命令请求转向至真正处理这个槽的节点

上面关于 MIGRATING 和 IMPORTING 的说明有些难懂, 让我们用一个实际的实例来说明一下。

假设现在, 我们有 A 和 B 两个节点, 并且我们想将槽 8 从节点 A 移动到节点 B , 于是我们:

  • 向节点 B 发送命令 CLUSTER SETSLOT 8 IMPORTING A
  • 向节点 A 发送命令 CLUSTER SETSLOT 8 MIGRATING B

每当客户端向其他节点发送关于哈希槽 8 的命令请求时, 这些节点都会向客户端返回指向节点 A 的转向信息:

  • 如果命令要处理的键已经存在于槽 8 里面, 那么这个命令将由节点 A 处理。
  • 如果命令要处理的键未存在于槽 8 里面(比如说,要向槽添加一个新的键), 那么这个命令由节点 B 处理。

这种机制将使得节点 A 不再创建关于槽 8 的任何新键。

与此同时,redisrediscliused在reshardings和Redis 集群配置期间会将节点 A 中槽 8 里面的键移动到节点 B 。使用以下命令执行:

键的移动操作由以下两个命令执行:

CLUSTER GETKEYSINSLOT slot count

上面的命令会让节点返回 count 个 slot 槽中的键, 对于命令所返回的每个键, redisrediscliused 都会向节点 A 发送一条 MIGRATE host port key destination-db timeout [COPY] [REPLACE] 命令, 该命令会将所指定的键原子地(atomic)从节点 A 移动到节点 B (在移动键期间,两个节点都会处于阻塞状态,以免出现竞争条件)。

以下为 MIGRATE host port key destination-db timeout [COPY] [REPLACE] 命令的运作原理:

MIGRATE target_host target_port key target_database id timeout
  • 执行 MIGRATE host port key destination-db timeout [COPY] [REPLACE] 命令的节点会连接到target节点,并将序列化后的key数据发送给target,一旦target反馈OK,节点就讲自己的key从数据库中删除
  • 从一个外部客户端的视觉来看,在某个时间点上,键key要么存在于节点A,要么存在于节点B,但不会同时存在于节点A和节点B
  • 因为redis集群只使用0好数据木,所以当 MIGRATE host port key destination-db timeout [COPY] [REPLACE] 命令被用于集群操作时,target_database 的值总是0
  • target_database 参数的存在时为了让 MIGRATE host port key destination-db timeout [COPY] [REPLACE] 命令成为一个通用命令,从而可以作用于集群以外的其他功能
  • 我们对 MIGRATE host port key destination-db timeout [COPY] [REPLACE] 命令做了优化,使得它即使在传输包含多个元素的列表键这样的复杂数据时,也可以保存高效
  • 不够,尽管 MIGRATE host port key destination-db timeout [COPY] [REPLACE] 非常高效,对一个键非常多,并且键的数据量非常大的集群来说,集群重配置还是会占用大量的时间,可能会导致集群没有版本适应那些对响应时间有着严格要求的应用程序。

redis cluster客户端需要实现的功能

Redis 集群中的节点(客户端)有以下责任:

  • 负责存储数据
  • 记录cluster的状态(包括:将key映射到对应节点上面)。
  • 自动发现其他节点,检测出没有正常工作的节点,并能够在需要的时候将slave提升为master,以确保有错误发生的时候,集群还能够继续工作。

为了执行这些任务

  • cluster的节点之间使用tcp进行连接,并使用私有的二进制协议进行信息传输(也称为:Redis Cluster Bus)。
  • 因为集群节点不能代理(proxy)命令请求, 所以客户端应该在节点返回 -MOVED 或者 -ASK 转向(redirection)错误时, 自行将命令请求转发至其他节点。
    • 因为客户端可以自由地向集群中的任何一个节点发送命令请求, 并可以在有需要时, 根据转向错误所提供的信息, 将命令转发至正确的节点, 所以在理论上来说, 客户端是无须保存集群状态信息的。
    • 不过, 如果客户端可以将键和节点之间的映射信息保存起来, 可以有效地减少可能出现的转向次数, 籍此提升命令执行的效率。

客户端第一次连接和重定向处理

  • 客户端必须要在内部缓存槽与对应节点的关系,否则这个客户端的性能会比较低
  • 虽然客户端需要缓存槽与对应节点的关系,但是它不需要实时去更新这个缓存,因为如果发送查询到了错误的节点,可以使用重定向来进行更新
  • 客户端在两种情况下,需要获取一个完整的槽和节点的对应关系表:
    • 启动的时候
    • 获取到了MOVED的请求
  • 客户端可以在获取到MOVED重定向请求的时候,只更新这个槽与节点的对应关系,但是这么做,很没有效率,因为通常情况是多个槽会同时进行关系的修改(比如:一个从节点提升为主节点,原主节点服务器的所有槽对应的节点关系都会发生变化)
  • 为了获取槽与节点的对应关系,redis集群提供了一个可选的命令CLUSTER NODES,这个命令不需要解析,只提供客户端需要的槽与节点的对应关系。
  • 另外还有一个cluster slost命令,提供了一个slots范围列表,和对应的主和从节点
127.0.0.1:7005> cluster slots
1) 1) (integer) 10923
   2) (integer) 16383
   3) 1) "127.0.0.1"
      2) (integer) 7002
      3) "45d9ea3a8abc9baade3873358b9bb515a010515d"
   4) 1) "127.0.0.1"
      2) (integer) 7005
      3) "ecebb8f92f2cccfd2ac530e75d0475c5f030577d"
2) 1) (integer) 0
   2) (integer) 5460
   3) 1) "127.0.0.1"
      2) (integer) 7000
      3) "a6167ddab7475a9c09000f65e438b684c85ebae7"
   4) 1) "127.0.0.1"
      2) (integer) 7003
      3) "5a901c8d7324b28fb882c1f45533776f68a3cb86"
3) 1) (integer) 5461
   2) (integer) 10922
   3) 1) "127.0.0.1"
      2) (integer) 7001
      3) "697ed75764e5dc8d0ecefc8bc78b1ac0deb7822a"
   4) 1) "127.0.0.1"
      2) (integer) 7004
      3) "6680c148565a1f14cda6bb48d0fad49bebaf0fac"
127.0.0.1:7005>

CLUSTER SLOTS如果群集配置错误,则不保证返回覆盖整个16384个插槽的范围,因此客户端应初始化插槽配置映射,用空对象填充目标节点,并在用户尝试执行有关属于未分配插槽的密钥的命令时报告错误。

当发现插槽未分配时,在向调用方返回错误之前,客户机应再次尝试获取插槽配置,以检查集群现在是否配置正确。

使用从节点扩展读

  • 一般情况下,从节点收到请求后,会将命令重定向(使用MOVED)到key对应槽的主节点上。但是客户端可以使用READONLY命令,使用从节点来扩展读
  • READONLY告诉redis集群的从节点,请求只对读数据感兴趣,对写不感兴趣
  • 当创建了只读连接之后,当请求中的key不是从节点对应主节点服务的槽,则会返回重定向给客户端。当这种情况发生了,client也必须按照前面提到的方法来更新slot到node的映射关系。


迁移

redis cluster提供了工具redis-trib可以让运维人员手动调整槽位的分配情况,它使用ruby语言开发,通过组合各种原生的redis cluster指令来实现。这一点codis做的更加人性化,它不但提供了UI界面可以让我们方便的迁移,还提供了自动化平衡槽位工具,无需人工干预就可以均衡集群负载。不过redis官方向来的策略就是提供最小可用的工具,其他交给社区完成。

接下来我们看看redis cluster的数据迁移过程

  • redis迁移的单位是槽,redis一个槽一个槽的进行迁移,当一个槽正在迁移时,这个槽就处于中间过渡状态。如下图,这个槽在源节点的状态为migrating,在目标节点的状态为importing,表示数据正从源节点流向目标节点

在这里插入图片描述

  • 迁移工具redis-trib首先会在源节点和目标节点设置好中间过渡状态,然后一次性获取源节点槽位的所有key列表(keysinslot指令,可以部分获取),再挨个key进行迁移
  • 每个key的迁移过程是以源节点作为目标节点的“客户端”,源节点对当前的key执行dump指令获得序列化内容,然后通过“客户端”向目标节点发送restore指令携带序列化的内容作为参数,目标节点再进行反序列化就可以将内容恢复到目标节点的内存中,然后返回“客户端”OK,源节点“客户端”收到后再把当前节点的key删除掉就完成了单个key迁移的全过程

大致流程如下:从源节点获取内容—>存到目标节点—> 从源节点删除内容

注意这里的迁移过程是同步的,在目标节点执行restore指令到源节点删除key之间,源节点的主线程会处于阻塞状态,直到key被成功删除。

如果迁移过程中突然出现了网络故障,整个槽的迁移只进行了一半,这时两个节点依然处于中间过渡状态,待下次迁移工具重新连上时,会提示用户继续进行迁移。

在迁移过程中,如果每个key的内容都很小,migrate指令会执行的很快,它就不会影响客户端的正常访问。如果key的内容很大,因为migrate指令是阻塞指令,会同时导致源节点和目标节点卡顿,影响集群的稳定性。所以在集群环境下,业务立即要尽可能避免产生很大的key。

在迁移过程中,客户端访问的流程会有很大的变化

  • 首先新旧两个节点对应的槽位都存在部分key数据
  • 客户端先尝试访问旧节点,如果对应的数据还在旧节点里面,那么旧节点正常处理
  • 如果对应的数据不在旧节点里面,那么有两种可能,要么该数据在新节点里,要么根本不存在
  • 旧节点不知道是哪种情况,所以它会向客户端返回一个-ASK targetNodeAddr的重定向指令。
  • 客户端收到这个重定向指令之后,先去目标节点执行一个不带任何参数的ASKING指令,然后在目标节点在重新执行原先的操作指令

为什么需要执行一个不带参数的ASKING指令呢?

  • 因为在迁移没有完成之前,按理来说这个槽位还是不归新节点管理的,如果这个时候向目标节点发送该槽位的指令,节点是不认的,它会向客户端返回一个-MOVED重定向指令告诉它去源节点去执行。如此就会形成“重定向循环”
  • ASKING指令的目标就是打开目标节点的选项,告诉它下一条指令不能不理,而要当成自己的槽位来处理

网络抖动

网络抖动:突然之间部分连接变得不可访问,然后很快又恢复正常

  • 为解决这个问题,redis cluster提供了一种选项cluster-node-timeout,表示当某个节点持续timeout的时间失联时,才可以认定该节点出现故障,需要进行主从切换。如果没有这个选项,网络抖动会导致主从频繁切换(数据的重新复制)

  • 还有另外一个选项cluster-slave-validity-factor作为倍乘系数放大这个超时时间来宽松容错的紧急程度。如果这个系统为0,那么主从切换时不会抗拒网络抖动的。如果这个系数大于1,它就成了主从切换的松弛系数。

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值