文章目錄
- 背景
-
- 為什麼使用主從模式?
- 選舉算法
- 什麼時候觸發選主?
- 選主過程
-
- 選舉臨時Master
- 投票與得票的實作
- 确立Master或加入叢集
- 選舉完成
elasticsearch中的Discovery子產品負責發現叢集中的節點,以及選擇主節點。在Elasticsearch 7.0以前,内置的實作稱為Zen Discovery。Zen Discovery封裝了節點發送(ping)、選主等實作過程。
背景
為什麼使用主從模式?
ES的典型場景中的另一個簡化是叢集中沒有那麼多節點。通常,節點的數量遠遠小于單個節點能夠維護的連接配接數,并且網絡環境不必經常處理節點的加入和離開。
選舉算法
Bully算法是Leader選舉的基本算法之一。它假定所有節點都有一個唯一的ID,使用該ID對節點進行排序。任何時候的目前Leader都是參與叢集的最高ID節點。該算法的優點是易于實作。
但是,當擁有最大ID的節點處于不穩定狀态的場景下會有問題。例如,Master負載過重而假死,叢集擁有第二大ID的節點被選為新主,這時原來的Master恢複,再次被選為新主,然後又假死…。ES通過推遲選舉,直到目前的Master失效來解決上述問題,隻要目前主節點不挂掉,就不重新選主。
但是容易産生腦裂(雙主),為此,再通過“法定得票人數過半”解決腦裂問題。
7.X之後的ES,采用一種新的選主算法,實際上是Raft的實作,但并非嚴格按照Raft論文實作,而是做了一些調整。
什麼時候觸發選主?
主要在以下三個場景會觸發選主:
- 叢集啟動初始化;
- 叢集的Master崩潰的時候;
- 任何一個節點發現目前叢集中的Master節點沒有得到
節點認可的時候。n/2 + 1
不執行失效檢測可能會産生腦裂(雙主或多主),是以需要啟動兩種失效探測器:
- 在Master節點,啟動NodesFaultDetection,簡稱NodesFD。定期探測加入叢集的節點是否活躍。檢查一下目前叢集總節點數是否達到法定節點數(過半),如果不足,則會放棄Master身份,重新加入叢集。
- 在非Master節點啟動MasterFaultDetection,簡稱MasterFD。定期探測Master節點是否活躍。探測Master離線的處理很簡單,重新加入叢集。本質上就是該節點重新執行一遍選主的流程。
NodesFaultDetection和MasterFaultDetection都是通過定期(預設為1秒)發送的ping請求探測節點是否正常的,當失敗達到一定次數(預設為3次),或者收到來自底層連接配接子產品的節點離線通知時,開始處理節點離開事件。
選主過程
ZenDiscovery選主的整體流程可以概括為:選舉臨時Master,如果本節點當選,則等待确立Master,如果其他節點當選,則嘗試加入叢集,最後選舉完成。流程圖如下:

