随着业务系统功能、模块、规模、复杂性的增加,我们对Redis的要求越来越高,尤其是在高低峰场景的动态伸缩能力,比如:电商平台平日流量较低且平稳,双十一大促流量是平日的数倍,两种情况下对于各系统的数量要求必然不同。如果始终配备高峰时的硬件及中间件配置,必然带来大量的资源浪费。
Redis作为业界优秀的缓存产品,成为了各类系统的必备中间件。哨兵模式虽然优秀,但由于其不具备动态水平伸缩能力,无法满足日益复杂的应用场景。在官方推出集群模式之前,业界就已经推出了各种优秀实践,比如:Codis、twemproxy等。
为了弥补这一缺陷,自3.0版本起,Redis官方推出了一种新的运行模式——Redis Cluster。
Redis Cluster采用无中心结构,具备多个节点之间自动进行数据分片的能力,支持节点动态添加与移除,可以在部分节点不可用时进行自动故障转移,确保系统高可用的一种集群化运行模式。按照官方的阐述,Redis Cluster有以下设计目标:
- 高性能可扩展,支持扩展到1000个节点。多个节点之间数据分片,采用异步复制模式完成主从同步,无代理方式完成重定向。
- 一定程度内可接受的写入安全:系统将尽可能去保留客户端通过大多数主节点所在网络分区所有的写入操作,通常情况下存在写入命令已确认却丢失的较短时间窗口。如果客户端连接至少量节点所处的网络分区,这个时间窗口可能较大。
- 可用性:如果大多数节点是可达的,并且不可达主节点至少存在一个可达的从节点,那么Redis Cluster可以在网络分区下工作。而且,如果某个主节点A无从节点,但是某些主节点B拥有多个(大于1)从节点,可以通过从节点迁移操作,把B的某个从节点转移至A。
简单概述。结合以上三个目标,我认为Redis Cluster最大的特点在于可扩展性,多个主节点通过分片机制存储所有数据,即每个主从复制结构单元管理部分key。因为在主从复制、哨兵模式下,同样具备其他优点。当系统容量足够大时,读请求可以通过增加从节点进行分摊压力,但是写请求只能通过主节点,这样存在以下风险点:
- 所有写入请求集中在一个Redis实例,随着请求的增加,单个主节点可能出现写入延迟。
- 每个节点都保存系统的全量数据,如果存储数据过多,执行rdb备份或aof重写时fork耗时增加,主从复制传输及数据恢复耗时增加,甚至失败;
- 如果该主节点故障,在故障转移期间可能导致所有服务短时的数据丢失或不可用。
所以,动态伸缩能力是Redis Cluster最耀眼的特色。好了,开始步入正题,本文将结合实例从整体上对Redis Cluster进行介绍,在后续文章深入剖析其工作原理。
集群结构
还是延续之前的风格,通过实例的搭建与演示,给大家建立对集群结构的直观感受;然后再以源码为基础梳理其中的逻辑关系;最后详细阐述集群构建的过程,循序渐进。
动手实践
按照官方文档的说明,在Redis版本5以上,集群搭建比较简单。本文使用6个Redis实例(版本是6.2.0),三个主节点,三个从节点,每个主节点有一个副本。
- 准备配置文件:在目录cluster-demo下创建6个文件夹,以Redis将要监听的端口号命名,依次为7000、7001、7002、7003、7004、7005。在每个目录放置一份Redis Cluster所需的最小化配置文件,命名为:cluster.conf,内容如下所示(注意修改端口):
port 7000
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes
复制代码
- 启动Redis实例:依次切换到6个目录,执行指令redis-server cluster.conf,以Cluster模式启动Redis实例。以7000为例,如下图:

- 创建集群:我使用的Redis版本为6.2.0,所以可以直接使用redis-cli。打开terminal输入--cluster create指令,使用我们刚刚开启的Redis实例创建集群,三主三从。
redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 \
127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 \
--cluster-replicas 1
复制代码
通过terminal看到输入如下图所示的内容:

上图以>>>是redis-cli创建集群时进行的一些核心操作,当然也还有一些日志中没有输出的部分,最终将建立起如下图所示的集群关系。

