elasticsearch集群建立源码分析

elasticsearch集群的建立和维护主要包括以下几个部分:

复制代码
1、结点发现
2、选主
3、集群建立
4、ClusterState更新与同步
5、集群健康检测

1 节点发现

要形成集群,每个节点需要知道集群中有哪些节点。elasticsearch通过gossip协议发现集群中有哪些节点

1.1 什么时机启动节点发现过程

elasticsearch节点发现的代码主要在discovery目录下,实现了一个默认的节点发现机制ZenDiscovery,elasticsearch Node初始化完成后,在它的start()方法中会调用discovery.start()方法

ZenDiscovery.start()方法会调用doStart()方法,在这个方法中,会初始化ClusterState,ClusterState维护了整个集群的状态。初始时,ClusterState中只有一个节点,就是节点自身。

然后Node.start()方法调用discovery.startInitialJoin()开始节点发现过程。

discovery.startInitialJoin()最终会调用ZenDiscovery.innerJoinCluster()方法,这个方法实现了节点发现的主流程。这个方法中,首先是一个while循环,如果没有选举出master,则执行findMaster()

java 复制代码
    while (masterNode == null && joinThreadControl.joinThreadActive(currentThread)) {

        // 选主,如果没有选择出主,则继续进行
        masterNode = findMaster();
    }

然后分为两种情况: 1)如果自己是master,则计算集群需要的最少节点数

arduino 复制代码
    // 需要加入的节点数
    final int requiredJoins = Math.max(0, electMaster.minimumMasterNodes() - 1); // we count as one

然后在masterElectionWaitForJoinsTimeout时间内等待requiredJoins个节点加入集群。如果成功建立集群,则结束;否则重新开始一个新的集群建立过程

2)如果自己是slave,则进入加入集群的流程

ini 复制代码
final boolean success = joinElectedMaster(masterNode);

同样,如果加入成功,则结束;否则,重新开始一个新的集群建立过程

findMaster()

java 复制代码
    // ping 其他的node,发现集群中所有的节点,每一个pingresponse表示一个发现的节点
    List<ZenPing.PingResponse> fullPingResponses = pingAndWait(pingTimeout).toList();

pingAndWait()会调用UnicastZenPing.ping()方法ping其他的节点,返回发现的所有节点。

seedNodes的获取: 1)节点启动的配置文件中获取,即elasticsearch.yml中配置项discovery.zen.ping.unicast.hosts配置的节点 2)ClusterState中记录的已经发现的Master节点 3)近期ping自己的节点。。这些节点保存在变量中temporalResponses中,temporalResponses中的节点会在接收到它之后的2倍的超时时间后删除

java 复制代码
public static class UnicastPingRequest extends TransportRequest {

    int id;
    TimeValue timeout;
    PingResponse pingResponse;
}

class PingResponse implements Streamable {
    private long id;
    private ClusterName clusterName;
    private DiscoveryNode node;
    private DiscoveryNode master;
    private long clusterStateVersion;
}

也就是说,发送ping报文的节点会告诉被ping的节点:自己集群的名字、自己的节点、自己选择的master节点和自己clusterstate的版本号

UnicastZenPing构造方法中注册了ping报文的处理handler

java 复制代码
// 注册处理unicast ping 请求报文的handler
transportService.registerRequestHandler(ACTION_NAME, UnicastPingRequest::new, ThreadPool.Names.SAME,
    new UnicastPingRequestHandler());

如果ping报文的集群与收到ping报文节点的集群是一样的,则会把自己发现的所有节点作为响应发现给对方

java 复制代码
static class UnicastPingResponse extends TransportResponse {
    int id;
    PingResponse[] pingResponses;
}

是一个PingResponse的列表,每一个PingResponse代表一个节点,这个列表的节点来源于: 1)接收到ping的节点自己 2)最近一段时间ping自己的节点。这些节点保存在变量中temporalResponses中,temporalResponses中的节点会在接收到它之后的2倍的超时时间后删除

一个PingRound会ping三轮:

java 复制代码
// 第一次
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);
// 结束
threadPool.schedule(scheduleDuration, ThreadPool.Names.GENERIC, new AbstractRunnable() {
    @Override
    protected void doRun() throws Exception {
        finishPingingRound(pingingRound);
    }

    @Override
    public void onFailure(Exception e) {
        logger.warn("unexpected error while finishing pinging round", e);
    }
});