選舉臨時Master
通過ZenDiscovery#findMaster确定臨時master。
為什麼是臨時Master?因為還需要等待下一個步驟,該節點的得票數足夠時,才确立為真正的Master。
private DiscoveryNode findMaster() {
// 1. PING 所有節點,擷取各節點儲存的叢集資訊
List<ZenPing.PingResponse> fullPingResponses = pingAndWait(pingTimeout).toList();
if (fullPingResponses == null) {
logger.trace("No full ping responses");
return null;
}
// 2. 由于上面是擷取的其他節點的資訊,這裡需要将本節點加上
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()));
// 3. 若設定了 master_election_ignore_non_masters 則去掉沒有 master 資格(node.master: false)的節點
final List<ZenPing.PingResponse> pingResponses = filterPingResponses(fullPingResponses, masterElectionIgnoreNonMasters, logger);
// 4. 将各節點認為的 master 加入 activeMasters 清單
List<DiscoveryNode> activeMasters = new ArrayList<>();
for (ZenPing.PingResponse pingResponse : pingResponses) {
// 避免未經其他節點檢查就将本節點選為 master
if (pingResponse.master() != null && !localNode.equals(pingResponse.master())) {
activeMasters.add(pingResponse.master());
}
}
// 5. 将 PING 到的具有 master 資格的節點加入 masterCandidates 清單作為候選節點
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()) {
// 6. 沒有活躍的 master
if (electMaster.hasEnoughCandidates(masterCandidates)) {
// 7. 擁有足夠的候選節點,則進行選舉
final ElectMasterService.MasterCandidate winner = electMaster.electMaster(masterCandidates);
return winner.getNode();
} else {
// 8. 無法選舉,無法得到 master,傳回 null
return null;
}
} else {
// 9. 有活躍的 master,從 activeMasters 中選擇
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);
}
}
臨時Master的選舉過程如下:
- “ping”所有節點,擷取節點清單fullPingResponses,ping結果不包含本節點,把本節點單獨添加到fullPingResponses中。
- 建構兩個清單。activeMasters清單:存儲叢集目前活躍Master清單。周遊第一步擷取的所有節點,将每個節點所認為的目前Master節點加入activeMasters清單中(不包括本節點)。在周遊過程中,如果配置了discovery.zen.master_election.ignore_non_master_pings為true(預設為false),而節點又不具備Master資格,則跳過該節點。masterCandidates清單:存儲master候選者清單。周遊第一步擷取清單,去掉不具備Master資格的節點,添加到這個清單中。
Elasticsearch中的Zen Discovery選主流程背景選主過程 - 如果activeMaster為空,則從masterCandidates中選舉,結果可能選舉成功,也可能選舉失敗。如果不為空,則從acitveMasters中選擇最合适的作為Master。
- 從masterCandidates中選主:與選主的具體細節實作封裝在ElectMasterService類中。首先需要判斷 目前候選者人數是否達到法定人數,否則選主失敗。接着從候選者中選一個出來做Master,優先把叢集狀态版本号高的節點放在前面。
- 從activeMasters清單中選擇:清單存儲着叢集目前存在活躍的Master,從中選擇一個作為選舉結果。上面第9步通過 ElectMasterService::tieBreakAcitveMasters使用ElectMasterService::compareNodes的規則從activeMasters中選擇。
投票與得票的實作
在ES中,發送投票就是發送加入叢集(JoinRequest)請求。
// ZenDiscovery#joinElectedMaster,加入一個新選出來的主節點
private boolean joinElectedMaster(DiscoveryNode masterNode) {
while (true) {
membership.sendJoinRequestBlocking(masterNode, transportService.getLocalNode(), joinTimeout);
}
}
得票就是申請加入該節點的請求的數量。收集投票,進行統計的實作在ZenDiscovery#handleJoinRequest方法中,其又調用了NodeJoinController#handleJoinRequest方法,收到的連接配接被存儲到ElectionContext#joinRequestAccumulator中。當節點檢查收到的投票是否足夠時,就是檢查加入它的連接配接數是否足夠,其中會去掉沒有Master資格節點的投票。
public synchronized int getPendingMasterJoinsCount() {
int pendingMasterJoins = 0;
// 周遊目前收到的join請求
for (DiscoveryNode node : joinRequestAccumulator.keySet()) {
// 過濾不具備master資格的節點
if (node.isMasterNode()) {
pendingMasterJoins++;
}
}
return pendingMasterJoins;
}
确立Master或加入叢集
選舉出的臨時Master有兩種情況:該臨時Master是本節點或非本節點。為此單獨處理。現在準備向其發送投票。根據選出的臨時 master是否本節點等待或投票
// ZenDiscovery#innerJoinCluster
// join線程的主函數。該函數被用來保證是加入叢集或是當失敗時建立一個新的join線程。
private void innerJoinCluster() {
if (transportService.getLocalNode().equals(masterNode)) {
// 選出的臨時 master 是本節點,則等待被選舉為真正的 master
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) {
// 成功被選舉為 master
synchronized (stateMutex) {
joinThreadControl.markThreadAsDone(currentThread);
}
}
@Override
public void onFailure(Throwable t) {
// 等待逾時,重新開始選舉流程
synchronized (stateMutex) {
joinThreadControl.markThreadAsDoneAndStartNew(currentThread);
}
}
}
);
} else {
// 選出的臨時 master 不是本節點,不再接收其他節點的 join 請求
nodeJoinController.stopElectionContext(masterNode + " elected");
// 向臨時節點發送 join 請求(投票),被選舉的臨時 master 在确認成為 master 并釋出新的叢集狀态後才會傳回
final boolean success = joinElectedMaster(masterNode);
// 成功加入之前選擇的臨時 master 節點,則結束線程,否則重新選舉
synchronized (stateMutex) {
if (success) {
DiscoveryNode currentMasterNode = this.clusterState().getNodes().getMasterNode();
if (currentMasterNode == null) {
joinThreadControl.markThreadAsDoneAndStartNew(currentThread);
} else if (currentMasterNode.equals(masterNode) == false) {
// update cluster state
joinThreadControl.stopRunningThreadAndRejoin("master_switched_while_finalizing_join");
}
joinThreadControl.markThreadAsDone(currentThread);
} else {
// failed to join. Try again...
joinThreadControl.markThreadAsDoneAndStartNew(currentThread);
}
}
}
}
如果臨時Master是本節點:
- 等待足夠多的具備Master資格的節點加入本節點(投票達到法定人數),以完成選舉。
- 逾時(預設為30秒,可配置)後還沒有滿足數量的join請求,則選舉失敗,需要進行新一輪選舉。
- 成功後釋出新的clusterState.
如果其他節點被選為Master:
- 不再接受其他節點的join請求。
- 向Master發送加入請求,并等待回複。逾時時間預設為1分鐘(可配置),如果遇到異常,則預設重試3次(可配置)。這個步驟在joinElectedMaster方法中實作。
- 最終當選的Master會先釋出叢集狀态,才确認客戶的join請求,是以,joinElectedMaster傳回代表收到了join請求的确認,并且已經收到了叢集狀态。本步驟檢查收到的叢集狀态中的Master節點如果為空,或者當選的Master不是之前選擇的節點,則重新選舉。
選舉完成
NodeJoinController::checkPendingJoinsAndElectIfNeeded在節點獲得足夠的得票時使節點成為正式master,并釋出新的叢集狀态
private synchronized void checkPendingJoinsAndElectIfNeeded() {
// 計算節點得票數
final int pendingMasterJoins = electionContext.getPendingMasterJoinsCount();
if (electionContext.isEnoughPendingJoins(pendingMasterJoins) == false) {
...
} else {
// 得票數足夠,成為 master
electionContext.closeAndBecomeMaster();
}
}
public synchronized int getPendingMasterJoinsCount() {
int pendingMasterJoins = 0;
// 統計節點得票數,隻計算擁有 master 資格節點的投票
for (DiscoveryNode node : joinRequestAccumulator.keySet()) {
if (node.isMasterNode()) {
pendingMasterJoins++;
}
}
return pendingMasterJoins;
}