当集群中大多数节点不可用时,不能通过 add_peer
/remove_peer
/change_peer
等接口进行配置变更,则可以使用 reset_peer
重置节点配置:
- 用户调用
reset_peers
重置某个节点的配置 - 若当前节点有配置变更正在进行,则返回
EBUSY
- 将当前节点的内存配置设为新配置,并调用
step_down
准备重新选举;在此之后,重置接口返回成功,但变更仍在继续。 - 超过
election_timeout_ms
时间后,集群进行重新选举,并且当前节点一定会被当选为 Leader,原因如下:- 选举是以节点配置为选举视角,而老集群中只有被重置了的节点才拥有新集群配置
- 新加入的节点没有任何日志
- 待其当选 Leader 后,会将新配置日志进行持久化,并复制给所有新集群中的成员:
- 5.1 若配置日志持久化成功,则重置成功
- 5.2 若在此之前 Leader Crash 了,则重置失败,用户需要重新调用
reset_peers
- 其他节点在接收到新配置后,则立即应用新配置
从上可以看出,reset_peer
是通过先将一个节点内存中的配置设为新配置,使其以新配置的视角进行重新选举,这样可以确保该节点能成为 Leader。待其成为 Leader 后,将新配置持久化,并且通过日志分发新配置,使新集群中的所有节点都使用新配置。
不建议使用 reset_peers
,因为其会破坏数据的一致性,而且可能会造成脑裂,下图这个列子将会说明这个问题:
- T1:由
ABCDE
5 个节点组成的集群中,CDE
宕机导致集群没有 Leader - T2:将
A
节点通过reset_peer
重置为AB
,其成功选举成 Leader,并将配置同步给B
- T3:节点
CDE
重新上线 - T4:由于不再收到 Leader
A
的心跳,CDE
重新选举并选出 LeaderC
,此时集群中将同时存在 2 个 Leader
此时 Leader A
和 C
的 term
是一样的,其中的 B
节点会接收到这 2 个 Leader 的日志复制请求,而当前接受日志只检测日志的 term
和 index
,这可能会导致数据错乱,也就是说 B
中的连续日志可能一部分来自 A
,一部分来自 C
。
class Node {
public:
// Reset the configuration of this node individually, without any repliation
// to other peers before this node beomes the leader. This function is
// supposed to be inovoked when the majority of the replication group are
// dead and you'd like to revive the service in the consideration of
// availability.
// Notice that neither consistency nor consensus are guaranteed in this
// case, BE CAREFULE when dealing with this method.
butil::Status reset_peers(const Configuration& new_peers);
};
用户调用 reset_peers
重置节点配置,其流程详见以下注释:
butil::Status NodeImpl::reset_peers(const Configuration& new_peers) {
...
// (1) 如果当前节点还是 Leader 并且有配置变更正在进行,则返回 EBUSY
// check concurrent conf change
if (_state == STATE_LEADER && _conf_ctx.is_busy()) {
...
return butil::Status(EBUSY, "Changing to another configuration");
}
// (2) 如果当前配置等于新配置,就返回成功
// 这个判断可以保证重复重置
// check equal, maybe retry direct return
if (_conf.conf.equals(new_peers)) {
return butil::Status::OK();
}
// (3) 将当前节点的内存配置设为新配置
Configuration new_conf(new_peers);
...
_conf.conf = new_conf;
_conf.old_conf.reset();
...
// (4) 调用 step_down 触发选举
step_down(_current_term + 1, false, status);
return butil::Status::OK();
}
void NodeImpl::step_down(const int64_t term, bool wakeup_a_candidate,
const butil::Status& status) {
...
// (5) 增加自身的 term、将 votedFor 设为空,并将其持久化
// meta state
if (term > _current_term) {
_current_term = term;
_voted_id.reset();
//TODO: outof lock
butil::Status status = _meta_storage->
set_term_and_votedfor(term, _voted_id, _v_group_id);
...
}
...
// (6) 启动选举定时器,该定时器超时后就会触发选举
// 先 PreVote 再 RequestVote
_election_timer.start();
}
该阶段就是集群进行选举,选举是以当前节点配置作为视角进行选举,所以只有执行重置的节点的配置是新集群配置,能选举成功。选举流程详见<3.1 选举流程>。
在节点成为 Leader 后,就会持久化新配置日志,并且通过复制将新配置日志同步给所有节点。所有节点接收到新配置日志后,就会立马应用该配置。具体的流程我们在<3.1 选举流程>中都已经介绍过了,详见: