最近在折腾 redis 各版本的测试,发现一个有趣的问题,关于 redis cluster 被动 failover,这里做下记录。
1. 问题描述
首先,把问题做简要描述,操作过程大致如图所示,
redis cluster 某 partition 有 3 个节点,A 是 master 节点,A1 和 A2 是挂在 A 下面的 replica。
做如下操作,挂掉 A 节点,A1 和 A2 会在不同时刻发起选举竞选 master。
假设 A1 当选为新的 master,A2 会收到 A1 成为新的 master 的 gossip 消息,此时 A2 会主动 connect A1 做 replication。
在 replication 还未完成时,马上挂掉 A1。
在这个场景下, 3.2 版本 redis 的 A2 是无法顺利切成 master 的。
2. 问题分析
2.1 变更 master
A1 节点成功当选为 master ,随后广播 gossip pong 消息, A2 收到该消息后开始 reset master,代码调用路径为 clusterUpdateSlotsConfigWith
-> clusterSetMaster
-> replicationSetMaster
。
这里需要重点关注 server.repl_down_since
这个变量值的变更。
void replicationSetMaster(char *ip, int port) {
...
if (server.master) freeClient(server.master);
...
server.repl_state = REPL_STATE_CONNECT;
server.master_repl_offset = 0;
server.repl_down_since = 0;
}
对于已经有了 master 的 A2 节点来说,需要先释放掉原来的 master 结构,代码调用路径为
freeClient
-> replicationCacheMaster
-> replicationHandleMasterDisconnection
。
void replicationHandleMasterDisconnection(void) {
server.master = NULL;
server.repl_state = REPL_STATE_CONNECT;
server.repl_down_since = server.unixtime;
}
经过上面的梳理,可以发现,A2 节点的 server.repl_down_since
变量先变更为当前时间戳,最后变更为 0。
2.2 发起选举
slave A2 节点检测到 master 为 fail 后,开始准备做 failover。
if (nodeIsMaster(myself) ||
myself->slaveof == NULL ||
(!nodeFailed(myself->slaveof) && !manual_failover) ||
myself->slaveof->numslots == 0)
{
server.cluster->cant_failover_reason = CLUSTER_CANT_FAILOVER_NONE;
return;
}
在上一步变更 master 的节点时,A2 就会把 A1 标记成 master,只是还没完成来得及做 replication,A1 就挂掉了。因此以上条件的检查是可以通过的。
但,在做 failover 之前,slave 节点会需要检查自己的 data_age 是否满足条件。
从数据一致性的角度来讲,如果 failing master 的 slave data 过旧,要阻止它做 failover。
过旧的 data 对部分 user 来说是不可接受的,宁愿让这个 partition 不可访问。但对另一部分 user 来说,可用性第一,要让服务先恢复。
那么如何衡量一个 slave 的 data 是否过旧呢?redis 使用了一个 data age 的概念,运作机制如下:
① 如果有多个 slave 都可以做 failover,它们会交换信息,通过 offset 做 rank,与 master 数据 diff 更小的 slave 会更早发起选举,即,谁的 data 更新,谁更有机会成为 master。
② 每个 slave 会记录与其 master 一次交互的时间。
如果与 master 仍然是 connected 状态,使用 lastinteraction
变量,每次收到 master 命令和 reply master 时做更新;
如果 replication link down 掉了,使用 repl_down_since
变量,它表示与 master 断开连接的时间。
关于第②条,user 可以自己调整。特别是,当一个 slave 与其 master 失联时间超过 (node-timeout * slave-validity-factor) + repl-ping-slave-period
秒,将不被允许执行 failover。
void clusterHandleSlaveFailover(void) {
...
if (server.repl_state == REPL_STATE_CONNECTED) {
data_age = (mstime_t)(server.unixtime - server.master->lastinteraction) * 1000;
} else {
data_age = (mstime_t)(server.unixtime - server.repl_down_since) * 1000;
}
...
if (server.cluster_slave_validity_factor &&
data_age >
(((mstime_t)server.repl_ping_slave_period * 1000) +
(server.cluster_node_timeout * server.cluster_slave_validity_factor)))
{
if (!manual_failover) {
clusterLogCantFailover(CLUSTER_CANT_FAILOVER_DATA_AGE);
return;
}
}
...
}
可以发现, slave-validity-factor 配置项的设置是很关键的。
如果 slave-validity-factor 值偏大,可能会允许那些持有过旧 data 的 slave 发起 failover,主从 data diff 差别过大;
如果 slave-validity-factor 值偏小,将会导致 redis cluster 无法完成自愈,可用性保证降低。
当然了,slave-validity-factor 也可以设置为 0,这就意味着 data age 不会阻挠 slave 发起 failover,但是它们依然会按照 rank 的顺序发起 failover。
回到上面做的测试里。由于 slave 节点 A2 尚未完成主从复制,复制状态机不会走到 REPL_STATE_CONNECTED,所以要使用 repl_down_since
来做 data_age
判断。
但是,它的 server.repl_down_since
值为 0, data_age
计算出来是一个很大的值,在设置了 cluster-slave-validity-factor 配置项,且不是手动 failover 的情况下,A2 节点永远也走不到后面发起选举的逻辑,这时会留下日志 "Disconnected from master for longer than allowed...."。
2.3 bug fix
对于上面使用 3.2 版本 redis 做测试遇到的问题,社区当成了 bug 来处理,在 4.0.11 版本做了 bug fix,具体可以查看 cluster failover bug #5081。
void replicationSetMaster(char *ip, int port) {
int was_master = server.masterhost == NULL;
sdsfree(server.masterhost);
server.masterhost = sdsnew(ip);
server.masterport = port;
if (server.master) {
freeClient(server.master);
}
disconnectAllBlockedClients(); /* Clients blocked in master, now slave. */
disconnectSlaves();
cancelReplicationHandshake();
if (was_master) replicationCacheMasterUsingMyself();
server.repl_state = REPL_STATE_CONNECT;
}
关键修改点是,freeClient
里设置的 repl_down_since
不会被置 0 。
3. 另一个场景
经过上面的分析,我又构造了另一个场景:将某个 patition 的主从节点同时挂掉,然后重启 slave 节点,看集群恢复情况。
slave 节点重启后,repl_down_since
被初始化为 0,将自己标记为 CLUSTER_NODE_SLAVE,在 clusterCron
定时函数中做 replicationSetMaster
,这又回到了上面场景中的函数调用链,依然无法顺利发起选举。
这个问题在最新的版本也是存在,有人在社区反馈过 github.com/redis/redis…
其实,这里 slave 启动后无法切主,可以使用 cluster failover force 命令去做主动 failover,这点在运维过程中需要格外。
其实,在我看来,repl_down_since
变量的设计,本来就是对可用性(A)和数据一致性(C) 做出的 tradeoff,也不能算是 bug 吧。