PingRound结束后,节点会根据自己接收到的PingResponse进行选主

2 选主

PingRound结束后,节点会根据自己接收到的PingResponse进行选主 首先,在这些接收到的PingResponse中,会筛除掉自己和非master节点

下面分两种情况: 1)所有的PingResponse的master node都是空,也就是说当前集群中没有master 则根据下面规则选择一个master: a)所有的PingResponse选择一个clusterStateVersion最大的节点 b)如果最大的clusterStateVersion节点有多个,则从这多个节点中选择node id最小的一个

ElectMasterService

java 复制代码
public static int compare(MasterCandidate c1, MasterCandidate c2) {
    // we explicitly swap c1 and c2 here. the code expects "better" is lower in a sorted
    // list, so if c2 has a higher cluster state version, it needs to come first.
    int ret = Long.compare(c2.clusterStateVersion, c1.clusterStateVersion);
    if (ret == 0) {
        ret = compareNodes(c1.getNode(), c2.getNode());
    }
    return ret;
}
  1. 如果PingResponse中存在一个或多个master,则从中选择一个节点作为master,选择的规则与上面相同

3 集群建立

master选择出来后,master节点等待其他节点发送JoinRequest给自己,而slave节点会向master节点发送JoinRequest

java 复制代码
public static class JoinRequest extends TransportRequest {

    DiscoveryNode node;
}

JoinRequest报文只包括一个信息,就是告诉master,想加入集群的是哪个节点

slave节点在发送完JoinRequest后,会等待响应,如果响应是NotMasterException,即对方认为自己并不是master,这时可能是对方还没有把自己选举为master,所以slave节点会稍等一段时间后重试。如果尝试了joinRetryAttempts次后还没有成功,或者确定对方确实不是master或遇到了其他的问题,则重新启动集群建立过程。

master节点在接收到JoinRequest后,并不会立即响应slave节点,因为master需要接收到minimum_master_nodes - 1个master node的JoinRequest,并把新的cluster state同步到这些节点后,才认为自己是真正的master

MembershipAction构造方法中注册了JoinRequestjoinValidatorsLeaveRequest报文的处理handler

master接收到JoinRequest之后,通过MembershipAction.JoinRequestRequestHandler最后会调用ZenDiscovery.MembershipListener.onJoin()方法,这个方法最终会向slave节点发送ValidateJoinRequest给slave节点,而slave节点目前没有做什么校验,而是直接返回response。

然后调用NodeJoinController.handleJoinRequest()真正处理JoinRequest,这个方法调用中,会把JoinRequest消息及它的回调处理方法添加到

swift 复制代码
// MembershipAction.JoinCallback 的处理为 向 发送 JoinRequest 的节点发送响应
private final Map<DiscoveryNode, List<MembershipAction.JoinCallback>> joinRequestAccumulator = new HashMap<>();

而回调方法即是发送响应报文

然后调用NodeJoinController.checkPendingJoinsAndElectIfNeeded()判断join的节点数是否达到要求

java 复制代码
public synchronized boolean isEnoughPendingJoins(int pendingMasterJoins) {
    final boolean hasEnough;
    if (requiredMasterJoins < 0) {
        // requiredMasterNodes is unknown yet, return false and keep on waiting
        hasEnough = false;
    } else {
        assert callback != null : "requiredMasterJoins is set but not the callback";
        hasEnough = pendingMasterJoins >= requiredMasterJoins;
    }
    return hasEnough;
}

如果已达到要求数量的node,则调用ElectionContext.closeAndBecomeMaster(),然后将JoinRequest的callback方法,以及JoinCallBack封装到JoinTaskListener中,同时还将electionFinishedListener封装到electionFinishedListener中,这些任务都提交给MasterService

bash 复制代码
masterService.submitStateUpdateTasks(source, tasks, ClusterStateTaskConfig.build(Priority.URGENT), joinTaskExecutor);

MasterService会把这些任务封装成UpdateTask,然后提交给线程池中执行。

JoinCallBack封装到JoinTaskLister,ElectionCallBack封装到ClusterStateTaskListener

ElectionCallBack 定义在 ZenDiscovery.innerJoinCluster()方法中 JoinCallback 定义在MembershipAction.JoinRequestRequestHandler 内部类中

然后又将 JoinTaskListener(ClusterStateTaskListener)封装到 SafeClusterStateTaskListener、

