Redis Cluster作用

Redis Cluster是Redis 3.0开始引入的分布式存储方案,集群由多个节点组成,Redis的数据分布在这些节点中。集群中的节点分为主节点和从节点:只有主节点负责读写请求和集群信息的维护,从节点只进行主节点数据和状态信息的复制。集群的作用,可以归纳为两点:

  • 数据分片:集群将数据分散到多个节点,突破了Redis单机内存大小的限制,存储容量大大增加。
  • 高可用:集群支持主从复制和主节点的自动故障转移(与哨兵类似),当任一节点发生故障时,集群仍然可以对外提供服务。

数据分区方案

常见的哈希分区方案包括:哈希取余分区、一致性哈希分区、带虚拟节点的一致性哈希分区等。

  • 哈希取余分区:哈希取余分区思路非常简单,首先计算key的hash值,然后对节点数量进行取余,从而决定数据映射到哪个节点上。该方案最大的问题是,当新增或删减节点时,节点数量发生变化,系统中所有的数据都需要重新计算映射关系,引发大规模数据迁移。
  • 一致性哈希分区:一致性哈希算法将整个哈希值空间组织成一个虚拟的圆环,范围为0 ~ 2^32-1。对于每个数据,根据key计算hash值,确定数据在环上的位置,然后从此位置沿环顺时针行走,找到的第一台服务器就是其应该映射到的服务器。与哈希取余分区相比,一致性哈希分区将增减节点的影响限制在相邻节点。
  • 一致性哈希分区(虚拟节点机制):普通的一致性哈希在服务器节点数量较少时容易产生数据倾斜问题,各个服务器的负载不均匀。为解决这个问题,引入了虚拟节点机制,每台机器可以负责更多节点,数据负担更加均匀。
  • 虚拟槽分区:Redis Cluser底层使用的虚拟槽分区,有一个长度为16384的虚拟槽,每个Master节点都会负责一部分的槽,Redis对key计算哈希值,使用的算法是CRC16,然后根据哈希值计算数据属于哪个槽,最后根据槽与节点的映射关系,计算数据属于哪个节点。其中哪个节点负责哪个槽,这是可以由用户指定的。

节点通信机制

两个端口

在redis sentinel中,节点分为数据节点和sentinel节点:前者存储数据,后者实现额外的控制功能。在redis cluster中,没有数据节点与非数据节点之分,所有的节点都存储数据,也都参与集群状态的维护。为此,集群中的每个节点都提供了两个TCP端口,普通端口主要用于为客户端提供服务,集群端口用于节点之间的通信,如搭建集群、增减节点、故障转移等操作时节点间的通信。

Gossip协议

Gossip过程是由种子节点发起,当一个种子节点有状态需要更新到网络中的其他节点时,它会随机的选择周围几个节点散播消息,收到消息的节点也会重复该过程,直至最终网络中所有的节点都收到了消息。这个过程可能需要一定的时间,由于不能保证某个时刻所有节点都收到消息,但是理论上最终所有节点都会收到消息,因此它是一个最终一致性协议。

Gossip协议的优点:

  • 负载低:比广播低,广播每条消息都要发送给所有节点,CPU、带宽等消耗较大。
  • 去中心化:Gossip协议不要求任何中心节点,所有节点都可以是对等的,任何一个节点无需知道整个网络状况,只要网络是连通的,任意一个节点就可以把消息散播到全网。
  • 容错性高:网络中任何节点的宕机和重启都不会影响Gossip消息的传播,Gossip协议具有天然的分布式系统容错特性。
    Gossip协议的缺点:
  • 消息的延迟:由于节点只会随机向少数几个节点发送消息,消息最终是通过多个轮次的散播而到达全网的,因此使用Gossip协议会造成不可避免的消息延迟,不适合用在对实时性要求较高的场景下。

消息类型

集群节点间发送的消息有以下几种类型:

  • meet:在节点握手阶段,当节点收到客户端的cluster meet命令时,会向新加入的节点发送meet消息,请求新节点加入到当前集群,新节点收到meet消息后会回复一个pong消息。
  • ping:集群里每个节点每秒钟会选择部分节点发送ping消息,接收者收到消息后会回复一个pong消息。ping消息使用Gossip协议发送,内容是自身节点和部分其他节点的状态信息,作用是彼此交换信息,以及检测节点是否在线。
  • pong:pong消息封装了自身状态数据,可以分为两种:第一种是在接到meet/ping消息后回复的pong消息,第二种是指节点向集群广播pong消息,这样其他节点可以获知该节点的最新信息,例如故障恢复后新的主节点会广播pong消息。
  • fail:当一个主节点判断另一个主节点客观下线后,会向集群广播这一fail消息,通知集群中所有节点标记故障节点为客观下线,并通知故障节点的从节点触发故障转移流程。
  • publish:节点收到publish命令后,会先执行该命令,然后向集群广播这一消息,接收节点也会执行该publish命令。

