Jelajahi Sumber

add its-cluster library

HANTE 1 bulan lalu
induk
melakukan
43927b0f55
23 mengubah file dengan 1514 tambahan dan 1 penghapusan
  1. 1 0
      .idea/gradle.xml
  2. 8 1
      build.gradle
  3. 47 0
      its-cluster/build.gradle
  4. 7 0
      its-cluster/install.bat
  5. 7 0
      its-cluster/src/main/java/com/its/common/ClusterMain.java
  6. 110 0
      its-cluster/src/main/java/com/its/common/cluster/master/ClusterMasterHandler.java
  7. 86 0
      its-cluster/src/main/java/com/its/common/cluster/master/ClusterMasterInitializer.java
  8. 163 0
      its-cluster/src/main/java/com/its/common/cluster/master/ClusterMasterService.java
  9. 148 0
      its-cluster/src/main/java/com/its/common/cluster/slave/ClusterSlave.java
  10. 47 0
      its-cluster/src/main/java/com/its/common/cluster/slave/ClusterSlaveHandler.java
  11. 173 0
      its-cluster/src/main/java/com/its/common/cluster/slave/ClusterSlaveService.java
  12. 18 0
      its-cluster/src/main/java/com/its/common/cluster/utils/ClusterMessage.java
  13. 24 0
      its-cluster/src/main/java/com/its/common/cluster/utils/ClusterMessageData.java
  14. 35 0
      its-cluster/src/main/java/com/its/common/cluster/utils/ClusterMessageDecoder.java
  15. 24 0
      its-cluster/src/main/java/com/its/common/cluster/utils/ClusterMessageEncoder.java
  16. 12 0
      its-cluster/src/main/java/com/its/common/cluster/utils/ClusterPlatform.java
  17. 49 0
      its-cluster/src/main/java/com/its/common/cluster/utils/ClusterSlaveBootstrapFactory.java
  18. 257 0
      its-cluster/src/main/java/com/its/common/cluster/utils/ClusterUtils.java
  19. 168 0
      its-cluster/src/main/java/com/its/common/cluster/vo/HaClusterConfig.java
  20. 23 0
      its-cluster/src/main/java/com/its/common/cluster/vo/HaClusterInfo.java
  21. 14 0
      its-cluster/src/main/java/com/its/common/cluster/vo/HaNET.java
  22. 92 0
      its-cluster/src/main/java/com/its/common/cluster/vo/HaNetState.java
  23. 1 0
      settings.gradle

+ 1 - 0
.idea/gradle.xml

@@ -10,6 +10,7 @@
           <set>
             <option value="$PROJECT_DIR$" />
             <option value="$PROJECT_DIR$/its-asn1" />
+            <option value="$PROJECT_DIR$/its-cluster" />
             <option value="$PROJECT_DIR$/its-common" />
             <option value="$PROJECT_DIR$/its-network" />
             <option value="$PROJECT_DIR$/its-spring" />

+ 8 - 1
build.gradle

@@ -66,7 +66,6 @@ project(':its-spring') {
     jar.enabled = true
     dependencies {
         implementation 'org.springframework.boot:spring-boot-starter-web'
-
         testImplementation 'org.springframework.boot:spring-boot-starter-test'
     }
 }
@@ -76,7 +75,15 @@ project(':its-network') {
     jar.enabled = true
     dependencies {
         implementation 'org.springframework.boot:spring-boot-starter-web'
+        testImplementation 'org.springframework.boot:spring-boot-starter-test'
+    }
+}
 
+project(':its-cluster') {
+    bootJar.enabled = false
+    jar.enabled = true
+    dependencies {
+        implementation 'org.springframework.boot:spring-boot-starter-web'
         testImplementation 'org.springframework.boot:spring-boot-starter-test'
     }
 }

+ 47 - 0
its-cluster/build.gradle

@@ -0,0 +1,47 @@
+plugins {
+    id 'java'
+}
+
+group = 'com.its'
+version = '0.0.1'
+
+sourceCompatibility = '1.8'
+targetCompatibility = '1.8'
+compileJava.options.encoding = 'UTF-8'
+
+repositories {
+    mavenCentral()
+}
+
+dependencies {
+    implementation 'io.netty:netty-all:4.1.52.Final'
+}
+
+processResources {
+    enabled = false
+}
+
+jar {
+    enabled = true
+}
+
+test {
+    useJUnitPlatform()
+}
+
+tasks.register('runInstallJarLibrary', Exec) {
+    doFirst {
+        println "its-cluster library install mvn repository..."
+        workingDir = file('.')
+        commandLine = ['cmd', '/C', 'start', 'install.bat']
+        // cmd /C start ./install.bat
+    }
+}
+jar.finalizedBy runInstallJarLibrary
+
+compileJava.options.encoding = 'UTF-8'
+tasks.withType(JavaCompile).configureEach {
+    options.compilerArgs << '-Xlint:unchecked'
+    options.deprecation = true
+    options.encoding = 'UTF-8'
+}

+ 7 - 0
its-cluster/install.bat

@@ -0,0 +1,7 @@
+@echo off
+chcp 65001
+@echo on
+copy build\libs\its-cluster-0.0.1.jar c:\java\repository\
+cd C:\java\apache-maven-3.9.6\bin
+cd C:\Users\OpenValue\.m2\wrapper\dists\apache-maven-3.6.3-bin\1iopthnavndlasol9gbrbg6bf2\apache-maven-3.6.3\bin
+.\mvn install:install-file -Dfile="C:\java\repository\its-cluster-0.0.1.jar" -DgroupId=com.its -DartifactId=its-cluster -Dversion=0.0.1 -Dpackaging=jar -DlocalRepositoryPath=C:\java\repository\

+ 7 - 0
its-cluster/src/main/java/com/its/common/ClusterMain.java

@@ -0,0 +1,7 @@
+package com.its.common;
+
+public class ClusterMain {
+    public static void main(String[] args) {
+        System.out.printf("com.its.common.cluster!");
+    }
+}

+ 110 - 0
its-cluster/src/main/java/com/its/common/cluster/master/ClusterMasterHandler.java