上图从两中视角对集群节点关系进行了描述:左侧是在不考虑节点角色情况下的物理结构,节点之间双向箭头代表了集群总线;右侧考虑了节点角色及主从分组,其中体现了主从复制关系及集群总线(集群总线仅绘制了主节点之间的,大家自行脑补,全都画出显得过于凌乱)。
在redis-cli的帮助下,Redis Cluster的搭建还是比较简单的,一条命令便解决了所有问题。从上面的过程我们可以清楚的了解到,在集群创建过程中,redis-cli是一个管理者,负责检查节点状态、主从关系建立以、数据分片以及协调节点间通过握手组建集群,但是这都离不了Redis Cluster能力的支持。
为了深入理解集群建立的过程,并为接下来其他部分的理解打好基础,接下来我将介绍Redis Cluster有关一些概念或结构,然后把集群建立的过程进行详细说明。
集群数据结构
上述示例集群中,有6个Redis实例构成了三主三从的集群结构,并且明确了每组主从节点负责的哈希槽范围,那Redis Cluster是如何描述这种关系的呢?有了上图的直观感受,我们还是要回归到数据结构,看看Redis是如何描述这种关系的。按照Redis源码数据结构之间的关系,我绘制了与Redis Cluster相关的重要数据结构的组织关系,如下图所示(以节点A的视角):

集群状态(clusterState)
我们知道,Redis Cluster是Redis的一种运行模式,一切都要归属于Redis内最核心的数据结构redisServer,以下仅摘取关于集群模式部分字段。
struct redisServer {
/* Cluster */
// 是否以集群模式运行
int cluster_enabled; /* Is cluster enabled? */
// 集群节点通信超时参数
mstime_t cluster_node_timeout; /* Cluster node timeout. */
// 自动生成的配置文件(nodes.conf),用户不能修改,存储了集群状态
char *cluster_configfile; /* Cluster auto-generated config file name. */
// 集群状态,从当前redis实例视角来看当前集群的状态
struct clusterState *cluster; /* State of the cluster */
}
复制代码

