在之前的 Elasticsearch 系列之一初识中,我们了解到:主节点负责管理集群的节点状态、分片、索引等等。只有设置node.master为 true 的节点,方可选举且被选举作为主节点。
在7.x版本前后,Elasticsearch 采用了不同的选主算法。7.x之前基于 Bully 算法选主,之后基于 Raft 算法。本篇我们重点介绍7.x之前的选主
Bully 算法
Bully 算法要求所有参与节点必须有 ID,将会选择存活中最大的 ID 作为主
以下算法介绍摘自百度百科
选举过程中会发送以下三种消息类型:
- Election 消息:表示发起一次选举
- Answer(Alive) 消息:对发起选举消息的应答
- Coordinator(Victory) 消息:选举胜利者向参与者发送选举成功消息
触发选举流程的事件包括:
- 当进程 P 从错误中恢复
- 检测到 Leader 失败
选举流程:
- 如果 P 是最大的 ID,直接向所有人发送 Victory 消息,成功新的 Leader ;否则向所有比他大的 ID 的进程发送 Election 消息
- 如果 P 再发送 Election 消息后没有收到 Alive 消息,则 P 向所有人发送 Victory 消息,成功新的 Leader
- 如果 P 收到了从比自己ID还要大的进程发来的 Alive 消息,P 停止发送任何消息,等待 Victory 消息(如果过了一段时间没有等到Victory 消息,重新开始选举流程)
- 如果 P 收到了比自己 ID 小的进程发来的 Election 消息,回复一个Alive消息,然后重新开始选举流程
- 如果 P 收到 Victory 消息,把发送者当做 Leader
假死
假设当前有5个节点。当 master 节点对 node1 的 ping 无法响应时,node1 将会认为 master 挂了,发起新的选举。此时如果 master 对其余3个节点的 ping 能正常响应,说明 master 实际并没有挂掉。
这种只有少数节点认为master挂了的现象就是假死。如果频繁假死就会导致集群频繁选举,影响集群稳定
ES 的解决方案:当一个节点认为 master 挂了时,会向其它节点询问 master 的状态,如果超过1/2的节点认为 master 挂了,才会重新选举。
脑裂
所有分布式系统都有网络分区问题。Bully 算法一旦发生网络分区,将在每个分区择出自己的主。此时整个集群将会有多个主出现。
ES的解决方案为:获得超过整个集群节点数的1/2+1的节点的承认,才是 master
ES选主源码分析
ES选主主要在以下时机触发:
- 节点启动、退出、
- master 定时 ping 其他节点,当发现存活数不足时,将放弃主身份,重新选举
- 其他节点定时 ping 主节点,如果不通,且超过1/2节点认为不通则重新选举
以下源码分析基于6.2.3
主流程
整个选主的大致流程如下:
选主核心逻辑在 ZenDiscovery 模块中,核心方法为 innerJoinCluster
private void innerJoinCluster() {
DiscoveryNode masterNode = null;
final Thread currentThread = Thread.currentThread();
//开始新一轮选举,此时开始接受投票
nodeJoinController.startElectionContext();
//死循环,直到选出自己认为的临时master
while (masterNode == null && joinThreadControl.joinThreadActive(currentThread)) {
masterNode = findMaster();
}
if (transportService.getLocalNode().equals(masterNode)) {
//如果自己被选为临时master,则阻塞等待其它节点投票确认,只需discovery.zen.minimum_master_nodes-1个节点确认即可
final int requiredJoins = Math.max(0, electMaster.minimumMasterNodes() - 1); // we count as one
nodeJoinController.waitToBeElectedAsMaster(requiredJoins, masterElectionWaitForJoinsTimeout,
new NodeJoinController.ElectionCallback() {
@Override
public void onElectedAsMaster(ClusterState state) {
//确认成功,结束本轮选举
synchronized (stateMutex) {
joinThreadControl.markThreadAsDone(currentThread);
}
}
@Override
public void onFailure(Throwable t) {
//确认失败,则开始新的一轮选举(新的选举线程)
synchronized (stateMutex) {
joinThreadControl.markThreadAsDoneAndStartNew(currentThread);
}
}
}
);
} else {
// 自己选的主不是自己,结束本轮选举,不再接收选票
nodeJoinController.stopElectionContext(masterNode + " elected");
//给别人投票,直到临时主被整个集群确认为master,或超时
final boolean success = joinElectedMaster(masterNode);
//当集群主是自己择出的临时主,则选主结束
synchronized (stateMutex) {
if (success) {
DiscoveryNode currentMasterNode = this.clusterState().getNodes().getMasterNode();
if (currentMasterNode == null) {
//集群仍无主,重新选举(新线程)
joinThreadControl.markThreadAsDoneAndStartNew(currentThread);
} else if (currentMasterNode.equals(masterNode) == false) {
//集群主节点不是自己选出的,重新选举
joinThreadControl.stopRunningThreadAndRejoin("master_switched_while_finalizing_join");
}
joinThreadControl.markThreadAsDone(currentThread);
} else {
// 给别人投票失败,重新选举
joinThreadControl.markThreadAsDoneAndStartNew(currentThread);
}
}
}
}
通过 discovery.zen.minimum_master_nodes 配置避免脑裂,通常为 节点数/2+1
等待其它节点确认
public void waitToBeElectedAsMaster(int requiredMasterJoins, TimeValue timeValue, final ElectionCallback callback) {
final CountDownLatch done = new CountDownLatch(1);
final ElectionCallback wrapperCallback = new ElectionCallback() {
@Override
public void onElectedAsMaster(ClusterState state) {
//满足最小投票数,则解除CountDownLatch阻塞
done.countDown();
callback.onElectedAsMaster(state);
}
@Override
public void onFailure(Throwable t) {
//失败,则解除CountDownLatch阻塞
done.countDown();
callback.onFailure(t);
}
};
//...
//设置最小投票数,回调函数
electionContext.onAttemptToBeElected(requiredMasterJoins, wrapperCallback);
//再检查一下
checkPendingJoinsAndElectIfNeeded();
//...
//通过CountDownLatch 阻塞,直到超时,或被countDown
//discovery.zen.master_election.wait_for_joins_timeout控制阻塞超时时间
if (done.await(timeValue.millis(), TimeUnit.MILLISECONDS)) {
return;
}
//...
}
当处理其它节点的投票请求时 ,handleJoinRequest 也会检查得票是否足够 checkPendingJoinsAndElectIfNeeded
public synchronized void handleJoinRequest(final DiscoveryNode node, final MembershipAction.JoinCallback callback) {
if (electionContext != null) {
electionContext.addIncomingJoin(node, callback);
checkPendingJoinsAndElectIfNeeded();
} else {
masterService.submitStateUpdateTask("zen-disco-node-join",
node, ClusterStateTaskConfig.build(Priority.URGENT),
joinTaskExecutor, new JoinTaskListener(callback, logger));
}
}
选临时主
findMaster 方法用于择出本轮选举,自己认为的主节点。
private DiscoveryNode findMaster() {
//ping其它节点,里面会将自身剔除
//pingTimeout通过配置discovery.zen.ping_timeout设置
List<ZenPing.PingResponse> fullPingResponses = pingAndWait(pingTimeout).toList();
final DiscoveryNode localNode = transportService.getLocalNode();
//加入自身到ping结果中,但不认为当前集群有活跃主节点
fullPingResponses.add(new ZenPing.PingResponse(localNode, null, this.clusterState()));
// 根据配置discovery.zen.master_election.ignore_non_master_pings决定是否移除(node.master=false)的节点
final List<ZenPing.PingResponse> pingResponses = filterPingResponses(fullPingResponses, masterElectionIgnoreNonMasters, logger);
//收集各节点认为已当前集群的活跃master
List<DiscoveryNode> activeMasters = new ArrayList<>();
for (ZenPing.PingResponse pingResponse : pingResponses) {
//非自身节点
if (pingResponse.master() != null && !localNode.equals(pingResponse.master())) {
activeMasters.add(pingResponse.master());
}
}
//node.master=true的节点作为候选主节点
List<ElectMasterService.MasterCandidate> masterCandidates = new ArrayList<>();
for (ZenPing.PingResponse pingResponse : pingResponses) {
if (pingResponse.node().isMasterNode()) {
masterCandidates.add(new ElectMasterService.MasterCandidate(pingResponse.node(), pingResponse.getClusterStateVersion()));
}
}
if (activeMasters.isEmpty()) {
//没有活跃的主节点
if (electMaster.hasEnoughCandidates(masterCandidates)) {
//但有足够(discovery.zen.minimum_master_nodes)的主资格节点(避免脑裂),则选举
final ElectMasterService.MasterCandidate winner = electMaster.electMaster(masterCandidates);
return winner.getNode();
} else {
//选举失败
return null;
}
} else {
//有活跃的主节点,择出一个主节点
return electMaster.tieBreakActiveMasters(activeMasters);
}
}
从代码可知:选主的最快时间取决于 pingAndWait 的执行时间
ping
通过 pingAndWait 方法收集选票,阻塞住,只能在 pingTimeout 时间后返回
private ZenPing.PingCollection pingAndWait(TimeValue timeout) {
final CompletableFuture<ZenPing.PingCollection> response = new CompletableFuture<>();
try {
zenPing.ping(response::complete, timeout);
} catch (Exception ex) {
// logged later
response.completeExceptionally(ex);
}
//通过CompletableFuture.get()阻塞住,直到response::complete触发
return response.get();
//...
}
详细看下 UnicastZenPing.ping 方法
protected void ping(final Consumer<PingCollection> resultsConsumer,
final TimeValue scheduleDuration,
final TimeValue requestDuration) {
//解析其它节点链接参数
//discovery.zen.ping.unicast.hosts 配置其它节点
//discovery.zen.ping.unicast.hosts.resolve_timeout 配置节点地址解析超时时间
seedNodes = resolveHostsLists(...)
//...
final ConnectionProfile connectionProfile =
ConnectionProfile.buildSingleChannelProfile(TransportRequestOptions.Type.REG, requestDuration, requestDuration);
final PingingRound pingingRound = new PingingRound(pingingRoundIdGenerator.incrementAndGet(), seedNodes, resultsConsumer,nodes.getLocalNode(), connectionProfile);
final AbstractRunnable pingSender = new AbstractRunnable() {
//...
protected void doRun() throws Exception {
sendPings(requestDuration, pingingRound);
}
};
//依次提交3次 ping 任务,每间隔1/3个 pingTimeout 执行一次。第一次 ping 通过 generic 线程池立刻执行,之后两轮通过 schedule 线程池延时执行,但实际执行线程池仍为generic
threadPool.generic().execute(pingSender);
threadPool.schedule(TimeValue.timeValueMillis(scheduleDuration.millis() / 3), ThreadPool.Names.GENERIC, pingSender);
threadPool.schedule(TimeValue.timeValueMillis(scheduleDuration.millis() / 3 * 2), ThreadPool.Names.GENERIC, pingSender);
//schedule线程池确保在 discovery.zen.ping_timeout时间 之后,通过 generic线程池完成本轮PingingRound
threadPool.schedule(scheduleDuration, ThreadPool.Names.GENERIC, new AbstractRunnable() {
@Override
protected void doRun() throws Exception {
//触发pingAndWait方法的CompletableFuture,解除选举线程的阻塞
finishPingingRound(pingingRound);
}
//...
});
}
每轮 ping 执行如下:
protected void sendPings(final TimeValue timeout, final PingingRound pingingRound) {
//...
//对所有节点,发出ping
nodesToPing.forEach(node -> sendPingRequestToNode(node, timeout, pingingRound, pingRequest));
}
private void sendPingRequestToNode(final DiscoveryNode node, TimeValue timeout, final PingingRound pingingRound,
final UnicastPingRequest pingRequest) {
//通过线程池,异步并发去ping
unicastZenPingExecutorService.submitToExecutor(new AbstractRunnable() {
@Override
protected void doRun() throws Exception {
//...
connection = pingingRound.getOrConnect(node);
transportService.sendRequest(...);
}
//...
});
}
通过 unicastZenPingExecutorService 线程池并发去 ping 其它节点的,连接超时为 pingTimeout,ping 的超时时间为1.25个 pingTimeout
discovery.zen.ping.unicast.concurrent_connects 参数控制 unicastZenPingExecutorService 的最大线程数
discovery.zen.ping_timeout 控制此处的 ping 超时时间
ping 响应回调时,将非本节点的响应,收集起来
public void addPingResponseToCollection(PingResponse pingResponse) {
if (localNode.equals(pingResponse.node()) == false) {
pingCollection.addPing(pingResponse);
}
}
响应 ping 请求:
class UnicastPingRequestHandler implements TransportRequestHandler<UnicastPingRequest> {
@Override
public void messageReceived(UnicastPingRequest request, TransportChannel channel) throws Exception {
//同一集群才响应
if (request.pingResponse.clusterName().equals(clusterName)) {
channel.sendResponse(handlePingRequest(request));
} else {
throw new IllegalStateException(xxx);
}
}
}
private UnicastPingResponse handlePingRequest(final UnicastPingRequest request) {
//...
//将集群状态作为入参
pingResponses.add(createPingResponse(contextProvider.clusterState()));
return new UnicastPingResponse(request.id, pingResponses.toArray(new PingResponse[pingResponses.size()]));
}
private PingResponse createPingResponse(ClusterState clusterState) {
DiscoveryNodes discoNodes = clusterState.nodes();
//返回自身node、集群当前master node、
return new PingResponse(discoNodes.getLocalNode(), discoNodes.getMasterNode(), clusterState);
}
选择
根据上一步得到的 ping 结果,分场景选主
1、当前集群无主:则从主资格节点中选择
public MasterCandidate electMaster(Collection<MasterCandidate> candidates) {
assert hasEnoughCandidates(candidates);
List<MasterCandidate> sortedCandidates = new ArrayList<>(candidates);
sortedCandidates.sort(MasterCandidate::compare);
return sortedCandidates.get(0);
}
对 MasterCandidate 排序,选择第0个
public static int compare(MasterCandidate c1, MasterCandidate c2) {
//版本高的在前
int ret = Long.compare(c2.clusterStateVersion, c1.clusterStateVersion);
if (ret == 0) {
ret = compareNodes(c1.getNode(), c2.getNode());
}
return ret;
}
/** master nodes go before other nodes, with a secondary sort by id **/
private static int compareNodes(DiscoveryNode o1, DiscoveryNode o2) {
if (o1.isMasterNode() && !o2.isMasterNode()) {
return -1;
}
if (!o1.isMasterNode() && o2.isMasterNode()) {
return 1;
}
//id小的在前
return o1.getId().compareTo(o2.getId());
}
首先比较节点的集群版本,再比较节点的 nodeId。
2、当前集群已有主
从活跃的 master 节点,通过比较 nodeId ,取最小的 node 作为主节点
public DiscoveryNode tieBreakActiveMasters(Collection<DiscoveryNode> activeMasters) {
return activeMasters.stream().min(ElectMasterService::compareNodes).get();
}
参考
推荐阅读
微信公众号
文章同步发布,政采云技术团队公众号,欢迎关注