一:主从复制
在集群中,为了保证集群的健壮性,通常设置一部分集群节点为主节点,另一部分集群节点为这些主节点的从节点。一般情况下,需要保证每个主节点至少有一个从节点。
集群初始化时,每个集群节点都是以独立的主节点角色而存在的,通过向集群节点发送”CLUSTER MEET <ip> <port>”命令,可以使集群节点间相互认识。节点间相互认识之后,可以通过向某些集群节点发送"CLUSTER REPLICATE <nodeID>"命令,使收到命令的集群节点成为<nodeID>节点的从节点。
在函数clusterCommand中,处理这部分的代码如下:
else if (!strcasecmp(c->argv[1]->ptr,"replicate") && c->argc == 3) {
/* CLUSTER REPLICATE <NODE ID> */
clusterNode *n = clusterLookupNode(c->argv[2]->ptr);
/* Lookup the specified node in our table. */
if (!n) {
addReplyErrorFormat(c,"Unknown node %s", (char*)c->argv[2]->ptr);
return;
}
/* I can't replicate myself. */
if (n == myself) {
addReplyError(c,"Can't replicate myself");
return;
}
/* Can't replicate a slave. */
if (nodeIsSlave(n)) {
addReplyError(c,"I can only replicate a master, not a slave.");
return;
}
/* If the instance is currently a master, it should have no assigned
* slots nor keys to accept to replicate some other node.
* Slaves can switch to another master without issues. */
if (nodeIsMaster(myself) &&
(myself->numslots != 0 || dictSize(server.db[0].dict) != 0)) {
addReplyError(c,
"To set a master the node must be empty and "
"without assigned slots.");
return;
}
/* Set the master. */
clusterSetMaster(n);
clusterDoBeforeSleep(CLUSTER_TODO_UPDATE_STATE|CLUSTER_TODO_SAVE_CONFIG);
addReply(c,shared.ok);
}
"CLUSTER REPLICATE"命令的格式是"CLUSTER REPLICATE <nodeID>";
首先,根据命令参数<nodeID>,从字典server.cluster->nodes中寻找对应的节点n;如果找不到n,或者,如果n就是当前节点,或者,n节点是个从节点,则回复客户端错误信息后返回;
如果当前节点为主节点,则当前节点不能有负责的槽位,当前节点的数据库也必须为空,如果不满足以上任一条件,则将不能置当前节点为从节点,因此回复客户端错误信息后,直接返回;
接下来,调用clusterSetMaster函数置当前节点为n节点的从节点,最后,回复客户端"OK";
clusterSetMaster函数的代码如下:
void clusterSetMaster(clusterNode *n) {
redisAssert(n != myself);
redisAssert(myself->numslots == 0);
if (nodeIsMaster(myself)) {
myself->flags &= ~REDIS_NODE_MASTER;
myself->flags |= REDIS_NODE_SLAVE;
clusterCloseAllSlots();
} else {
if (myself->slaveof)
clusterNodeRemoveSlave(myself->slaveof,myself);
}
myself->slaveof = n;
clusterNodeAddSlave(n,myself);
replicationSetMaster(n->ip, n->port);
resetManualFailover();
}
首先,必须保证n不是当前节点,而且当前节点没有负责任何槽位;
如果当前节点已经是主节点了,则将节点标志位中的REDIS_NODE_MASTER标记清除,并增加REDIS_NODE_SLAVE标记;然后调用clusterCloseAllSlots函数,置server.cluster->migrating_slots_to和server.cluster->importing_slots_from为空;
如果当前节点为从节点,并且目前已有主节点,则调用clusterNodeRemoveSlave函数,将当前节点从其当前主节点的slaves数组中删除,解除当前节点与其当前主节点的关系;
然后,置myself->slaveof为n,调用clusterNodeAddSlave函数,将当前节点插入到n->slaves中;
然后,调用replicationSetMaster函数,这里直接复用了主从复制部分的代码,相当于向当前节点发送了"SLAVE OF"命令,开始主从复制流程;
最后,调用resetManualFailover函数,清除手动故障转移状态;
二:故障转移
1:纪元(epoch)
理解Redis集群中的故障转移,必须要理解纪元(epoch)在分布式Redis集群中的作用,Redis集群使用RAFT算法中类似term的概念,在Redis集群中这被称之为纪元(epoch)。纪元的概念在介绍哨兵时已经介绍过了,在Redis集群中,纪元的概念和作用与哨兵中的纪元类似。Redis集群中的纪元主要是两种:currentEpoch和configEpoch。
a、currentEpoch
这是一个集群状态相关的概念,可以当做记录集群状态变更的递增版本号。每个集群节点,都会通过server.cluster->currentEpoch记录当前的currentEpoch。
集群节点创建时,不管是主节点还是从节点,都置currentEpoch为0。当前节点接收到来自其他节点的包时,如果发送者的currentEpoch(消息头部会包含发送者的currentEpoch)大于当前节点的currentEpoch,那么当前节点会更新currentEpoch为发送者的currentEpoch。因此,集群中所有节点的currentEpoch最终会达成一致,相当于对集群状态的认知达成了一致。
currentEpoch作用在于,当集群的状态发生改变,某个节点为了执行一些动作需要寻求其他节点的同意时,就会增加currentEpoch的值。目前currentEpoch只用于从节点的故障转移流程,这就跟哨兵中的sentinel.current_epoch作用是一模一样的。
当从节点A发现其所属的主节点下线时,就会试图发起故障转移流程。首先就是增加currentEpoch的值,这个增加后的currentEpoch是所有集群节点中最大的。然后从节点A向所有节点发包用于拉票,请求其他主节点投票给自己,使自己能成为新的主节点。
其他节点收到包后,发现发送者的currentEpoch比自己的currentEpoch大,就会更新自己的currentEpoch,并在尚未投票的情况下,投票给从节点A,表示同意使其成为新的主节点。
b、configepoch
这是一个集群节点配置相关的概念,每个集群节点都有自己独一无二的configepoch。所谓的节点配置,实际上是指节点所负责的槽位信息。
每一个主节点在向其他节点发送包时,都会附带其configEpoch信息,以及一份表示它所负责槽位的位数组信息。而从节点向其他节点发送包时,包中的configEpoch和负责槽位信息,是其主节点的configEpoch和负责槽位信息。节点收到包之后,就会根据包中的configEpoch和负责槽位信息,记录到相应节点属性中。
configEpoch主要用于解决不同的节点的配置发生冲突的情况。举个例子就明白了:节点A宣称负责槽位1,其向外发送的包中,包含了自己的configEpoch和负责槽位信息。节点C收到A发来的包后,发现自己当前没有记录槽位1的负责节点(也就是server.cluster->slots[1]为NULL),就会将A置为槽位1的负责节点(server.cluster->slots[1]= A),并记录节点A的configEpoch。后来,节点C又收到了B发来的包,它也宣称负责槽位1,此时,如何判断槽位1到底由谁负责呢?这就是configEpoch起作用的时候了,C在B发来的包中,发现它的configEpoch,要比A的大,说明B是更新的配置,因此,就将槽位1的负责节点设置为B(server.cluster->slots[1] = B)。
在从节点发起选举,获得足够多的选票之后,成功当选时,也就是从节点试图替代其下线主节点,成为新的主节点时,会增加它自己的configEpoch,使其成为当前所有集群节点的configEpoch中的最大值。这样,该从节点成为主节点后,就会向所有节点发送广播包,强制其他节点更新相关槽位的负责节点为自己。
2:故障转移概述
集群中,当某个从节点发现其主节点下线时,就会尝试在未来某个时间点发起故障转移流程。具体而言就是先向其他集群节点发送CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST包用于拉票,集群主节点收到这样的包后,如果在当前选举纪元中没有投过票,就会向该从节点发送CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK包,表示投票给该从节点。从节点如果在一段时间内收到了大部分主节点的投票,则表示选举成功,接下来就是升级为主节点,并接管原主节点所负责的槽位,并将这种变化广播给其他所有集群节点,使它们感知这种变化,并修改自己记录的配置信息。
接下来,就是故障转移中各个环节的详细描述:
3:从节点的选举和升级
3.1、从节点发起故障转移的时间
从节点在发现其主节点下线时,并不是立即发起故障转移流程,而是要等待一段时间,在未来的某个时间点才发起选举。这个时间点是这样计算的:
mstime() + 500ms + random()%500ms + rank*1000ms
其中,固定延时500ms,是为了留出时间,使主节点下线的消息能传播到集群中其他节点,这样集群中的主节点才有可能投票;随机延时是为了避免两个从节点同时开始故障转移流程;rank表示从节点的排名,排名是指当前从节点在下线主节点的所有从节点中的排名,排名主要是根据复制数据量来定,复制数据量越多,排名越靠前,因此,具有较多复制数据量的从节点可以更早发起故障转移流程,从而更可能成为新的主节点。
rank主要是通过调用clusterGetSlaveRank得到的,该函数的代码如下:
int clusterGetSlaveRank(void) {
long long myoffset;
int j, rank = 0;
clusterNode *master;
redisAssert(nodeIsSlave(myself));
master = myself->slaveof;
if (master == NULL) return 0; /* Never called by slaves without master. */
myoffset = replicationGetSlaveOffset();
for (j = 0; j < master->numslaves; j++)
if (master->slaves[j] != myself &&
master->slaves[j]->repl_offset > myoffset) rank++;
return rank;
}
</