说明
本文以开源框架nacos,版本号为1.3.2针对leader 的选举进行源码解析 ,leader 选举采用raft 算法实现,所以对raft 算法原理也会进行阐述;其中本文针对源码的理解提出了自己的理解,如有疑问请留言
Nacos源码下载地址:https://github.com/alibaba/nacos
Raft 算法原理
Raft理论是分布式数据一致性算法,为了便于理解Raft算法分成了4个部分:
-Leader选举
-日志复制
-成员变更
-日志压缩
本文章只讲述leader 选举
系统角色:
Leader、Candidate、Follower
- Leader:接受客户端请求,并向Follower同步请求日志,当日志同步到大多数节点上后告诉Follower提交日志。
- Follower:接受并持久化Leader同步的日志,在Leader告之日志可以提交之后,提交日志。
- Candidate:Leader选举过程中的临时角色。
term
Raft 算法将时间划分成为任意不同长度的任期(term)。任期用连续的数字进行表示。每一个任期的开始都是一次选举(election),一个或多个候选人会试图成为领导人。如果一个候选人赢得了选举,它就会在该任期的剩余时间担任领导人。在某些情况下,选票会被瓜分,有可能没有选出领导人,那么,将会开始另一个任期,并且立刻开始下一次选举。Raft 算法保证在给定的一个任期最多只有一个领导人
选举原则:
典型的投票选举算法(少数服从多数),也就是说,在一定周期内获得投票最多的节点成为主节点。每个term都是一个选举时间,选择出来的leader 都有一个任期时间,超时后在重新选择.
原理参考网址:RAFT算法详解_青萍之末的博客-优快云博客_raft算法
常见问题
Raft 选举以问题的方式来理解如何通过raft 算法来实现选举
p1.leader选举什么时候发生:
- 刚启动时,所有节点都是follower,这个时候发起选举,选出一个leader;
- 当leader挂掉后,时钟最先跑完的follower发起重新选举操作,选出一个新的leader。
- 成员变更的时候会发起选举操作。
p2.Raft中选举中给候选人投票的前提?
Raft确保新当选的Leader包含所有已提交(集群中大多数成员中已提交)的日志条目。这个保证是在RequestVoteRPC阶段做的,candidate在发送RequestVoteRPC时,会带上自己的last log entry的term_id和index,follower在接收到RequestVoteRPC消息时,如果发现自己的日志比RPC中的更新,就拒绝投票。日志比较的原则是,如果本地的最后一条log entry的term id更大,则更新,如果term id一样大,则日志更多的更大(index更大)
p3 如何避免网络恢复后,不发生切主?
某分区的2个节点由于永远得不到超过半数的投票,所以任值周期不断累加。当网络恢复后,原来的Leader由于任值周期小,切换为Follower状态,集群重新选主。
如何避免重新选主,将投票阶段分拆成两阶段,即预投票阶段和投票阶段。
预投票阶段:任值周期不累加,选出得票数过半的节点。
投票阶段:由在预投票阶段选出的节点发起投票请求,任值周期累加,最终选出主节点。
这样,某分区2个节点的任值周期就会小于等于原Leader的任值周期,当网络恢复后就不会重新选主。
Nacos leader 选举
Raft 竞选流程图
RaftCore核心类
raftCore#init 初始化方法,粘贴部分代码
//todo 每500毫秒就行leader 选举
GlobalExecutor.registerMasterElection(new MasterElection());
//todo 每500毫秒心跳
GlobalExecutor.registerHeartbeat(new HeartBeat());
MasterElection--leader 选举线程
任期超时才能发起选举请求,并重置心跳,leader任期时间,避免重复发起;
心跳时间每个节点都是不一样,采用随机值(15~20s);这样做的好处在于避免多节点同时发起选举,提高选举成功率
public void run() {
try {
//可以开始选举的标记
if (!peers.isReady()) {
return;
}
RaftPeer local = peers.local();
local.leaderDueMs -= GlobalExecutor.TICK_PERIOD_MS;//-定时任务500毫秒
//任期超时没到
if (local.leaderDueMs > 0) {
return;
}
// reset timeout 重置任期超时时间和心跳时间,准备开始拉票
local.resetLeaderDue();
local.resetHeartbeatDue();
//拉票
sendVote();
} catch (Exception e) {
Loggers.RAFT.warn("[RAFT] error while master election {}", e);
}
}
sendVote ---发起投票
任期term+1 ,并且状态设置为CANDIDATE候选者,投票自己,并异步的发送给其它节点,票数过半的为leader
private void sendVote() {
RaftPeer local = peers.get(NetUtils.localServer());
Loggers.RAFT.info("leader timeout, start voting,leader: {}, term: {}", JacksonUtils.toJson(getLeader()),
local.term);
//把投票信息设为null
peers.reset();
//term+1
local.term.incrementAndGet();
local.voteFor = local.ip;//投票给自己
local.state = RaftPeer.State.CANDIDATE;//切换角色为候选者
Map<String, String> params = new HashMap<>(1);
params.put("vote", JacksonUtils.toJson(local));
for (final String server : peers.allServersWithoutMySelf()) {
final String url = buildUrl(server, API_VOTE);
try {
HttpClient.asyncHttpPost(url, null, params, new AsyncCompletionHandler<Integer>() {
@Override
public Integer onCompleted(Response response) throws Exception {
if (response.getStatusCode() != HttpURLConnection.HTTP_OK) {
Loggers.RAFT
.error("NACOS-RAFT vote failed: {}, url: {}", response.getResponseBody(), url);
return 1;
}
RaftPeer peer = JacksonUtils.toObj(response.getResponseBody(), RaftPeer.class);
Loggers.RAFT.info("received approve from peer: {}", JacksonUtils.toJson(peer));
//统计投票结果,过半的为成功选举为leader
peers.decideLeader(peer);
return 0;
}
});
} catch (Exception e) {
Loggers.RAFT.warn("error while sending vote to server: {}", server);
}
}
}
}
RaftController#vote
Vote方法实际调的也是RaftCore的receivedVote;该方法用synchronized 修饰;目的确保选举的同步性,确保变量term的数据安全性;
投票规则:远程任期小于本地就投self;简单粗暴
public synchronized RaftPeer receivedVote(RaftPeer remote) {
if (!peers.contains(remote)) {
throw new IllegalStateException("can not find peer: " + remote.ip);
}
RaftPeer local = peers.get(NetUtils.localServer());
if (remote.term.get() <= local.term.get()) {//远程任期小于本地就投self
String msg = "received illegitimate vote" + ", voter-term:" + remote.term + ", votee-term:" + local.term;
Loggers.RAFT.info(msg);
if (StringUtils.isEmpty(local.voteFor)) {
local.voteFor = local.ip;
}
return local;
}
//重置leader term
local.resetLeaderDue();
//本地node 状态变更为跟随者
local.state = RaftPeer.State.FOLLOWER;
local.voteFor = remote.ip;
local.term.set(remote.term.get());//任期同步更新
Loggers.RAFT.info("vote {} as leader, term: {}", remote.ip, remote.term);
return local;
}
问题1:
心细的同学应该发现了问题;在讲解raft 原理中针对leader 选举提到投票阶段分拆成两阶段;可目前并木有,这样有可能会造成已有数据丢失;场景描述如下
场景:五个节点ABCDE,其中A,B,C 在一个网段Net1,其中A 为leader; D,E在另外一个网段Net2;那么Net2由于某种原因,某短暂时间不能和Net1通信,导致net2 节点D,E 进行leader选举,term 任期不断加1,而此时有服务注册,使ABC 节点服务数据有新增;
那么问题来了,当Net1和Net2网络恢复正常了,此时D 发起leader选举,由于term>A,B,C ,有可能选举通过并且最后成为leader ,这样就会造成数据丢失,因为A,B,C 的数据明显多于D,最后通过心跳 会把其它节点数据保存和新leaderD一制;数据成功丢失
针对这个问题,nacos 技术员巧妙的避开了,就是当有新数据添加时,term+100,也就是大概有25(一个任期在15~20秒)分钟时间恢复网络,所以很难有数据丢失,但是同样有易主现象(在数据没有新增的情况下);
问题2:
同样会出现双leader 问题;
场景:五个节点ABCDE,其中A,B 在一个网段Net1,其中A 为leader; C、D,E在另外一个网段Net2;那么Net2由于某种原因,某短暂时间不能和Net1通信,导致net2 节点C、D,E 进行leader选举,最终选出其中一个比如C;形成双主模式;(两个不同的term 有一个leader)
Raft算法采用任期过期来精选重选,避开了集群中有两个leader的现象,而nacos 自制的raft采用心跳机制避开双leader 现象;leader 发起心跳时会把其它节点包含另一个leader状态重置为follower(采用先下手为强的原则);即使两个leader 都重置为follower ;leader 任期超时后也会重新发起选举;
RaftCore的decideLeader--确定leader
超过半数表示投票成功,确定为leader; node个数在ServerMemberManager#init 初始化时回去读取cluster.conf 文件,或者远程读取等方式加入到缓存中
public RaftPeer decideLeader(RaftPeer candidate) {
peers.put(candidate.ip, candidate);
SortedBag ips = new TreeBag();
int maxApproveCount = 0;
String maxApprovePeer = null;
for (RaftPeer peer : peers.values()) {
if (StringUtils.isEmpty(peer.voteFor)) {
continue;
}
ips.add(peer.voteFor);
if (ips.getCount(peer.voteFor) > maxApproveCount) {//超过最大投票数就行更新
maxApproveCount = ips.getCount(peer.voteFor);
maxApprovePeer = peer.voteFor;
}
}
//超过半数
if (maxApproveCount >= majorityCount()) {
RaftPeer peer = peers.get(maxApprovePeer);
peer.state = RaftPeer.State.LEADER;
if (!Objects.equals(leader, peer)) {
leader = peer;
ApplicationUtils.publishEvent(new LeaderElectFinishedEvent(this, leader, local()));
Loggers.RAFT.info("{} has become the LEADER", leader.ip);
}
}
return leader;
}
HeartBeat ----心跳
心跳只能是leader 发送,并且附带数据同步给其它节点,使所有节点数据最终一致性
public class HeartBeat implements Runnable {
@Override
public void run() {
try {
if (!peers.isReady()) {
return;
}
RaftPeer local = peers.local();
local.heartbeatDueMs -= GlobalExecutor.TICK_PERIOD_MS;
if (local.heartbeatDueMs > 0) {
return;
}
//重置心跳
local.resetHeartbeatDue();
//发生心跳
sendBeat();
} catch (Exception e) {
Loggers.RAFT.warn("[RAFT] error while sending beat {}", e);
}
}
sendBeat -- 发送心跳
单机或者不是leader 不会发送心跳,把leader 的相关数据打包给其它节点
private void sendBeat() throws IOException, InterruptedException {
RaftPeer local = peers.local();
//单机或者不是leader 不会发心跳
if (ApplicationUtils.getStandaloneMode() || local.state != RaftPeer.State.LEADER) {
return;
}
if (Loggers.RAFT.isDebugEnabled()) {
Loggers.RAFT.debug("[RAFT] send beat with {} keys.", datums.size());
}
//重置任期
local.resetLeaderDue();
// build data
ObjectNode packet = JacksonUtils.createEmptyJsonNode();
packet.replace("peer", JacksonUtils.transferToJsonNode(local));
ArrayNode array = JacksonUtils.createEmptyArrayNode();
if (switchDomain.isSendBeatOnly()) {
Loggers.RAFT.info("[SEND-BEAT-ONLY] {}", String.valueOf(switchDomain.isSendBeatOnly()));
}
if (!switchDomain.isSendBeatOnly()) {
for (Datum datum : datums.values()) {
ObjectNode element = JacksonUtils.createEmptyJsonNode();
if (KeyBuilder.matchServiceMetaKey(datum.key)) {
element.put("key", KeyBuilder.briefServiceMetaKey(datum.key));
} else if (KeyBuilder.matchInstanceListKey(datum.key)) {
element.put("key", KeyBuilder.briefInstanceListkey(datum.key));
}
element.put("timestamp", datum.timestamp.get());
array.add(element);
}
}
packet.replace("datums", array);
// broadcast
Map<String, String> params = new HashMap<String, String>(1);
params.put("beat", JacksonUtils.toJson(packet));
String content = JacksonUtils.toJson(params);
ByteArrayOutputStream out = new ByteArrayOutputStream();
GZIPOutputStream gzip = new GZIPOutputStream(out);
gzip.write(content.getBytes(StandardCharsets.UTF_8));
gzip.close();
byte[] compressedBytes = out.toByteArray();
String compressedContent = new String(compressedBytes, StandardCharsets.UTF_8);
if (Loggers.RAFT.isDebugEnabled()) {
Loggers.RAFT.debug("raw beat data size: {}, size of compressed data: {}", content.length(),
compressedContent.length());
}
for (final String server : peers.allServersWithoutMySelf()) {
try {
final String url = buildUrl(server, API_BEAT);
if (Loggers.RAFT.isDebugEnabled()) {
Loggers.RAFT.debug("send beat to server " + server);
}
//发送给其它节点
HttpClient.asyncHttpPostLarge(url, null, compressedBytes, new AsyncCompletionHandler<Integer>() {
@Override
public Integer onCompleted(Response response) throws Exception {
if (response.getStatusCode() != HttpURLConnection.HTTP_OK) {
Loggers.RAFT.error("NACOS-RAFT beat failed: {}, peer: {}", response.getResponseBody(),
server);
MetricsMonitor.getLeaderSendBeatFailedException().increment();
return 1;
}
//添加raftPeer
peers.update(JacksonUtils.toObj(response.getResponseBody(), RaftPeer.class));
if (Loggers.RAFT.isDebugEnabled()) {
Loggers.RAFT.debug("receive beat response from: {}", url);
}
return 0;
}
@Override
public void onThrowable(Throwable t) {
Loggers.RAFT.error("NACOS-RAFT error while sending heart-beat to peer: {} {}", server, t);
MetricsMonitor.getLeaderSendBeatFailedException().increment();
}
});
} catch (Exception e) {
Loggers.RAFT.error("error while sending heart-beat to peer: {} {}", server, e);
MetricsMonitor.getLeaderSendBeatFailedException().increment();
}
}
}
RaftController#beat --- 接收心跳
Beat 方法直接调用的是raftCore#receivedBeat 所以我们直接看此方法,由于此方法太长分段解析;
final RaftPeer local = peers.local();
final RaftPeer remote = new RaftPeer();
JsonNode peer = beat.get("peer");
remote.ip = peer.get("ip").asText();
remote.state = RaftPeer.State.valueOf(peer.get("state").asText());
remote.term.set(peer.get("term").asLong());
remote.heartbeatDueMs = peer.get("heartbeatDueMs").asLong();
remote.leaderDueMs = peer.get("leaderDueMs").asLong();
remote.voteFor = peer.get("voteFor").asText();
if (remote.state != RaftPeer.State.LEADER) {
Loggers.RAFT.info("[RAFT] invalid state from master, state: {}, remote peer: {}", remote.state,
JacksonUtils.toJson(remote));
throw new IllegalArgumentException("invalid state from master, state: " + remote.state);
}
//保证只有大于本地任期才接收心跳
if (local.term.get() > remote.term.get()) {
Loggers.RAFT
.info("[RAFT] out of date beat, beat-from-term: {}, beat-to-term: {}, remote peer: {}, and leaderDueMs: {}",
remote.term.get(), local.term.get(), JacksonUtils.toJson(remote), local.leaderDueMs);
throw new IllegalArgumentException(
"out of date beat, beat-from-term: " + remote.term.get() + ", beat-to-term: " + local.term.get());
}
//本地不是跟随者,可能是候选人,所以将自己变为跟随者,选远程的leader
if (local.state != RaftPeer.State.FOLLOWER) {
Loggers.RAFT.info("[RAFT] make remote as leader, remote peer: {}", JacksonUtils.toJson(remote));
// mk follower
local.state = RaftPeer.State.FOLLOWER;
local.voteFor = remote.ip;
}
final JsonNode beatDatums = beat.get("datums");
local.resetLeaderDue();
local.resetHeartbeatDue();
//如果有其它leader 信息则进行更新
peers.makeLeader(remote);
接下来就是同步leader data 了,根据数据key 和更新时间,来找出需要更新的数据在去拉取leader 数据就行更新,并且重值term
// update datum entry
String url = buildUrl(remote.ip, API_GET) + "?keys=" + URLEncoder.encode(keys, "UTF-8");
HttpClient.asyncHttpGet(url, null, null, new AsyncCompletionHandler<Integer>() {
@Override
public Integer onCompleted(Response response) throws Exception {
if (response.getStatusCode() != HttpURLConnection.HTTP_OK) {
return 1;
}
List<JsonNode> datumList = JacksonUtils
.toObj(response.getResponseBody(), new TypeReference<List<JsonNode>>() {
});
for (JsonNode datumJson : datumList) {
Datum newDatum = null;
OPERATE_LOCK.lock();
try {
Datum oldDatum = getDatum(datumJson.get("key").asText());
if (oldDatum != null && datumJson.get("timestamp").asLong() <= oldDatum.timestamp
.get()) {
Loggers.RAFT
.info("[NACOS-RAFT] timestamp is smaller than that of mine, key: {}, remote: {}, local: {}",
datumJson.get("key").asText(),
datumJson.get("timestamp").asLong(), oldDatum.timestamp);
continue;
}
if (KeyBuilder.matchServiceMetaKey(datumJson.get("key").asText())) {
Datum<Service> serviceDatum = new Datum<>();
serviceDatum.key = datumJson.get("key").asText();
serviceDatum.timestamp.set(datumJson.get("timestamp").asLong());
serviceDatum.value = JacksonUtils
.toObj(datumJson.get("value").toString(), Service.class);
newDatum = serviceDatum;
}
if (KeyBuilder.matchInstanceListKey(datumJson.get("key").asText())) {
Datum<Instances> instancesDatum = new Datum<>();
instancesDatum.key = datumJson.get("key").asText();
instancesDatum.timestamp.set(datumJson.get("timestamp").asLong());
instancesDatum.value = JacksonUtils
.toObj(datumJson.get("value").toString(), Instances.class);
newDatum = instancesDatum;
}
if (newDatum == null || newDatum.value == null) {
Loggers.RAFT.error("receive null datum: {}", datumJson);
continue;
}
raftStore.write(newDatum);
datums.put(newDatum.key, newDatum);
notifier.addTask(newDatum.key, ApplyAction.CHANGE);
local.resetLeaderDue();
if (local.term.get() + 100 > remote.term.get()) {
getLeader().term.set(remote.term.get());
local.term.set(getLeader().term.get());
} else {
local.term.addAndGet(100);
}
raftStore.updateTerm(local.term.get());
总结
Nacos 制定自己raft时做了一些变更;
变更一:
leader 任期没有超时现象,在发起心跳的时候都会在重置任期时间,导致不超时,除非宕机;避免了node之间频繁通讯;同时通过心跳机制重置其它节点为follower,避免长时间双leader 现象
变更二:
选举未采用双阶段选举模式,简化了模式;通过数据变更term+100 的方式来解决短时间分区问题;
特征:
一、term 的变更发生在两个地方:1.leader 选举,加1;2.数据更新,加100;
二、心跳只能leader 发送;
三、数据同步term必须大于等于本地term才是更新的前提;
四、选举是发起方的term必须大于本地term
针对双leader 项目后续1.4版本会避开,已咨询过