关于redis:Redis-Cluster-write-safety-分析

2次阅读

共计 6183 个字符,预计需要花费 16 分钟才能阅读完成。

redis cluster 是 redis 的分布式实现。如同官网文档 cluster-spec 强调的那样,其设计 优先思考高性能和线性扩大能力,并尽最大致力保障 write safety

这里所说的 write 失落是指,回复 client ack 后,后续申请中呈现数据未做变更或失落的状况,次要有主从切换、实例重启、脑裂等三种状况可能导致该问题,上面顺次剖析。

case 1 主从切换

failover 会带来路由的变更,被动 / 被动状况须要离开探讨。

被动 failover

为表白不便,有以下假如,cluster 状态失常,node C 为 master,负责 slot 1-100,对应 slave 为 C’。

master C 挂掉后,slave C’ 在 最多 2 倍 cluster_node_timeout 的工夫 内把 C 标记成 FAIL,进而触发 failover 逻辑。

在 slave C’ 胜利切换为 master 前,1-100 slot 依然由 C 负责,拜访会报错。C’ 切为 master 后,gossip 播送路由变更,在这个过程中,client 拜访 C’,仍能够失去失常的回应,而拜访其余持有老路由的 node,申请会被 MOVED 到挂掉的 C,拜访报错。

惟一可能呈现 write 失落的 case 由主从异步复制机制导致
如果写到 master 上的数据还没有来得及同步到 slave 就挂掉了,那么这部分数据就会失落(重启后不存在 merge 操作)。master 回复 client ack 与同步 slave 简直是同时进行的,这种状况很少产生,但这是一个危险,工夫窗口很小。

被动 failover

被动 failover 通过 sysadmin 在 slave node 上执行 CLUSTER FAILOVER [FORCE|TAKEOVER] 命令触发。

残缺 manual failover 过程在之前的博客具体探讨过,概括为以下 6 个步骤,

  1. slave 发动申请,gossip 音讯携带 CLUSTERMSG_TYPE_MFSTART 标识。
  2. master 阻塞 client,停服工夫为 2 倍 CLUSTER_MF_TIMEOUT,目前版本为 10s。
  3. slave 追赶主从复制 offset 数据。
  4. slave 开始发动选举,并最终入选。
  5. slave 切换本身 role,接管 slots,并播送新的路由信息。
  6. 其余节点更改路由,cluster 路由打平。

三个选项别离有不同的行为,剖析如下,
(1)默认选项。
执行残缺的 mf 流程,master 有停服行为,因而不存在 write 失落的问题。

(2)FORCE 选项。
从第 4 步开始执行。在 slave C’ 统计选票阶段,master C 依然能够失常接管用户申请,且主从异步复制,这些都可能导致 write 失落。mf 将在将来的某个工夫点开始执行,timeout 工夫为 CLUSTER_MF_TIMEOUT(现版本为 5s),每次 clusterCron 都会查看。

(3)TAKEOVER 选项。
从第 5 步开始执行。slave 间接减少本人的 configEpoch(无需其余 node 批准),接管 slots。从 slave C’ 切换为 master,到原 master 节点 C 更新路由,发到 C 的申请,都可能存在 write 失落的可能,个别在一个 ping 的工夫内实现,工夫窗口很小。C 和 C’ 以外节点更新路由滞后只会带来多一次的 MOVED 谬误,不会导致 write 失落。

case 2 master 重启

cluster 状态初始化

clusterState 构造体中有一个 state 成员变量,示意 cluster 的全局状态,管制着以后 cluster 是否能够提供服务,有以下两种取值,

 #define CLUSTER_OK 0 /* Everything looks ok */
 #define CLUSTER_FAIL 1 /* The cluster can't work */

server 重启后,state 被初始化为 CLUSTER_FAIL,代码逻辑能够在 clusterInit 函数中找到。

对于 CLUSTER_FAIL 状态的 cluster 是拒绝接受拜访的,代码参考如下,

 int processCommand(client *c) {
      ...
      if (server.cluster_enabled &&
      !(c->flags & CLIENT_MASTER) &&
      !(c->flags & CLIENT_LUA &&
      server.lua_caller->flags & CLIENT_MASTER) &&
      !(c->cmd->getkeys_proc == NULL && c->cmd->firstkey == 0 &&
      c->cmd->proc != execCommand))
     {
          int hashslot;
          int error_code;

          clusterNode *n = getNodeByQuery(c,c->cmd,c->argv,c->argc,
          &hashslot,&error_code);
          ...
     }
     ...
 }