客户端路由

moved重定向

moved异常代表槽已经确认迁移至别的节点。


ask重定向

在集群缩容扩容的时候,要对槽进行迁移,在迁移的过程中访问一个key,但是key已经迁移到目标节点,那么就会返回一个ask异常。

Smart Client

redis-cli这一类客户端称为Dummy客户端,因为它们在执行命令前不知道数据在哪个节点上,因此需要借助moved异常重定向。为了追求性能,我们不可能每次都随机访问一个节点,再根据moved或ask异常去重定向到目标节点,因此需要实现一个Smart客户端,比如说JedisCluster。JedisCluster的基本原理大致如下:

  1. 从集群中选一个可运行节点,使用cluster slots命令并将结果映射到本地,这样本地就有了slot->node的映射关系缓存。
  2. JedisCluster为每个节点创建连接池(即JedisPool)。
  3. 当执行命令时,JedisCluster根据key->slot->node选择需要连接的节点,发送命令。如果成功,则命令执行完毕。如果执行失败,则会随机选择其他节点进行重试,并在出现moved错误时,刷新本地的映射关系缓存。

这里需要注意的是,JedisCluster中已经包含所有节点的连接池,因此JedisCluster要使用单例。

集群伸缩

集群伸缩的核心是槽迁移,通过修改槽与节点的对应关系,实现槽(即数据)在节点之间的移动。例如,如果槽均匀分布在集群的3个节点中,此时增加一个节点,则需要从3个节点中分别拿出一部分槽给新节点,从而实现槽在4个节点中的均匀分布。

增加节点

  1. 启动节点
  2. 加入集群:使用cluster meet命令。
  3. 迁移槽和数据

减少节点

  1. 迁移槽和数据
  2. 忘记节点:使用cluster forget命令
  3. 关闭节点

这里要注意应先下线从节点再下线主节点,因为若主节点先下线,会触发故障的自动转移。

在槽迁移未完成时,客户端访问了负责该槽的节点,但key此时已经迁移到了别的节点下,这时候会返回ask异常,通过这个机制使得redis cluster可以无痛的完成扩缩容操作。

故障转移

集群对故障发现与故障转移的实现与哨兵思路类似:通过定时任务发送ping消息检测其他节点状态,若某个主节点发现另一个主节点不可用(与参数cluster-node-timeout有关),则标记该节点进行主观下线,而当半数以上持有槽的主节点都标记该节点主观下线,则对该节点进行客观下线,并向集群广播fail消息,让集群中所有节点都将其标记为客观下线,并触发从节点的故障转移。

在故障转移阶段,主要有以下几个步骤:

  • 检查资格:每个从节点都会检查与故障主节点的断线时间,如果超过默认值150s(cluster-node-timeout * cluster-slave-validity-factor)则会取消资格。
  • 准备选举时间:为了保证偏移量比较大的从节点更有可能成为主节点,会将该从节点的延迟时间设置更小一些。
  • 选举投票:从节点选举胜出需要的票数为N/2+1,其中N为主节点数量(包括故障主节点),但故障主节点实际上不能投票。因此为了能够在故障发生时顺利选出从节点,集群中至少需要3个主节点。
  • 替换主节点。

与哨兵一样,集群只实现了主节点的故障转移,从节点故障时只会被下线,不会进行故障转移。因此,使用集群时,应谨慎使用读写分离技术,因为从节点故障会导致读服务不可用,可用性变差。

参数优化

cluster_node_timeout

cluster_node_timeout的默认值是15s,影响包括:

  • 值越大对延迟容忍度越高,并且由于节点发现与其它节点最后通信时间超过cluster_node_timeout / 2时会直接发送ping消息,因此调大该参数还可以降低带宽消耗,但同时也会降低收敛速度。
  • 影响故障转移的判定和时间,值越大越不容易误判,但完成转移消耗时间越长。

cluster-require-full-coverage

cluster-require-full-coverage参数设置为yes时,当主节点发生故障而故障转移尚未完成,原主节点中的槽不在任何节点中,此时集群会处于下线状态,无法响应客户端的请求。但在实际应用中为了保证服务的高可用性,都会将该参数设置为no。

参考资料