@@ -0,0 +1,110 @@
+package com.its.common.cluster.master;
+
+import com.its.common.cluster.utils.ClusterMessage;
+import com.its.common.cluster.utils.ClusterUtils;
+import com.its.common.cluster.vo.HaClusterConfig;
+import com.its.common.cluster.vo.HaClusterInfo;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.handler.timeout.IdleState;
+import io.netty.handler.timeout.IdleStateEvent;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.slf4j.MDC;
+
+@Slf4j
+@RequiredArgsConstructor
+public class ClusterMasterHandler extends ChannelInboundHandlerAdapter {
+
+    private final HaClusterConfig clusterConfig;
+
+    @Override
+    public void channelRead(ChannelHandlerContext ctx, Object msg) {
+        if (msg instanceof ClusterMessage) {
+            ClusterMessage clusterMsg = (ClusterMessage) msg;
+            log.info("ClusterMasterHandler.channelRead: [{}], {}, [FROM: serverId: {}, serverTime: {}, infos: {}]",
+                    this.clusterConfig.getServerId(), ClusterUtils.getTcpAddress(ctx.channel()),
+                    clusterMsg.getServerId(), clusterMsg.getServerTime(), clusterMsg.getInfos().size());
+
+            HaClusterInfo cluster = ctx.channel().attr(HaClusterConfig.CLUSTER_ATTRIBUTE_KEY).get();
+            if (cluster == null) {
+                log.error("RECV: [{}]. Not Found Channel Cluster Object... Oops Will be closed.", ClusterUtils.getAddress(ctx.channel()));
+                closeChannel(ctx.channel());
+                return;
+            }
+
+            cluster.getElectionState().setLastRecvTime();
+//            ctx.writeAndFlush(clusterMsg);
+        }
+    }
+
+    @Override
+    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
+        HaClusterInfo cluster = ctx.channel().attr(HaClusterConfig.CLUSTER_ATTRIBUTE_KEY).get();
+        if (cluster == null) {
+            log.error("{}.++channelInactive: Unknown Cluster: {}.", this.getClass().getSimpleName(), ClusterUtils.getAddress(ctx.channel()));
+            return;
+        }
+        try {
+            MDC.put("id", cluster.getLogKey());
+            log.info("{}.++channelInactive: [{}, {}].", this.getClass().getSimpleName(), cluster.getServerId(), cluster.getIpAddress());
+            cluster.getElectionState().disConnect();
+
+            ctx.channel().attr(HaClusterConfig.CLUSTER_ATTRIBUTE_KEY).set(null);
+            ctx.fireChannelInactive();
+        }
+        finally {
+            MDC.remove(cluster.getLogKey());
+            MDC.clear();
+        }
+    }
+
+    @Override
+    public void userEventTriggered(ChannelHandlerContext ctx, Object e) throws Exception {
+        if (e instanceof IdleStateEvent) {
+            HaClusterInfo cluster = ctx.channel().attr(HaClusterConfig.CLUSTER_ATTRIBUTE_KEY).get();
+            if (cluster == null) {
+                log.error("{}.userEventTriggered: Unknown Cluster: {}.", this.getClass().getSimpleName(), ClusterUtils.getAddress(ctx.channel()));
+                return;
+            }
+
+            IdleStateEvent evt = (IdleStateEvent) e;
+
+            MDC.put("id", cluster.getLogKey());
+            log.info("{}.++userEventTriggered: {}. {}", this.getClass().getSimpleName(), ClusterUtils.getAddress(ctx.channel()), evt);
+
+            if (evt.state() == IdleState.READER_IDLE) {
+                long recvTimeout = System.currentTimeMillis() - cluster.getElectionState().getLastRecvTime();
+                long heartbeatTimeout = this.clusterConfig.getSyncSeconds() * 1000L * 3;
+                if (recvTimeout > heartbeatTimeout) {
+                    log.info("{}.++userEventTriggered: {}. [{}, {}]. Heartbeat timeout, {}, {} ms. Will be closed.",
+                            this.getClass().getSimpleName(), ClusterUtils.getAddress(ctx.channel()),
+                            cluster.getLogKey(), cluster.getIpAddress(), recvTimeout, heartbeatTimeout);
+                    closeChannel(ctx.channel());
+                }
+            }
+            MDC.remove(cluster.getLogKey());
+            MDC.clear();
+        }
+        ctx.fireUserEventTriggered(e);
+    }
+
+    public static void closeChannel(Channel channel) {
+        try {
+            if (channel != null) {
+                channel.flush();
+                channel.disconnect();
+                channel.close();
+            }
+        }
+        catch (Exception e) {
+            log.error("ApplicationRepository.closeChannel Exception: {}", e.getMessage());
+        }
+    }
+
+    @Override
+    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
+        ctx.close();
+    }
+}

+ 86 - 0
its-cluster/src/main/java/com/its/common/cluster/master/ClusterMasterInitializer.java

@@ -0,0 +1,86 @@
+package com.its.common.cluster.master;
+
+import com.its.common.cluster.utils.ClusterMessageDecoder;
+import com.its.common.cluster.utils.ClusterMessageEncoder;
+import com.its.common.cluster.utils.ClusterUtils;
+import com.its.common.cluster.vo.HaClusterConfig;
+import com.its.common.cluster.vo.HaClusterInfo;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelPipeline;
+import io.netty.handler.logging.LogLevel;
+import io.netty.handler.logging.LoggingHandler;
+import io.netty.handler.timeout.IdleStateHandler;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.slf4j.MDC;
+
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+@RequiredArgsConstructor
+public class ClusterMasterInitializer extends ChannelInitializer<Channel> {
+
+    private final HaClusterConfig clusterConfig;
+
+    @Override
+    protected void initChannel(Channel channel) throws Exception {
+//        InetSocketAddress remoteAddress = (InetSocketAddress) ctx.channel().remoteAddress();
+//        String clientIP = remoteAddress.getAddress().getHostAddress();
+//        int clientPort = remoteAddress.getPort();
+
+        String ipAddress  = ClusterUtils.getRemoteIpAddress(channel);
+        int clientPort = ClusterUtils.getRemotePort(channel);
+        int serverId = clientPort - this.clusterConfig.getSyncPort();
+        log.info("ClusterMasterInitializer.----initChannel: connected from: {}:{}, ServerId: {}.",
+                ipAddress, clientPort, serverId);
+//        HaClusterConfig.HaCluster cluster = this.clusterConfig.get(ipAddress);
+        HaClusterInfo cluster = this.clusterConfig.getClusterMap().get(serverId);
+        if (cluster == null) {
+            log.error("ClusterMasterInitializer.----initChannel: [ServerId: {}, IP Address: {}], Unknown Server Id. will be closed.", serverId, ipAddress);
+            channel.disconnect();
+            channel.close();
+            return;
+        }
+        if (!cluster.getIpAddress().equals(ipAddress)) {
+            log.error("ClusterMasterInitializer.----initChannel: [ServerId: {}, IP Address: {}], Unknown IP Address. will be closed.", serverId, ipAddress);
+            channel.disconnect();
+            channel.close();
+            return;
+        }
+
+        try {
+            MDC.put("id", cluster.getLogKey());
+
+            log.info("ClusterMasterInitializer.----initChannel: [{}, {}].", cluster.getLogKey(), cluster.getIpAddress());
+            if (cluster.getElectionState().getChannel() != null) {
+                log.warn("ClusterMasterInitializer.----initChannel: {}, {}, Already Connected. Old Connection will be closed.", ipAddress, cluster.getServerId());
+                // 이벤트 핸들러 에서 중복 처리 되지 않도록 속성 값을 제거
+                channel.attr(HaClusterConfig.CLUSTER_ATTRIBUTE_KEY).set(null);
+                cluster.getElectionState().disConnect();
+
+                channel.disconnect();
+                channel.close();
+            }
+
+            cluster.getElectionState().connect(channel);
+            channel.attr(HaClusterConfig.CLUSTER_ATTRIBUTE_KEY).set(cluster);
+
+            IdleStateHandler idleStateHandler = new IdleStateHandler(this.clusterConfig.getSyncSeconds(), 0, 0, TimeUnit.SECONDS);
+            ChannelPipeline pipeline = channel.pipeline();
+            if (this.clusterConfig.isLogging()) {
+                pipeline.addLast(new LoggingHandler(LogLevel.INFO));
+            }
+            pipeline.addLast(idleStateHandler);
+
+            pipeline.addLast(new ClusterMessageDecoder());
+            pipeline.addLast(new ClusterMessageEncoder());
+            pipeline.addLast(new ClusterMasterHandler(this.clusterConfig));
+        }
+        finally {
+            MDC.remove(cluster.getLogKey());
+            MDC.clear();
+        }
+    }
+
+}

+ 163 - 0
its-cluster/src/main/java/com/its/common/cluster/master/ClusterMasterService.java

@@ -0,0 +1,163 @@
+package com.its.common.cluster.master;
+
+import com.its.common.cluster.utils.ClusterPlatform;
+import com.its.common.cluster.utils.ClusterUtils;
+import com.its.common.cluster.vo.HaClusterConfig;
+import com.its.common.cluster.vo.HaClusterInfo;
+import com.its.common.cluster.vo.HaNET;
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelOption;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.epoll.Epoll;
+import io.netty.channel.nio.NioEventLoopGroup;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.PostConstruct;
+import java.util.Map;
+import java.util.concurrent.ScheduledFuture;
+
+@Slf4j
+@RequiredArgsConstructor
+public class ClusterMasterService {
+
+    private final ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
+    private ScheduledFuture<?> taskFuture;
+
+    private final HaClusterConfig clusterConfig;
+
+    private EventLoopGroup acceptGroup;
+    private EventLoopGroup workerGroup;
+    private ChannelFuture channelFuture;
+
+    @PostConstruct
+    void init() {
+        this.taskScheduler.setPoolSize(1);
+        this.taskScheduler.initialize();
+    }
+
+    public void start() {
+        if (!ClusterPlatform.isWindows()) {
+            if (!Epoll.isAvailable()) {
+                Epoll.unavailabilityCause().printStackTrace();
+            }
+        }
+        if (ClusterUtils.isEpollAvailable()) {
+            log.info("The Cluster Master runs in LINUX EPOLL mode.");
+        }
+        else {
+            log.info("The Cluster Master runs in Windows NIO mode.");
+        }
+
+        this.acceptGroup = new NioEventLoopGroup();
+        this.workerGroup = new NioEventLoopGroup();
+        ServerBootstrap serverBootstrap = createBootstrap();
+
+        log.info("*********************************************************************************");
+        log.info("**              UTIC HA Cluster Master Server Information                      **");
+        log.info("**     bindAddress: {}", this.clusterConfig.getIpAddress());
+        log.info("**      listenPort: {}", this.clusterConfig.getSyncPort());
+        log.info("**        isMaster: {}", this.clusterConfig.isMaster());
+        log.info("*********************************************************************************");
+
+        try {
+            if (this.clusterConfig.getIpAddress().equals("0.0.0.0")) {
+                this.channelFuture = serverBootstrap.bind(this.clusterConfig.getSyncPort());
+            }
+            else {
+                this.channelFuture = serverBootstrap.bind(this.clusterConfig.getIpAddress(), this.clusterConfig.getSyncPort());
+            }
+            electionMasterSchedule();
+        }
+        catch (Exception e) {
+            log.error("cluster start, InterruptedException");
+            shutdown();
+        }
+    }
+    public ServerBootstrap createBootstrap() {
+        ServerBootstrap serverBootstrap = new ServerBootstrap();
+        EventLoopGroup acceptGroups;
+        EventLoopGroup workerGroups;
+
+        acceptGroups = ClusterUtils.newEventLoopGroup(1, "Accept");
+        workerGroups = ClusterUtils.newEventLoopGroup(1, "Worker");
+        serverBootstrap.channel(ClusterUtils.getServerSocketChannel());
+        serverBootstrap.group(acceptGroups, workerGroups);
+
+        serverBootstrap.option(ChannelOption.AUTO_READ, true);
+        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.childOption(ChannelOption.SO_LINGER, 0);           // 4way-handshake 비활성
+        serverBootstrap.childOption(ChannelOption.SO_KEEPALIVE, false);    // KEEPALIVE 비활성(활성: true)
+        serverBootstrap.childOption(ChannelOption.SO_REUSEADDR, true);     // 소켓 재사용
+        serverBootstrap.childOption(ChannelOption.TCP_NODELAY, true);      // Nagle 알고리즘 비활성화
+
+        ClusterMasterInitializer clusterMasterInitializer = new ClusterMasterInitializer(
+                this.clusterConfig
+        );
+        serverBootstrap.childHandler(clusterMasterInitializer);
+
+        return serverBootstrap;
+    }
+
+    private void electionMasterSchedule() {
+        this.taskFuture = this.taskScheduler.scheduleAtFixedRate(() -> {
+            int masterId = Integer.MAX_VALUE;
+            for (Map.Entry<Integer, HaClusterInfo> entry : this.clusterConfig.getClusterMap().entrySet()) {
+                HaClusterInfo cluster = entry.getValue();
+                if (cluster.getElectionState().getState() != HaNET.CLOSED) {
+                    if (cluster.getServerId() < masterId) {
+                        masterId = cluster.getServerId();
+                    }
+                }
+            }
+//            log.info("ClusterMasterService:electionMasterSchedule: serverId: {}, masterId: {}", this.clusterConfig.getServerId(), masterId);
+            if (masterId == Integer.MAX_VALUE || masterId > this.clusterConfig.getServerId()) {
+                this.clusterConfig.setMaster(true);
+            }
+            else {
+                this.clusterConfig.setMaster(false);
+            }
+            log.info("ClusterMasterService:electionMasterSchedule: serverId: {}, Master: {}.",
+                    this.clusterConfig.getServerId(), this.clusterConfig.isMaster());
+        }, 2 * 1000L);
+    }
+
+    public void shutdown() {
+        if (this.taskFuture != null) {
+            this.taskFuture.cancel(true);
+        }
+        this.taskScheduler.shutdown();
+
+        try {
+            if (this.acceptGroup != null) {
+                this.acceptGroup.shutdownGracefully();
+            }
+        }
+        catch (Exception e) {
+            log.info("ClusterMasterService.acceptGroup.shutdownGracefully");
+        }
+        try {
+            if (this.workerGroup != null) {
+                this.workerGroup.shutdownGracefully();
+            }
+        }
+        catch (Exception e) {
+            log.info("ClusterMasterService.workerGroup.shutdownGracefully");
+        }
+        try {
+            if (this.channelFuture != null && this.channelFuture.channel() != null) {
+                this.channelFuture.channel().closeFuture();
+            }
+        }
+        catch (Exception e) {
+            log.info("ClusterMasterService.closeFuture");
+        }
+    }
+}

+ 148 - 0
its-cluster/src/main/java/com/its/common/cluster/slave/ClusterSlave.java

