주키퍼 (Apache Zookeepr) 2. 주키퍼 클러스터 설치

2021. 1. 18. 05:19빅데이터 플랫폼 (Bigdata Platforms)/아파치 주키퍼 (Apache Zookeeper)


시작하면서

 

주키퍼에 대한 기본 개요는 이전글을 참고해주시길 바랍니다.

주키퍼 설치는 Ubuntu 18.04 LTS에서 진행하였습니다.

 

이전 HDFS 완전 분산 모드를 설치한 포스팅에서는 각 설치 단계 안에서 필요한 요소를 정의했지만, 해당 포스트는 제가 어떤 플랫폼을 설치할 때 접근하는 과정을 설명하는 방향으로 진행하겠습니다.

 


설치 준비

 

주키퍼를 단일(standalone) 모드로 설치하고자 하는 분들은 여기를 눌러 'Standalone Operation' 부분을 참고해주시길 바랍니다.

 

주키퍼를 설치하기 위해서는 우선 요구 사양을 확인해야 합니다.

본 장은 리눅스 환경을 가정했으므로, GNU/Linux 부분만 참고하면 됩니다.

 

또한 자바 버전과 성능도 따져봐야 합니다.

명시된 바로는 8, 11, 12 버전에서 잘 동작하며 각 서버는 각기 다른 장치에서 구현하여야 하고 듀얼코어에 2GB 메모리, 80GB의 저장소 공간을 추천하고 있습니다.

ZooKeeper runs in Java, release 1.8 or greater (JDK 8 LTS, JDK 11 LTS, JDK 12 - Java 9 and 10 are not supported). It runs as an ensemble  of ZooKeeper servers. Three ZooKeeper servers is the minimum recommended size for an ensemble, and we also recommend that they run on separate machines. At Yahoo!, ZooKeeper is usually deployed on dedicated RHEL boxes, with dual-core processors, 2GB of RAM, and 80GB IDE hard drives.
https://zookeeper.apache.org/doc/r3.6.2/zookeeperAdmin.html#sc_systemReq 

 

 

저번 글에서 앙상블 내 서버의 수와 쿼럼의 관계에 대해 설명했으므로, 바로 서버를 어떻게 구성할 지 설계해봅니다.

 

 

자, 이제 본격적으로 설치 과정을 여기를 참고해 알아보겠습니다.

 

  1. Java를 설치한다.
  2. 자바 힙(heap) 크기를 조정한다.
  3. 주키퍼를 다운로드한다.
  4. 주키퍼를 설정할 값들을 알아본다.
  5. 환경 변수를 설정한다.
  6. 주키퍼를 설정한다.
  7. 주키퍼를 실행한다.

 

Java를 설치하는 과정은 건너뛰고 2번 과정부터 시작해보겠습니다.

 

 


2. 자바 힙(heap) 크기를 조정한다.

 

주키퍼는 JVM(Java Virtual Machine) 내에서 동작합니다. 이때 JVM는 마치 독립적으로 RAM를 가지면서, 내부적으로 프로세스를 실행할 수 있도록 합니다. 따라서 이를 위해서는 먼저 JVM에 메모리를 할당해야 되며 이를 힙(heap) 메모리라고 합니다.

 

JVM에 만약 본인의 힙 메모리를 다 사용하면 어떻게 될까요?

만약 JVM의 메모리가 다 할당된 상황에서 어떤 프로세스가 1GB 만큼의 메모리를 더 요구했다고 가정해보겠습니다. 그럼 JVM은 초과된 메모리 분량에 대해서 기존에 사용하고 있던 메모리 공간을 일부 디스크 저장 공간에 저장하고 해당 공간 만큼 메모리를 추가로 할당해줍니다. 따라서 해당 디스크 저장 공간은 물리적 메모리(RAM) 공간을 위해 만들어진 여유 공간이라는 의미로 스왑(swap) 공간이라고 불립니다.

 

자, 그럼 이어서 2가지를 더 논의해볼 수 있습니다.

  • 스왑은 성능에 얼마나 영향을 미칠까?
  • 스왑 공간의 크기는 어떻게 지정할 수 있을까?

스왑핑(swapping)은 디스크에 데이터를 읽고 쓰는 시간이 추가로 존재합니다. 대부분 알고 있겠지만 이는 굉장히 긴 시간입니다. 특히 실시간 처리를 위한 프로세스에게 '잠깐만 멈춰줄래?' 하는 행위 자체가 매우 치명적이죠.

 

하지만 예상하지 못하게 메모리 할당 크기가 기존 용량보다 많이 필요한 경우, 프로세스가 죽지 않기를 바라는 입장(java.lang.OutOfMemoryError: Java heap space 자주 보셨나요?)에서는 스왑 공간이 필요하죠.

 

여기에 방문하면 우툰부에서 어떻게 스왑 공간의 크기를 할당하고 있는 지 확인할 수 있습니다.

park@oim:~$ sudo swapon --show
[sudo] park의 암호: 
NAME      TYPE SIZE USED PRIO
/swapfile file   2G   0B   -2

 

 추가로 어떻게 스왑 공간 크기를 지정할 수 있는 지에 대해 알려드리겠습니다. 여기를 방문하면 더 자세한 설명을 보실 수 있습니다.

park@oim:~$ sudo swapoff /swapfile
park@oim:~$ sudo fallocate -l 3G /swapfile
park@oim:~$ sudo mkswap /swapfile
mkswap: /swapfile: warning: wiping old swap signature.
Setting up swapspace version 1, size = 3 GiB (3221221376 bytes)
no label, UUID=43141bdd-b2a1-4cac-9caa-afd1b1ff600e
park@oim:~$ sudo swapon /swapfile
park@oim:~$ sudo swapon --show
NAME      TYPE SIZE USED PRIO
/swapfile file   3G   0B   -2
park@oim:~$ free -h
              total        used        free      shared  buff/cache   available