SafeClusterStateTaskListener和JoinTaskExecutor封装到 TaskBatcher.UpdateTask extends BatchedTask

4 ClusterState更新与同步

通过MasterService.submit()提交ClusterState 更新及同步任务。最后会执行TaskBatcher.runIfNotProcessed()执行提交任务。这个方法又会调用MasterService.Batcher.run()方法,

将任务封装进TaskInputs, 然后执行runTasks(TaskInputs taskInputs)方法,这个方法中,执行task batcher,计算更新后的ClusterState,同时ClusterState的版本号增加1

ini 复制代码
// 执行task后的新clusterstate
TaskOutputs taskOutputs = calculateTaskOutputs(taskInputs, previousClusterState, startTimeNS);

然后将ClusterState变化构造ClusterChangedEvent发送给集群中其他节点

ini 复制代码
final ClusterState previousClusterState = state();
ClusterState newClusterState = taskOutputs.newClusterState;
// publish new clusterstate 给其他节点
clusterStatePublisher.accept(clusterChangedEvent, taskOutputs.createAckListener(threadPool, newClusterState));

publish完成后,调用前面封装到UpdateTask中的回调方法

scss 复制代码
// 调用回调方法,JoinCallBack和ElectionCallBack
taskOutputs.processedDifferentClusterState(previousClusterState, newClusterState);

publish过程: 上面clusterStatePublisher.accept()实际调用的是ZenDiscovery.publish()方法,这个方法的关键内容

java 复制代码
   public void publish(ClusterChangedEvent clusterChangedEvent, AckListener ackListener) {

        // 检查ClusterState合法性
        if (clusterChangedEvent.previousState() != this.committedState.get()) {
            throw new FailedToCommitClusterStateException("state was mutated while calculating new CS update");
        }

        // 待发布的 ClusterState 入队列;master和slave使用的都是这个队列
        pendingStatesQueue.addPending(newState);

        try {
            // 把 ClusterState 发布给其他节点
            publishClusterState.publish(clusterChangedEvent, electMaster.minimumMasterNodes(), ackListener);
        } catch (FailedToCommitClusterStateException t) {
            // 省略
        }

        final DiscoveryNode localNode = newState.getNodes().getLocalNode();
        final CountDownLatch latch = new CountDownLatch(1);
        final AtomicBoolean processedOrFailed = new AtomicBoolean();
        
        // 标记为已提交
        pendingStatesQueue.markAsCommitted();

        synchronized (stateMutex) {

            // 更新 ClusterState 的内容为新ClusterState的内容,并更新 MasterFaultDetection 和 NodsFaultDetection 中的节点
            boolean sentToApplier = processNextCommittedClusterState("master " + newState.nodes().getMasterNode() +
                " committed version [" + newState.version() + "] source [" + clusterChangedEvent.source() + "]");
            
        }
        // indefinitely wait for cluster state to be applied locally
        try {
            latch.await();
        } catch (InterruptedException e) {
            
        }
    }

publish cluster state 最终会调用 PublishClusterStateAction.innerPublish(), 通过BytesTransportRequest报文发送给其他节点

PublishClusterStateAction构造方法注册了BytesTransportRequestCommitClusterStateRequest的handler,

BytesTransportRequest的handler会调用Zendiscovery.onIncomingClusterState()方法,

java 复制代码
/**
 * 接收到 ClusterState 同步消息后,调用的处理方法
 * @param incomingState
 */
@Override
public void onIncomingClusterState(ClusterState incomingState) {
    // 检查 版本号
    validateIncomingState(logger, incomingState, committedState.get());
    pendingStatesQueue.addPending(incomingState);
}

检查clusterstate的版本号需要比自己的clusterstate的版本号大,然后添加到pendingStatesQueue中,然后发送一条响应消息给master

master如果已经接收到了minimum_master_nodes个确认消息,标记这个clusterstate更新已提交,然后想其他节点发送 CommitClusterStateRequest消息

节点接收到master的commit消息后,会调用ZenDiscovery.onClusterStateCommitted()方法,将该clusterstate更新标记为commit,并修改本地的cluster state

java 复制代码
/**
 * ClusterState commit 处理方法
 * @param stateUUID
 * @param processedListener
 */
