Redis Cluster 通信流程深入剖析
1. Redis Cluster 介绍和搭建
请查看这篇博客:Redis Cluster 介绍与搭建
这篇博客会介绍Redis Cluster
的数据分区理论和一个三主三从集群的搭建。
Redis Cluster文件详细注释
本文会详细剖析搭建 Redis Cluster 的通信流程
2. Redis Cluster 和 Redis Sentinel
Redis 2.8
之后正式提供了Redis Sentinel(哨兵)
架构,而Redis Cluster(集群)
是在Redis 3.0
正式加入的功能。
Redis Cluster
和 Redis Sentinel
都可以搭建Redis
多节点服务,而目的都是解决Redis
主从复制的问题,但是他们还是有一些不同。
Redis
主从复制可将主节点数据同步给从节点,从节点此时有两个作用:
- 一旦主节点宕机,从节点作为主节点的备份可以随时顶上来。
- 扩展主节点的读能力,分担主节点读压力。
但是,会出现以下问题:
- 一旦主节点宕机,从节点晋升成主节点,同时需要修改应用方的主节点地址,还需要命令所有从节点去复制新的主节点,整个过程需要人工干预。
- 主节点的写能力或存储能力受到单机的限制。
Redis的解决方案:
Redis Sentinel
旨在解决第一个问题,即使主节点宕机下线,Redis Sentinel
可以自动完成故障检测和故障转移,并通知应用方,真正实现高可用性(HA)。Redis Cluster
则是Redis
分布式的解决方案,解决后两个问题。当单机内存、并发、流量等瓶颈时,可以采用Cluster
架构达到负载均衡的目的。
关于
Redis Sentinel
的介绍和分析:
3. 搭建 Redis Cluster的通信流程深入剖析
在Redis Cluster 介绍与搭建一文中介绍了搭建集群的流程,分为三步:
- 准备节点
- 节点握手
- 分配槽位
我们就根据这个流程分析Redis Cluster
的执行过程。
3.1 准备节点
我们首先要准备6
个节点,并且准备号对应端口号的配置文件,在配置文件中,要打开cluster-enabled yes
选项,表示该节点以集群模式打开。因为集群节点服务器可以看做一个普通的Redis
服务器,因此,集群节点开启服务器的流程和普通的相似,只不过打开了一些关于集群的标识。
当我们执行这条命令时,就会执行主函数
sudo redis-server conf/redis-6379.conf
在main()
函数中,我们需要关注这几个函数:
loadServerConfig(configfile,options)
载入配置文件。
- 底层最终调用
loadServerConfigFromString()
函数,会解析到cluster-
开头的集群的相关配置,并且保存到服务器的状态中。
- 底层最终调用
initServer()
初始化服务器。
- 会为服务器设置时间事件的处理函数
serverCron()
,该函数会每间隔100ms
执行一次集群的周期性函数clusterCron()
。 - 之后会执行
clusterInit()
,来初始化server.cluster
,这是一个clusterState
类型的结构,保存的是集群的状态信息。 - 接着在
clusterInit()
函数中,如果是第一次创建集群节点,会创建一个随机名字的节点并且会生成一个集群专有的配置文件。如果是重启之前的集群节点,会读取第一次创建的集群专有配置文件,创建与之前相同名字的集群节点。
- 会为服务器设置时间事件的处理函数
verifyClusterConfigWithData()
该函数在载入AOF文件或RDB文件后被调用,用来检查载入的数据是否正确和校验配置是否正确。aeSetBeforeSleepProc()
在进入事件循环之前,为服务器设置每次事件循环之前都要执行的一个函数beforeSleep()
,该函数一开始就会执行集群的clusterBeforeSleep()
函数。aeMain()
进入事件循环,一开始就会执行之前设置的beforeSleep()
函数,之后就等待事件发生,处理就绪的事件。
以上就是主函数在开启集群节点时会执行到的主要代码。
在第二步初始化时,会创建一个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 */
// 保存集群节点的字典,键是节点名字,值是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];
// 槽映射到键的有序集合
zskiplist *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;
/* Manual failover state in common. */
// 如果为0,表示没有正在进行手动的故障转移。否则表示手动故障转移的时间限制
mstime_t mf_end;
/* Manual failover state of master. */
// 执行手动孤战转移的从节点
clusterNode *mf_slave; /* Slave performing the manual failover. */
/* Manual failover state of slave. */
// 从节点记录手动故障转移时的主节点偏移量
long long mf_master_offset;
// 非零值表示手动故障转移能开始
int mf_can_start;
/* The followign fields are used by masters to take state on elections. */
// 集群最近一次投票的纪元
uint64_t lastVoteEpoch; /* Epoch of the last vote granted. */
// 调用clusterBeforeSleep()所做的一些事
int todo_before_sleep; /* Things to do in clusterBeforeSleep(). */
// 发送的字节数
long long stats_bus_messages_sent; /* Num of msg sent via cluster bus. */
// 通过Cluster接收到的消息数量
long long stats_bus_messages_received; /* Num of msg rcvd via cluster bus.*/
} clusterState;
初始化完当前集群状态后,会创建集群节点,执行的代码是这样的:
myself = server.cluster->myself = createClusterNode(NULL,CLUSTER_NODE_MYSELF|CLUSTER_NODE_MASTER);
首先myself
是一个全局变量,定义在cluster.h
中,它指向当前集群节点,server.cluster->myself
是集群状态结构中指向当前集群节点的变量,createClusterNode()
函数用来创建一个集群节点,并设置了两个标识,表明身份状态信息。
该函数会创建一个如下结构来描述集群节点。
typedef struct clusterNode {
// 节点创建的时间
mstime_t ctime; /* Node object creation time. */
// 名字
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 */
// 指向主节点,即使是从节点也可以为NULL
struct clusterNode *slaveof;
// 最近一次发送PING的时间
mstime_t ping_sent; /* Unix time we sent latest ping */
// 接收到PONG的时间
mstime_t pong_received; /* Unix time we received the pong */
// 被设置为FAIL的下线时间
mstime_t fail_time; /* Unix time when FAIL flag was set */
// 最近一次为从节点投票的时间
mstime_t voted_time; /* Last time we voted for a slave of this master */
// 更新复制偏移量的时间
mstime_t repl_offset_time; /* Unix time we received offset for this node */
// 孤立的主节点迁移的时间
mstime_t orphaned_time; /* Starting time of orphaned master condition */
// 该节点已知的复制偏移量
long long repl_offset; /* Last known repl offset for this node. */
// ip地址
char ip[NET_IP_STR_LEN]; /* Latest known IP address of this node */
// 节点端口号
int port; /* Latest known port of this node */
// 与该节点关联的连接对象
clusterLink *link; /* TCP/IP link with this node */
// 保存下线报告的链表
list *fail_reports; /* List of nodes signaling this as failing */
} clusterNode;
初始化该结构时,会创建一个link
为空的节点,该变量是clusterLink
的指针,用来描述该节点与一个节点建立的连接。该结构定义如下:
typedef struct clusterLink {
// 连接创建的时间
mstime_t ctime; /* Link creation time */
// TCP连接的文件描述符
int fd; /* TCP socket file descriptor */
// 输出(发送)缓冲区
sds sndbuf; /* Packet send buffer */
// 输入(接收)缓冲区
sds rcvbuf; /* Packet reception buffer */
// 关联该连接的节点
struct clusterNode *node; /* Node related to this link if any, or NULL */
} clusterLink;
该结构用于集群两个节点之间相互发送消息。如果节点A发送MEET
消息给节点B,那么节点A会创建一个clusterLink
结构的连接,fd
设置为连接后的套节字,node
设置为节点B,最后将该clusterLink
结构保存到节点B的link
中。
3.2 节点握手
当我们创建好了6个节点时,需要通过节点握手来感知到到指定的进程。节点握手是指一批运行在集群模式的节点通过Gossip
协议彼此通信。节点握手是集群彼此通信的第一步,可以详细分为这几个过程:
myself
节点发送MEET
消息给目标节点。- 目标节点处理
MEET
消息,并回复一个PONG
消息给myself
节点。 myself
节点处理PONG
消息,回复一个PING
消息给目标节点。
这里只列出了握手阶段的通信过程,之后无论什么节点,都会每隔1s
发送一个PING
命令给随机筛选出的5
个节点,以进行故障检测。
接下来会分别以myself
节点和目标节点的视角分别剖析这个握手的过程。
3.2.1 myself
节点发送 MEET 消息
由客户端发起命令:cluster meet <ip> <port>
当节点接收到客户端的cluster meet
命令后会调用对应的函数来处理命令,该命令的执行函数是clusterCommand()
函数,该函数能够处理所有的cluster
命令,因此我们列出处理meet
选项的代码:
// CLUSTER MEET <ip> <