Mem:            23G        3.5G         17G        910M        2.7G         18G
스왑:        3.0G          0B        3.0G​

 

하지만 더욱 근본적으로 힙 메모리를 늘려야겠죠? 먼저 java -X를 눌러 도움말을 확인해봅니다.

해당 도움말에 있는 정보들은 Java를 이해하는 데 많은 도움이 되니까 자주 방문하면 좋습니다.

park@oim:~$ java -X
    -Xmixed           혼합 모드를 실행합니다(기본값).
    -Xint             해석된 모드만 실행합니다.
    -Xbootclasspath:<:(으)로 구분된 디렉토리 및 zip/jar 파일>
                      부트스트랩 클래스 및 리소스에 대한 검색 경로를 설정합니다.
    -Xbootclasspath/a:<:(으)로 구분된 디렉토리 및 zip/jar 파일>
                      부트스트랩 클래스 경로 끝에 추가합니다.
    -Xbootclasspath/p:<:(으)로 구분된 디렉토리 및 zip/jar 파일>
                      부트스트랩 클래스 경로 앞에 추가합니다.
    -Xdiag            추가 진단 메시지를 표시합니다.
    -Xnoclassgc       클래스의 불필요한 정보 모음을 사용 안함으로 설정합니다.
    -Xincgc           증분적인 불필요한 정보 모음을 사용으로 설정합니다.
    -Xloggc:<file>    시간 기록과 함께 파일에 GC 상태를 기록합니다.
    -Xbatch           백그라운드 컴파일을 사용 안함으로 설정합니다.
    -Xms<size>        초기 Java 힙 크기를 설정합니다.
    -Xmx<size>        최대 Java 힙 크기를 설정합니다.
    -Xss<size>        Java 스레드 스택 크기를 설정합니다.
    -Xprof            CPU 프로파일 작성 데이터를 출력합니다.
    -Xfuture          미래 기본값을 예측하여 가장 엄격한 검사를 사용으로 설정합니다.
    -Xrs              Java/VM에 의한 OS 신호 사용을 줄입니다(설명서 참조).
    -Xcheck:jni       JNI 함수에 대한 추가 검사를 수행합니다.
    -Xshare:off       공유 클래스 데이터 사용을 시도하지 않습니다.
    -Xshare:auto      가능한 경우 공유 클래스 데이터를 사용합니다(기본값).
    -Xshare:on        공유 클래스 데이터를 사용해야 합니다. 그렇지 않을 경우 실패합니다.
    -XshowSettings    모든 설정을 표시한 후 계속합니다.
    -XshowSettings:all
                      모든 설정을 표시한 후 계속합니다.
    -XshowSettings:vm 모든 VM 관련 설정을 표시한 후 계속합니다.
    -XshowSettings:properties
                      모든 속성 설정을 표시한 후 계속합니다.
    -XshowSettings:locale
                      모든 로케일 관련 설정을 표시한 후 계속합니다.

-X 옵션은 비표준 옵션이므로 통지 없이 변경될 수 있습니다.​

 

여기서 -Xmx 명령어를 주목합니다. 아직 주키퍼를 위해 힙 크기를 얼마나 할당하는 건 정답이 없습니다. 하지만 그 방법은 알아야하기에, 추후 단계에서 주키퍼를 설치하면 해당 프로세스에 대한 최대 힙 메모리 크기를 3GB로 변경하겠습니다.

힙 메모리 크기를 제어하는 방법에 대한 설명은 여기를 참고해주시길 바랍니다.

Set the Java heap size. This is very important to avoid swapping, which will seriously degrade ZooKeeper performance. To determine the correct value, use load tests, and make sure you are well below the usage limit that would cause you to swap. Be conservative - use a maximum heap size of 3GB for a 4GB machine.
https://zookeeper.apache.org/doc/r3.6.2/zookeeperAdmin.html#sc_zkMulitServerSetup 

 

 


3. 주키퍼를 다운로드한다.

 

먼저 이전에 HDFS 완전 분산 모드를 설치한 글에서 ssh에 대한 글을 참고하여 컴퓨터들을 연결해주세요.

 

저는 주키퍼 3.6.2 버전을 다운로드 했습니다. 만약 다운로드가 완료되었다면, 다음 명령어로 다운로드 받은 파일의 압축을 풀고 모든 파일을 /usr/local/zookeeper/ 디렉토리에 위치시켜주세요.

