분산 코디네이터
분산 시스템 내의 여러 노드(서버)들이 서로 엇갈리지 않게 관리하고 조율하는 시스템(소프트웨어)을 말함
주요 기능
- 리더 선출
- 분산 락 (동시에 하나의 서버만 특정 자원에 접근하도록 제어)
- 서비스 디스커버리 (IP와 포트 정보를 관리해 통신 서비스를 지원)
대표적인 도구에 ZooKeeper, k8s의 etcd... 가 있음
Master-Workers 아키텍처
분산 시스템을 구축할 때 마주하게 되는 난제들이 있음
1. 리더 선출의 어려움
2. 수 많은 노드로 이루어진 대규모 클러스터에서, 누가 리더인지에 대해 모두가 동의 하는 과정은 훨씬 더 어려움
3. 기본적으로 각 노드는 자기 자신만 알기에, service registry나 service Discovery 기능이 필수적임
4. Failure Detection 메커니즘을 통해 자동적으로 클러스터에서 리더 선출이 가능해야 함
이러한 어려움들을 도와주기 위한 도구가 ZooKeeper임

Znodes

Znodes는 주키퍼가 데이터를 저장하는 기본적 단위를 말함
Znode의 특징
① 파일이면서 동시에 폴더
Znode는 데이터를 저장(파일 역할)할 수도 있고, 그 아래에 또 다른 Znode를 거느릴(폴더 역할) 수도 있음
- 예: /app1이라는 Znode 안에 "설정 정보"를 저장하면서, 동시에 /app1/server-1이라는 자식 노드를 만들 수 있음
② In-Memory
모든 Znode와 그 계층 구조는 메모리(RAM)에 로드되어 있음
그래서 데이터를 읽고 쓰는 속도가 엄청나게 빠지만, 하드디스크처럼 대용량 데이터를 저장하는 용도가 아니라 설정 정보, 상태 정보 같은 아주 작은 데이터를 저장하는 데 최적화되어 있음
③ Persistent Node와 Ephemeral Node
영구 노드: 서버가 주키퍼와 연결을 끊거나, 컴퓨터를 껐다 켜도 데이터가 사라지지 않고 계속 남아있는 노드
임시 노드: 세션이 끝나면 삭제됨(서버와 주키퍼 사이 연결이 끊어지면 자동으로 삭제되는 노드)
- 임시 노드는 주로 헬스 체크나, lock관리, 리더 선출을 위해서 사용됨
리더 선출 알고리즘
주키퍼를 이용해 리더를 선출하는 알고리즘을 알아보자
가장 먼저 온 노드가 리더가 되는 방식임

1. 후보 등록(Volunteering)
주키퍼에 연결되어있는 모든 노드들이 리더가 되고 싶다고 자원을 함
election을 통해 부모 노드 아래에 자신의 Znode를 생성하며, 이때 주키퍼는 들어온 순서대로 노드 이름 뒤에 번호를 붙여줌
2. Querying
Zonde를 만든 후, 각 노드들은 election 부모 노드 아래에 있는 모든 자식 노드 목록을 조회함
즉, 대기하고 있는 노드들은 부모 노드의 자식 목록을 통해 명단을 확인하는 과정임
각 노드끼리는 대화를 하지 않기에, 부모 노드의 목록판을 통해서 확인함
만약, 앞쪽에 우선순위가 높은 노드(번호가 더 작은 노드)가 없다면 자기가 리더라고 판단을 하게 됨
3. 리더 결정
조회한 목록에서 자신이 만든 Znode의 번호를 확인하며, 가장 작은 숫자(가장 먼저 들어온 노드)를 가진 노드를 리더로 선출함
나머지들은 Follower가 됨
이런 방식을 사용하는 이유
- 대칭성 깨기 (Break the Symmetry): 모든 서버가 똑같은 코드로 동작하지만, 주키퍼가 부여해 준 '고유 번호' 덕분에 누가 우선순위인지 명확하게 구별할 수 있음
- 전역 합의 (Global Agreement): 주키퍼가 보장하는 순서(Global Order) 덕분에, 모든 서버가 "누가 1번인지"에 대해 이견 없이 동의할 수 있음
주키퍼 실습
https://zookeeper.apache.org/releases.html#download
Apache ZooKeeper
<!-- Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or ag
zookeeper.apache.org
여기서 다운받을 수 있음
안에 내용물들을 보면

