共计 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 个步骤,
- slave 发动申请,gossip 音讯携带 CLUSTERMSG_TYPE_MFSTART 标识。
- master 阻塞 client,停服工夫为 2 倍 CLUSTER_MF_TIMEOUT,目前版本为 10s。
- slave 追赶主从复制 offset 数据。
- slave 开始发动选举,并最终入选。
- slave 切换本身 role,接管 slots,并播送新的路由信息。
- 其余节点更改路由,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 失落。