由此可知,集群模式下每个redisServer通过clusterState来描述在它看来整个集群中所有节点的信息与状态。clusterState不仅包含当前节点本身的状态(myself),而且还包含集群内其他节点的状态(nodes)。
另外,比较关键的一点是“在它看来”,因为集群是一个无中心的分布式系统,节点之间通过网络传播信息,而网络并不是百分百可靠的,可能存在分区或断连等问题,所以每个节点维护的集群状态可能不准确或者更新不及时。
以下为clusterState的完整结构,我们在这里先做简单的了解,在稍后的章节中会陆续涉及到这里的内容。
// 这个结构存储的是从当前节点视角,整个集群所处的状态
typedef struct clusterState {
// 当前节点信息
clusterNode *myself; /* This node */
// 集群的配置纪元
uint64_t currentEpoch;
// 集群状态
int state; /* CLUSTER_OK, CLUSTER_FAIL, ... */
// 负责哈希槽主节点的数量
int size; /* Num of master nodes with at least one slot */
// 节点字典:name->clusterNode
dict *nodes; /* Hash table of name -> clusterNode structures */
// 黑名单
dict *nodes_black_list; /* Nodes we don't re-add for a few seconds. */
// 正在执行迁出的哈希槽及目标节点
clusterNode *migrating_slots_to[CLUSTER_SLOTS];
// 正在执行导入的哈希槽及源节点
clusterNode *importing_slots_from[CLUSTER_SLOTS];
// 哈希槽与节点的映射关系
clusterNode *slots[CLUSTER_SLOTS];
// 每个哈希槽中存储key的数量
uint64_t slots_keys_count[CLUSTER_SLOTS];
rax *slots_to_keys;
/* The following fields are used to take the slave state on elections. */
// 故障转移授权时间
mstime_t failover_auth_time; /* Time of previous or next election. */
// 故障转移获得投票数
int failover_auth_count; /* Number of votes received so far. */
// 是否发起投票
int failover_auth_sent; /* True if we already asked for votes. */
//
int failover_auth_rank; /* This slave rank for current auth request. */
// 当前故障转移的配置纪元
uint64_t failover_auth_epoch; /* Epoch of the current election. */
int cant_failover_reason; /* Why a slave is currently not able to
failover. See the CANT_FAILOVER_* macros. */
/* Manual failover state in common. */
mstime_t mf_end; /* Manual failover time limit (ms unixtime).
It is zero if there is no MF in progress. */
/* Manual failover state of master. */
clusterNode *mf_slave; /* Slave performing the manual failover. */
/* Manual failover state of slave. */
long long mf_master_offset; /* Master offset the slave needs to start MF
or zero if still not received. */
int mf_can_start; /* If non-zero signal that the manual failover
can start requesting masters vote. */
/* The following fields are used by masters to take state on elections. */
// 最近一次投票的配置纪元
uint64_t lastVoteEpoch; /* Epoch of the last vote granted. */
int todo_before_sleep; /* Things to do in clusterBeforeSleep(). */
/* Messages received and sent by type. */
long long stats_bus_messages_sent[CLUSTERMSG_TYPE_COUNT];
long long stats_bus_messages_received[CLUSTERMSG_TYPE_COUNT];
// 达到PFAIL的节点数量
long long stats_pfail_nodes; /* Number of nodes in PFAIL status,
excluding nodes without address. */
} clusterState;
复制代码
简单说下几个字段,方便理解集群的基础字段:
- currentEpoch:集群当前纪元,相当于是集群所处的时代,由于重新分片、故障转移等会引起当前纪元升级;
- myself:数据类型为clusterNode,存储当前节点的状态,稍后解释;
- nodes:字典类型,以k-v结构存储集群内所有的节点信息,k为节点名称(也叫节点ID),v的数据类型也是clusterNode。
- slots:哈希槽与节点的映射关系,clusterNode数组,以哈希槽编号为索引,指向负责节点;
后面三个字段描述了节点自身的状态,也记录了集群中的其他兄弟节点,同时保存了集群内哈希槽的分配情况。在节点初次启动时,只会存在节点自身的情况,需要等待其他节点加入或者加入已有的集群才会有兄弟节点和哈希槽分配信息。这三个字段都与clusterNode结构有关。
节点属性(clusterNode)
Redis Cluster通过数据结构clusterNode来描述一个集群节点信息与状态。从不同视角来看,它既可以来描述节点自身的状态,也可以用来描述其他节点的状态。
- 当Redis以集群模式启动后,就会初始化一个clusterNode对象,来维护自身状态。
- 当节点通过握手或者心跳过程发现其他节点后,也会创建一个clusterNode来记录其他节点的信息。
无论是自身还是其他节点,都会存储在由Redis核心数据结构redisServer维护的clusterState中,随着集群状态的变化而不断更新。
clusterNode维护的信息有些是比较稳定或者是静态的,比如节点ID、ip和端口;也有一些会随着集群状态发生改变,比如节点负责的哈希槽范围、节点状态等。我们以源码+注释的方式来认识一下这个数据结构:
// 这是对集群节点的描述,是集群运作的基础
typedef struct clusterNode {
// 节点创建时间
mstime_t ctime; /* Node object creation time. */
// 节点名称,也叫做节点ID,启动后会存储在node.conf中,除非文件删除,否则不会改变
char name[CLUSTER_NAMELEN]; /* Node name, hex string, sha1-size */
// 节点状态,以状态机驱动集群运作
int flags; /* CLUSTER_NODE_... */
// 节点的配置纪元
uint64_t configEpoch; /* Last configEpoch observed for this node */
// 代表节点负责的哈希槽
unsigned char slots[CLUSTER_SLOTS/8]; /* slots handled by this node */
// 节点负责哈希槽的数量
int numslots; /* Number of slots handled by this node */
// 如果当前节点是主节点,则存储从节点数量
int numslaves; /* Number of slave nodes, if this is a master */
// 如果当前节点是主节点,则存储从节点列表(数组)
struct clusterNode **slaves; /* pointers to slave nodes */
// 如果当前节点是从节点,则存储其主从复制的主节点
struct clusterNode *slaveof; /* pointer to the master node. Note that it
may be NULL even if the node is a slave
if we don't have the master node in our
tables. */
// 最近一次发送ping请求的时间
mstime_t ping_sent; /* Unix time we sent latest ping */
// 最近一次接收pong回复的时间
mstime_t pong_received; /* Unix time we received the pong */
// 最近一次接收到数据的时

RedisCluster通过数据分片、节点心跳、故障转移等机制实现高可用。数据分片使用哈希槽分配key,通过MOVED和ASK重定向处理。心跳机制中,节点间通过PING/PONG保持连接,检测PFAIL/Fail状态。故障转移时,从节点通过选举成为新主节点,旧主节点在恢复后作为新主节点的从节点。

1144

被折叠的 条评论
为什么被折叠?