@@ -0,0 +1,148 @@
+package com.its.common.cluster.slave;
+
+import com.its.common.cluster.utils.ClusterMessageDecoder;
+import com.its.common.cluster.utils.ClusterMessageEncoder;
+import com.its.common.cluster.utils.ClusterSlaveBootstrapFactory;
+import com.its.common.cluster.vo.HaClusterConfig;
+import com.its.common.cluster.vo.HaClusterInfo;
+import io.netty.bootstrap.Bootstrap;
+import io.netty.channel.*;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.handler.logging.LogLevel;
+import io.netty.handler.logging.LoggingHandler;
+import io.netty.handler.timeout.IdleStateHandler;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+import org.slf4j.MDC;
+
+import java.net.InetSocketAddress;
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+@Getter
+@Setter
+@RequiredArgsConstructor
+public class ClusterSlave  implements Callable<Object> {
+
+    private final ClusterSlaveService slaveService;
+    private final HaClusterConfig clusterConfig;
+    private final HaClusterInfo cluster;
+    private final ClusterSlaveBootstrapFactory bootstrapFactory;
+
+    private Bootstrap bootstrap = null;
+    private EventLoopGroup nioEventLoopGroup = null;
+    private ChannelFuture channelFuture = null;
+    private String ipAddress;
+    private int port;
+
+    @Override
+    public Object call() {
+
+        try {
+            MDC.put("id", this.cluster.getLogKey());
+
+            this.ipAddress = this.cluster.getIpAddress();
+            this.port = this.cluster.getSyncPort();
+
+            if (this.bootstrap == null) {
+                log.info("ClusterSlave >>>>>>>>Start: [{}, {}], {}", this.cluster.getServerId(), this.ipAddress, this.port);
+                this.bootstrap = this.bootstrapFactory.createBootstrap();
+                this.bootstrap.option(ChannelOption.SO_REUSEADDR, true);
+                this.bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5 * 1000);
+                this.bootstrap.handler(new ChannelInitializer<SocketChannel>() {
+                    // 핸들러가 실행되는 순서는 추가된 순서에 의해 결정된다.(Inbound: head=>tail, Outbound: tail=>head, name2ctx)
+                    @Override
+                    public void initChannel(SocketChannel ch) {
+                        if (cluster.isLogging()) {
+                            ch.pipeline().addLast(new LoggingHandler(LogLevel.INFO));
+                        }
+                        IdleStateHandler idleStateHandler = new IdleStateHandler(10, 0, 0, TimeUnit.SECONDS);
+
+                        ch.pipeline().addLast(idleStateHandler);
+                        ch.pipeline().addLast(new ClusterMessageDecoder());
+                        ch.pipeline().addLast(new ClusterMessageEncoder());
+                        ch.pipeline().addLast(new ClusterSlaveHandler(slaveService, cluster));
+                    }
+                });
+                // 바인드 로컬 포트 설정
+                this.bootstrap.localAddress(new InetSocketAddress(this.port + this.clusterConfig.getServerId()));
+            }
+
+            log.info("ClusterSlave >>Connect Try: [{}, {}], {}", this.cluster.getServerId(), this.ipAddress, this.port);
+            if (this.channelFuture != null && this.channelFuture.channel() != null) {
+                this.channelFuture.channel().close();
+                this.channelFuture = null;
+            }
+            this.channelFuture = this.bootstrap.connect(new InetSocketAddress(this.ipAddress, this.port));
+
+            // 연결 리스너 추가
+            this.channelFuture.addListener(new ChannelFutureListener() {
+                @Override
+                public void operationComplete(ChannelFuture future) {
+                    channelOpen(future);
+                }
+            });
+
+            // 연결 종료 리스너 추가
+            this.channelFuture.channel().closeFuture().addListener(new ChannelFutureListener() {
+                @Override
+                public void operationComplete(ChannelFuture future) {
+                    channelClosed(future);
+                }
+            });
+            return null;
+        }
+        finally {
+            MDC.remove(this.cluster.getLogKey());
+            MDC.clear();
+        }
+    }
+
+    /**
+     * 연결 성공시 처리 이벤트
+     */
+    protected void channelOpen(ChannelFuture future) {
+        try {
+            MDC.put("id", this.cluster.getLogKey());
+            if (future.isSuccess()) {
+                Channel channel = future.channel();
+                log.info("ClusterSlave ..channelOpen: [{}, {}], {}, Channel: {}", this.cluster.getServerId(), this.ipAddress, this.port, channel);
+                channel.attr(HaClusterConfig.CLUSTER_ATTRIBUTE_KEY).set(this.cluster);
+                this.cluster.getSyncState().connect(channel);
+            }
+            else {
+                log.warn("ClusterSlave ConnectFailed: [{}, {}], {}, Cause: {}", cluster.getServerId(), cluster.getIpAddress(), cluster.getSyncPort(), future.cause().getMessage());
+            }
+        }
+        finally {
+            MDC.remove(this.cluster.getLogKey());
+            MDC.clear();
+        }
+    }
+
+    /**
+     * 연결 종료시 처리 이벤트
+     * @param future
+     */
+    protected synchronized void channelClosed(ChannelFuture future) {
+        try {
+            MDC.put("id", this.cluster.getLogKey());
+
+            Channel channel = future.channel();
+            log.warn("ClusterSlave channelClosed: [{}, {}], {}, Channel: {}", this.cluster.getServerId(), this.ipAddress, this.port, channel);
+
+            channel.attr(HaClusterConfig.CLUSTER_ATTRIBUTE_KEY).set(null);
+            this.cluster.getSyncState().disConnect();
+            channel.close();
+            channel.eventLoop().schedule(this, 5, TimeUnit.SECONDS);
+        }
+        finally {
+            MDC.remove(this.cluster.getLogKey());
+            MDC.clear();
+        }
+    }
+
+}

+ 47 - 0
its-cluster/src/main/java/com/its/common/cluster/slave/ClusterSlaveHandler.java

@@ -0,0 +1,47 @@
+package com.its.common.cluster.slave;
+
+import com.its.common.cluster.vo.HaClusterInfo;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.util.concurrent.ScheduledFuture;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+
+@Slf4j
+@RequiredArgsConstructor
+public class ClusterSlaveHandler extends ChannelInboundHandlerAdapter {
+
+    private final ClusterSlaveService slaveService;
+    private final HaClusterInfo cluster;
+
+    private ScheduledFuture<?> future;
+
+    @Override
+    public void channelActive(final ChannelHandlerContext ctx) {
+        this.slaveService.sendSyncData(this.cluster, ctx.channel(), null);
+    }
+
+//    @Override
+//    public void channelRead(ChannelHandlerContext ctx, Object msg) {
+//        if (msg instanceof ClusterMessage) {
+//            ClusterMessage clusterMsg = (ClusterMessage) msg;
+//            log.info("ClusterSlaveHandler.channelRead: serverId: {}, serverTime: {}, infos: {}",
+//                    clusterMsg.getServerId(), clusterMsg.getServerTime(), clusterMsg.getInfos().size());
+//        }
+//    }
+
+    @Override
+    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
+        if (future != null) {
+            future.cancel(true);
+        }
+//        this.cluster.getSyncState().disConnect();
+        super.channelInactive(ctx);
+    }
+
+    @Override
+    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
+        ctx.close();
+    }
+}

+ 173 - 0
its-cluster/src/main/java/com/its/common/cluster/slave/ClusterSlaveService.java