重点在 getNodeByQuery 函数,在 cluster 模式开启后,用来查找到真正要执行 command 的 node。

留神:redis cluster 采纳去中心化的路由管理策略,每一个 node 都能够间接拜访,如果要执行 command 的 node 不是以后连贯的,它会返回一个 -MOVED 的重定向谬误,指向真正要执行 command 的 node。

上面看 getNodeByQuery 函数的局部逻辑,

 clusterNode *getNodeByQuery(client *c, 
    struct redisCommand *cmd, robj **argv, 
    int argc, int *hashslot, 
    int *error_code) {
        ...
        if (server.cluster->state != CLUSTER_OK) {if (error_code) *error_code = CLUSTER_REDIR_DOWN_STATE;
            return NULL;
    }
    ...
 }

能够看到,必须是 CLUSTER_OK 状态的 cluster 能力失常拜访。

咱们说,这种限度对于 保障 Write safety 是十分有必要的!
能够设想,如果 master A 挂掉后,对应的 slave A’ 通过选举胜利入选为新 master。此时,A 重启,且恰好有一些 client 看到的路由没有更新,它们依然会往 A 上写数据,如果承受这些 write,就会丢数据!A’ 才是这个 sharding 大家公认的 master。所以,A’ 重启后须要先禁用服务,直到路由变更实现。

cluster 状态变更

那么,什么时候 cluster 才会呈现 CLUSTER_FAIL -> CLUSTER_OK 的状态变更呢?答案要在 clusterCron 定时工作里找。

 void clusterCron(void) {
    ...
    if (update_state || server.cluster->state == CLUSTER_FAIL)
        clusterUpdateState();}

要害逻辑在 clusterUpdateState 函数里。

#define CLUSTER_WRITABLE_DELAY 2000
void clusterUpdateState(void) {
    static mstime_t first_call_time = 0;
    ...
    if (first_call_time == 0) first_call_time = mstime();
    if (nodeIsMaster(myself) &&
        server.cluster->state == CLUSTER_FAIL &&
        mstime() - first_call_time < CLUSTER_WRITABLE_DELAY) return;
  
    new_state = CLUSTER_OK;
    ...
    if (new_state != server.cluster->state) {
        ...
        server.cluster->state = new_state;
    }
}

在以上逻辑里能够看到,cluster 状态变更要提早 CLUSTER_WRITABLE_DELAY 毫秒,目前版本为 2s。

拜访提早就是为了期待路由变更,那么,什么时候触发路由变更呢?
咱们晓得,一个新 server 刚启动,它与其余 node 进行 gossip 通信的 link 都是 null,在 clusterCron 里查看进去后会顺次连贯,并发送 ping。作为一个路由过期的老节点,收到其余节点发来的 update 音讯,更改本身路由。

CLUSTER_WRITABLE_DELAY 毫秒后,A 节点复原拜访,咱们认为 CLUSTER_WRITABLE_DELAY 的工夫窗口足够更新路由。

case 3 partition

partition 产生

因为网络的不牢靠,网络分区是一个必须要思考的问题,也即 CAP 实践中的 P。

partition 产生后,cluster 被割裂成 majority 和 minority 两局部,这里以分区中 master 节点的数量来辨别。

(1)对于 minority 局部,slave 会发动选举,然而不能收到大多数 master 的选票,也就无奈实现失常的 failover 流程。同时在 clusterCron 里大部分节点会被标记为 CLUSTER_NODE_PFAIL 状态,进而触发 clusterUpdateState 的逻辑,大略如下,

void clusterCron(void) {
    ...
    di = dictGetSafeIterator(server.cluster->nodes);
    while((de = dictNext(di)) != NULL) {
        ...
        delay = now - node->ping_sent;
        if (delay > server.cluster_node_timeout) {if (!(node->flags & (CLUSTER_NODE_PFAIL|CLUSTER_NODE_FAIL))) {serverLog(LL_DEBUG,"*** NODE %.40s possibly failing", node->name);
                 node->flags |= CLUSTER_NODE_PFAIL;
                update_state = 1;
            }
        } 
    }
    ...
    if (update_state || server.cluster->state == CLUSTER_FAIL)
        clusterUpdateState();}

而在 clusterUpdateState 函数里,会扭转 cluster 的状态。

void clusterUpdateState(void) {
    static mstime_t among_minority_time;
    ...
    {
        dictIterator *di;
        dictEntry *de;
        server.cluster->size = 0;
        di = dictGetSafeIterator(server.cluster->nodes);
        while((de = dictNext(di)) != NULL) {clusterNode *node = dictGetVal(de);
            if (nodeIsMaster(node) && node->numslots) {
                server.cluster->size++;
                if ((node->flags & (CLUSTER_NODE_FAIL|CLUSTER_NODE_PFAIL)) == 0)
                    reachable_masters++;
            }
        }
        dictReleaseIterator(di);
    }
    {int needed_quorum = (server.cluster->size / 2) + 1;
​
        if (reachable_masters < needed_quorum) {
            new_state = CLUSTER_FAIL;
            among_minority_time = mstime();}
    }
    ...
}

由下面代码能够看出,在 minority 中,cluster 状态在一段时间后会被更改为 CLUSTER_FAIL。但,对于一个划分到 minority 的 master B,在状态更改前是始终能够拜访的,这就有一个工夫窗口,会导致 write 失落!!

clusterCron 函数中能够计算出这个工夫窗口的大小。
从 partition 工夫开始算起,cluster_node_timeout 工夫后才会有 node 标记为 PFAIL,加上 gossip 音讯流传会偏差于携带 PFAIL 的节点,node B 不用等到 cluster_node_timeout/2 把 cluster nodes ping 遍,就能够将 cluster 标记为 CLUSTER_FAIL。能够推算出,工夫窗口大概为 cluster_node_timeout

另外,会记录下禁用服务的工夫,即 among_minority_time。

(2)对于 majority 局部 ,slave 会发动选举,以 B 的 slave B’ 为例,failover 切为新的 master,并提供服务。
如果 partition 工夫小于 cluster_node_timeout,以至于没有 PFAIL 标识呈现,就不会有 write 失落。

partition 复原

当 partition 复原后,minority 中的 老 master B 从新加进 cluster,B 要想提供服务,就必须先将 cluster 状态从 CLUSTER_FAIL 批改为 CLUSTER_OK,那么,应该什么时候改呢?

咱们晓得 B 中是旧路由,此时它应该变更为 slave,所以,还是须要期待一段时间做路由变更,否则有可能呈现 write 失落的问题(后面剖析过),同样在 clusterUpdateState 函数的逻辑里。

#define CLUSTER_MAX_REJOIN_DELAY 5000
#define CLUSTER_MIN_REJOIN_DELAY 500
void clusterUpdateState(void) {
    ...
    if (new_state != server.cluster->state) {
        mstime_t rejoin_delay = server.cluster_node_timeout;
​
        if (rejoin_delay > CLUSTER_MAX_REJOIN_DELAY)
            rejoin_delay = CLUSTER_MAX_REJOIN_DELAY;
        if (rejoin_delay < CLUSTER_MIN_REJOIN_DELAY)
            rejoin_delay = CLUSTER_MIN_REJOIN_DELAY;
​
        if (new_state == CLUSTER_OK &&
            nodeIsMaster(myself) &&
            mstime() - among_minority_time < rejoin_delay) 
        {return;}
    }
}

能够看出,工夫窗口为 cluster_node_timeout,最多 5s,起码 500ms。

小结

failover 可能因为选举和主从异步复制数据偏差带来 write 失落。
master 重启通过 CLUSTER_WRITABLE_DELAY 提早,等 cluster 状态变更为 CLUSTER_OK,能够从新拜访,不存在 write 失落。
partition 中的 minority 局部,在 cluster 状态变更为 CLUSTER_FAIL 之前,可能存在 write 失落。
partition 复原后,通过 rejoin_delay 提早,等 cluster 状态变更为 CLUSTER_OK,能够从新拜访,不存在 write 失落。

正文完
 0