- bin (Binary)
- 역할: 주키퍼를 실행하고 끄는 명령 파일(스크립트)들이 모여 있는 곳
- 주요 파일:
- zkServer.sh (또는 윈도우용 .cmd): 서버를 켜고 끄는 파일
- zkCli.sh (또는 윈도우용 .cmd): 주키퍼에 접속해서 명령을 내리는 클라이언트 파일
- 사용 시점: 서버를 시작하거나 테스트할 때 이 폴더에 들어가서 명령어를 칩니다.
- conf
- 역할: 주키퍼의 설정 파일들이 있는 곳
- 주요 파일: zoo.cfg(메인 환경 설정 파일)가 있음
- lib
- 역할: 주키퍼가 실행되는 데 필요한 자바 라이브러리(JAR 파일)들이 모여있음
- logs
- 역할: 주키퍼가 실행되면서 발생하는 데이터(트랜잭션 로그)와 스냅샷이 저장될 장소
- 사용 시점: conf/zoo.cfg 파일 설정에서 dataDir 항목을 이 폴더 경로로 지정해주면, 앞으로 주키퍼의 모든 기록이 기록됨
- docs (Documentation)
- 역할: 주키퍼 사용 설명서와 API 문서가 들어있음
주키퍼 서버 시작을 해보면...

주키퍼 클라이언트도 실행을 해보자
./zkCli.sh
주키퍼 서버안에 들어갈 수 있도록 해주는 도구임
help명령어를 통해 명령어 사용법을 알 수 있음

부모 노드를 만들어보면

부모 아래 자식도 만들어보면

get -s /parent
parent의 정보들을 확인할 수 있음

Zxid는 번호표를 나타내고, cZxid는 생성될때의 트랜잭션 id를 나타냄
mZxid는 마지막으로 수정된 트랜잭션 id를 나타냄
pZxid는 노드의 자식 목록이 마지막으로 변경된 트랜잭션 id임
cversion은 자식 노드에 대한 변경 횟수
aclVersion은 acl변경횟수를 나타냄
ephemeralOwner값이 0x0이면 영구 노드라는 뜻이며, 다른 값이면 임시 노드를 나타냄
자식 노드의 정보도 비슷하게 알 수 있음

부모를 지우고 election(리더 선출용)노드를 만들어보면

