Infra/리눅스

[WAS 이중화] 서로 다른 Jar 를 Fail over 하는 방법 (Java 코드 구현/ JSch 라이브러리 사용)

Nellie Kim 2024. 7. 16. 17:14
728x90

카프카 스트림즈 애플리케이션을 이중화 하기 위해, 원래 자바 프로그램에서 빼고 따로 프로젝트를 만들어서 포트만 다르게 하여 jar 로 만들었다. 운영 시, 부하 문제로 카프카 스트림즈 애플리케이션이 죽을 수도 있을 것 같아서 따로 생성을 했다. 

 

사전 작업으로는  8088, 8089 포트를 지정하여 jar로 만든 후, 원격 서버에 scp 명령어로 전송한 상태이다.

 

JSch 라이브러리를 사용하여 Java에서 SSH 연결을 설정하여 jar1을 10초에 한번씩 헬스체크하고, jar1이 죽으면 jar2를 원격 서버에서 실행하는 방법으로 코드를 구현했다. 

1. 자바 코드 구현

1. JSch 라이브러리 추가

먼저, Maven을 사용한다면 pom.xml 파일에 JSch 라이브러리를 추가

<dependency>
    <groupId>com.jcraft</groupId>
    <artifactId>jsch</artifactId>
    <version>0.1.55</version> 
</dependency>

 

2. Java 코드 작성 

import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.io.ByteArrayOutputStream;
import java.io.IOException;

/**
 * 원격 스트림즈 애플리케이션의 헬스 체크 및 JAR 파일 실행을 담당하는 클래스
 */
@Component
@Slf4j
public class RemoteJarHealthChkAndExecutor {

    // 원격 서버 접속 정보
    private static final String HOST = "192.168.2.99";
    private static final String USERNAME = "root";
    private static final String PASSWORD = "dfg0sfd!";
    private static final int PORT = 22;

    // JAR 파일 경로
    private static final String JAR_FILE_PATH_1 = "/app/AutoDriving-1.0.0-STREAMS-8088.jar";
    private static final String JAR_FILE_PATH_2 = "/app/AutoDriving-1.0.0-STREAMS-8089.jar";

    // JAR 파일 실행 명령어
    private static final String JAVA_COMMAND_1 = "java -jar " + JAR_FILE_PATH_1;
    private static final String JAVA_COMMAND_2 = "java -jar " + JAR_FILE_PATH_2;

    // 현재 구동 중인 포트
    private static int currentPort = 8088;

    /**
     * 1. 10초마다 헬스 체크를 수행하는 메서드
     */
    @Scheduled(fixedRate = 10000)
    public void startHealthCheck() {
        try {
            boolean isJarHealthy = checkHealth(HOST, currentPort);

            if (!isJarHealthy) {
                handleUnhealthyPort();
            } else {
                log.info("😃 {} 포트의 JAR 서버가 정상 상태입니다.", currentPort);
            }
        } catch (Exception e) {
            log.error("헬스 체크 중 오류가 발생했습니다.", e);
        }
    }

    /**
     * 1-1) 원격 서버에서 지정된 포트의 상태를 체크하는 메서드
     */
    private boolean checkHealth(String host, int port) throws JSchException {
        log.info("✨ 현재 포트 : {}", currentPort);

        JSch jsch = new JSch();
        // SSH 세션 설정
        Session session = jsch.getSession(USERNAME, host, PORT);
        session.setPassword(PASSWORD);
        session.setConfig("StrictHostKeyChecking", "no");
        session.connect();

        // exec 채널을 열고 nc 명령어로 포트 체크
        ChannelExec channelExec = (ChannelExec) session.openChannel("exec");
        channelExec.setCommand("nc -zv " + host + " " + port);

        // 명령어 출력 및 에러 스트림 설정
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        ByteArrayOutputStream errorStream = new ByteArrayOutputStream();
        channelExec.setOutputStream(outputStream);
        channelExec.setErrStream(errorStream);

        channelExec.connect();

        // 명령어가 완료될 때까지 대기
        while (!channelExec.isClosed()) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // nc 명령어의 종료 상태로 포트 상태 확인
        int exitStatus = channelExec.getExitStatus();
        System.out.println(port + " 연결 상태 (0이 정상): " + exitStatus);
        System.out.println("Error: " + new String(errorStream.toByteArray()));

        // 채널과 세션 종료
        channelExec.disconnect();
        session.disconnect();

        return exitStatus == 0;
    }

    /**
     * 1-2) 8088 포트 JAR 가 이상 상태일 때 실행하는 메서드
     */
    private void handleUnhealthyPort() throws JSchException, IOException {
        if (currentPort == 8089) {
            log.warn("🚨🚨 8088, 8089 모두 이상 상태입니다. 조치를 취해주세요.");
        } else {
            int newPort = 8089;
            log.warn("🚨 {} 포트의 JAR 서버에 이상이 발생했습니다. {} 포트의 JAR를 실행합니다.", currentPort, newPort);

            // 원격 서버에서 8089 포트의 JAR 파일 실행
            executeRemoteJar(JAVA_COMMAND_2);

            // 포트 업데이트
            currentPort = newPort;
        }
    }

    /**
     * 1-2-1) 원격 서버에서 JAR 파일을 실행하는 메서드
     */
    private void executeRemoteJar(String javaCommand) throws JSchException {
        JSch jsch = new JSch();
        // SSH 세션 설정
        Session session = jsch.getSession(USERNAME, HOST, PORT);
        session.setPassword(PASSWORD);
        session.setConfig("StrictHostKeyChecking", "no");
        session.connect();

        // exec 채널을 열고 JAR 파일 실행 명령어 설정
        ChannelExec channelExec = (ChannelExec) session.openChannel("exec");
        channelExec.setCommand(javaCommand);
        channelExec.connect();

        // 채널과 세션 종료
        channelExec.disconnect();
        session.disconnect();
    }
}

 

주의!  서버 사용자 계정에 jar를 실행시킬 수 있는 권한을 준다. (IllegalStateException 방지)

sudo chmod -R 777 /app

실제 운영 중에는 777로 주면 안된다..

 

2. 이제 서버에서 jar1 을 실행 시키기

java -jar /app/AutoDriving-1.0.0-STREAMS-8088.jar

 

3. failover 테스트

8088 포트에서 실행 중인 jar1 프로세스를 종료한다.

ps aux | grep AutoDriving-1.0.0-STREAMS-8088.jar

위 명령어를 통해 해당 프로세스의 PID 확인 후, 아래 명령어로 종료

$ kill -9 53157

jar2로 failover 되는 것을 확인