【Elasticsearch選主流程】

Discovery模塊負責發現集羣中的節點,以及選擇主節點。ES支持多種不同的Discovery選擇,內置的實現稱爲Zen Discovery,其封裝了節點發現(ping)、選主等實現過程。本文基於ES 6.7。

這三種Node你曉得伐

ESNode

Node Name Node Role 看不懂英文? 配置(其他配成false)
Master-eligible node A node that has node.master set to true (default), which makes it eligible to be elected as the master node, which controls the cluster. 主節點:負責集羣層面的相關操作,管理集羣變更。儘可能做少量的工作,生產環境應該儘量分離主節點和數據節點。 node.master: true
Data node A node that has node.data set to true (default). Data nodes hold data and perform data related operations such as CRUD, search, and aggregations. 數據節點:負責保存數據、執行數據相關操作:CRUD、搜索、聚合等。對CPU、內存、IO要求較高。一般情況下,數據讀寫流程只和數據節點交互,不會和主節點打交道(異常情況除外)。 node.data: true
Ingest node A node that has node.ingest set to true (default). Ingest nodes are able to apply an ingest pipeline to a document in order to transform and enrich the document before indexing. With a heavy ingest load, it makes sense to use dedicated ingest nodes and to mark the master and data nodes as node.ingest: false. 預處理節點:從5.0引入的概念。通過定義一系列的processors處理器和pipeline管道,對數據進行某種轉換、富化。 node.ingest: true

Master節點的特殊性

ES中有一項工作是Master獨有的:維護集羣狀態。集羣狀態信息,只由Master節點進行維護,並且同步到集羣中所有節點,其他節點只負責接收從Master同步過來的集羣信息而沒有維護的權利。集羣狀態包括以下信息:

  • 集羣層面的配置
  • 集羣內有哪些節點
  • 各索引的設置,映射,分析器和別名等
  • 索引內各分片所在的節點位置

【思維拓展】ES集羣中的每個節點都會存儲集羣狀態,知道索引內各分片所在的節點位置,因此在整個集羣中的任意節點都可以知道一條數據該往哪個節點分片上存儲。反之也知道該去哪個分片讀。所以,Elasticsearch不需要將讀寫請求發送到Master節點,任何節點都可以作爲數據讀寫的切入點對請求進行響應。這樣進一步減輕了Master節點的網絡壓力,同時提高了集羣的整體路由性能。

主從模式 VS. 無主模式

分佈式系統的集羣方式大致可以分爲主從模式(Master-Slave)和無主模式。

模式 代表組件 優點 缺點
主從模式 ES/HDFS/HBase 簡化系統設計,Master作爲權威節點,負責維護集羣原信息。 Master節點存在單點故障,需要解決在被問題,並且集羣規模會受限於Master節點的管理能力。
無主模式 Cassandra 分佈式哈希表(DHT),支持每小時數千個節點的離開和加入。集羣沒有master的概念,所有節點都是同樣的角色,徹底避免了整個系統的單點問題導致的不穩定性。 多個節點可能操作同一條數據,數據一致性上可能比較難以保證。

爲什麼主從模式更適合ES

ES的典型場景中的節點數沒有那麼多(目前官方推薦是幾百節點)。一般情況下,節點的數量遠遠小於單個節點能夠維護的連接數,並且網絡環境下不必經常處理節點的加入和離開。這就是爲什麼主從模式更加適合ES。

主節點的選舉機制

【舉個栗子】通常一個HBase集羣存在多個HMaster節點(有資格成爲Active HMaster),每個節點都會向ZooKeeper註冊,在正常情況下有且僅有一個節點會成爲Active Master,其餘都爲Backup Master。它們將一直處於阻塞狀態,直至/hbase/master節點發生delete事件,當Zookeeper Watcher監聽到此事件,回喚醒阻塞的Backup Master再次去/master節點註冊,如果註冊成功就會成爲Active HMaster,對外提供服務;如果註冊失敗,說明已經有節點註冊成功,就只能再次阻塞等待被喚醒。

