본문 바로가기
Server-side 개발 & 트러블 슈팅/🦍 ZooKeeper (주키퍼)

[ZooKeeper] 주키퍼 워처와 트리거를 활용한 시스템 장애 감지와 오토 힐러 클러스터 (Watcher, Trigger, Autohealer)

by 코딩하는 동현 2025. 5. 11.

주키퍼의 워처와 트리거 메커니즘을 활용한 오토힐링 구현 가이드

분산 시스템에서 노드 장애를 감지하고 자동으로 복구하는 메커니즘은 시스템의 안정성과 가용성을 유지하는 데 매우 중요하다.

주키퍼(ZooKeeper)는 이러한 분산 시스템의 조정을 위한 강력한 도구를 제공하며, 그 중에서도 워처(Watcher)와 트리거(Trigger) 메커니즘은 장애 감지와 자동 복구에 필수적인 요소이다.

이 글에서는 주키퍼의 워처와 트리거에 대해 상세히 알아보고, 이를 활용한 오토힐링 구현 방법을 살펴보겠다.

주키퍼의 워처(Watcher)와 트리거(Trigger) 개념

워처는 주키퍼에서 변경사항 발생 시 알림 이벤트를 받기 위해 등록하는 객체이다. 주키퍼는 getChildren(), getData(), exists() 같은 메서드를 호출할 때 워처 객체 참조를 전달할 수 있는 기능을 제공한다. 이를 통해 특정 ZNode(주키퍼에서 데이터를 저장하는 노드)의 상태 변화를 감지하고 이에 대응할 수 있다.

주키퍼의 워처가 가진 가장 큰 특징은 바로 일회성 트리거라는 점이다. 워처는 등록된 이벤트가 발생했을 때 딱 한 번만 알림을 받는다. 이후 동일한 이벤트를 계속 감지하고 싶다면 워처를 다시 등록해야 한다. 이러한 특성은 주키퍼의 워처 메커니즘을 이해하는 데 매우 중요하다.

 

워처를 활용한 이벤트 감지 구현

워처를 활용한 이벤트 감지를 구현하기 위해 다음과 같은 코드를 작성할 수 있다.

import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;

import java.io.IOException;
import java.util.List;

/**
 * Watchers, Triggers and Introduction to Failure Detection
 */
public class WatchersDemo implements Watcher {
    private static final String ZOOKEEPER_ADDRESS = "localhost:2181";
    private static final int SESSION_TIMEOUT = 3000;
    private static final String TARGET_ZNODE = "/target_znode";
    private ZooKeeper zooKeeper;

    public static void main(String[] args) throws InterruptedException, IOException, KeeperException {
        WatchersDemo watchersDemo = new WatchersDemo();
        watchersDemo.connectToZookeeper();
        watchersDemo.watchTargetZnode();
        watchersDemo.run();
        watchersDemo.close();
    }

    public void connectToZookeeper() throws IOException {
        this.zooKeeper = new ZooKeeper(ZOOKEEPER_ADDRESS, SESSION_TIMEOUT, this);
    }

    public void run() throws InterruptedException {
        synchronized (zooKeeper) {
            zooKeeper.wait();
        }
    }

    public void close() throws InterruptedException {
        zooKeeper.close();
    }

    public void watchTargetZnode() throws KeeperException, InterruptedException {
        Stat stat = zooKeeper.exists(TARGET_ZNODE, this);
        if (stat == null) {
            return;
        }

        byte[] data = zooKeeper.getData(TARGET_ZNODE, this, stat);
        List<String> children = zooKeeper.getChildren(TARGET_ZNODE, this);

        System.out.println("Data : " + new String(data) + ", children : " + children);
    }

    @Override
    public void process(WatchedEvent event) {
        switch (event.getType()) {
            case None:
                if (event.getState() == Event.KeeperState.SyncConnected) {
                    System.out.println("Successfully connected to Zookeeper");
                } else {
                    synchronized (zooKeeper) {
                        System.out.println("Disconnected from Zookeeper event");
                        zooKeeper.notifyAll();
                    }
                }
                break;
            case NodeDeleted:
                System.out.println(TARGET_ZNODE + " was deleted");
                break;
            case NodeCreated:
                System.out.println(TARGET_ZNODE + " was created");
                break;
            case NodeDataChanged:
                System.out.println(TARGET_ZNODE + " data changed");
                break;
            case NodeChildrenChanged:
                System.out.println(TARGET_ZNODE + " children changed");
                break;
        }

        try {
            watchTargetZnode();
        } catch (KeeperException e) {
        } catch (InterruptedException e) {
        }
    }
}

이 코드는 주키퍼의 워처 메커니즘을 이해하는 데 도움이 되는 간단한 데모이다. 코드를 자세히 살펴보면:

  1. Watcher 인터페이스를 구현하여 이벤트 처리 메커니즘을 구축한다.
  2. connectToZookeeper() 메서드를 통해 주키퍼에 연결한다.
  3. watchTargetZnode() 메서드에서 특정 ZNode에 대한 워처를 등록한다.
    • exists() 메서드로 ZNode 생성/삭제 이벤트를 감지한다.
    • getData() 메서드로 ZNode 데이터 변경 이벤트를 감지한다.
    • getChildren() 메서드로 자식 ZNode 목록 변경 이벤트를 감지한다.
  4. process() 메서드에서 다양한 이벤트 타입에 따른 처리를 구현한다.
    • NodeDeleted: ZNode 삭제 이벤트
    • NodeCreated: ZNode 생성 이벤트
    • NodeDataChanged: ZNode 데이터 변경 이벤트
    • NodeChildrenChanged: 자식 ZNode 목록 변경 이벤트
  5. 이벤트 처리 후 다시 watchTargetZnode()를 호출하여 워처를 재등록한다.

주키퍼의 워처는 일회성 트리거이므로, 이벤트가 발생한 후에도 계속해서 이벤트를 감지하려면 워처를 다시 등록해야 한다. 위 코드에서는 process() 메서드 마지막에 watchTargetZnode()를 다시 호출하여 이를 구현하고 있다.

 

주키퍼 워처 실습

1. 주키퍼 실행

 

2. 워처 java 파일 실행

 

3. /target_znode 등록후 데이터 추가

zkCli 진입 후 노드 생성

 

java파일에서 워처가 정상적으로 트리거 되는것을 로그로 확인할 수 있다.

 

노드 데이터 변경

 

데이터 변경 트리거

 

4. /target_znode 삭제

 

 

5. 주키퍼 서버 정지

zkCli에서 ctrl+c로 cli 나간후, 주키퍼 서버를 정지시킨다.


군집 효과(Herd Effect)와 그 문제점

분산 시스템에서 워처를 사용할 때 주의해야 할 중요한 문제 중 하나는 군집 효과(Herd Effect)이다.

군집 효과는 많은 노드가 특정 이벤트 발생을 기다리고 있다가, 해당 이벤트가 발생하면 모든 노드가 동시에 깨어나 동일한 작업을 시도하는 현상을 말한다.

예를 들어, 리더 노드가 중단될 경우 모든 노드가 리더 ZNode를 지켜보고 있다면, 리더 노드 장애 시 모든 노드가 동시에 알림을 받고 getChildren() 메서드를 호출하게 된다. 이는 주키퍼 서버에 과도한 부하를 주게 되어 성능 저하나 시스템 장애로 이어질 수 있다.

이러한 군집 효과를 방지하기 위해 각 노드가 계층적으로 다른 노드를 감시하도록 설계할 수 있다. 예를 들어, 각 노드가 자신보다 앞선 순서의 노드만 감시하도록 구성하면, 리더 노드 장애 시 그 다음 순서의 노드만 알림을 받게 되어 군집 효과를 피할 수 있다.


오토힐링(Auto Healing) 구현

