|
|
@@ -1,11 +1,11 @@
|
|
|
package com.its.common.cluster.service;
|
|
|
|
|
|
-import com.its.common.cluster.vo.ClusterMessage;
|
|
|
+import com.its.common.cluster.config.AbstractClusterConfig;
|
|
|
import com.its.common.cluster.utils.ClusterPlatform;
|
|
|
import com.its.common.cluster.utils.ClusterUtils;
|
|
|
-import com.its.common.cluster.config.AbstractClusterConfig;
|
|
|
-import com.its.common.cluster.vo.ClusterNode;
|
|
|
+import com.its.common.cluster.vo.ClusterMessage;
|
|
|
import com.its.common.cluster.vo.ClusterNET;
|
|
|
+import com.its.common.cluster.vo.ClusterNode;
|
|
|
import io.netty.bootstrap.ServerBootstrap;
|
|
|
import io.netty.channel.Channel;
|
|
|
import io.netty.channel.ChannelFuture;
|
|
|
@@ -19,6 +19,8 @@ import lombok.extern.slf4j.Slf4j;
|
|
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
|
|
|
|
|
import javax.annotation.PostConstruct;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.List;
|
|
|
import java.util.Map;
|
|
|
import java.util.concurrent.ScheduledFuture;
|
|
|
|
|
|
@@ -96,7 +98,10 @@ public abstract class AbstractClusterMasterService {
|
|
|
serverBootstrap.option(ChannelOption.SO_BACKLOG, 2);
|
|
|
serverBootstrap.option(ChannelOption.SO_RCVBUF, 65535);//config.getRcvBuf());
|
|
|
serverBootstrap.option(ChannelOption.SO_REUSEADDR, true);
|
|
|
- serverBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5*1000);
|
|
|
+
|
|
|
+ // serverBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5*1000);
|
|
|
+ int connectTimeoutMillis = this.clusterConfig.getConnectTimeoutSeconds() * 1000;
|
|
|
+ serverBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeoutMillis);
|
|
|
|
|
|
serverBootstrap.childOption(ChannelOption.SO_LINGER, 0); // 4way-handshake 비활성
|
|
|
serverBootstrap.childOption(ChannelOption.SO_KEEPALIVE, false); // KEEPALIVE 비활성(활성: true)
|
|
|
@@ -185,48 +190,203 @@ public abstract class AbstractClusterMasterService {
|
|
|
entry.getValue().setMaster(cluster.getId() == minClusterNodeId);
|
|
|
}
|
|
|
}
|
|
|
- return (minClusterNodeId >= this.clusterConfig.getId());
|
|
|
+ //return (minClusterNodeId >= this.clusterConfig.getId());
|
|
|
+ return (minClusterNodeId == this.clusterConfig.getId()); // 연결된 가장 작은 노드가 나의 노드와 같은 경우에 마스터임
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean electionMasterQuorum() {
|
|
|
+ // --- 1. 과반수(Quorum) 계산 로직 추가 ---
|
|
|
+ int totalNodes = this.clusterConfig.getClusterMap().size();
|
|
|
+ // 클러스터가 2개 이하일 때는 과반수 로직이 의미 없으므로, 3개 이상일 때만 적용
|
|
|
+ int quorum = (totalNodes >= 3) ? (totalNodes / 2) + 1 : totalNodes;
|
|
|
+
|
|
|
+ // --- 2. 현재 연결된 노드 수를 세고, 가장 작은 ID를 찾는 로직 ---
|
|
|
+ int connectedNodesCount = 0;
|
|
|
+ int minClusterNodeId = Integer.MAX_VALUE;
|
|
|
+
|
|
|
+ for (Map.Entry<Integer, ClusterNode> entry : this.clusterConfig.getClusterMap().entrySet()) {
|
|
|
+ ClusterNode cluster = entry.getValue();
|
|
|
+ if (cluster.getSyncState().getState() != ClusterNET.CLOSED) {
|
|
|
+ // 연결된 노드 수 증가
|
|
|
+ connectedNodesCount++;
|
|
|
+ // 가장 작은 ID 찾기
|
|
|
+ if (cluster.getId() < minClusterNodeId) {
|
|
|
+ minClusterNodeId = cluster.getId();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // --- 3. 과반수 검증 로직 추가 ---
|
|
|
+ // 만약 연결된 노드 수가 과반수에 미치지 못하면, 절대 Master가 될 수 없다.
|
|
|
+ if (connectedNodesCount < quorum) {
|
|
|
+ log.warn("ClusterNodeId: {}, Quorum not met. Connected nodes: {}, Quorum: {}. Cannot become master.",
|
|
|
+ this.clusterConfig.getId(), connectedNodesCount, quorum);
|
|
|
+ // Master ID를 유효하지 않은 값(-1)으로 설정하여 현재 Master가 없음을 명확히 함
|
|
|
+ this.clusterConfig.setMasterId(-1);
|
|
|
+ return false; // Master가 될 수 없음을 반환
|
|
|
+ }
|
|
|
+
|
|
|
+ // --- 4. 기존 로직 (과반수가 충족되었을 경우에만 실행) ---
|
|
|
+ if (minClusterNodeId == Integer.MAX_VALUE) {
|
|
|
+ minClusterNodeId = this.clusterConfig.getId();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (this.clusterConfig.getMasterId() != minClusterNodeId) {
|
|
|
+ this.clusterConfig.setMasterId(minClusterNodeId);
|
|
|
+ for (Map.Entry<Integer, ClusterNode> entry : this.clusterConfig.getClusterMap().entrySet()) {
|
|
|
+ ClusterNode cluster = entry.getValue();
|
|
|
+ entry.getValue().setMaster(cluster.getId() == minClusterNodeId);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 연결된 가장 작은 노드가 나의 노드와 같은 경우에 마스터임
|
|
|
+ return (minClusterNodeId == this.clusterConfig.getId());
|
|
|
+ }
|
|
|
+// 파일: src/main/java/com/its/common/cluster/service/AbstractClusterMasterService.java
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 클러스터의 마스터를 선출하는 핵심 로직.
|
|
|
+ * 1. 과반수(Quorum) 규칙을 적용하여 스플릿 브레인을 방지합니다.
|
|
|
+ * 2. Witness 노드는 마스터 후보에서 제외합니다.
|
|
|
+ * @return 이 노드가 마스터가 되어야 하면 true, 아니면 false.
|
|
|
+ */
|
|
|
+ private boolean electionMasterWitness() {
|
|
|
+ // --- 1. 과반수(Quorum) 계산 ---
|
|
|
+ // 전체 노드 수를 기반으로 마스터가 되기 위해 필요한 최소 연결 노드 수를 계산.
|
|
|
+ int totalNodes = this.clusterConfig.getClusterMap().size();
|
|
|
+ // 클러스터가 2개 이하일 때는 과반수 로직이 큰 의미가 없지만, 일관성을 위해 규칙을 적용.
|
|
|
+ // (2개일 경우 Quorum=2, 1개일 경우 Quorum=1)
|
|
|
+ int quorum = (totalNodes / 2) + 1;
|
|
|
+
|
|
|
+ // --- 2. 연결된 노드 수와 마스터 후보(가장 작은 ID) 찾기 ---
|
|
|
+ int connectedNodesCount = 0;
|
|
|
+ int minClusterNodeId = Integer.MAX_VALUE;
|
|
|
+
|
|
|
+ for (Map.Entry<Integer, ClusterNode> entry : this.clusterConfig.getClusterMap().entrySet()) {
|
|
|
+ ClusterNode cluster = entry.getValue();
|
|
|
+
|
|
|
+ // 현재 노드가 연결된 상태인지 확인합니다.
|
|
|
+ if (cluster.getSyncState().getState() != ClusterNET.CLOSED) {
|
|
|
+ connectedNodesCount++; // 연결된 노드 수 증가
|
|
|
+
|
|
|
+ // [Witness 로직] 만약 노드가 Witness 노드라면, 마스터 후보에서 제외.
|
|
|
+ if (cluster.isWitness()) {
|
|
|
+ continue; // 다음 노드로 넘어감
|
|
|
+ }
|
|
|
+
|
|
|
+ // Witness가 아닌 노드 중에서 가장 작은 ID를 찾습니다.
|
|
|
+ if (cluster.getId() < minClusterNodeId) {
|
|
|
+ minClusterNodeId = cluster.getId();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // --- 3. 과반수(Quorum) 검증 ---
|
|
|
+ // 현재 연결된 노드 수가 과반수를 넘지 못하면, 클러스터는 마스터를 선출할 자격이 없음.
|
|
|
+ if (connectedNodesCount < quorum) {
|
|
|
+// log.warn("ClusterNodeId: {}, Quorum not met. Connected nodes: {}, Quorum: {}. No master will be elected.",
|
|
|
+// this.clusterConfig.getId(), connectedNodesCount, quorum);
|
|
|
+
|
|
|
+ // 클러스터에 유효한 마스터가 없음을 명시적으로 설정합니다.
|
|
|
+ if (this.clusterConfig.getMasterId() != -1) {
|
|
|
+ this.clusterConfig.setMasterId(-1);
|
|
|
+ for (Map.Entry<Integer, ClusterNode> entry : this.clusterConfig.getClusterMap().entrySet()) {
|
|
|
+ entry.getValue().setMaster(false);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false; // 마스터가 될 수 없음
|
|
|
+ }
|
|
|
+
|
|
|
+ // --- 4. 마스터 ID 업데이트 (과반수 충족 시) ---
|
|
|
+ // 만약 minClusterNodeId가 초기값 그대로라면(모든 노드가 Witness이거나 연결이 끊긴 경우),
|
|
|
+ // 유효한 마스터 후보가 없는 것이므로 -1로 설정.
|
|
|
+ if (minClusterNodeId == Integer.MAX_VALUE) {
|
|
|
+ minClusterNodeId = -1;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 클러스터의 마스터 정보가 변경되었다면, 모든 노드에 전파.
|
|
|
+ if (this.clusterConfig.getMasterId() != minClusterNodeId) {
|
|
|
+ this.clusterConfig.setMasterId(minClusterNodeId);
|
|
|
+ for (Map.Entry<Integer, ClusterNode> entry : this.clusterConfig.getClusterMap().entrySet()) {
|
|
|
+ ClusterNode cluster = entry.getValue();
|
|
|
+ entry.getValue().setMaster(cluster.getId() == minClusterNodeId);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // --- 5. 최종 결정 ---
|
|
|
+ // [Witness 로직] 이 코드를 실행하는 '나 자신'이 Witness 노드라면, 절대 마스터가 될 수 없습니다.
|
|
|
+ if (this.clusterConfig.isWitnessNode()) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 마스터로 선출된 노드 ID가 '나 자신'의 ID와 일치하는지 확인합니다.
|
|
|
+ // minClusterNodeId가 -1(유효한 후보 없음)인 경우, 이 조건은 항상 false가 됩니다.
|
|
|
+ return (minClusterNodeId == this.clusterConfig.getId());
|
|
|
}
|
|
|
|
|
|
private void electionMasterSchedule() {
|
|
|
// 2초 주기로 실행되며 클러스터의 마스터/슬래이브 정보를 업데이트 함
|
|
|
// 클러스터맵에는 나 자신의 정보가 포함되어 있음.
|
|
|
// scheduleAtFixedRate ==> scheduleWithFixedDelay 로 변경(혹시 모를 작업 병목을 위해서)
|
|
|
- this.taskFuture = this.taskScheduler.scheduleWithFixedDelay(this::electionMasterCluster, 2 * 1000L);
|
|
|
+ // this.taskFuture = this.taskScheduler.scheduleWithFixedDelay(this::electionMasterCluster, 2 * 1000L);
|
|
|
+ long scheduleMillis = this.clusterConfig.getElectionScheduleSeconds() * 1000L;
|
|
|
+ this.taskFuture = this.taskScheduler.scheduleWithFixedDelay(this::electionMasterCluster, scheduleMillis);
|
|
|
}
|
|
|
|
|
|
public void shutdown() {
|
|
|
- log.info("ClusterNodeId: {}, ClusterMasterService.shutdown", this.clusterConfig.getId());
|
|
|
- if (this.taskFuture != null) {
|
|
|
- this.taskFuture.cancel(true);
|
|
|
+ log.info("ClusterNodeId: {}, ClusterMasterService shutdown process started.", this.clusterConfig.getId());
|
|
|
+
|
|
|
+ List<Throwable> shutdownErrors = new ArrayList<>();
|
|
|
+
|
|
|
+ try {
|
|
|
+ if (this.taskFuture != null) {
|
|
|
+ this.taskFuture.cancel(true);
|
|
|
+ }
|
|
|
+ this.taskScheduler.shutdown();
|
|
|
+ } catch (Exception e) {
|
|
|
+ shutdownErrors.add(new RuntimeException("taskFuture shutdown failed", e));
|
|
|
}
|
|
|
- this.taskScheduler.shutdown();
|
|
|
|
|
|
try {
|
|
|
if (this.acceptGroup != null) {
|
|
|
- this.acceptGroup.shutdownGracefully();
|
|
|
+ // shutdownGracefully()는 Future를 반환하므로, await()로 완료를 기다릴 수 있습니다.
|
|
|
+ // this.acceptGroup.shutdownGracefully();
|
|
|
+ this.acceptGroup.shutdownGracefully().awaitUninterruptibly();
|
|
|
}
|
|
|
}
|
|
|
catch (Exception e) {
|
|
|
- log.error("ClusterNodeId: {}, ClusterMasterService.acceptGroup.shutdownGracefully", this.clusterConfig.getId());
|
|
|
+ shutdownErrors.add(new RuntimeException("acceptGroup shutdown failed", e));
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
if (this.workerGroup != null) {
|
|
|
- this.workerGroup.shutdownGracefully();
|
|
|
+ // this.workerGroup.shutdownGracefully();
|
|
|
+ this.workerGroup.shutdownGracefully().awaitUninterruptibly();
|
|
|
}
|
|
|
}
|
|
|
catch (Exception e) {
|
|
|
- log.error("ClusterNodeId: {}, ClusterMasterService.workerGroup.shutdownGracefully", this.clusterConfig.getId());
|
|
|
+ shutdownErrors.add(new RuntimeException("workerGroup shutdown failed", e));
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
if (this.channelFuture != null && this.channelFuture.channel() != null) {
|
|
|
- this.channelFuture.channel().closeFuture();
|
|
|
+ // this.channelFuture.channel().closeFuture();
|
|
|
+ this.channelFuture.channel().close().awaitUninterruptibly();
|
|
|
}
|
|
|
}
|
|
|
catch (Exception e) {
|
|
|
- log.error("ClusterNodeId: {}, ClusterMasterService.closeFuture", this.clusterConfig.getId());
|
|
|
+ shutdownErrors.add(new RuntimeException("channelFuture closure failed", e));
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!shutdownErrors.isEmpty()) {
|
|
|
+ log.error("ClusterNodeId: {}, ClusterMasterService shutdown encountered {} error(s).",
|
|
|
+ this.clusterConfig.getId(), shutdownErrors.size());
|
|
|
+ // 각 예외를 상세히 로깅합니다.
|
|
|
+ for (int ii = 0; ii < shutdownErrors.size(); ii++) {
|
|
|
+ log.error("Shutdown error #{}: {}", ii + 1, shutdownErrors.get(ii).getMessage(), shutdownErrors.get(ii));
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ log.info("ClusterNodeId: {}, ClusterMasterService shutdown completed gracefully.", this.clusterConfig.getId());
|
|
|
}
|
|
|
}
|
|
|
}
|