주키퍼 클라이언트의 스레딩 모델과 주키퍼 Java API
자바 코드에서 new ZooKeeper(...)를 호출해서 객체를 생성하면, 내부적으로 두 개의 백그라운드 스레드가 자동으로 만들어져서 돌아감
I/O Thread (SendThread)
주키퍼 서버와의 네트워크 통신을 담당함
- 요청 전송: 우리가 보낸 명령어(create, get 등)를 서버로 보냄
- 응답 수신: 서버에서 온 결과를 받아서 처리 대기열에 넣음
- 하트비트: 아무것도 안 하고 가만히 있어도, 이 스레드가 주기적으로 서버에게 Ping 신호를 보내서 Session이 끊기지 않게 함
- 세션관리, 세션 timeout 등등을 관리함..
EvnetThread
서버로부터 받은 결과나 이벤트 알림을 사용자 코드(Watcher)에게 전달
Watcher 실행: process(WatchedEvent event) 메서드를 실제로 호출해주는게 바로 이 스레드임
-
- 순차 처리: 이벤트가 여러 개 오면 순서대로(Queue) 하나씩 처리함
- 특징: 이 스레드는 네트워크 통신과는 무관하게, 오직 결과 처리와 알림에만 집중함
두 개로 나눈 이유
만약 스레드가 하나면, 사용자의 코드 부분이 느리게 동작하면, 서버에게 ping을 보낼 수가 없음
그렇게 되면 세션이 종료되기에 이렇게 분리를 해둠
그렇기에 EventThread에서 실행되는 process()메서드 안에는 긴 시간 동안 멈추는 작업(sleep, 무한 루프 등..)을 하면 안됨
자바 프로젝트를 하나 만들고 실습을 해보자
pom.xml에 주키퍼 라이브러리를 추가해줘야함
pom.xml에서 작성을 해주면됨
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>distributed-system</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.9.4</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-reload4j</artifactId>
<version>2.0.16</version>
</dependency>
</dependencies>
</project>
package org.example;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.server.quorum.Leader;
import java.io.IOException;
public class LeaderElection implements Watcher {
private static final String ZOOKEEPER_ADDRESS = "localhost:2181";
private ZooKeeper zooKeeper;
private static final int SESSION_TIMEOUT = 3000;
public static void main(String[] args) throws IOException, InterruptedException {
LeaderElection leaderElection = new LeaderElection();
leaderElection.connectToZooKeeper();
leaderElection.run();
}
public void connectToZooKeeper() throws IOException {
this.zooKeeper = new ZooKeeper(ZOOKEEPER_ADDRESS,SESSION_TIMEOUT, this);
}
public void run() throws InterruptedException {
synchronized (this) {
// 누군가 깨울 때까지(notify) 무한정 대기합니다.
// 프로그램이 바로 종료되지 않게 해줍니다.
this.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("Connected to ZooKeeper Successfully");
}
//주키퍼와 연결이 끊어지면 메인 스레드를 깨워서 run메서드 탈출할 수 있게 해줌
else {
synchronized (zooKeeper){
zooKeeper.notifyAll();
}
}
}
}
}

log4j파일도 설정을 해줘야 로그레벨도 찍힘
정지를 해보면


참고)

디버깅을 할때 계속 끊기는 문제가 발생함
이때 모든 스레드가 일시 정지되는데, heartbeat가 일어나는 스레드 또한 중지가 발생해, Seesion Timeout이 발생하게 됨
그렇기 Thread를 체크해서 해당 스레드만 정지하도록 유지를 하면 좋음
리더 선출
/election
├── c_0000000001
├── c_0000000002
└── c_0000000003
구조가 이런식으로 형성됨
추가된 코드를 확인하면
public void volunteerForLeadership() throws KeeperException, InterruptedException {
// election폴더 안에 c_로 시작하는 후보자를 만드는거임
String znodePrefix = ELECTION_NAMESPACE + "/c_";
String znodeFullpath = zooKeeper.create(znodePrefix, new byte[]{}, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println("znode namea" + znodeFullpath);
this.currentZnodename = znodeFullpath.replace(ELECTION_NAMESPACE+"/", "");
}
public void electionLeader() throws KeeperException, InterruptedException {
//election안에 있는 모든 후보를 다 가져옴
List<String> children = zooKeeper.getChildren(ELECTION_NAMESPACE, false);
Collections.sort(children);
//숫자가 가장 작은게 맨 앞에 있도록
String smallestChild = children.get(0);
if(smallestChild.equals(currentZnodename)) {
System.out.println("Leader");
return;
}
System.out.println("not leader"+ smallestChild+"is the leader");
}
프로젝트에 연결된 주키퍼 같은 외부 라이브러리들까지 전부 압축해서 JAR파일에 넣기위해서 아래 같이 수정을 해줘야함
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
<configuration>
<archive>
<manifest>
<mainClass>org.example.LeaderElection</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
</plugins>
</build>
<mainClass>org.example.LeaderElection<mainClass> 이부분이 어떤 파일의 main()부터 시작할지 알려주는 역할을 함
jar파일을 뽑고 서버를 가동시킨 상태에서 새로운 터미널을 열어서 테스트를 해보자



리더 선출이 잘 이루어진 것을 확인할 수 있음
다음편에서는 watcher와 트리거를 활용한 시스템 장애 감지와 리더 재선출에 대해 알아 볼거임
'Distributed System' 카테고리의 다른 글
| 분산 코디네이터와 분산 알고리즘(2) (0) | 2026.02.02 |
|---|