Elasticsearch不像Solr,HDFS和HBase依賴於ZooKeeper,Elasticsearch自己有一套選舉機制來保證集羣的協同服務。

  1. Bully算法
    Leader選舉的基本算法之一,優點是易於實現,該算法和Solr Leader Shard選舉非常相似。
    該算法假定所有節點都有一個唯一的ID,使用該ID對節點進行排序,選擇最小的節點作爲Master。參考ElectMasterService的函數electMaster
    但是節點處於不穩定狀態下會出問題,比如Master負載過重而假死(推遲選舉解決假死 + 法定得票過半解決腦裂)。
  • 防止腦裂、防止數據丟失的極其重要的參數:
    discovery.zen.minimum_master_nodes=(master_eligible_nodes)/2+1
  • 這個參數的實際作用早已超越了其表面的含義(那建議換一個更霸氣側漏的名字以彰顯其重要性),會用於至少以下多個重要時機的判斷:
    1. 觸發選主:進入選舉臨時的Master之前,參選的節點數需要達到法定人數。
    2. 決定Master:選出臨時的Master之後,得票數需要達到法定人數,才確認選主成功。
    3. gateway選舉元信息:向有Master資格的節點發起請求,獲取元數據,獲取的響應數量必須達到法定人數,也就是參與元信息選舉的節點數。
    4. Master發佈集羣狀態:成功向節點發布集羣狀態信息的數量要達到法定人數。
    5. NodesFaultDetection事件中是否觸發rejoin:當發現有節點連不上時,會執行removeNode。接着審視此時的法定人數是否達標(discovery.zen.minimum_master_nodes),不達標就主動放棄Master身份執行rejoin以避免腦裂。
  • Master擴容場景:目前有3個master_eligible_nodes,可以配置quorum爲2。如果將master_eligible_nodes擴容到4個,那麼quorum就要提高到3。此時需要先把discovery.zen.minimum_master_nodes配置設置爲3,再擴容Master節點。這個配置可以動態設置:
    PUT /_cluster/settings
    {
    “persistent”: {
    “discovery.zen.minimum_master_nodes”: 3
    }
    }
  • Master減容場景:縮容與擴容是完全相反的流程,需要先縮減Master節點,再把quorum數降低。
  • 修改Master以及集羣相關的配置一定要非常謹慎!配置錯誤很有可能導致腦裂,甚至數據寫壞、數據丟失等場景。
  • 注意:最新版本ES 7已經移除minimum_master_nodes配置,讓Elasticsearch自己選擇可以形成仲裁的節點
  1. Paxos算法
    非常強大,選舉的靈活性比簡單的Bully算法有很大的優勢。但Paxos實現起來非常複雜。

流程解析

ES選主流程圖
【舉個栗子】節點啓動場景
Node.java -> start()
ZenDiscovery.java -> startInitialJoin() -> innerJoinCluster()

  1. ping所有節點,並獲取PingResponse返回結果(findMaster)
  2. 過濾出具有Master資格的節點(filterPingResponses)
  3. 選出臨時Master。根據PingResponse結果構建兩個列表:activeMasters和masterCandidates。
    – 如果activeMasters非空,則從activeMasters中選擇最合適的作爲Master;
    – 如果activeMasters爲空,則從masterCandidates中選舉,結果可能選舉成功,也可能選舉失敗。
  4. 判斷臨時Master是否是本節點。
    – 如果臨時Master是本節點:則等待其他節點選我,默認30秒超時,成功的話就發佈新的clusterState。(當選總統候選人,只等選票過半了)
    – 如果臨時Master是其他節點:則不再接受其他節點的join請求,並向Master節點發送加入請求。(沒資格選舉,就只能送人頭了)