@Override
public void onClusterStateCommitted(String stateUUID, ActionListener<Void> processedListener) {
    // 标记为已提交
    final ClusterState state = pendingStatesQueue.markAsCommitted(stateUUID,
        new PendingClusterStatesQueue.StateProcessedListener() {
            @Override
            public void onNewClusterStateProcessed() {
                processedListener.onResponse(null);
            }

            @Override
            public void onNewClusterStateFailed(Exception e) {
                processedListener.onFailure(e);
            }
        });
    if (state != null) {
        synchronized (stateMutex) {
            // 更新 ClusterState
            processNextCommittedClusterState("master " + state.nodes().getMasterNode() +
                " committed version [" + state.version() + "]");
        }
    }
}

5 集群健康检测

MasterFaultDetection slave node ping master node,检测master node的健康状态

NodesFaultDetection master node ping slave nodes,检测slave node的健康状态

ClusterState 提交后,在DenDiscovery.processNextCommittedClusterState()方法中会执行下面语句:

java 复制代码
if (adaptedNewClusterState.nodes().isLocalNodeElectedMaster()) {
    // update the set of nodes to ping
    nodesFD.updateNodesAndPing(adaptedNewClusterState);
} else {
    // check to see that we monitor the correct master of the cluster
    if (masterFD.masterNode() == null || !masterFD.masterNode().equals(adaptedNewClusterState.nodes().getMasterNode())) {
        masterFD.restart(adaptedNewClusterState.nodes().getMasterNode(),
            "new cluster state received and we are monitoring the wrong master [" + masterFD.masterNode() + "]");
    }
}

MasterFaultDetection.innerStart()会周期性的调用MasterPinger.run(), MasterPinger.run()会向master发送MasterPingRequest,数据格式:

java 复制代码
public static class MasterPingRequest extends TransportRequest {

    private DiscoveryNode sourceNode;

    private DiscoveryNode masterNode;
    private ClusterName clusterName;
}

MasterFaultDetection构造方法中注册了MasterPingRequest的handler。 master接收到MasterPingRequest后,会检查自己是否是master以及集群名称是否匹配,然后发送MasterPingResponseResponse。 如果slave发现master健康检测失败,则会调用它的listener,在ZenDiscovery构造方法中注册的listener为MasterNodeFailureListener, 处理master gone的处理方法:

java 复制代码
    private void handleMasterGone(final DiscoveryNode masterNode, final Throwable cause, final String reason) {
        if (lifecycleState() != Lifecycle.State.STARTED) {
            // not started, ignore a master failure
            return;
        }
        if (localNodeMaster()) {
            // we might get this on both a master telling us shutting down, and then the disconnect failure
            return;
        }

        logger.info((Supplier<?>) () -> new ParameterizedMessage("master_left [{}], reason [{}]", masterNode, reason), cause);

        synchronized (stateMutex) {
            if (localNodeMaster() == false && masterNode.equals(committedState.get().nodes().getMasterNode())) {
                // flush any pending cluster states from old master, so it will not be set as master again
                pendingStatesQueue.failAllStatesAndClear(new ElasticsearchException("master left [{}]", reason));
                rejoin("master left (reason = " + reason + ")");
            }
        }
    }

NodesFaultDetection.updateNodesAndPing()周期性调用 NodeFD.run()方法,向slave节点发送PingRequest消息。如果发现slave node不可以达,则移除node。 NodesFaultDetection的构造方法注册了PingRequest消息的handler

相关推荐
Z_z在努力3 分钟前
【杂类】Spring 自动装配原理
java·spring·mybatis
程序员爱钓鱼15 分钟前
Go语言实战案例-开发一个Markdown转HTML工具
前端·后端·go
2301_7816686123 分钟前
Elasticsearch 02
大数据·elasticsearch·搜索引擎
小小菜鸡ing31 分钟前
pymysql
java·服务器·数据库
getapi33 分钟前
shareId 的产生与传递链路
java
桦说编程43 分钟前
爆赞!完全认同!《软件设计的哲学》这本书深得我心
后端
thinktik1 小时前
还在手把手教AI写代码么? 让你的AWS Kiro AI IDE直接读飞书需求文档给你打工吧!
后端·serverless·aws
我没想到原来他们都是一堆坏人2 小时前
(未完待续...)如何编写一个用于构建python web项目镜像的dockerfile文件
java·前端·python
沙二原住民2 小时前
提升数据库性能的秘密武器:深入解析慢查询、连接池与Druid监控
java·数据库·oracle