실습에서는 Zookeeper 3.7.1
버전을 사용한다.
System requirement: https://zookeeper.apache.org/doc/r3.3.5/zookeeperAdmin.html#sc_systemReq
wget https://dlcdn.apache.org/zookeeper/zookeeper-3.7.1/apache-zookeeper-3.7.1-bin.tar.gz
wget https://dlcdn.apache.org/zookeeper/zookeeper-3.7.1/apache-zookeeper-3.7.1-bin.tar.gz
지금 미러서버에서 내려온 상태라서 직접 사이트에서 다운을 받아주자.
tar xvzf apache-zookeeper-3.7.1-bin.tar.gz
mv apache-zookeeper-3.7.1-bin zookeeper
cd zookeeper
(선택)ZK_HOME 설정
echo "export ZK_HOME=$(pwd)" >> ~/.bashrc && source ~/.bashrc
java가 설치되어있어야 한다.
sudo apt install openjdk-8-jre-headless -y
이번 실습에서는 실습의 용이성과 비용절감을 위해 single mode 로 진행한다.
실제 환경에서는 멀티노드환경으로 진행을 해야 함.
$ZK_HOME/conf/zoo.cfg
를 다음과 같이 구성한다.
initLimit=10
# The number of ticks that can pass between
# sending a request and getting an acknowledgement
syncLimit=5
# the directory where the snapshot is stored.
# do not use /tmp for storage, /tmp here is just
# example sakes.
dataDir=/home/ubuntu/zookeeper/data/single
# the port at which the clients will connect
clientPort=2181
# the maximum number of client connections.
# increase this if you need to handle more clients
#maxClientCnxns=60
#
# Be sure to read the maintenance section of the
# administrator guide before turning on autopurge.
#
# http://zookeeper.apache.org/doc/current/zookeeperAdmin.html#sc_maintenance
#
# The number of snapshots to retain in dataDir
#autopurge.snapRetainCount=3
# Purge task interval in hours
# Set to "0" to disable auto purge feature
#autopurge.purgeInterval=1
## Metrics Providers
#
# https://prometheus.io Metrics Exporter
metricsProvider.className=org.apache.zookeeper.metrics.prometheus.PrometheusMetricsProvider
metricsProvider.httpPort=7000
metricsProvider.exportJvmInfo=true
다음 명령어로 시작한다.
cd $ZK_HOME
./bin/zkServer.sh start conf/zoo.cfg
mac 환경에서 주키퍼를 실행하려고 하면
macOS의 ControlCenter 가 7000번 포트를 점유 중이기 때문에 오류가 난다.
port 7000 번에서 7100 으로 바꿔주자.
잘 실행된 모습
종료
cd $ZK_HOME
./bin/zkServer.sh stop conf/zoo.cfg
멀티 노드를 위해서는 앙상블을 이루는 모든 zookeeper 서버의 zoo.cfg 에 다음 내용이 추가되어야 한다.
새로운 서버가 추가되거나 삭제되었다? 이것들 전부 수정해서 재기동 시켜 줘야 함.
server.1=$host:$port_follower_connect_leader:$port_for_leader_election
server.2=$host:$port_follower_connect_leader:$port_for_leader_election
server.3=$host:$port_follower_connect_leader:$port_for_leader_election
$host:$port_follower_connect_leader:$port_for_leader_election
로 구성된다.$host
: zookeeper 서버에서 검색/접근이 가능한 hostname 또는 IP주소$port_follower_connect_leader
: follower 가 leader 와 connection을 맺을때 사용하는 port$port_for_leader_election
: leader election 때 사용하는 포트뿐만 아니라 자신의 dataDir 하위에 myid 라는 파일에 자신의 server number 를 기록해야한다.
echo "1" > $dataDirOfServer1/myid
echo "2" > $dataDirOfServer2/myid
echo "3" > $dataDirOfServer1/myid
각 인스턴스 별 myid 파일을 만든다. (zoo.cfc 에서는 $dataDir/myid 를 보고서 자신이 몇번 서버인지 인식한다.)
Zookeeper binary 에 Cli 가 동봉되어있다.
cd $ZK_HOME
./bin/zkCli.sh -server $host:$clientPort
이렇게 호스트랑 포트가 있다는 건 권한만 있다면 리모트에 있어도 cli 파일로 접근할 수 있다는 것.
create /myroot rootvalue
create /myroot/app1 app1
create /myroot/app2 app2
get /myroot
get /myroot/app1
get /myroot/app2
-s
옵션으로 해당 znode 의 모든 정보를 볼 수 있따.set /myroot rootupdated
dataversion이 0>1로 바뀌었다.
delete /myroot
numChildren 2 > 1 로 변경됨
addWatch /myroot
printwatches # > printwatches is on
set /myroot update2
#> WatchedEvent state:SyncConnected type:NodeDataChanged path:/myroot
-m
모드로 watch level 을 조정할 수 있다.PERSISTENT
: 해당 노드 자신만PERSISTENT_RECURSIVE
: (default) 해당 노드와 자식노드까지자식노드를 변경해보자
watch를 root에 걸었는데 자식을 바꿔도 데이터가 변경되었다는 이벤트가 나왔고 path는 자식 노드 주소를 보여 줌.
PERSISTENT_RECURSIVE
모드로 watch가 걸렸기 때문에 이렇게 나옴.
PERSISTENT
로 한 번 해보자
자식노드의 변경 메세지가 안 나오는 모습
이렇게 끄면 메세지가 안 나온다.
build.gradle 에 다음 dependency 를 추가하고 따라한다.
dependencies {
implementation 'org.apache.zookeeper:zookeeper:3.7.1'
}
Zookeeper zk = new ZooKeeper(connectionString, sessionTimeout, new Watcher() {
public void process(WatchedEvent we) {
if (we.getState() == KeeperState.SyncConnected) {
// do your first job
}
}
});
public class Main {
public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
ZooKeeper zk = new ZooKeeper("localhost:2181", 5000, new Watcher() {
int retry_cnt = 0;
@Override
public void process(WatchedEvent event) {
if (event.getState() == Event.KeeperState.SyncConnected) {
System.out.println("Connected to ZooKeeper!");
}
}
});
Thread.sleep(5000);
String rootPath = zk.create("/exroot", "root".getBytes(StandardCharsets.UTF_8 ), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
System.out.println("root path: " + rootPath);
}
}
Zookeeper.create 함수로 znode 를 생성할 수 있다.
CreateMode 를 원하는 종류로 잘 지정해야한다.
zk.getData(path, watcher, stat);
zk.getData(path, null, null);
Zookeeper.setData 함수를 이용한다.
int version = zk.exists(path, true).getVersion();
zk.setData(path, data, version);
주키퍼에서도 확인을 해보자.
-1
을 넣으면, 버전과 상관 없이 업데이트 된다.zk.addWatch(path, mode)
Leader Election 은 ZooKeeper가 기본적으로 제공하는 기능은 아니지만, ZooKeeper의 코디네이션 기능과 ephemeral znode를 이용해 쉽게 구현할 수 있다.
일반적으로 리더 선출을 직접 구현하려면 락, 동기화, 장애 복구, 서비스 디스커버리 등 복잡한 분산 이슈를 직접 처리해야 한다.
하지만 ZooKeeper는 다음과 같은 기능을 통해 이런 복잡도를 낮춰줄 수 있다
ephemeral znode
: 세션이 종료되면 자동으로 사라지는 노드sequential znode
: 자동으로 순서 번호가 붙는 노드이 두 가지를 조합하면 다음과 같이 리더 선출을 할 수 있다
/election
경로 아래에 ephemeral + sequential
노드를 생성pseudo code
when start application:
1. create my node to znode of namespace for leader election as ephemeral child node
with larger number value than existing children have.
2. elect()
when elect:
1. get children in znode of namespace for leader election.
2. sort children by value
3. get first node in children
4. if first node is my node, I am a leader
else I am a follower
5. watch znode of namespace for leader election on mode - PERSISTENT_RECURSIVE
when watch triggered:
1. if event is NodeDeleted, elect()
else do nothing.
when stop application
1. my node is removed from znode of namespace for leader election as child node because my node was ephemeral node.
leader election 전용 namespace 를 가지는 znode 를 생성하고, CreateMode.EPHEMERAL_SEQUENTIAL
을 이용하면 leader election 을 간단하게 구현할 수 있다.
참고 Example Github
build.gradle
plugins {
id 'java'
id 'application'
id 'com.github.johnrengelman.shadow' version '7.1.2'
}
group 'de.zookeeper'
version '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
application {
mainClass.set("de.zookeeper.leaderelection.App")
}
dependencies {
implementation 'org.apache.zookeeper:zookeeper:3.7.1'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
}
test {
useJUnitPlatform()
}
App.java
package de.zookeeper.leaderelection;
import java.io.IOException;
import org.apache.zookeeper.KeeperException;
public class App {
public static void main( String[] args ) throws IOException, InterruptedException, KeeperException
{
LeaderElection leaderElection = new LeaderElection();
leaderElection.connectToZookeper();
leaderElection.volunteerForLeadership();
leaderElection.reelectLeader();
leaderElection.run();
leaderElection.close();
System.out.println("Disconnected from zooKeeper, exiting application!!");
}
}
LeaderElection.java
package de.zookeeper.leaderelection;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
public class LeaderElection implements Watcher
{
private static final String ZOOKEEPER_ADDRESS = "localhost:2181";
private ZooKeeper zooKeeper;
private static final int SESSION_TIMEOUT = 3000;
private static final String ELECTION_NAMESPACE = "/election";
private String currentZNodeName;
public void volunteerForLeadership() throws KeeperException, InterruptedException {
String zNodePrefix = ELECTION_NAMESPACE + "/c_";
String zNodeFullPath = zooKeeper.create(zNodePrefix, new byte[] {}, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println("znode_name: " + zNodeFullPath);
this.currentZNodeName = zNodeFullPath.replace(ELECTION_NAMESPACE + "/", "");
}
public void reelectLeader() throws KeeperException, InterruptedException {
Stat predecessorStat = null;
String predecessorZnodeName = "";
while (predecessorStat == null) {
List<String> children = zooKeeper.getChildren(ELECTION_NAMESPACE, false);
Collections.sort(children);
String smallestChild = children.get(0);
if (smallestChild.equals(currentZNodeName)) {
System.out.println("I am the leader !!");
} else {
System.out.println("I am not the leader ");
int predecessorIndex = Collections.binarySearch(children, currentZNodeName) - 1;
predecessorZnodeName = children.get(predecessorIndex);
predecessorStat = zooKeeper.exists(ELECTION_NAMESPACE + "/" + predecessorZnodeName, this);
}
}
System.out.println("Watching node: " + predecessorZnodeName);
System.out.println();
}
public void watchTargetZNode() throws KeeperException, InterruptedException {
Stat stat = zooKeeper.exists(ELECTION_NAMESPACE, this);
if (stat == null) {
return;
}
byte[] data = zooKeeper.getData(ELECTION_NAMESPACE, this, stat);
List<String> children = zooKeeper.getChildren(ELECTION_NAMESPACE, this);
System.out.println("Data: " + new String(data) + " Children: " + children);
}
public void connectToZookeper() 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 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:
try {
reelectLeader();
} catch(KeeperException e) {
} catch(InterruptedException e) {
}
System.out.println(ELECTION_NAMESPACE + " got deleted!!");
case NodeCreated:
System.out.println(ELECTION_NAMESPACE + " got created!!");
case NodeDataChanged:
System.out.println(ELECTION_NAMESPACE + " data changed!!");
case NodeChildrenChanged:
System.out.println(ELECTION_NAMESPACE + " gchildren changed!!");
default:
break;
}
try {
watchTargetZNode();
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
ZOOKEEPER_ADDRESS
를 자신의 zookeeper 서버의 IP:port 로 변경./gradlew build
또는 IDEA에서 더블클릭./gradlew shadowJar
또는 IDEA에서 더블클릭-all 적혀 있는 Jar는 fat Jar 라고 해서 라이브러리를 다 포함하고 있는 거라서 사이즈가 크다. 이걸로 실행시켜주면 됨.
일단 주키퍼 서버 띄워져 있는지 확인해보자.
./bin/zkCli.sh -server localhost:2181
/election
znode 를 만들어놓는다.java -jar $myjar.jar
다시 가서 ls 해보면 새로 생김.
터미널의 여러 session에서 실행해보자
zkCli 를 실행해서 ls 로 znode 를 확인한다. leader election app 을 실행, 죽여가면서 확인한다.
터미널을 하나 더 열어서 java-jar 또 실행시켜 보자.
watching node 하고 watching node에 미리 등록되어 있는 children이 보이게 된다.
자기 자신의 zNode : /election/c_0000000001
Cli로 돌아가서 ls 해보면 00000 , 0001 가 등록된 것을 확인할 수 있다.
get -s 옵션으로 등록된 시간이나 데이터 볼 수 있음
세 번째 터미널에서도 실행해준다.
znode_name : /election/c_0000000002
cli 확인해보면 잘 만들어진 것 확인할 수 있었다.
이제 터미널 3번, c_0000000002 이 친구를
ctrl + c 로 종료시켜보자.
종료 시키고 터미널 2번으로 가서 확인을 해보면
/election node children Changed!
라고 메세지가 나온다. 하지만
터미널 1로 가서 확인을 해 보면 리더는 바뀌지 않은 것을 볼 수 있다.
children 은 바뀌었지만 leader의 번호는 앞 번호인 c_0000000000 번 친구가 아직 살아있기 때문에 바뀌지 않았고
잠시 뒤
ls 해보면 c_0000000002 번 친구가 사라진 것을 볼 수 있다.
다시 3번 터미널에서 java -jar 로실행시켜서 살려주자.
이렇게 c_0000000003번으로 만들어졌고
그 다음엔 리더인 c_0000000000 번을 죽인 다음 확인을 해보자.
터미널 1번을 ctrl + c
다른 친구들이 changed라는 걸 받았고,
아까 c_0000000001 번 이던 친구는 자기가 리더가 되었다고 메세지가 쭉 나온다.
Cli 가서 확인을 해보면
c_0000000001번, c_0000000003 번이 있고 c_0000000001번이 앞 번호니까 리더가 된 모습
터미널 2번도 죽이면
이제는 터미널 3번에서 자기가 리더라고 하는 모습.
이제 이 친구 까지 죽여주면
ephemeral, sequencial 이었으니까
ephemeral은 connection이 znode가 삭제된 걸 확인할 수 있다.
Leader election이라는 기능을 직접 구현하려면, 단순히 누가 리더인지 결정하는 것 이상의 요소들을 고려해야 한다.
예를 들어, 네트워크가 일시적으로 끊겼다가 다시 연결되는 상황, 노드가 실제로 죽은 건지 잠깐 통신이 안 되는 건지 구분하는 문제, 동시 접근이나 락 이슈 등… 복잡도가 매우 높아진다.
그런데 이런 복잡한 동기화 문제를 ZooKeeper를 이용하면 훨씬 간단하게 해결할 수 있다.
예제에서는 이 리더 일렉션 로직이 단 하나의 클래스로 구성되어 있는데, 이건 기능의 복잡도에 비해 구현 난이도가 말이 안 될 정도로 낮은 것이다.
즉, ZooKeeper의 강력한 API 덕분에 원래 수십, 수백 줄은 짜야 할 로직을 아주 간결하게 작성할 수 있다.
ZooKeeper가 제공하는 서비스 코디네이션 기능을 활용하면, 여러 노드 간에 공통 설정 정보나 메타데이터, 상태 정보 등을 안정적으로 공유하고 동기화할 수 있다.
이런 부분들을 ZooKeeper에 위임하면, 우리는 애플리케이션 로직 자체에 더 집중할 수 있고, 시스템의 신뢰성과 구현 속도 모두를 향상시킬 수 있다.
즉, 코디네이션을 ZooKeeper가 맡고, 애플리케이션은 본질적인 기능에 집중하는 방식으로 개발의 복잡도를 줄일 수 있는 것이다.