private DiscoveryNode findMaster() {
	logger.trace("starting to ping");
	List<ZenPing.PingResponse> fullPingResponses = pingAndWait(pingTimeout).toList(); // ping所有節點,並獲取返回結果
	if (fullPingResponses == null) {
		logger.trace("No full ping responses");
		return null;
	}
	if (logger.isTraceEnabled()) {
		StringBuilder sb = new StringBuilder();
		if (fullPingResponses.size() == 0) {
			sb.append(" {none}");
		} else {
			for (ZenPing.PingResponse pingResponse : fullPingResponses) {
				sb.append("\n\t--> ").append(pingResponse);
			}
		}
		logger.trace("full ping responses:{}", sb);
	}

	final DiscoveryNode localNode = transportService.getLocalNode();

	// add our selves
	assert fullPingResponses.stream().map(ZenPing.PingResponse::node)
		.filter(n -> n.equals(localNode)).findAny().isPresent() == false;

	fullPingResponses.add(new ZenPing.PingResponse(localNode, null, this.clusterState()));

	// filter responses 選出具有Master資格的節點
	final List<ZenPing.PingResponse> pingResponses = filterPingResponses(fullPingResponses, masterElectionIgnoreNonMasters, logger);

	List<DiscoveryNode> activeMasters = new ArrayList<>();
	for (ZenPing.PingResponse pingResponse : pingResponses) {
		// We can't include the local node in pingMasters list, otherwise we may up electing ourselves without
		// any check / verifications from other nodes in ZenDiscover#innerJoinCluster()
		if (pingResponse.master() != null && !localNode.equals(pingResponse.master())) {
			activeMasters.add(pingResponse.master());
		}
	}

	// nodes discovered during pinging
	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)) {
			final ElectMasterService.MasterCandidate winner = electMaster.electMaster(masterCandidates);
			logger.trace("candidate {} won election", winner);
			return winner.getNode();
		} else {
			// if we don't have enough master nodes, we bail, because there are not enough master to elect from
			logger.warn("not enough master nodes discovered during pinging (found [{}], but needed [{}]), pinging again",
						masterCandidates, electMaster.minimumMasterNodes());
			return null;
		}
	} else {
		assert !activeMasters.contains(localNode) :
			"local node should never be elected as master when other nodes indicate an active master";
		// lets tie break between discovered nodes
		return electMaster.tieBreakActiveMasters(activeMasters);
	}
}
/**
 * the main function of a join thread. This function is guaranteed to join the cluster
 * or spawn a new join thread upon failure to do so.
 */
private void innerJoinCluster() {
	DiscoveryNode masterNode = null;
	final Thread currentThread = Thread.currentThread();
	nodeJoinController.startElectionContext();
	while (masterNode == null && joinThreadControl.joinThreadActive(currentThread)) {
		masterNode = findMaster();
	}

	if (!joinThreadControl.joinThreadActive(currentThread)) {
		logger.trace("thread is no longer in currentJoinThread. Stopping.");
		return;
	}

	if (transportService.getLocalNode().equals(masterNode)) { // 如果是本節點當選爲Master
		final int requiredJoins = Math.max(0, electMaster.minimumMasterNodes() - 1); // we count as one
		logger.debug("elected as master, waiting for incoming joins ([{}] needed)", requiredJoins);
		nodeJoinController.waitToBeElectedAsMaster(requiredJoins, masterElectionWaitForJoinsTimeout, // (1)等待其他節點的投票數超過requiredJoins數(即爲discovery.zen.minimum_master_nodes配置數)。
				new NodeJoinController.ElectionCallback() {
					@Override
					public void onElectedAsMaster(ClusterState state) { // (2)成功選舉自己爲Master之後,發送集羣狀態到所有節點
						synchronized (stateMutex) {
							joinThreadControl.markThreadAsDone(currentThread);
						}
					}

					@Override
					public void onFailure(Throwable t) { // (3)如果等待超時後投票數沒有超過半數,則認爲選舉失敗,重新開始
						logger.trace("failed while waiting for nodes to join, rejoining", t);
						synchronized (stateMutex) {
							joinThreadControl.markThreadAsDoneAndStartNew(currentThread);
						}
					}
				}

		);
	} else { // 如果是其他節點當選爲Master
		// process any incoming joins (they will fail because we are not the master)
		nodeJoinController.stopElectionContext(masterNode + " elected"); // (1)不再接受其他節點的join請求。

		// send join request
		final boolean success = joinElectedMaster(masterNode); // (2)向Master發送請求,申請加入集羣。最終當選的Master會先發布集羣狀態,才確認客戶的join請求。

		synchronized (stateMutex) {
			if (success) {
				DiscoveryNode currentMasterNode = this.clusterState().getNodes().getMasterNode();
				if (currentMasterNode == null) { // 檢查收到的集羣狀態中的Master節點如果爲空,則重新選舉。
					// Post 1.3.0, the master should publish a new cluster state before acking our join request. we now should have
					// a valid master.
					logger.debug("no master node is set, despite of join request completing. retrying pings.");
					joinThreadControl.markThreadAsDoneAndStartNew(currentThread);
				} else if (currentMasterNode.equals(masterNode) == false) { // 檢查當選的Master是不是之前選擇的節點,不符合的話則重新選舉。
					// update cluster state
					joinThreadControl.stopRunningThreadAndRejoin("master_switched_while_finalizing_join");
				}

				joinThreadControl.markThreadAsDone(currentThread);
			} else { // 獲取集羣狀態,如果集羣狀態中與選擇的Master不一致,則重新開始
				// failed to join. Try again...
				joinThreadControl.markThreadAsDoneAndStartNew(currentThread);
			}
		}
	}
}
/**
 * checks if there is an on going request to become master and if it has enough pending joins. If so, the node will
 * become master via a ClusterState update task.
 */