오토힐링은 클라우드 컴퓨팅에서 클러스터를 모니터링하고 결함이 있는 응용 프로그램 인스턴스를 감지하여 자동으로 교체하는 기능이다. 주키퍼의 워처 메커니즘을 활용하면 결함 있는 인스턴스를 감지하고 새로운 인스턴스를 시작하는 오토힐링 시스템을 구현할 수 있다.

다음은 주키퍼를 사용한 오토힐링 구현의 예시 코드이다:

import org.apache.zookeeper.*;

import java.io.File;
import java.io.IOException;
import java.util.List;

public class Autohealer implements Watcher {

    private static final String ZOOKEEPER_ADDRESS = "localhost:2181";
    private static final int SESSION_TIMEOUT = 3000;

    // Parent Znode where each worker stores an ephemeral child to indicate it is alive
    private static final String AUTOHEALER_ZNODES_PATH = "/workers";

    // Path to the worker jar
    private final String pathToProgram;

    // The number of worker instances we need to maintain at all times
    private final int numberOfWorkers;
    private ZooKeeper zooKeeper;

    public Autohealer(int numberOfWorkers, String pathToProgram) {
        this.numberOfWorkers = numberOfWorkers;
        this.pathToProgram = pathToProgram;
    }

    public void startWatchingWorkers() throws KeeperException, InterruptedException {
        if (zooKeeper.exists(AUTOHEALER_ZNODES_PATH, false) == null) {
            zooKeeper.create(AUTOHEALER_ZNODES_PATH, new byte[]{}, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        }
        launchWorkersIfNecessary();
    }

    public void connectToZookeeper() throws IOException {
        this.zooKeeper = new ZooKeeper(ZOOKEEPER_ADDRESS, SESSION_TIMEOUT, this);
    }

    public void run() throws InterruptedException {
        synchronized (zooKeeper) {
            zooKeeper.wait();
        }
    }

    public void close() throws InterruptedException {
        zooKeeper.close();
    }

    @Override
    public void process(WatchedEvent event) {
        switch (event.getType()) {
            case None:
                if (event.getState() == Event.KeeperState.SyncConnected) {
                    System.out.println("Successfully connected to Zookeeper");
                } else {
                    synchronized (zooKeeper) {
                        System.out.println("Disconnected from Zookeeper event");
                        zooKeeper.notifyAll();
                    }
                }
                break;
            case NodeChildrenChanged:
                launchWorkersIfNecessary();
        }
    }

    private void launchWorkersIfNecessary() {
        try {
            List<String> children = zooKeeper.getChildren(AUTOHEALER_ZNODES_PATH, this);
            System.out.println(String.format("Currently there are %d workers", children.size()));

            if (children.size() < numberOfWorkers) {
                startNewWorker();
            }
        } catch (InterruptedException | KeeperException | IOException e) {
            e.printStackTrace();
            System.exit(1);
        }
    }

    private void startNewWorker() throws IOException {
        File flakyWorkerJarFile = new File(pathToProgram);
        ProcessBuilder processBuilder =
                new ProcessBuilder("java","-jar",flakyWorkerJarFile.getCanonicalPath());

        File log = new File("log.txt");
        processBuilder.redirectErrorStream(true);
        processBuilder.redirectOutput(ProcessBuilder.Redirect.appendTo(log));
        System.out.println("Launching new worker instance");
        processBuilder.start();
    }
}

 

이 오토힐링 구현을 실행하는 메인 애플리케이션 코드는 다음과 같다:

import org.apache.zookeeper.KeeperException;

import java.io.IOException;

public class Application {
    public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
        if (args.length != 2) {
            System.out.println("Expecting parameters <number of workers> <path to worker jar file>");
            System.exit(1);
        }

        int numberOfWorkers = Integer.parseInt(args[0]);
        String pathToWorkerProgram = args[1];
        Autohealer autohealer = new Autohealer(numberOfWorkers, pathToWorkerProgram);
        autohealer.connectToZookeeper();
        autohealer.startWatchingWorkers();
        autohealer.run();
        autohealer.close();
    }
}