@@ -0,0 +1,173 @@
+package com.its.common.cluster.slave;
+
+import com.its.common.cluster.utils.ClusterMessage;
+import com.its.common.cluster.utils.ClusterMessageData;
+import com.its.common.cluster.utils.ClusterSlaveBootstrapFactory;
+import com.its.common.cluster.utils.ClusterUtils;
+import com.its.common.cluster.vo.HaClusterConfig;
+import com.its.common.cluster.vo.HaClusterInfo;
+import com.its.common.cluster.vo.HaNET;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFuture;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.slf4j.MDC;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.PostConstruct;
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledFuture;
+
+@Slf4j
+@RequiredArgsConstructor
+public class ClusterSlaveService {
+
+    private final ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
+    private ScheduledFuture<?> taskFuture;
+
+    private final HaClusterConfig clusterConfig;
+    private ClusterSlaveBootstrapFactory bootstrapFactory;
+
+    private final ExecutorService executorService= Executors.newFixedThreadPool(1);
+    private final List<ClusterSlave> clientTasks = Collections.synchronizedList(new ArrayList<>());
+
+    @PostConstruct
+    void init() {
+        this.bootstrapFactory = new ClusterSlaveBootstrapFactory(1, 5);
+        this.taskScheduler.setPoolSize(1);
+        this.taskScheduler.initialize();
+    }
+
+    public void start() throws Exception {
+        log.info("ClusterSlaveService.run: Start.");
+
+        /**
+         * Cluster 접속
+         */
+        for (Map.Entry<Integer, HaClusterInfo> entry : this.clusterConfig.getClusterMap().entrySet()) {
+            HaClusterInfo cluster = entry.getValue();
+            if (cluster.getServerId() == this.clusterConfig.getServerId()) {
+                continue;
+            }
+            ClusterSlave slaveClient = new ClusterSlave(this, clusterConfig, cluster, this.bootstrapFactory);
+            this.clientTasks.add(slaveClient);
+        }
+
+        try {
+            List<Future<Object>> futures = this.executorService.invokeAll(this.clientTasks);
+            log.info("ClusterSlaveService.run: futures, {} EA.", (long)futures.size());
+
+            dataSyncSchedule();
+        }
+        catch(InterruptedException e) {
+            log.error("ClusterSlaveService.run: Exception: InterruptedException");
+            Thread.currentThread().interrupt();
+        }
+        log.info("ClusterSlaveService.run: ..End.");
+    }
+
+    public void shutdown() {
+        if (this.taskFuture != null) {
+            this.taskFuture.cancel(true);
+        }
+        this.taskScheduler.shutdown();
+
+        for (Map.Entry<Integer, HaClusterInfo> entry : this.clusterConfig.getClusterMap().entrySet()) {
+            HaClusterInfo cluster = entry.getValue();
+            if (cluster.getServerId() == this.clusterConfig.getServerId()) {
+                continue;
+            }
+            channelClose(cluster.getSyncState().getChannel());
+        }
+        try {
+            if (this.bootstrapFactory != null && this.bootstrapFactory.getEventLoopGroup() != null) {
+                this.bootstrapFactory.getEventLoopGroup().shutdownGracefully();
+            }
+        }
+        catch (Exception e) {
+            log.info("ClusterSlaveService.shutdownGracefully");
+        }
+    }
+
+    private void channelClose(Channel channel) {
+        try {
+            if (channel != null) {
+                channel.close();
+            }
+        } catch (Exception e) {
+            log.info("ClusterSlaveService.channelClose");
+        }
+    }
+    private String getSysTime() {
+        SimpleDateFormat sdfDate = new SimpleDateFormat("yyyyMMddHHmmss");
+        Date dtLog = new Date();
+        return sdfDate.format(dtLog);
+    }
+    private ClusterMessage getClusterMessage() {
+        List<ClusterMessageData> details = new ArrayList<>();
+//        List<String> keySet = new ArrayList<>(ApplicationRepository.CENTER_MAP.keySet());
+//        Collections.sort(keySet);
+//        for (String key : keySet) {
+//            CenterDto region = ApplicationRepository.CENTER_MAP.get(key);
+//            if (region == null) {
+//                continue;
+//            }
+////            details.add(region.getClusterData());
+//        }
+        return ClusterMessage.builder()
+                .serverId(this.clusterConfig.getServerId())
+                .serverTime(getSysTime())
+                .infos(details)
+                .build();
+    }
+
+    private void dataSyncSchedule() {
+        log.info("ClusterSlaveService:dataSyncSchedule: {} seconds.", this.clusterConfig.getSyncSeconds());
+        this.taskFuture = this.taskScheduler.scheduleAtFixedRate(() -> {
+            ClusterMessage clusterMsg = getClusterMessage();
+            for (Map.Entry<Integer, HaClusterInfo> entry : this.clusterConfig.getClusterMap().entrySet()) {
+                HaClusterInfo cluster = entry.getValue();
+                if (cluster.getServerId() == this.clusterConfig.getServerId()) {
+                    continue;
+                }
+                if (cluster.getSyncState().getState() != HaNET.CLOSED) {
+                    sendSyncData(cluster, cluster.getSyncState().getChannel(), clusterMsg);
+                }
+            }
+        }, this.clusterConfig.getSyncSeconds() * 1000L);
+
+    }
+
+    public void sendSyncData(final HaClusterInfo cluster, final Channel channel, ClusterMessage clusterMsg) {
+        if (null == clusterMsg) {
+            clusterMsg = getClusterMessage();
+        }
+        try {
+            MDC.put("id", cluster.getLogKey());
+            ChannelFuture f = channel.writeAndFlush(clusterMsg);
+            f.awaitUninterruptibly();
+            if (f.isDone() || f.isSuccess()) {
+                log.info("ClusterSlaveService.sendSyncData: [{}], {}, [--TO: serverId: {}, serverTime: {}, infos: {}]",
+                        this.clusterConfig.getServerId(), ClusterUtils.getTcpAddress(channel),
+                        clusterMsg.getServerId(), clusterMsg.getServerTime(), clusterMsg.getInfos().size());
+            }
+        }
+        catch (Exception e) {
+            log.info("ClusterSlaveService.sendSyncData: [{}], {}, Failed: [--TO: serverId: {}, serverTime: {}, infos: {}]",
+                    this.clusterConfig.getServerId(), ClusterUtils.getTcpAddress(channel),
+                    clusterMsg.getServerId(), clusterMsg.getServerTime(), clusterMsg.getInfos().size());
+            log.info("ClusterSlaveService.sendSyncData: [{}], {}, Failed: {}",
+                    this.clusterConfig.getServerId(), ClusterUtils.getTcpAddress(channel), e.getMessage());
+        }
+        finally {
+            MDC.remove(cluster.getLogKey());
+            MDC.clear();
+        }
+    }
+}
+

+ 18 - 0
its-cluster/src/main/java/com/its/common/cluster/utils/ClusterMessage.java

@@ -0,0 +1,18 @@
+package com.its.common.cluster.utils;
+
+import lombok.Builder;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+@Builder
+@Data
+public class ClusterMessage implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private int serverId;
+    private String serverTime;
+
+    private List<ClusterMessageData> infos;
+}

+ 24 - 0
its-cluster/src/main/java/com/its/common/cluster/utils/ClusterMessageData.java

@@ -0,0 +1,24 @@
+package com.its.common.cluster.utils;
+
+import lombok.Builder;
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Builder
+@Data
+public class ClusterMessageData implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private String centerId;
+    private int state;
+    private String connTm;
+    private String disConnTm;
+
+    private String lastSendTm;
+    private int totalSends;
+    private long baseTm;
+    private long sendTm;
+    private int sendSeconds;
+
+}

+ 35 - 0
its-cluster/src/main/java/com/its/common/cluster/utils/ClusterMessageDecoder.java

@@ -0,0 +1,35 @@
+package com.its.common.cluster.utils;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.ByteToMessageDecoder;
+
+import java.io.ByteArrayInputStream;
+import java.io.ObjectInputStream;
+import java.util.List;
+
+public class ClusterMessageDecoder extends ByteToMessageDecoder {
+
+    @Override
+    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
+        if (in.readableBytes() < 4) {
+            return;
+        }
+
+        in.markReaderIndex();
+        int dataLength = in.readInt();
+
+        if (in.readableBytes() < dataLength) {
+            in.resetReaderIndex();
+            return;
+        }
+
+        byte[] bytes = new byte[dataLength];
+        in.readBytes(bytes);
+        ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
+        ObjectInputStream ois = new ObjectInputStream(bis);
+        out.add(ois.readObject());
+        ois.close();
+        bis.close();
+    }
+}