private synchronized void checkPendingJoinsAndElectIfNeeded() { // waitToBeElectedAsMaster等待時間結束,檢查投票數是否足夠。
	assert electionContext != null : "election check requested but no active context";
	final int pendingMasterJoins = electionContext.getPendingMasterJoinsCount();
	if (electionContext.isEnoughPendingJoins(pendingMasterJoins) == false) { // 選票不夠,需要進行新一輪選舉。
		if (logger.isTraceEnabled()) {
			logger.trace("not enough joins for election. Got [{}], required [{}]", pendingMasterJoins,
				electionContext.requiredMasterJoins);
		}
	} else { // 票數過半,即將成爲Master。
		if (logger.isTraceEnabled()) {
			logger.trace("have enough joins for election. Got [{}], required [{}]", pendingMasterJoins,
				electionContext.requiredMasterJoins);
		}
		electionContext.closeAndBecomeMaster();
		electionContext = null; // clear this out so future joins won't be accumulated
	}
}
public synchronized void addIncomingJoin(DiscoveryNode node, MembershipAction.JoinCallback callback) {
	ensureOpen();
	joinRequestAccumulator.computeIfAbsent(node, n -> new ArrayList<>()).add(callback);
}

// 查看投票數是否已經足夠,標準是達到requiredMasterJoins數(即爲discovery.zen.minimum_master_nodes配置數)
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;
}

private Map<DiscoveryNode, ClusterStateTaskListener> getPendingAsTasks() {
	Map<DiscoveryNode, ClusterStateTaskListener> tasks = new HashMap<>();
	joinRequestAccumulator.entrySet().stream().forEach(e -> tasks.put(e.getKey(), new JoinTaskListener(e.getValue(), logger)));
	return tasks;
}

// 統計各個候選人的得票數,如果被推選爲Master,則pendingMasterJoins自增1。
public synchronized int getPendingMasterJoinsCount() {
	int pendingMasterJoins = 0;
	for (DiscoveryNode node : joinRequestAccumulator.keySet()) {
		if (node.isMasterNode()) {
			pendingMasterJoins++;
		}
	}
	return pendingMasterJoins;
}

節點失效檢測

選主流程之後不可或缺的步驟,不執行失效檢測可能會產生腦裂現象。
定期(默認爲1s)發送ping請求探測節點是否正常,當失敗達到一定次數(默認爲3次),或者收到節點的離線通知時,開始處理節點離開事件。