이 오토힐링 시스템의 주요 특징은 다음과 같다:

  1. 임시(Ephemeral) ZNode 활용: 각 워커 인스턴스는 자신이 살아있음을 나타내기 위해 임시 ZNode를 생성한다. 워커가 충돌하거나 종료되면 해당 ZNode는 자동으로 삭제된다.
  2. 워처를 통한 자동 감지: 오토힐러는 /workers 경로에 워처를 등록하여 자식 ZNode 목록의 변경(워커 추가/제거)을 감지한다.
  3. 자동 복구 메커니즘: 활성 워커 수가 지정된 수(numberOfWorkers) 미만일 경우, 새로운 워커 인스턴스를 시작한다.
  4. ProcessBuilder를 통한 프로세스 관리: 새로운 워커 인스턴스는 Java ProcessBuilder를 사용하여 외부 프로세스로 시작된다.

이 구현에서 특히 주목할 점은 워처의 일회성 트리거 특성을 활용하는 방법이다. launchWorkersIfNecessary() 메서드 내에서 getChildren()을 호출할 때 this(워처 객체)를 전달하여 워처를 재등록하고 있다. 이를 통해 워커 목록이 변경될 때마다 계속해서 알림을 받을 수 있다.

 

오토힐링 실습

1. 임의의 worker jar파일을 가져온후, autohealer 프로젝트 폴더 안에다가 복사한다.

임의의 worker 파일 flaky.worker-1.0-SNAPSHOT-jar-with-dependencies.jar

 

2. 인자를 이용해 워커의 갯수랑 워커 jar 파일 경로 입력

시작하자마자 3개의 워커 jar파일이 실행되는것을 볼 수 있다.

 

워커 3개가 등록된 모습

 

3. 임의로 워커를 삭제 테스트

 

오토힐러가 자동으로 삭제를 감지하고 워커 jar파일을 실행시킨다.

 

반영된 모습

 

오토힐링의 실제 응용

오토힐링 시스템은 다양한 분산 시스템과 클라우드 환경에서 활용될 수 있다. 예를 들어:

  1. 마이크로서비스 아키텍처: 각 서비스 인스턴스의 상태를 모니터링하고 실패한 서비스를 자동으로 재시작할 수 있다.
  2. 데이터 처리 파이프라인: 데이터 처리 작업자의 상태를 감시하고 작업자가 충돌할 경우 새 작업자를 시작하여 데이터 처리의 연속성을 보장할 수 있다.
  3. 클라우드 인프라 관리: 클라우드 환경에서 가상 머신이나 컨테이너의 상태를 모니터링하고 장애가 발생한 인스턴스를 자동으로 교체할 수 있다.

주키퍼의 임시 ZNode와 워처 메커니즘을 활용한 오토힐링 구현은 분산 시스템의 안정성과 가용성을 크게 향상시킬 수 있다. 워커 인스턴스의 상태를 지속적으로 모니터링하고 필요시 자동으로 복구함으로써, 시스템 관리자의 수동 개입 없이도 시스템이 정상적으로 작동하도록 보장할 수 있다.

결론

주키퍼의 워처와 트리거 메커니즘은 분산 시스템에서 상태 변화를 감지하고 대응하는 강력한 도구이다. 이를 활용하면 노드 장애를 감지하고 자동으로 복구하는 오토힐링 시스템을 구현할 수 있다. 하지만 워처의 일회성 트리거 특성과 군집 효과와 같은 잠재적인 문제점을 이해하고 적절히 대응하는 것이 중요하다. 적절히 설계된 오토힐링 시스템은 분산 시스템의 안정성과 가용성을 크게 향상시킬 수 있으며, 클라우드 컴퓨팅 환경에서 시스템의 자가 복구 능력을 강화하는 데 기여할 수 있다.

반응형

댓글