+ 24 - 0
its-cluster/src/main/java/com/its/common/cluster/utils/ClusterMessageEncoder.java

@@ -0,0 +1,24 @@
+package com.its.common.cluster.utils;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.MessageToByteEncoder;
+
+import java.io.ByteArrayOutputStream;
+import java.io.ObjectOutputStream;
+
+public class ClusterMessageEncoder extends MessageToByteEncoder<ClusterMessage> {
+
+    @Override
+    protected void encode(ChannelHandlerContext ctx, ClusterMessage msg, ByteBuf out) throws Exception {
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        ObjectOutputStream oos = new ObjectOutputStream(bos);
+        oos.writeObject(msg);
+        oos.flush();
+        byte[] bytes = bos.toByteArray();
+        out.writeInt(bytes.length); // 데이터 길이를 포함해서 전송
+        out.writeBytes(bytes);
+        oos.close();
+        bos.close();
+    }
+}

+ 12 - 0
its-cluster/src/main/java/com/its/common/cluster/utils/ClusterPlatform.java

@@ -0,0 +1,12 @@
+package com.its.common.cluster.utils;
+
+public class ClusterPlatform {
+    private static final String OS = System.getProperty("os.name").toLowerCase();
+
+    private ClusterPlatform() {
+    }
+
+    public static boolean isWindows() {
+        return OS.contains("win");
+    }
+}

+ 49 - 0
its-cluster/src/main/java/com/its/common/cluster/utils/ClusterSlaveBootstrapFactory.java

@@ -0,0 +1,49 @@
+package com.its.common.cluster.utils;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.channel.ChannelOption;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.epoll.Epoll;
+import io.netty.channel.epoll.EpollEventLoopGroup;
+import io.netty.channel.epoll.EpollSocketChannel;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.nio.NioSocketChannel;
+import io.netty.util.concurrent.DefaultThreadFactory;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+@RequiredArgsConstructor
+public class ClusterSlaveBootstrapFactory {
+    private final int workerThread;
+    private final int connectTimeout;
+    private EventLoopGroup nioEventLoopGroup = null;
+
+    public Bootstrap createBootstrap() {
+        if (this.nioEventLoopGroup == null) {
+            this.nioEventLoopGroup = ClusterUtils.newEventLoopGroup(this.workerThread, "itsClusterEventGroup");
+            //new NioEventLoopGroup(this.workerThread);  //EpollEventLoopGroup
+        }
+        Bootstrap bootstrap = new Bootstrap();
+        bootstrap.group(this.nioEventLoopGroup);
+
+        if (ClusterUtils.isEpollAvailable()) {
+            bootstrap.channel(EpollSocketChannel.class);
+        }
+        else {
+            bootstrap.channel(NioSocketChannel.class);
+        }
+        bootstrap.option(ChannelOption.AUTO_READ, true);
+        bootstrap.option(ChannelOption.TCP_NODELAY, true);
+        bootstrap.option(ChannelOption.SO_RCVBUF, 8192);
+        bootstrap.option(ChannelOption.SO_SNDBUF, 8192);
+        bootstrap.option(ChannelOption.SO_KEEPALIVE, false);
+        //bootstrap.option(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(2048));
+        bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, this.connectTimeout * 1000);
+        return bootstrap;
+    }
+
+    public EventLoopGroup getEventLoopGroup() {
+        return this.nioEventLoopGroup;
+    }
+}

+ 257 - 0
its-cluster/src/main/java/com/its/common/cluster/utils/ClusterUtils.java