我們需要啓動兩種失效探測器:

  • NodesFaultDetection:在Master節點啓動,簡稱NodesFD。定期探測加入集羣的節點是否活躍。當有節點連不上時,會執行removeNode。然後需要審視此時的法定人數是否達標(沒錯!老壇酸菜牛肉麪,仍然是那個熟悉的配方:discovery.zen.minimum_master_nodes),不達標就主動放棄Master身份執行rejoin以避免腦裂。
@Override
public ClusterTasksResult<Task> execute(final ClusterState currentState, final List<Task> tasks) throws Exception {
	final DiscoveryNodes.Builder remainingNodesBuilder = DiscoveryNodes.builder(currentState.nodes());
	boolean removed = false;
	for (final Task task : tasks) {
		if (currentState.nodes().nodeExists(task.node())) {
			remainingNodesBuilder.remove(task.node());
			removed = true;
		} else {
			logger.debug("node [{}] does not exist in cluster state, ignoring", task);
		}
	}

	if (!removed) {
		// no nodes to remove, keep the current cluster state
		return ClusterTasksResult.<Task>builder().successes(tasks).build(currentState);
	}

	final ClusterState remainingNodesClusterState = remainingNodesClusterState(currentState, remainingNodesBuilder);

	final ClusterTasksResult.Builder<Task> resultBuilder = ClusterTasksResult.<Task>builder().successes(tasks);
	if (electMasterService.hasEnoughMasterNodes(remainingNodesClusterState.nodes()) == false) {
		final int masterNodes = electMasterService.countMasterNodes(remainingNodesClusterState.nodes());
		rejoin.accept(LoggerMessageFormat.format("not enough master nodes (has [{}], but needed [{}])",
												 masterNodes, electMasterService.minimumMasterNodes()));
		return resultBuilder.build(currentState);
	} else {
		ClusterState ptasksDisassociatedState = PersistentTasksCustomMetaData.disassociateDeadNodes(remainingNodesClusterState);
		return resultBuilder.build(allocationService.disassociateDeadNodes(ptasksDisassociatedState, true, describeTasks(tasks)));
	}
}
  • MasterFaultDetection:在非Master節點啓動,簡稱MasterFD。定期探測Master節點是否活躍,Master下線則觸發rejoin重新選舉。
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(() -> 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 + ")");
		}
	}
}

觸發選舉的時機

  • 集羣啓動,從無主狀態到產生新主時
  • 集羣在正常運行過程中,Master探測到節點離開時(NodesFaultDetection)
  • 集羣在正常運行過程中,非Master節點探測到Master離開時(MasterFaultDetection)

遺留問題

  1. 集羣狀態信息的同步方式是怎麼樣的?
  2. ES官方推薦幾百節點 -> 我們期望大集羣,怎麼拓展Master的管理能力?
  3. Master擴容場景資料優化
  4. Master擴容是否會觸發選主?只擴容Master節點不會觸發選主,只要當前設置的法定人數不變,ES集羣就認爲自己的選舉是合法的。
  5. 現網遇到Master長時間無法選出,根因未知。目前是靠重啓所有Master節點來規避。最新版本ES 7已經移除minimum_master_nodes配置
  6. ES實例管理界面添加Master主備顯示?如果有查看Master的必要,可以加上主備信息的顯示。
  7. 配置discovery.zen.minimum_master_nodes修改完之後,Master重啓,需要重啓普通節點嗎? —— 動態生效!!!推薦!可以動態生效,爲什麼要重啓節點呢?

Reference

Solr選主流程
ES Master機制及腦裂分析
Elasticsearch Reference [7.0] » Modules » Node
Elasticsearch 7.0中引入的新集羣協調子系統如何使用?
Leader Election, Why Should I Care?
開源分佈式NoSQL數據庫系統——Cassandra
分佈式數據庫的取捨——Cassandra的選擇及其後果

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章