cluster failover bug

181 阅读4分钟

最近在折腾 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 吧。