@@ -0,0 +1,257 @@
+package com.its.common.cluster.utils;
+
+import io.netty.channel.Channel;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.epoll.Epoll;
+import io.netty.channel.epoll.EpollEventLoopGroup;
+import io.netty.channel.epoll.EpollServerSocketChannel;
+import io.netty.channel.epoll.EpollSocketChannel;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.ServerSocketChannel;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.channel.socket.nio.NioServerSocketChannel;
+import io.netty.channel.socket.nio.NioSocketChannel;
+import io.netty.util.concurrent.DefaultThreadFactory;
+
+import java.net.*;
+import java.util.ArrayList;
+import java.util.Enumeration;
+
+public class ClusterUtils {
+    public static final String OS_NAME = System.getProperty("os.name");
+    private static boolean isLinuxPlatform = false;
+    private static boolean isWindowsPlatform = false;
+
+    private ClusterUtils() {
+    }
+    public static String getAddress(Channel ch) {
+        String localIp = "local-unknown";
+        String remoteIp = "remote-unknown";
+        int localPort = 0;
+        int remotePort = 0;
+        InetSocketAddress localAddr = (InetSocketAddress)ch.localAddress();
+        if (localAddr != null) {
+            localIp = localAddr.getAddress().getHostAddress();
+            localPort = localAddr.getPort();
+        }
+
+        InetSocketAddress remoteAddr = (InetSocketAddress)ch.remoteAddress();
+        if (remoteAddr != null) {
+            remoteIp = remoteAddr.getAddress().getHostAddress();
+            remotePort = remoteAddr.getPort();
+        }
+
+        return "[Local #(" + localIp + ":" + localPort + ") Remote #(" + remoteIp + ":" + remotePort + ")]";
+    }
+
+    public static String getRemoteAddress(Channel ch) {
+        String ip = getRemoteIpAddress(ch);
+        int port = getRemotePort(ch);
+        return "[Remote #(" + ip + ":" + port + ")]";
+    }
+
+    public static String getLocalAddress(Channel ch) {
+        String ip = getLocalIpAddress(ch);
+        int port = getLocalPort(ch);
+        return "[Local #(" + ip + ":" + port + ")]";
+    }
+
+    public static String getRemoteIpAddress(Channel ch) {
+        String ip = "255.255.255.255";
+        InetSocketAddress inetAddr = (InetSocketAddress)ch.remoteAddress();
+        if (inetAddr != null) {
+            ip = inetAddr.getAddress().getHostAddress();
+        }
+
+        return ip;
+    }
+
+    public static long getRemoteIpAddressToLong(Channel ch) {
+        String[] ipAddressInArray = getRemoteIpAddress(ch).split("\\.");
+        long result = 0L;
+
+        for(int i = 0; i < ipAddressInArray.length; ++i) {
+            int power = 3 - i;
+            int ip = Integer.parseInt(ipAddressInArray[i]);
+            result += (long)((double)ip * Math.pow((double)256.0F, (double)power));
+        }
+
+        return result;
+    }
+
+    public static int getRemotePort(Channel ch) {
+        int port = 0;
+        InetSocketAddress inetAddr = (InetSocketAddress)ch.remoteAddress();
+        if (inetAddr != null) {
+            port = inetAddr.getPort();
+        }
+
+        return port;
+    }
+
+    public static String getLocalIpAddress(Channel ch) {
+        String ip = "127.0.0.1";
+        InetSocketAddress inetAddr = (InetSocketAddress)ch.localAddress();
+        if (inetAddr != null) {
+            ip = inetAddr.getAddress().getHostAddress();
+        }
+
+        return ip;
+    }
+
+    public static int getLocalPort(Channel ch) {
+        int port = 0;
+        InetSocketAddress inetAddr = (InetSocketAddress)ch.localAddress();
+        if (inetAddr != null) {
+            port = inetAddr.getPort();
+        }
+
+        return port;
+    }
+
+    public static boolean isEpollAvailable() {
+        return Epoll.isAvailable();
+    }
+
+    public static EventLoopGroup newEventLoopGroup(int nThreads, String threadPoolName) {
+        if (isEpollAvailable()) {
+            return threadPoolName.isEmpty() ? new EpollEventLoopGroup(nThreads) : new EpollEventLoopGroup(nThreads, new DefaultThreadFactory("epo" + threadPoolName));
+        } else {
+            return threadPoolName.isEmpty() ? new NioEventLoopGroup(nThreads) : new NioEventLoopGroup(nThreads, new DefaultThreadFactory("nio" + threadPoolName));
+        }
+    }
+
+    public static Class<? extends SocketChannel> getSocketChannel() {
+        return isEpollAvailable() ? EpollSocketChannel.class : NioSocketChannel.class;
+    }
+
+    public static Class<? extends ServerSocketChannel> getServerSocketChannel() {
+        return isEpollAvailable() ? EpollServerSocketChannel.class : NioServerSocketChannel.class;
+    }
+
+    public static String getTcpAddress(Channel ch) {
+        String localIp = "local-unknown";
+        String remoteIp = "remote-unknown";
+        int localPort = 0;
+        int remotePort = 0;
+        InetSocketAddress localAddr = (InetSocketAddress)ch.localAddress();
+        if (localAddr != null) {
+            localIp = localAddr.getAddress().getHostAddress();
+            localPort = localAddr.getPort();
+        }
+
+        InetSocketAddress remoteAddr = (InetSocketAddress)ch.remoteAddress();
+        if (remoteAddr != null) {
+            remoteIp = remoteAddr.getAddress().getHostAddress();
+            remotePort = remoteAddr.getPort();
+        }
+
+        return "[Local #(" + localIp + ":" + localPort + ") Remote #(" + remoteIp + ":" + remotePort + ")]";
+    }
+
+    public static String getLocalAddress() {
+        Enumeration<NetworkInterface> enumeration = null;
+
+        try {
+            enumeration = NetworkInterface.getNetworkInterfaces();
+        } catch (SocketException var7) {
+            return null;
+        }
+
+        ArrayList<String> ipv4Result = new ArrayList<>();
+        ArrayList<String> ipv6Result = new ArrayList<>();
+
+        while(enumeration.hasMoreElements()) {
+            NetworkInterface networkInterface = (NetworkInterface)enumeration.nextElement();
+            Enumeration<InetAddress> en = networkInterface.getInetAddresses();
+
+            while(en.hasMoreElements()) {
+                InetAddress address = (InetAddress)en.nextElement();
+                if (!address.isLoopbackAddress()) {
+                    if (address instanceof Inet6Address) {
+                        ipv6Result.add(normalizeHostAddress(address));
+                    } else {
+                        ipv4Result.add(normalizeHostAddress(address));
+                    }
+                }
+            }
+        }
+
+        if (!ipv4Result.isEmpty()) {
+            for(String ip : ipv4Result) {
+                if (!ip.startsWith("127.0") && !ip.startsWith("192.168")) {
+                    return ip;
+                }
+            }
+
+            return (String)ipv4Result.get(ipv4Result.size() - 1);
+        } else if (!ipv6Result.isEmpty()) {
+            return (String)ipv6Result.get(0);
+        } else {
+            InetAddress localHost;
+            try {
+                localHost = InetAddress.getLocalHost();
+            } catch (UnknownHostException var6) {
+                return null;
+            }
+
+            return normalizeHostAddress(localHost);
+        }
+    }
+
+    public static String normalizeHostAddress(InetAddress localHost) {
+        return localHost instanceof Inet6Address ? "[" + localHost.getHostAddress() + "]" : localHost.getHostAddress();
+    }
+
+    public static SocketAddress string2SocketAddress(String addr) {
+        String[] s = addr.split(":");
+        InetSocketAddress isa = new InetSocketAddress(s[0], Integer.parseInt(s[1]));
+        return isa;
+    }
+
+    public static String socketAddress2String(SocketAddress addr) {
+        StringBuilder sb = new StringBuilder();
+        InetSocketAddress inetSocketAddress = (InetSocketAddress)addr;
+        sb.append(inetSocketAddress.getAddress().getHostAddress());
+        sb.append(":");
+        sb.append(inetSocketAddress.getPort());
+        return sb.toString();
+    }
+
+    public static String parseChannelRemoteAddr(Channel channel) {
+        if (null == channel) {
+            return "";
+        } else {
+            SocketAddress remote = channel.remoteAddress();
+            String addr = remote != null ? remote.toString() : "";
+            if (addr.length() > 0) {
+                int index = addr.lastIndexOf("/");
+                return index >= 0 ? addr.substring(index + 1) : addr;
+            } else {
+                return "";
+            }
+        }
+    }
+
+    public static String parseSocketAddressAddr(SocketAddress socketAddress) {
+        if (socketAddress != null) {
+            String addr = socketAddress.toString();
+            if (addr.length() > 0) {
+                return addr.startsWith("/") ? addr.substring(1) : addr;
+            }
+        }
+
+        return "";
+    }
+
+    static {
+        if (OS_NAME != null && OS_NAME.toLowerCase().contains("linux")) {
+            isLinuxPlatform = true;
+        }
+
+        if (OS_NAME != null && OS_NAME.toLowerCase().contains("windows")) {
+            isWindowsPlatform = true;
+        }
+
+    }
+}

+ 168 - 0
its-cluster/src/main/java/com/its/common/cluster/vo/HaClusterConfig.java