park@oim:~$ cd Downloads/
park@oim:~/Downloads$ tar -xf apache-zookeeper-3.6.2-bin.tar.gz
park@oim:~/Downloads$ sudo mkdir /usr/local/zookeeper
park@oim:~/Downloads$ sudo chown park:park /usr/local/zookeeper/
park@oim:~/Downloads$ mv apache-zookeeper-3.6.2-bin/* /usr/local/zookeeper/

 

다른 컴퓨터에도 /usr/local/zookeeper/ 디렉토리를 만들고 scp 명령어로 모든 파일을 복사해주세요.

park@oim:~/Downloads$ ssh jong@jim
Welcome ... (생략)
jong@jim:~& sudo mkdir /usr/local/zookeeper
jong@jim:~& sudo chown jong:jong /usr/local/zookeeper/
jong@jim:~& logout 
park@oim:~/Downloads$ scp -r /usr/local/zookeeper/* jong@jim:/usr/local/zookeeper/
(dim에도 반복해서 파일 복사)

 

 

 


4. 주키퍼를 설정할 값들을 알아본다.

 

주키퍼 디렉토리를 방문해서 먼저 conf/zoo_sample.cfg 파일을 열어봅니다.

그러면 tickTime, initLimit, syncLimit, dataDir, clientPort 이름의 파라미터를 볼 수 있습니다.

 

이 밖에도 클러스터를 만들 때 사용할 파라미터들이 존재합니다. 이번에는 이에 대해서 알아보겠습니다.
더욱 다양한 파라미터에 대한 설명은 여기를 방문해주시길 바랍니다.

 

파라미터 이름 설정값 설명
dataDir /usr/local/zookeeper/data 데이터베이스 스냅샷이 저장될 장소입니다. 
tickTime 2000 ms 단위로 timeout에 대한 시간 단위(tick)입니다. 당연히 heartbeat 주기는 해당 파라미터 값을 고려해야 합니다.
dataLogDir /usr/local/zookeeper/logs 트랜잭션 로그가 저장될 장소입니다.
initLimit 10 tickTime을 곱한 시간 안에 팔로워들이 리더와 동기화하도록 합니다.
connectToLearnerMasterLimit 5 tickTime을 곱한 시간 안에 팔로워들이 선출된 리더들과 연결하도록 합니다.
leaderServes yes 리더에 클라이언트가 접속할 수 있도록 합니다. 당연히 yes가 아니겠냐고 생각할 수 있는데, 사실 리더는 업데이트 버전 관리(coordinate the update)도 수행하므로 클라이언트의 요청에 사용될 자원을 위 관리 작업에 집중할 수도 있습니다.
syncLimit 5 tickTime을 곱한 시간 안에 팔로워들이 주키퍼와 연동되도록 합니다. 연동되지 못한 팔로워는 없어집니다.(be dropped)
group.x 1=1:2:3 쿼럼을 만드는 그룹을 정의합니다. 해당 포스트에서는 3개의 장치만 다루므로 우선 1개의 그룹만 정의합니다. weight.x 를 변경함으로 쿼럼의 투표 가중치를 변경할 수도 있지만 지금은 기본값인 1로 놔둡니다.
standaloneEnabled false 복제(분산) 모드로 동작하기 위해 false로 설정합니다.
localSessionEnabled false 앙상블 내 서버 하나와 연결하여 읽기 전용의 클라이언트 세션을 만듭니다.

 

- group.x 파라미터의 경우 여기를 방문하면 더욱 자세한 설명을 볼 수 있습니다.

 

- tcpKeepAlive 파라미터의 경우, NATs 또는 방화벽이 쿼럼 멤버들 간의 연결을 방해할 때에도 서로 접속되게끔 만들어준다고 설명되어 있습니다. 하지만 정확히 무엇을 의미하는 지 아직 몰라 기본값인 false로 놓겠습니다.

tcpKeepAlive : (Java system property: zookeeper.tcpKeepAlive) New in 3.5.4: Setting this to true sets the TCP keepAlive flag on the sockets used by quorum members to perform elections. This will allow for connections between quorum members to remain up when there is network infrastructure that may otherwise break them. Some NATs and firewalls may terminate or lose state for long running or idle connections. Enabling this option relies on OS level settings to work properly, check your operating system's options regarding TCP keepalive for more information. Defaults to false.
https://zookeeper.apache.org/doc/r3.6.2/zookeeperAdmin.html#sc_configuration

 

- localSessionsEnabled 파라미터의 경우 여기에 방문하면 로컬 세션의 필요성에 대해 알 수 있습니다.

 

위에서 알 수 있듯, dataDir과 dataLogDir 파라미터의 값으로 설정된 디렉토리를 생성해야 되며, 추가로 dataDir 디렉토리 아래에는 myid라고 서버의 아이디 역할을 하는 파일을 만들어야 합니다. 해당 값은 위에서 클러스터를 설계한 이미지를 참고해주세요.

park@oim:~$ cd /usr/local/zookeeper/
park@oim:/usr/local/zookeeper$ mkdir data
park@oim:/usr/local/zookeeper$ vim data/myid
(2 작성 후 저장&종료)
park@oim:/usr/local/zookeeper$ cat data/myid 
2
park@oim:/usr/local/zookeeper$ mkdir logs
(위 과정을 dim, jim 에 myid 값만 다르게 하여 반복)

 

 


옵저버 모드란?

 

파라미터 이름 설정값 설명
observerMasterPort 2191 클라이언트가 옵저버 모드의 주키퍼 서버에 접속하기 위한 포트 번호입니다.
syncEnabled false 관찰자도 참가자처럼 트랜잭션 로그와 스냅샷을 디스크에 저장합니다.
observer.reconnectDelayMs 100 ms 단위로, 옵저버와 리더 간의 연결이 끊어졌을 때 다시 시도하기 전 지연 시간입니다. 이를 통해 리더 선출 시 영향을 주지 않도록 합니다.
observer.election.DelayMs 200 ms 단위로, 옵저버가 리더 선출에 참여하기 전 지연 시간입니다. 이를 통해 리더 선출 시 영향을 주지 않도록 합니다.

 

observerMasterPort 파라미터에서 옵저버 모드(observer mode)라는 말이 등장합니다. 여기서 옵저버는 관찰자라는 의미와 일맥상통합니다.

 

그렇다면 옵저버 서버는 왜 필요할까요?

주키퍼는 클라이언트 요청을 주키퍼 앙상블 내 주키퍼 서버들로 분산시킴으로써 매우 빠르게 처리할 수 있습니다. 하지만 쿼럼(quorum)의 존재는 확장성(scalability)에 중대한 영향을 줄 수 있는데, 이는 곧 앙상블 내 절반 이상의 서버가 클라이언트의 쓰기(write) 요청에 동의해야 하기 때문입니다. 따라서 주키퍼 서버들을 추가하면 추가할수록, 쿼럼을 형성하기 위한 투표 비용이 증가하기 때문에 오히려 전반적인 성능에 악영향을 줄 수 있습니다.

Although ZooKeeper performs very well by having clients connect directly to voting members of the ensemble, this architecture makes it hard to scale out to huge numbers of clients. The problem is that as we add more voting members, the write performance drops. This is due to the fact that a write operation requires the agreement of (in general) at least half the nodes in an ensemble and therefore the cost of a vote can increase significantly as more voters are added.
https://zookeeper.apache.org/doc/r3.4.13/zookeeperObservers.html

 

따라서 이 문제를 해결하기 위해 투표하지 않는 서버를 만들었고, 그 결과가 옵저버 서버입니다. 옵저버는 투표에 참여하지 않으며 그 결과만 조용히 듣습니다. 그리고 쿼럼 내 팔로워와 동일하게 동작합니다.

We have introduced a new type of ZooKeeper node called an Observer which helps address this problem and further improves ZooKeeper's scalability. Observers are non-voting members of an ensemble which only hear the results of votes, not the agreement protocol that leads up to them. Other than this simple distinction, Observers function exactly the same as Followers
https://zookeeper.apache.org/doc/r3.4.13/zookeeperObservers.html

 

따라서 옵저버 서버의 장점을 정리하면,

투표하지 않기 때문에 리더 선출 성능에 영향을 주지 않으며,

실제로 리더나 팔로워가 아니므로 실패하거나 연결이 끊어졌을 때 주키퍼 앙상블에 크게 영향을 주지도 않습니다.

그리고 투표 프로토콜(protocol)을 지킬 필요가 없어, 교환 메시지의 크기도 작아 네트워크 트래픽(network traffic)이 작습니다.

Observers have other advantages. Because they do not vote, they are not a critical part of the ZooKeeper ensemble. Therefore they can fail, or be disconnected from the cluster, without harming the availability of the ZooKeeper service. The benefit to the user is that Observers may connect over less reliable network links than Followers. In fact, Observers may be used to talk to a ZooKeeper server from another data center. Clients of the Observer will see fast reads, as all reads are served locally, and writes result in minimal network traffic as the number of messages required in the absence of the vote protocol is smaller.
https://zookeeper.apache.org/doc/r3.4.13/zookeeperObservers.html

 

하지만 기본 배경이 팔로워와 같으면서 여러 성능상의 장점을 가지기 때문에 옵저버를 마치 팔로워처럼 사용하는 것은 지양해야 됩니다. 리더가 누가 되느냐가 주키퍼 서버에 큰 영향을 준다면 리더 선출 투표에 참여할 팔로워의 역할도 매우 중요하기 때문입니다. 정작 중요하게 기여할 팔로워가 모두 옵저버가 된다면, 오히려 주키퍼 서버 전체적으로 큰 악영향을 끼칠 수 있습니다.

 

주키퍼 공식 사이트에서는 옵저버의 바람직한 예를 소개하고 있습니다.

  • 데이터 센터 간의 브릿지로써
  • 퍼블리싱&서브스크라이빙 메시지 모델로써

단순히 말해 첫 번째는 두 데이터 센터에 있는 주키퍼 서버들이 모두 팔로워가 되면 리더를 선출할 때 지연 시간이 매우 높으니까 하나의 데이터 센터에 있는 주키퍼 서버는 팔로워로, 다른 하나는 옵저버로 만들어 성능 향상을 꾀한다는 말이고, 두 번째는 옵저버가 앙상블에 영향을 주지 않으므로 지속적이고 안정적인 메시징 모델로 사용하겠다는 의미입니다.

As a datacenter bridge: Forming a ZK ensemble between two datacenters is a problematic endeavour as the high variance in latency between the datacenters could lead to false positive failure detection and partitioning. However if the ensemble runs entirely in one datacenter, and the second datacenter runs only Observers, partitions aren't problematic as the ensemble remains connected. Clients of the Observers may still see and issue proposals.
As a link to a message bus: Some companies have expressed an interest in using ZK as a component of a persistent reliable message bus. Observers would give a natural integration point for this work: a plug-in mechanism could be used to attach the stream of proposals an Observer sees to a publish-subscribe system, again without loading the core ensemble.
https://zookeeper.apache.org/doc/r3.4.13/zookeeperObservers.html

 

 

 


리더 선출 알고리즘

 

파라미터 이름 설정값 설명
electionAlg 3 리더를 선출하기 위한 알고리즘입니다.
cnxTimeout 5 s 단위로, 리더 선출 알림을 보내기 위한 연결의 timeout 시간입니다. 
quorumCnxnTimeoutMs
-1 -1의 경우 syncLimit * tickTime 시간입니다. 리더 선출 알림을 받을 때의 timeout 시간입니다. 
electionPortBindRetry 3 리더 선출을 위한 포트에 접속하지 못했을 때, 재접속할 횟수입니다.

 

주키퍼 앙상블에서, 리더가 어떠한 이유로 고장났을 때 최대한 빠르게 다른 팔로워를 리더로 대체하는 것이 매우 중요합니다. 또한 아무나 리더로 데려다 놓는 것이 아닌, 최대한 제 역할을 해줄 리더를 선출하는 것이 중요하죠.

 

electionAlg 파라미터는 리더를 선출하기 위한 알고리즘을 1, 2, 3 중에 선택할 수 있습니다. 다만 3.4.0 버전부터는 UDP 방식을 사용하던 1, 2 방법이 구식화(depregate)었고 3.6.0 버전부터는 아예 제거되었습니다. 따라서 TCP 방식의 3 방법에 대한 내용만 소개하겠습니다.

 

알고리즘을 소개하기에 앞서 각 서버가 어떻게 리더가되고 팔로워가 되는 지 알아야 합니다. 사실 모든 서버는 실행 시 LOOKING 상태로부터 출발합니다. 이때 해당 서버는 다음 두 가지 경우에 맞닥뜨리게 됩니다.

  • 리더가 없을 경우
  • 리더가 있을 경우

리더가 없을 경우 LOOKING 상태의 서버들은 모여 리더를 선출합니다. 이때 리더가 된 서버는 LEADING 상태를, 나머지 서버는 FOLLOWING 상태를 가집니다. 이것이 우리가 서버를 리더 또는 팔로워라고 부르는 이유죠.

리더가 일단 선출되면, 리더 선출 알림(leader election notification)이 모든 서버에게 전송됩니다.

 

따라서 기존에 리더가 있을 경우, 새로운 서버가 등장하여 LOOKING 상태가 되면 해당 알림을 받은 다른 서버가 새로운 서버에게 알림을 전송해줍니다. 그럼 새로운 서버도 리더의 정보를 알고 FOLLOWING 상태로 전환할 수 있죠.

 

그렇다면 리더는 어떻게 선출할까요?

 

 

주키퍼는 리더 선출을 위해 순차적(sequencial)이고 임시적(ephemeral)인 성격의 제트노드(znode)를 사용합니다.(편의상 제트노드를 노드라고 부르겠습니다) 각 노드에는 번호가 붙는데, 해당 번호를 플래그(flag)라고 부르겠습니다.

 

리더 선출 과정은 아래와 같습니다.

  1. 주키퍼 서버들이 리더로써 출마할 수 있도록 '/election' 이름의 노드를 만듭니다.
  2. 리더가 되고자 하는 서버들은 '/election' 노드의 자식 노드로써 'guid-n_ + 플래그' 이름의 노드를 추가함으로 출마합니다.
  3. 일정 시간(기본적으로 200ms)이 지나고 모든 서버는 자신이 가장 작은 플래그를 가지는 지 확인합니다.
  4. 이때 가장 플래그가 작은 노드를 만든 서버가 리더가 됩니다.

플래그를 만드는 규칙은 다음과 같습니다.

  • '/election' 노드의 자식 노드 중 가장 큰 플래그보다 커야됩니다.
  • 순차적인 성격을 띄고 있기에, 기본적으로 '/election' 노드의 자식노드 중 (가장 큰 플래그 + 1)로 결정합니다.

따라서 플래그를 만들기 위해서는 '/election' 노드의 모든 자식 노드를 읽어야하는 과정이 반드시 수반되어야 합니다. 

A simple way of doing leader election with ZooKeeper is to use the SEQUENCE|EPHEMERAL flags when creating znodes that represent "proposals" of clients. The idea is to have a znode, say "/election", such that each znode creates a child znode "/election/guid-n_" with both flags SEQUENCE|EPHEMERAL. With the sequence flag, ZooKeeper automatically appends a sequence number that is greater than any one previously appended to a child of "/election". The process that created the znode with the smallest appended sequence number is the leader.
https://zookeeper.apache.org/doc/current/recipes.html#sc_leaderElection

 

 

리더가 선출되면, 다른 모든 서버에게 이 사실을 알려야 합니다. 이를 위해 주키퍼는 각 서버가 '/election' 노드에 자식 노드를 추가할 때, 아래 규칙을 통해 다른 서버의 상태를 지켜보도록 합니다.

  • 만약 '/election' 노드에 i 라는 플래그로 노드를 만들 때, 해당 'guid-n_i' 이름의 노드는 '/election' 노드의 자식 노드 중 i 보다는 작으면서 가장 큰 플래그의 상태를 지켜보도록(watch) 합니다.

더욱 쉽게 예를 들어보겠습니다.

서버 이름을 A, B, C, D 라고 가정했을 때 A, B, C 서버는 다음과 같은 이름으로 노드를 추가했다고 해봅니다.

  • B : /election/guid-n_0
  • C : /election/guid-n_1
  • A : /election/guid-n_2

서버 D는 새로운 노드를 추가하기에 앞서 '/election' 노드의 자식 노드를 모두 읽어옵니다. 이때 가장 큰 플래그가 2 임을 확인하고 다음과 같은 이름으로 노드를 추가합니다.

  • D : /election/guid-n_3

이 뿐만이 아닙니다. 각 서버들은 다른 서버의 상태를 지켜봐야되므로 이전에 설명했던 규칙대로 지켜볼 노드의 플래그를 찾습니다.

  • B : /election/guid-n_0 - 없음
  • C : /election/guid-n_1 - /election/guid-n_0 을 지켜봐야됨
  • A : /election/guid-n_2 - /election/guid-n_1 을 지켜봐야됨
  • D : /election/guid-n_3 - /election/guid-n_2 를 지켜봐야됨

일정 시간이 지나고 각 서버들은 다음과 같이 시간순으로 동작합니다.

  1. 서버 B는 자신이 가장 작은 플래그를 가진 노드를 만들었으므로 리더로 선출되었다고 생각하고 /election/guid-n_0의 상태값을 LEADING으로 바꾸고 본인의 아이디를 기록합니다.
  2. 서버 C는 /election/guid-n_0를 지켜보았으므로 내부값이 바뀜을 알아차립니다. 따라서 해당 노드에서 리더의 아이디를 찾아 본인이 만든 /election/guid-n_1에 기록하고 상태를 FOLLOWING으로 바꿉니다.
  3. 서버 A는 /election/guid-n_1 를 지켜보았으므로 내부값이 바뀜을 알아차립니다. 따라서 서버 C와 같이 동작합니다.
  4. 서버 D도 서버 A와 같이 동작합니다.

따라서 최종적으로 다음과 같이 됩니다.

  • B : /election/guid-n_0 [LEADING] - 없음
  • C : /election/guid-n_1 [FOLLOWING] - /election/guid-n_0 을 지켜봐야됨
  • A : /election/guid-n_2 [FOLLOWING] - /election/guid-n_1 을 지켜봐야됨
  • D : /election/guid-n_3 [FOLLOWING] - /election/guid-n_2 를 지켜봐야됨

 

만일 서버가 사라지면 어떻할까요?

우선 서버가 사라지면, '/election' 에서 해당 서버의 자식 노드가 제거됩니다. 그리고 당연히 해당 노드를 지켜보고 있던 노드가 이에 대한 알림을 받습니다.

따라서 알림을 받은 노드는 다음과 같이 동작합니다.

  • 이제 본인이 가장 작은 플래그를 가진다면 리더가 된다.
  • 아니라면, 지켜봐야 될 플래그를 갱신한다.

 

이를 위에서 설명한 예를 활용해 설명해보겠습니다.

  • B : /election/guid-n_0 - 없음
  • C : /election/guid-n_1 - /election/guid-n_0 을 지켜봐야됨
  • A : /election/guid-n_2 - /election/guid-n_1 을 지켜봐야됨
  • D : /election/guid-n_3 - /election/guid-n_2 를 지켜봐야됨

여기서 서버 C가 사라졌다고 가정해보겠습니다. 

  1. 서버 A는 /election/guid-n_1 을 지켜보았으므로 노드가 제거되었음을 알아차립니다. 따라서 지켜보아야 하는 플래그를 갱신합니다. 이제 /election/guid-n_0 을 지켜봅니다.
  2. 서버 D는 /election/guid-n_2 을 지켜보았으므로 내부값이 바뀜을 알아차립니다. 따라서 지켜보아야 하는 플래그를 갱신합니다. 하지만 여전히 /election/guid-n_2을 지켜봅니다.

서버 B는 지켜보는 노드가 없기 때문에 알림이 가지 않습니다. 따라서 아무런 동작도 할 필요가 없습니다.

그렇다면 서버 B가 사라졌을 때 어떻게 될까요?

  1. 서버 A는 /election/guid-n_0 을 지켜보았으므로 노드가 제거되었음을 알아차립니다. 따라서 지켜보아야 하는 플래그를 갱신합니다. 그러나 이제 본인이 가장 작은 플래그이므로 상태를 LEADING으로 바꾸고 본인의 아이디를 리더로 기록합니다.
  2. 서버 D는 /election/guid-n_2 을 지켜보았으므로 내부값이 바뀜을 알아차립니다. 따라서 해당 노드에서 리더의 아이디를 찾아 본인이 만든 /election/guid-n_3에 기록하고 지켜보아야 하는 플래그를 갱신합니다. 하지만 여전히 /election/guid-n_2을 지켜봅니다.

 

자, 이제 정리해보겠습니다.

리더가 아닌 서버들은 지켜봐야되는 노드가 1개 존재하고,

지켜보는 노드에게서 알림이 오면 본인의 노드 값을 갱신합니다.

 

이제 보니, 리더 선출만 엄청 작성했네요.

참고했던 사이트는 주키퍼 공식 사이트stackoverflow 사이트입니다.

 

 

 


관리자 서버

 

주키퍼 3.5.3 버전부터 주키퍼 서버가 사용할 명령어를 제한할 수 있습니다.

이는 4lw.commands.whitelist 파라미터에 콤마로 구분하여 명령어를 작성하면 되는데, 여기에 등록된 명령어는 환경 변수들을 보거나 정상 동작하는 서버들을 보는 등에 대한 기능으로 커맨드라인 명령어(CLI)을 통해 실행할 수 있습니다. 다만 주요 파라미터 테이블에 해당 파라미터를 명시하지 않은 이유는 향후 구식화되기(deprecated) 때문입니다.

Moving forward, Four Letter Words will be deprecated, please use AdminServer instead.
https://zookeeper.apache.org/doc/r3.6.2/zookeeperAdmin.html#sc_4lw

 

주키퍼는 대신 AdminServer를 사용하도록 장려하고 있습니다. 이는 관리자 서버로 불리며, http 인터페이스로 동작합니다. 그리고 "/${admin.commandURL}/[명령어 이름]"을 통해 각 명령어에 대한 설정 페이지를 접속할 수 있습니다. 여기를 참고하여 아래 파라미터들에 대한 자세한 설명을 볼 수 있습니다.

 

파라미터 이름 설정값 설명
admin.enableServer true 관리자 서버를 사용할 지 결정합니다.
admin.serverAddress 0.0.0.0 관리자 서버 주소를 설정합니다. 편의를 위해 0.0.0.0으로 했습니다.
admin.serverPort 8080 관리자 서버 포트 번호를 설정합니다.
admin.idleTimeout 30000 ms 단위로, 데이터를 보내거나 받을 때의 timeout 시간입니다.
admin.commandURL /commands 사이트에 명시된 기본값을 그대로 사용합니다.
admin.portUnification enable http 뿐만 아니라 https 연결 또한 ${admin.serverPort}에서 처리하도록 합니다.

 

 

실제 접속 시 화면

 

 

 


5. 환경 변수를 설정한다.

 

항상 모든 플랫폼은 편의를 위해 환경 변수를 두고 있습니다.

주키퍼도 마찬가지로 bin/zkEnv.sh 파일을 방문하면 다루고 있는 환경 변수들을 확인할 수 있는데, 여기서 정의해야되는 값을 찾아 지정해줘야 됩니다.

# This script should be sourced into other zookeeper
# scripts to setup the env variables

# We use ZOOCFGDIR if defined,
# otherwise we use /etc/zookeeper
# or the conf directory that is
# a sibling of this script's directory.
# Or you can specify the ZOOCFGDIR using the
# '--config' option in the command line.

ZOOBINDIR="${ZOOBINDIR:-/usr/bin}"
ZOOKEEPER_PREFIX="${ZOOBINDIR}/.."

 

위와 같은 문구를 확인했다면, ZOOCFGDIR 이라는 환경변수를 등록해줘야 됨을 알 수 있습니다.

먼저 환경 변수를 편집하기 위해 다음 명령어로 ~/.bashrc 파일을 엽니다.

park@oim:~$ gedit ~/.bashrc

편집기가 등장하면 아래 변수를 맨 밑에 추가하여 저장합니다.

export ZOOBINDIR=/usr/local/zookeeper/bin
export PATH=$PATH:$ZOOBINDIR

그리고 source 를 통해 환경 변수를 등록하고 echo를 통해 확인합니다.

park@oim:~$ source ~/.bashrc
park@oim:~$ echo ${ZOOBINDIR}
/usr/local/zookeeper/bin

해당 과정을 dim과 jim에게도 진행해주세요.

 

 

이전 3번 단계에서 힙 메모리 크기를 3GB로 바꾼다고 했었습니다.

이제 주키퍼 설치파일이 어디 위치하는 지 알고 있으므로, 이와 관련해 진행하겠습니다.

 

우선 bin/zkEnv.sh 파일을 확인해서 가장 밑 부분을 보면 다음 코드를 볼 수 있습니다.

# default heap for zookeeper server
ZK_SERVER_HEAP="${ZK_SERVER_HEAP:-1000}"
export SERVER_JVMFLAGS="-Xmx${ZK_SERVER_HEAP}m $SERVER_JVMFLAGS"

# default heap for zookeeper client
ZK_CLIENT_HEAP="${ZK_CLIENT_HEAP:-256}"
export CLIENT_JVMFLAGS="-Xmx${ZK_CLIENT_HEAP}m $CLIENT_JVMFLAGS"​

여기서 $ZK_SERVER_HEAP 변수를 변경하면 서버의 힙 메모리 크기를 변경할 수 있음을 알 수 있습니다.

또한 중간을 보면 다음 코드도 확인할 수 있습니다.

if [ -f "$ZOOCFGDIR/java.env" ]
then
    . "$ZOOCFGDIR/java.env"
fi

${ZOOCFGDIR}의 값은 ${ZOOBINDIR}/../conf 입니다. 따라서 conf 디렉토리에 java.env 를 추가하고, $ZK_SERVER_HEAP 변수를 정의하면 됩니다.

park@oim:/usr/local/zookeeper$ cd conf/
park@oim:/usr/local/zookeeper/conf$ gedit java.env

다음과 같이 작성해주세요.

#!/bin/sh

export ZK_SERVER_HEAP=3000

해당 과정을 dim과 jim에게도 진행해주세요.

 

이제 환경 변수를 다 정의했습니다. 대망의 주키퍼를 설정하는 일만 남았네요.

 

 

 

 


6. 주키퍼를 설정한다.

 

이제 중요하게 설정해야되는 값을 소개해드리겠습니다.

바로 서버입니다.

 

버전 3.5.0 부터는 서버를 정의하는 방식다음과 같이 변경되었습니다.

server.<positive id> = <address1>:<port1>:<port2>[:role];[<client port address>:]<client port>**

이와 더불어 기존에 사용했던 clientPort와 clientPortAddress 파라미터는 더이상 사용하지 않는다고 하니, 참고해주세요.

 

아래처럼 서버를 선언한 예시를 보면, 역할(role)이 참여자(participant)와 옵저버(observer)로 나뉨을 알 수 있습니다.

server.5 = 125.23.63.23:1234:1235;1236
server.5 = 125.23.63.23:1234:1235:participant;1236
server.5 = 125.23.63.23:1234:1235:observer;1236
server.5 = 125.23.63.23:1234:1235;125.23.63.24:1236
server.5 = 125.23.63.23:1234:1235:participant;125.23.63.23:1236

옵저버는 이전에 설명했던 역할이고, 참여자는 옵저버가 아닌 일반 서버(기본 서버)라고 생각하시면 됩니다.

 

zoo.cfg 파일을 만들었던 정적(static)인 방법은 해당 설정 파일의 값을 바꾸어도 주키퍼 서버를 다시 실행시키지 않는 한 설정값이 갱신되지 않았습니다. 하지만 다이나믹 설정 파일(dynamic configuration file)을 사용하면 해당 파일을 저장함과 동시에 주키퍼 설정값들을 덮어쓰고 갱신하도록 해줍니다. 이는 현재 권장되는 방법이기도 합니다.

 

앞서 설계했던 내용에 따라 conf/zoo.cfg.dynamic 파일을 만들어 다음과 같이 작성합니다.

park@oim:/usr/local/zookeeper/conf$ gedit zoo.cfg.dynamic
server.1=192.168.1.6:2888:3888:participant;2181
server.2=192.168.1.132:2888:3888:participant;2181
server.3=192.168.1.11:2888:3888:participant;2181

group.1=1:2:3

 

자, 이제 zoo.cfg를 만들어 다음과 같이 작성합니다.

park@oim:/usr/local/zookeeper/conf$ gedit zoo.cfg
# Directories
dataDir=/usr/local/zookeeper/data
dataLogDir=/usr/local/zookeeper/logs

# Times
tickTime=2000
initLimit=10
connectToLearnerMasterLimit=5
syncLimit=5

# Services
leaderServes=yes
standaloneEnabled=false

# Elections
electionAlg=3
cnxTimeout=5
quorumCnxnTimeoutMs=-1
electionPortBindRetry=3

# AdminServer
admin.enableServer=true
admin.serverAddress=0.0.0.0
admin.serverPort=8080
admin.idleTimeout=30000
admin.commandURL=/commands
admin.portUnification=enable

# Dynamic Configuration
dynamicConfigFile=/usr/local/zookeeper/conf/zoo.cfg.dynamic

 

이제 scp 명령어를 통해 zoo.cfg와 zoo.cfg.dynamic 파일을 dim, jim에게 분배해줍니다.

park@oim:/usr/local/zookeeper/conf$ scp zoo.* denny@dim:/usr/local/zookeeper/conf/
zoo.cfg                                       100%  580   572.0KB/s   00:00    
zoo.cfg.dynamic                               100%  146   148.9KB/s   00:00    
park@oim:/usr/local/zookeeper/conf$ scp zoo.* jong@jim:/usr/local/zookeeper/conf/
zoo.cfg                                       100%  580   623.9KB/s   00:00    
zoo.cfg.dynamic                               100%  146   190.7KB/s   00:00 

 

 

 

 


7. 주키퍼를 실행한다.

 

주키퍼를 실행하려면 다음 명령어를 실행하면 됩니다.

주키퍼 서버를 3개 정의했으므로, ssh를 통해 3개 서버 모두 실행하도록 합니다.

park@oim:~$ zkServer.sh start
ZooKeeper JMX enabled by default
Using config: /usr/local/zookeeper/bin/../conf/zoo.cfg
Starting zookeeper ... STARTED
park@oim:~$ ssh denny@dim
denny@dim:~$ zkServer.sh start
ZooKeeper JMX enabled by default
Using config: /usr/local/zookeeper/bin/../conf/zoo.cfg
Starting zookeeper ... STARTED
denny@dim:~$ ssh jong@jim
jong@jim:~$ zkServer.sh start
ZooKeeper JMX enabled by default
Using config: /usr/local/zookeeper/bin/../conf/zoo.cfg
Starting zookeeper ... STARTED

 

주키퍼 서버가 정상적으로 동작한다면, 아래 명령어를 실행했을 때 Mode가 정상적으로 출력됩니다.

park@oim:~$ zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /usr/local/zookeeper/bin/../conf/zoo.cfg
Client port not found in static config file. Looking in dynamic config file.
Client port found: 2181. Client address: localhost. Client SSL: false.
Mode: leader

 

또한 아래처럼 클라이언트로써 접속할 수도 있습니다.

park@oim:~$ zkCli.sh -server 192.168.1.132:2181
Connecting to 192.168.1.132:2181
2021-01-18 04:41:21,310 [myid:] - INFO  [main:Environment@98] - Client environment:zookeeper.version=3.6.2--803c7f1a12f85978cb049af5e4ef23bd8b688715, built on 09/04/2020 12:44 GMT
2021-01-18 04:41:21,312 [myid:] - INFO  [main:Environment@98] - Client environment:host.name=oim
2021-01-18 04:41:21,312 [myid:] - INFO  [main:Environment@98] - Client environment:java.version=1.8.0_221
... (생략)
JLine support is enabled
2021-01-18 04:41:21,384 [myid:192.168.1.132:2181] - INFO  [main-SendThread(192.168.1.132:2181):ClientCnxn$SendThread@999] - Socket connection established, initiating session, client: /192.168.1.132:33198, server: oim/192.168.1.132:2181
2021-01-18 04:41:21,408 [myid:192.168.1.132:2181] - INFO  [main-SendThread(192.168.1.132:2181):ClientCnxn$SendThread@1433] - Session establishment complete on server oim/192.168.1.132:2181, session id = 0x20002bae62f0001, negotiated timeout = 30000

WATCHER::

WatchedEvent state:SyncConnected type:None path:null
[zk: 192.168.1.132:2181(CONNECTED) 0] 

 

help 커맨드를 입력하면 도움말을 볼 수 있습니다. (근데 왜 명령어를 찾을 수 없다고 하는 지 모르겠네요...)

[zk: 192.168.1.132:2181(CONNECTED) 0] help
ZooKeeper -server host:port -client-configuration properties-file cmd args
	addWatch [-m mode] path # optional mode is one of [PERSISTENT, PERSISTENT_RECURSIVE] - default is PERSISTENT_RECURSIVE
	addauth scheme auth
	close 
	config [-c] [-w] [-s]
	connect host:port
	create [-s] [-e] [-c] [-t ttl] path [data] [acl]
	delete [-v version] path
	deleteall path [-b batch size]
	delquota [-n|-b] path
	get [-s] [-w] path
	getAcl [-s] path
	getAllChildrenNumber path
	getEphemerals path
	history 
	listquota path
	ls [-s] [-w] [-R] path
	printwatches on|off
	quit 
	reconfig [-s] [-v version] [[-file path] | [-members serverID=host:port1:port2;port3[,...]*]] | [-add serverId=host:port1:port2;port3[,...]]* [-remove serverId[,...]*]
	redo cmdno
	removewatches path [-c|-d|-a] [-l]
	set [-s] [-v version] path data
	setAcl [-s] [-v version] [-R] path acl
	setquota -n|-b val path
	stat [-w] path
	sync path
	version 
Command not found: Command not found help
[zk: 192.168.1.132:2181(CONNECTED) 1] version
ZooKeeper CLI version: 3.6.2--803c7f1a12f85978cb049af5e4ef23bd8b688715, built on 09/04/2020 12:44 GMT

 

 

 


예제 수행해보기

 

마지막으로 주키퍼 공식 사이트에 소개되어 있는 예제를 수행해보겠습니다.

[zk: 192.168.1.132:2181(CONNECTED) 2] ls /
[zookeeper]
[zk: 192.168.1.132:2181(CONNECTED) 3] create /zk_test my_data
Created /zk_test
[zk: 192.168.1.132:2181(CONNECTED) 4] ls /
[zk_test, zookeeper]
[zk: 192.168.1.132:2181(CONNECTED) 5] get /zk_test
my_data
[zk: 192.168.1.132:2181(CONNECTED) 6] set /zk_test junk
[zk: 192.168.1.132:2181(CONNECTED) 7] get /zk_test
junk
[zk: 192.168.1.132:2181(CONNECTED) 8] delete /zk_test
[zk: 192.168.1.132:2181(CONNECTED) 9] ls /
[zookeeper]
[zk: 192.168.1.132:2181(CONNECTED) 10] quit

결과가 다 잘 출력되었다면 주키퍼 설치를 성공한 것입니다!

 

수고하셨습니다.

 

 

 

 


끝내면서

 

예상 외로 엄청난 분량의 포스팅을 작성하면서 하루 반나절의 시간을 다 사용했네요...

그래도 정확히 어떤 파라미터가 어떤 기능을 하고, 또 서버가 어떻게 동작하는 지에 대해서 시원하게 해결해준 것 같아 뿌듯합니다.

 

이제 구현한 주키퍼를 이용해 많은 빅데이터 플랫폼을 연동하여 많은 일을 할 수 있습니다.

특히 주키퍼는 그 자체도 굉장히 훌륭한 메시징 모델이기 때문에 굳이 Heartbeat가 아니더라도 다양하게 프로그래밍할 수 있습니다.

따라서 앞으로 주키퍼 포스팅을 멈추고 다른 플랫폼을 연동해서 설명하는 데 중점이 될 것 같네요.

 

물론 막연히 다른 주제가 떠오르면 다시 주키퍼로 복귀하겠습니다.