@@ -0,0 +1,168 @@
+package com.its.common.cluster.vo;
+
+import io.netty.util.AttributeKey;
+import lombok.Builder;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.LongAdder;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+@Slf4j
+@Data
+public class HaClusterConfig {
+
+    public static final AttributeKey<HaClusterInfo> CLUSTER_ATTRIBUTE_KEY = AttributeKey.valueOf("clusterInfo");
+
+    private boolean master = false;
+
+    private int syncSeconds = -1;
+    private int serverId = -1;
+    private String ipAddress;
+    private int syncPort = -1;       // 포트 1: 데이터 동기화를 위한 포트
+//    private int electionPort;   // 포트 2: 리더선출을 위한 포트
+
+    private String configFile;
+    private boolean logging = false;
+
+    private final HashMap<Integer, HaClusterInfo> clusterMap = new HashMap<>();
+
+    @PostConstruct
+    private void init() throws IOException {
+
+        log.info("[{}] -------------------------", this.getClass().getSimpleName());
+        loadClusterConfig();
+        log.info("{}", this);
+    }
+
+    public HaClusterInfo get(String ipAddress) {
+        for (Map.Entry<Integer, HaClusterInfo> entry : this.clusterMap.entrySet()) {
+            HaClusterInfo cluster = entry.getValue();
+            if (cluster.getIpAddress().equals(ipAddress)) {
+                return cluster;
+            }
+        }
+        return null;
+    }
+
+    private int parseConfigData(String data) {
+        try {
+            return Integer.parseInt(data);
+        }
+        catch (NumberFormatException e) {
+            return -1;
+        }
+    }
+
+    private String getStringValue(String item, String defValue) throws IOException {
+        Pattern stringPattern = Pattern.compile(item + "=([a-zA-Z0-9]+)");
+        try (BufferedReader reader = new BufferedReader(new FileReader(this.configFile))) {
+            String line;
+            while ((line = reader.readLine()) != null) {
+                Matcher matcher = stringPattern.matcher(line);
+                if (matcher.matches()) {
+                    return matcher.group(1);
+                }
+            }
+        }
+        return defValue;
+    }
+
+    private int getIntValue(String item, int defValue) throws IOException {
+        Pattern serverIdPattern = Pattern.compile(item+"=(\\d+)");
+        try (BufferedReader reader = new BufferedReader(new FileReader(this.configFile))) {
+            String line;
+            while ((line = reader.readLine()) != null) {
+                Matcher matcher = serverIdPattern.matcher(line);
+                if (matcher.matches()) {
+                    return parseConfigData(matcher.group(1));
+                }
+            }
+        }
+        return defValue;
+    }
+
+//    private void setServerId() throws IOException {
+//        Pattern serverIdPattern = Pattern.compile("server\\.id=(\\d+)");
+//        try (BufferedReader reader = new BufferedReader(new FileReader(this.configFile))) {
+//            String line;
+//            while ((line = reader.readLine()) != null) {
+//                Matcher matcher = serverIdPattern.matcher(line);
+//                if (matcher.matches()) {
+//                    this.serverId = parseConfigData(matcher.group(1));
+//                    break;
+//                }
+//            }
+//        }
+//    }
+
+    private void loadClusterConfig() throws IOException {
+        log.info("loadClusterConfig.configFile: {}", this.configFile);
+
+        this.serverId = getIntValue("server.id", 1);
+        this.syncSeconds = getIntValue("syncSeconds", 5);
+        if (this.syncSeconds < 5) {
+            this.syncSeconds = 5;
+        }
+        if (this.syncSeconds > 60) {
+            this.syncSeconds = 60;
+        }
+
+        Pattern serverPattern = Pattern.compile("server\\.(\\d+)=([\\d\\.]+):(\\d+)");
+//        Pattern pattern = Pattern.compile("server\\.(\\d+)=([\\d\\.]+):(\\d+):(\\d+)");
+        int masterId = Integer.MAX_VALUE;
+        try (BufferedReader reader = new BufferedReader(new FileReader(this.configFile))) {
+            String line;
+            while ((line = reader.readLine()) != null) {
+                if (line.startsWith("server.")) {
+                    Matcher matcher = serverPattern.matcher(line);
+                    if (matcher.matches()) {
+                        int serverId = parseConfigData(matcher.group(1));
+                        String ipAddress = matcher.group(2);
+                        int syncPort = parseConfigData(matcher.group(3));
+
+                        if (serverId == this.serverId) {
+                            this.ipAddress = ipAddress;
+                            this.syncPort = syncPort;
+                        }
+                        if (serverId < masterId) {
+                            masterId = serverId;
+                        }
+                        HaClusterInfo haCluster = HaClusterInfo.builder()
+                                .master(false)
+                                .serverId(serverId)
+                                .ipAddress(ipAddress)
+                                .syncPort(syncPort)
+                                .logging(false)
+                                .electionState(new HaNetState())
+                                .syncState(new HaNetState())
+                                .build();
+                        this.clusterMap.put(haCluster.getServerId(), haCluster);
+                        log.info("{}", haCluster);
+                    }
+                }
+            }
+        }
+        if (this.serverId == masterId) {
+            this.master = true;
+        }
+
+        for (Map.Entry<Integer, HaClusterInfo> entry : this.clusterMap.entrySet()) {
+            HaClusterInfo cluster = entry.getValue();
+            if (cluster.getServerId() == masterId) {
+                cluster.setMaster(true);
+                break;
+            }
+        }
+    }
+
+}

+ 23 - 0
its-cluster/src/main/java/com/its/common/cluster/vo/HaClusterInfo.java

@@ -0,0 +1,23 @@
+package com.its.common.cluster.vo;
+
+import lombok.Builder;
+import lombok.Data;
+
+import java.util.concurrent.atomic.LongAdder;
+
+@Data
+@Builder
+public class HaClusterInfo {
+    private boolean master;
+    private int serverId;
+    private String ipAddress;
+    private int syncPort;       // 포트 1: 데이터 동기화를 위한 포트
+    private boolean logging;
+
+    private HaNetState electionState;
+    private HaNetState syncState;
+
+    public String getLogKey() {
+        return String.valueOf(this.serverId);
+    }
+}

+ 14 - 0
its-cluster/src/main/java/com/its/common/cluster/vo/HaNET.java

@@ -0,0 +1,14 @@
+package com.its.common.cluster.vo;
+
+public class HaNET {
+
+    private HaNET() {
+        throw new IllegalStateException("HaNET class");
+    }
+
+    public final static int CLOSED = 0; /* 종료된 상태 */
+    public final static int LOGIN_WAIT = 1;      /* 최초 연결후 로그인 기다림 */
+    public final static int DATA_TRANS = 2;      /* data trans state */
+    public final static int TERMINATE = 2;
+
+}

+ 92 - 0
its-cluster/src/main/java/com/its/common/cluster/vo/HaNetState.java

@@ -0,0 +1,92 @@
+package com.its.common.cluster.vo;
+
+import io.netty.channel.Channel;
+import lombok.Data;
+
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+
+@Data
+public class HaNetState {
+
+    private int state;
+    private Channel channel;
+    private long connectCount;
+    private long lastRecvTime;
+    private long lastSendTime;
+    private int retryCount;
+    private Date connectTime;
+    private Date disconnectTime;
+
+    public HaNetState() {
+        init();
+    }
+
+    public void init() {
+        this.state = HaNET.CLOSED;
+        this.channel = null;
+        this.connectCount = 0;
+        this.lastRecvTime = 0;
+        this.lastSendTime = 0;
+        this.retryCount = 0;
+        this.connectTime = null;
+        this.disconnectTime = null;
+    }
+
+    public void setLastRecvTime() {
+        this.lastRecvTime = System.currentTimeMillis();
+    }
+    public void setLastSendTime() {
+        this.lastSendTime = System.currentTimeMillis();
+    }
+    public void retry() {
+        this.retryCount++;
+    }
+    public void request() {
+        this.retryCount = 0;
+    }
+
+    public void connect(Channel channel) {
+        this.state = HaNET.LOGIN_WAIT;
+        this.channel = channel;
+        this.connectCount++;
+        this.connectTime = new Date();
+        this.retryCount = 0;
+        setLastRecvTime();
+    }
+    public void loginOk() {
+        this.state = HaNET.DATA_TRANS;
+    }
+    public void disConnect() {
+        if (this.state != HaNET.CLOSED) {
+            this.disconnectTime = new Date();
+        }
+        this.state = HaNET.CLOSED;
+        this.channel = null;
+        this.retryCount = 0;
+    }
+    public void terminate() {
+        this.state = HaNET.TERMINATE;
+    }
+    public boolean isActive() {
+        return this.channel != null && this.channel.isActive();
+    }
+    public String getConnectTimeString() {
+        if (this.connectTime == null) {
+            return "----/--/-- --:--:--";
+        }
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(this.connectTime);
+        return (new SimpleDateFormat("yyyy/MM/dd HH:mm:ss")).format(cal.getTime());
+    }
+
+    public String getDisconnectTimeString() {
+        if (this.disconnectTime == null) {
+            return "----/--/-- --:--:--";
+        }
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(this.disconnectTime);
+        return (new SimpleDateFormat("yyyy/MM/dd HH:mm:ss")).format(cal.getTime());
+    }
+}

+ 1 - 0
settings.gradle

@@ -3,4 +3,5 @@ include 'its-common'
 include 'its-spring'
 include 'its-network'
 include 'its-asn1'
+include 'its-cluster'