ZooKeeper에 대해서 연구 분석 @.@

2015. 12. 21. 18:25OS/개발환경구축

Zookeeper is a distributed Coordination Service for distributed applications 
Zookeeper란 분산된 어플리케이션들을 위한 분산 조율,조정 서비스 랍니다.
Coordination(조정,조율)란 뜻은 다양한 노드가 함께 동작하도록 만드는 행위라고 하면, Coordinator 라 함은 조율사 정도의 의미가 되겠죠.
하지만 Coordinator 라는 이름을 사용하지 않고, ZooKeeper라는 이름을 사용하였습니다. 

사내에 ZooKeeper를 사용하고 있어서,
이 부분에 대한 스터디 혹은 정리가 필요로 해서, ZooKeeper 사이트를 찾아서 번역을 시작해 봅시다.

Zookeeper는 분산된 어플리케이션을 위한 분산 오픈 소스 조율 서비스 입니다. 
이 서비스는 분산 어플리케이션들이 동기화(synchronization), 구성 관리(configuration maintenance)
, 그룹(groups) 및 명명(naming)과 같은 높은 수준의 서비스를 구현할 수 있는 간단한 기본적인 셋을 제공합니다.
프로그래밍 하기에 쉽게 설계되어 있고, 익숙한 파일 시스템의  디렉토리 트리 구조로 꾸며진  데이터 모델을 사용합니다.
JAVA에서 동작하고 Java와 C 둘 모두 바인딩들을 가지고 있습니다. 코디네이션 서비스들은 어렵기로 악명이 높습니다.
그들은 특히 데드락 과 race conditions 같은 에러들에 대한 발생이 잦습니다.
Zookeeper가 나온 동기는 위의 문제들이 만연한 분산 어플리케이션들이 조율 서비스들을 구현하는 것에 대한 책임을 줄이기 위함입니다.

도대체.. 쉽게 한눈에 이해가 안되는 문제가 발생했습니다. T.T 
해석도 엉망이거니와.. 이해가 전혀 안갑니다.

검색을 시도 했습니다.  이번에는 조대협 님이 정리 해 놓은 ZooKeeper에 대한 정의 입니다.

코디네이션 서비스는 분산 시스템 내에서 중요한 상태 정보 나 설정 정보등을 유지하기 때문에,
코디네이션 서비스의 장애는 전체 시스템의 장애를 유발하기에  이중화,삼중화!!! 등을 통해 고가용성을 제공해야 한다.
분산 시스템을 조율하는 용도로 디자인이 되었기 때문에, 빠른 Data Access , 자체적인 장애 대응력 을 가져야 한다.
따라서 Zookeeper는 자체적으로 클러스트링을 제공하며, 장애에도 데이터 유실없이 fail over/  fail back 이 가능하다.

링크를 쫒아 가서 보면 활용시나리오 정도를 읽어보고 감을 잡아보면...
어느 정도는 이해 할 수 있겠지만, 사실 아래 naver 개발자 블로그가 정말 대박으로  이해하기가 편했던 곳입니다. 


- 분산 처리 환경에서 사용 가능한 데이터 저장소 
- 분산 서버간의 정보 공유, 서버 투입/제거 시 이벤트 처리, 서버 모니터링, 시스템 관리, 분산 락 처리, 장애 상황 판단 등 다양한 분야에서 활용
- ZooKeeper는 데이터를 디렉토리 구조로 저장하고, 데이터가 변경되면 클라이언트에게 어떤 노드가 변경됐는지 콜백을 통해 알려줌.
- 영속성(Persistent)를 유지하기 위해서 트랜잭션 로그와 스냅샷 파일이 디스크에 저장되고, 시스템을 재시작해도 데이터가 유지됨.
- ZooKeeperer Server에 접속한 클라이언트가 특정 노드를 Ephermeral Node 로 생성했다면, 그 클라이언트와 서버간의 세션이 끊어지면(ping를 제대로 처리 못하면) 해당 노드는 자동 삭제.
- ZooKeeper 서버는 Leader 와 Follower로 구성되어 있다. 서버들끼지 자동으로 Leader 를 선정하며 모든 데이터 저장을 주도 한다.
- 클라이언트에서 Server(Follower)로 데이터 저장을 시도 할때, Server(Follower)-> Server(Leader) -> 나머지 Server(Follower) 로 데이터를 전달 하는 구조.
    모든 서버에 동일한 데이터가 저장된 후 클라이언트에게 성공/실패 여부를 알려주는 동기 방식으로 작동함.
- 주의!!!
  1. 데이터 변경이 자주 발생하는 서비스에서는 ZooKeeper 를 데이터 저장소로 사용하는 것을 추천하지 않는다.
  2. Zookeeper 서버가 제대로 실행되지 않을 때가 있는데, 서버간의 데이터 불일치로 인한 데이터 동기화 실패( 이럴땐 데이터 초기화 후에 서버 실행 하면 됨)
  3. zoo.cfc 설정 파일에서의 ZooKeeper 서버 목록을 꼼꼼히 확인해야 한다. 
    서버 목록이 정확히 맞지 않아도 서버가 실행되기 하지만,.... 로그 파일 확인하면 미친듯이 재시작 혹은 데이터를 엉뚱한 곳에 저장하기도..


이번에는 ZooKeeper의 정의 이상으로 ZooKeeper Ensemble , Multi-Tenancy 에 대한 정의가 나온다. 


 - ZooKeeper는 분산작업을 제어하기 위한 트리 형태의 데이터 저장소
 - 프로그래밍 언어별로 제공되는 ZooKeeper 클라이언트 라이브러리를 통해 트리의 특정 노드에 데이터를 저장하고 변경
 - 특정 노드에 감시자(Watcher)를 등록하면 콜백을 통해 클라이언트에게 노드 변경 여부를 알려준다.
 - 데이터 변경을 감시하여 콜백을 실행하는 감시자(Watcher)가 활용도가 높음.
     감시자를 트리 내의 특정 노드에 등록하면 별도로 데이터 폴링을 하지 않더라도 노드 변경 사항을 전달 받을 수 있다.
ZooKeeper 앙상블
  ZooKeeper 서버가 중단시 ZooKeeper에 의존하는 모든 서버가 영향으로 ZooKeeper는 최대한 정상 동작을 보장받아야 함.
  여러 대의 ZooKeeper 서버를 클러스터링 하여 고 가용성을 지원하도록 설정이 가능하며, ZooKeeper 클러스터를 앙상블(Ensemble) 이라고 부른다.
 - 앙상블로 묶인 ZooKeeper 서버 중 한 대는 쓰기 명령을 총괄하는 리더 역활을 수행. 나머지는 Follower 역활을 수행한다.
    클라이언트에서 ZooKeeper 앙상블에 연결할 때는 커넥션 문자열에 앙상블을 구성하는 ZooKeeper 서버 주소를 다수 포함할 수 있다.
    클라이언트는 커넥션 문자열에 포함된 ZooKeeper 주소 중 하나에 접근하여 세션을 맺는다.
    클라이언트가 전달한 읽기 명령은 현재 연결된 ZooKeeper 서버에서 바로 반환한다.
    이에 비해 쓰기 명령은 앙상블 중 리더 역활을 수행하는 ZooKeeper 서버로 전달되며, 리더 ZooKeeper 는 모든 팔로어 ZooKeeper에게 해당 쓰기를 수행 할 수 있는지 질의한다.
    만약 Follower 중 과반수(> n/2)의 Follower로 부터 쓸 수 있다는 응답을 받으면 리더는 팔로어에게 데이터를 쓰도록 지시한다.

  - 모든 ZooKeeper 서버 주소를 클라이언트로 지정해 놓았다면, 현재 연결한 ZooKeeper 서버가 중단되더라도 자동으로 다른 ZooKeeper 서버에 세션을 유지한 채로 다시 연결한다.
     이와 같은 특성으로 인해 앙상블을 구성하는 ZooKeeper 서버 중 과반수이 서버가 현재 동작한다면 데이터 읽기 쓰기를 정상적으로 처리한다.
      즉, ZooKeeper 서버 3대로 앙상블을 구성했으면 ZooKeeper 한대가 중단되더라도 문제 없다.
 -  ZooKeeper 서버(2 Core/4G Ram) 3대를 앙상블로 묶어 테스트를 진행해본 결과.. 쓰기읽기 10:90 으로 배분시 초당 약 23,000번 트랜잭션을 수행할 수 있다.
 - 여러 어플리케이션이 한 개의 ZooKeeper 앙상블을 공유할 수 있도록 Multi-Tenancy 기능 을 일부 지원
      ZooKeeper 단독으로 서버를 구성할 때는 자원 낭비가 심함.
     '분산 처리 제어'에 동원되는 CPU 나 I/O 자원은 그다지 크지 않고, 수십 개 이하의 노드만 조작하면 되고, 조작 자체도 자주 일어나지 않는다.
     But 앙상블 구성을 위해 3대 이상의 ZooKeeper 서버를 동원해야 한다는 것은 가동률 측면에서 다소 과도하다.
    예를 들어, 초당 100번만 쓴다고 해도, 0.5% 미만의 사용률이다. 

 - 서버 다른 어플리케이션이 쉽게 트리상의 다른 노드 공간을 사용하게 해 서버 가동률을 높이 는 방식이다.
     클라이언트 세션을 생성할 때 특정 노드 경로를 지정하고 , 이후 접근은 이 경로 하위만을 사용하도록 한다.
     예를 들어 기본 경로를 /hello/world 라고 지정했을 떄, 클라이언트로 /wow 라는 노드를 접근하면 /hello/world/wow 에 접근한다.

//커넥션 문자열 내 기본 경로로 /hello/world를 지정한다.
String connectionString = "zookeeper.example.com:2181/hello/world";
//ZooKeeper 세션을 생성한다.
ZooKeeper zk = new ZooKeeper(connectionString, sessionTimeout, watcher);
//실제로는 /hello/world/wow 에서 데이터를 가져 온다.
byte[] result = zk.getData("wow", null, null);

  -  이러한 접근 방법은 ZooKeeper 클라이언트가 모두 ZooKeeper 서버 운영자의 통제 하에 있을 때만 사용 할 수 있다.
        ZooKeeper는 노드별 ACL(Access Control List)을 지원하므로, ZooKeeper 클라이언트로 노드를 만들 때 다른 클라이언트 중 일부만 읽기 또는 쓰기를 할 수 있도록 설장 할 수 있다. 
  

이 정도면 충분하게 이해가 되었을 것 같다.
위 링크를 잘 이해하면 ZooKeeper를 잘 활용할 수 있다고 생각합니다. 

아래는  https://zookeeper.apache.org/doc/trunk/zookeeperOver.html 에서 해석한 내용을 추가적으로 기재한다.

===================================================================================================
Design Goals 

Zookeeper is simple!!

Zookeeper는 표준 파일 시스템에 비슷하게 구성되어진 공유된 계층 네임스페이스를 통해 각각 다른 객체들과의 조율하기 위한 분산처리가 가능합니다. 
그 네임 스페이스는  Zookeeper parlance 안에 znode들로 구성되어 있고, 파일들과 디렉토리들과 비슷합니다.

저장소를 의도하여 만든 일반적인 파일 시스템과는 다르게,
Zookeeper 데이터는 Zookeeper는 높은 처리량과 낮은 대기 시간 값을 얻을수 있는 내장메모리로 유지 되어 집니다. 
Zookeeper 구현은 높은 성능, 높은 사용성, 엄격한 정렬된 접근을 제공합니다. 
Zookeeper의 성능면에는 크고, 분산된 시스템들상에서 사용되어 질수 있다는 것을 의미 합니다.
엄격한 정렬이라 함은 정교한 동기화 기본이 클라이언트에게 구현되어 질수 있다는 것을 의미 한다.

암튼 좋다 좋다.. ZooKeeper 쓰면 좋다라는 뜻이구나.

ZooKeeper is replicated.
분산 프로세스들 같이  순서대로 나열합니다. ZooKeeper 스스로 앙상블이라고 불리는 호스트들의 하나의 세트로 복제되어 질 것입니다.


Zookeeper Service







Zookeeper 서비스를 구성하는 서버들은 반드시 각각 다른 서버들에 관해야 모두 알아야 합니다.
그 서버들은 persistent store 안에 트랜잭션 로그들과 스냅샷들을 가지고 상태의 내장 메모리 이미지를 유지합니다.
과반수의 서버들이 이용 가능한것 만큼,  Zookeeper 서비스도 이용가능할 것입니다.

클라이언트들은 단일 Zookeeper 서버에 연결합니다.
클라이언트는 요청들을 보내는 것, 응답들을 받는 것, watch 이벤트들을 얻는 것 그리고 heart beats를 보내는 것등을 통해 TCP 연결을 유지 합니다. 
만일 서버와의 TCP 연결이  끊어지면, 클라이언트는 다른 서버에 연결을 할것 입니다.

그렇군요.. 클라이언트는 단일 Zookeeper 서버에 연결이 된다는 거죠.

ZooKeeper is ordered.
Zookeeper는 모든 ZooKeeper 트랜잭션들의 순서를 반영하는 숫자를 갖는 각각의 수정사항을 찍어냅니다. 
다음 동작은 예를 들면 동기화 기본들(synchronization primitives)같은 높은 레벨의 추상화들을 구현하는 해당 순서를 사용할 수 있습니다.

ZooKeeper is fast.
특히 읽기 기능에서 빠릅니다.
수천개의 머신에서 동작하고, 읽는 곳에서 최대한 동작하는 Zookeeper 어플리케이션들은 쓰는 것보다 비율적으로 대략 10: 1로 일반적으로 좀 더 잘 동작합니다.  

Data Model and the hierarchical namespace

Zookeeper에 의해 제공되는 네임 스페이스는 표준 파일 시스템과 같이 많습니다.
이름은 (/)슬러시에 의해 나눠진 경로 요소의 시퀀스 입니다.
Zookeeper의 네임 스페이스 상에 모든 노드 path에 의해 구분되어집니다.

ZooKeeper's Hierarchical Namespace
Nodes and ephemeral nodes

표준 파일 시스템과는 다르게, ZooKeeper 네임스페이스상에 각 노드는 자식을 가질수 있습니다.
파일이 디렉토리에 있는 것이 가능한 파일 시스템을 갖는 것과 같습니다. 
(ZooKeeper는 조율 데이터를 저장하도록 설계되었습니다. 상태 정보, 설정, 위치 정보 등등..)
그래서 각 노드에 저장된 데이터는 일반적으로 작습니다. (in the byte to kilobyte range)
우리가 ZooKeeper 데이터 노드들에 대해서 이야기하고 있는 것을 명확하게 하기 위해서 znode 라는 단위를 사용합니다.

Znodes는 데이터 변경사항들, ACL(Access Control List) 변경사항들 그리고 cache validations과 순서대로
나열된 수정사항들을 허가하기 위한 timestamps을 위한 버전 넘버들을 포함한 통계 구조를 유지합니다. 
 예를 들어, 클라이언트가 데이터를 주고 받을때 마다, 데이터 버전을 주고 받습니다.

하나의 네임스페이스의 각  znode에 저장된 데이터는 자동으로 읽고 작성되어집니다. 
읽기들은 znode에 연관되어진 모든 데이터 바이트들을 얻고, 쓰기는 모든 데이터를 대신합니다. 
각 노드는 누군가와 무언가를  할수 있는 주고 받는 Access Control List(ACL)를 가지고 있습니다.

Zookeeper는 또한 짧은주기의 노드들에 대한 개념도 가지고 있습니다.
이들 znodes들은  znode가 활성화 될때 생성된 세션 만큼 유지합니다.
세션이 끝날때, znode는 삭제됩니다.


디렉토리 구조기반으로 znode 라는 데이터 저장 객체를 제공(key-value)
znode 상에 데이터를 넣고 빼는 기능만 제공.
일반 디렉토리 형식을 사용하기 때문에, 데이터를 계층화된 구조로 저장하기 용이함. 

데이터 모델은 간단함.  디렉토리 구조의 각 노드에 데이터를 저장할 수 있음.

노드는 아래와 같이 기능에 따라 몇가지 종류로 나뉨

 - Persistence Node : 노드에 데이터를 저장하면 일부러 삭제하지 않는 이상 삭제되지 않고 영구히 저장된다. 
 - Ephemeral Node : 노드를 생성한 클라이언트의 세션이 연결되어 있을 경우에만 유효!
                                          클라이언트 연결이 끊어지는 순간 삭제됨.
                                          이를 통해서 클라이언트가 연결이 되어 있는지 아닌지를 판단하는데 사용 할 수 있음.
 - Sequence Node : 노드를 생성할 때 자동으로 sequence 번호가 붙는 노드이다. 주로 분산 락을 구현하는데 이용된다.

* 클러스터 구성시에 클러스터 내에 서버가 들어오면 Ephemeral Node로 등록을 해 놓으면 된다.

Watch 기능은 Zookeeper 클라이언트가 특정 Znode 에 Watch를 걸어 놓으면, 이넘은 Watcher가 되겠죠.
해당 znode가 변경이 되었을 때, 클라이언트로 callback 호출을 날려서 클라이언트에 해당 znode 가 변경이 되었음을 알려준다.
그리고 해당 watcher는 삭제 된다.

 Conditional updates and watches

Zookeeper은 watches 라는 컨셉을 지원합니다.
클라이언트는 znodes상에 watch를 세팅할수 있습니다.
watch는 트리거 되어지고 znode가 변경할 때, 삭제되어 질 것입니다.
watch가 트리거 될때, 클라이언트는 znode가 변경되어 졌다라는 말하는 패킷을 받습니다. 
그리고 만약 클라이언트와 ZooKeeper 서버들중 하나 사이에 연결이 끊어졌을때, 클라이언트는 로컬 노티피케이션을 받을 것입니다.


Guarantees

Zookeeper는 매우 빠르고 매우 간단합니다.
그것의 목표는 좀더 복잡한 서비스들 예를 들면 동기화 같은것,에 대해서  기본이 존재하는 것입니다. 
그것은 guarantees 의 셋을 제공합니다.

- sequential Consistency :  클라이언트로부터의 수정사항들은 그들이 보낸 순서대로 적용되어 질것입니다. 
    (변경 요청은 순서대로 적용)
- Atomicity : 성공이든 실패든지 간에 업데이트 합니다. 어떠한 부분적인 결과는 없습니다.
   (변경은 성공 또는 실패)
- Single System Image : 클라이언트는 서비스의 같은 view를 볼것입니다. 그것이 연결되어진 서버 랄지라도.
  (클라이언트는 연결한 서버에 상관없이 동일 서비스 사용) 
- Reliability - 한번 적용되어진 업데이트, 그것은 클라이언트가 수정사항을 오버라이팅 하기 전까지 변하지 않을 것입니다.
   (클라이언트가 성공 코드를 받으면, 서버에 반영됨을 보장)
- Timeliness - 시스템의 클라이언트들의 뷰는 확실한 타임 바운드 와 함게 최신정보(up-to-date) 되어지기 위해 승인됩니다. 
   (클라이언트는 일정 시간 내에 최신 상태를 사용)

Simple API

Zookeeper의 설계 목표중 하나는 매우 간단한 프로그래밍 인터페이스를 제공하는 것이다.
결과적으로 아래 동작들만 제공한다.

create 
     creates a node at a location in the tree 
          (create -e /xxx ''  라고 하면 ephemeral Node 가 생성이 됩니다.)
          (create -s /xxx '' 라고 하면 sequence Node 가 생성이 됩니다... 분산 락) 
delete
     deletes a node
          (rmr /sequ0000000324 : 삭제 됨) 
exists
     tests if a node exists at a location  
get data
     read the data from a node
set data
     writes data to a node
get children
     retrieves a list of children of a node
sync
     waits for data to be propagated

설치  

http://apache.tt.co.kr/zookeeper/zookeeper-3.4.7/ 에서 gz 파일 다운 받아, 압축 풀어요.
버전은 3.4.7 이겠죠.

8-1 Standard Operation

싱글 모드의 Zookeeper 서버를 세팅하는 것은 직관적입니다.
서버는 싱글 Jar 파일에 포함되어 있습니다. 그래서 설치는 설정을 생성하는 것을 구성하고 있습니다. 
ZooKeeper를 시작하기 위해서, 여러분은 설정 파일이 필요하겠죠... 
$ZooKeeper_home/conf/zoo.cfg 에 아래 내용을 추가 하세요.

#클라이언트 연결들을 대기하고 있는 포트
clientPort=2181
#session Time 설정 2초 (zookeeper에 의해 사용되는 기본시간 단위, heartbeats 하는데 사용되고, 최소 세션 타임아웃은 Tick time의 2배가 될것입니다.)
tickTime=2000
#data Directory 설정 : 인메모리 데이터베이스 스냇샵과 특별히 다르게 열거하지 않는한, 데이터 베이스상에 수정사항에 대한 트랜젝션 로그를 저장하는 장소랍니다
dataDir=C:\\Server\\zookeeper-3.4.7\\temp\\


그 다음 ./bin/zkServer.cmd 로 Zookeeper 서버를 시작합니다.

이때, 하나의 서버만 구동하기 때문에, standalone 모드 이다.
(원 Production에서는 데이터 복제를 통한 fail over를 지원하기 위해서 여러개의 서버를 띄운다.)






Managing Zookeeper Storage 
 
오래 제품 시스템들을 구동하는 동안, Zookeeper 저장소는 외부에서(dataDir and logs) 관리 되어야 합니다.

Connecting to ZooKeeper

한번 Zookeeper가 가동되면, 우리는 그것에 연결할 수 있는 몇개의 옵션을 가질수 있습니다.

JAVA :  bin/zkCli -server 127.0.0.1:2181



연결을 하면, 아래와 같이 메세지와 함께 입력 shell이 나타나죠.


다음으로 새로운 Znode를 만들어 본다.(create /znode_name value)
리스트도 보고(ls /), znode 도 불러 보고(get /znode_name)




Programming to ZooKeeper

ZooKeeper는 Java 바인딩들을 가지고 있습니다. Java bindings은 기능적으로 동등합니다.
C와 JAVA 바인딩의 차이점은 메세징 루프가 어떻게 끝나는 것에 대한 차이 가 있답니다... 뭐 중요하진 않은 듯.


Running Replicated Zookeeper (중요!!! 복제된 Zookeeper의 가동)

싱글모드상에 Zookeeper가 동작하는 것은 평가하고, 약간의 개발과 테스팅이 편리하다.
그러나 제품상에서는, 여러분은 replicated mode 상에서 Zookeeper를 가동해야 합니다.
같은 어플리케이션상에 서버들의 replicated group 은 quorum 이라고 부르고, replicated mode 상에서,  quorum 안에 모든 서버들은 같은 설정 파일의 복사본들을 갖습니다.

NOTE!!!!!
For replicated mode, a minimun of three servers are required, and it is strongly recommended that you have an odd number of servers.
리플리케이트 모드를 운영하는 동안 최소 3대의 서버가 필요로 하고, 이것은 여러분이 홀수의 서버 개수를 갖는 것을 아주 중요하게 추천 합니다.
If you only have two servers, then you are in a situation where if one of them fails, there are not enough machines to form a majority quorum.
만약 두대 서버를 가지고 있다면, 그땐 우리는 그들 중 하나가 실패를 했을 때의 경우에 , 하나의 주요 quorum 를 구성하는데 충분하지 않은 장비들이 있는 것입니다. 
Two servers is inherently less stable than a single server, because there are two single points of failure.
두대의 서버들은 내부적으로 싱글 서버보다 덜 안정적이다. 이유는 두개의 싱글 포인트에 실패가가 있기 때문이다.

conf/zoo.cfg 파일에는 replicated 모드에 필요로 한것은 싱글 모드와 비슷한데, 
추가적으로 다른 것이 좀 더 들어가죠

#클라이언트 연결들을 대기하고 있는 포트
clientPort=2181
#session Time 설정 2초 (zookeeper에 의해 사용되는 기본시간 단위, heartbeats 하는데 사용되고, 최소 세션 타임아웃은 Tick time의 2배가 될것입니다.)
tickTime=2000
#data Directory 설정 : 인메모리 데이터베이스 스냇샵과 특별히 다르게 열거하지 않는한, 데이터 베이스상에 수정사항에 대한 트랜젝션 로그를 저장하는 장소랍니다
dataDir=C:\\Server\\zookeeper-3.4.7\\temp\\
initLimit=5
syncLimit=2
server.1 = zoo1:2888:3888
server.2 = zoo2:2888:3888
server.3 = zoo3:2888:3888

새로운 엔트리로 initLimit 는 Zookeeper가  quorum 안에 Zookeeper 서버들이  leader에 연결해야만 하는  시간 길이를 제한하는데 사용하는 타임아웃입니다.
또 새로운 엔트리로 syncLimit는 서버가 leader가 될수 있는 시간이 얼마만큼 경과하는지 제약한다.
이들 타임아웃에 대한 둘 모두, 여러분이 TickTime를 사용하는 시간의 단위를 명시합니다.
예를 들면, initLimit에 대한 타임아웃이 2000 밀리초에 1 tick에서 5 ticks 입니다. 또는 10 초입니다.
server.x 형태의 엔트리는 ZooKeeper service가 구성된 서버들의 리스트입니다.
서버가 시작되었을때, 그것을 제공하는 것은 데이타 디렉토리에서 myid 라는 파일을 찾는 것입니다.
그 파일은 server number를 ASCII로 포함한 것들을 가지고 있습니다.

마지막으로 두개의 포트 넘버를 각각의 서버명 뒤에 기록하는 것은 기록하세요.
두 쌍은 다른 peers로 연결하기 위한 former port 를 사용합니다. 
그런 연결은 peers는 커뮤니케이션 할수 있기에 필수적입니다. 
예를 들면 수정사항들의 순서를 맞추기 위해서죠.
좀 더 명확하게, ZooKeeper 서버는 이 포인트를 leader 에게 follwers이 연결하기 위해서 이 포트를 사용합니다.
새로운 리더가 발생 했을때, follower은 TCP 연결을 이 포트를 사용하는 leader에게 엽니다.
기본 리더 선택 또한 TCP를 사용하기 때문에, 우리는 일반적으로 leader 선출을 위해 다른 포트를 요구합니다.
이것은 서버 엔트리에서 두번째 포트 입니다.

NOTE
If you want to test multiple servers on a single machine, specify the servername as localhost with unique quorum & leader election (i.e. 2888:3888 , 2889:3889, 2890:3890 in the example above)
for each server.X in that server's config file. Of course seperate dataDirs and distinct clientPorts are also necessary (in the above replicated example , running on a single localhost, you would still have three config tiles).

Please be aware that setting up multiple servers on a single machine will not create any redundancy.
If something were to happen which caused the machine to die, all of the zookeeper servers would be offline.
Full redundancy requires that each server have its own machine.
It must be a completely separate physical server.
Multiple virtual machines on the same physical host are still vulnerable to the complete failure of that host.


Other Optimizations

성능을 크게 증가 시킬수 있는 두개의 파라미터가 더 있습니다.

- 낮은 레이턴시을 수정할때 얻고자 한다면, 여러분은 제공된 트랜젝션 로그 디렉토리를 갖는것이 중요합니다.
   기본적으로 트랜젝션 로그들은 같은 디렉토리에 놓여 있습니다. 데이터 스냅샷들과 myid 파일로써 말이죠
   dataLogDir 파라미터는 트랜젝션 로그들을 위해 다른 디렉토리를 사용하는 것을 가르키고 있습니다.



9-1. A Simple Watch Client

 간단한  Watch Client 를 만들어 봅시다.
 이 Zookeeper  Watch Client 는 프로그램의 시작 과 종료 의한 응답과 변화들에 대한  Zookeeper node를 보고 있습니다. 

  ==> wc (watch client 를 줄여서)는 프로그램 시작,종료 응답 및 상태에 대해 zookeeper node를 보고 있답니다.

9-2. Requirements

 Watch Client는 4가지 요건을 가지고 있습니다.

 A.  아래 4가지를 파라미터를 갖습니다. 
      > Zookeeper service의 주소
      > znode의 이름 - 보여지는 대상 (the one to be watched)
      > 출력이 작성되는 파일의 이름 (the name of a file to write the output to)
      > 인자들을 가지고 실행 가능한 것(an executable with arguements)

 B. znode와 연관된 데이터를 가져오고, 실행 프로그램을 시작합니다.  
 C. znode 가 변경되면, 클라이언트는 컨텐츠들을 다시 가져오고, 프로그램을 재시작합니다.
 D. znode가 사라지면, 클라이언트는 프로그램을 종료합니다.(kill)

==> Zookeeper 서비스 주소 , znode의 이름, 출력될 작성할 파일의 이름, 인자들을 가지고 실행하는 것
==> 위의 주소에서 znode 와 연관된 데이터를 가져오고 실행할거 실행... znode가 변경시 감지, znode 삭제시 감지 

9-3 Program Design

일반적으로, Zookeeper 어플리케이션들은 두개의 유닛으로 나눠져 있는데,
하나는 연결을 유지하는 것 그리고 다른 하나는 데이터를 모니터하는 것이다.

이 어플리케이션에서는 그 Executor라는클래스는 Zookeeper 연결을 유지 합니다.
그리고 DataMonitor라는  클래스는 Zookeeper tree 안에 데이터를 모니터 합니다.
또한 Executor는 메인 스레드를 유지하고, 실행 로직을 포함합니다.

여러분이 하나의 인자로 전달한 실행 가능한 프로그램과의 인터렉션과 그리고 샘플(요구사항들 마다)이 znode의 단계에 따라서 셧다운 하고 재시작하는 것과 마찬가지로
작은 사용자 인터렉션이 있는 것에 대해 책임이 있는 것입니다.


9-4 The Executor Class

Executor 객체는 샘플 어플리케이션의 최우선 컨테이너 입니다.
위에서 설명했다 시피, ZooKeeper 객체 와 DataMoniotr 둘다 포함한다.

  // from the Executor class...
   
    public static void main(String[] args) {
        if (args.length < 4) {
            System.err
                    .println("USAGE: Executor hostPort znode filename program [args ...]");
            System.exit(2);
        }
        String hostPort = args[0];
        String znode = args[1];
        String filename = args[2];
        String exec[] = new String[args.length - 3];
        System.arraycopy(args, 3, exec, 0, exec.length);
        try {
            new Executor(hostPort, znode, filename, exec).run();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // 주어진 서버 Address 로 ZooKeeper Server 접속
    // DataMonitor를 만든다.... 이 dm은 서버에 이미 생성된 znoe를 모니터링 한다.
    // 해당 node에 변경이 생기면 비동기로 메소드를 호출해준다. 
    public Executor(String hostPort, String znode, String filename,
            String exec[]) throws KeeperException, IOException {
        this.filename = filename;
        this.exec = exec;
        zk = new ZooKeeper(hostPort, 3000, this);
        dm = new DataMonitor(zk, znode, null, this);
    }
     
     

    public void run() {
        try {
            synchronized (this) {
                while (!dm.dead) {
                    wait();
                }
            }
        } catch (InterruptedException e) {
        }
    }

 * args[0] = "127.0.0.1:2181" ; // Zookeeper 서버에 연결하겠죠.
 * args[1] = "/zk_node";              // 이 znode가 서버에 이미 생성되어 있어야 합니다.
 * args[2] = "c:\\Server\\testExecutorlog.txt";      //znode의 값을 기록할 로컬 파일명.
 * args[3] = "c:\\Server\\test.bat";                    //수행할 비즈니스 실행 스크립트 

Zookeeper를 생성하는데.. 생성하는 건 아니고, ZooKeeper 서버 객체라고 보면 되겠다.
해당 객체를 DataMonitor(이게 바로 watcher)에 인자로 넣어주면 된다.

Executor의 job은 여러분이 커맨드 라인상에서 전달하는 이름의 프로그램을 시작하고 종료하기 위한 것으로 Recall 합니다.
그건 Zookeeper 객체에 의해 발생한 이벤트들에 대한 응답으로 this 합니다. 
위 코드에 보다 시피, Executor는 Zookeeper 생성자에 Watcher 인자로써 자신에게 참조로 전달합니다.
또한 DataMoniotor 생성자에 DataMonitorListener 인자로써 자신에게 참조를 전달합니다.
Executor의 정의 마다, 이들 인터페이스 둘다 상속합니다. 

public class Executor implements Watcher, Runnable, DataMonitor.DataMonitorListener {
...

Watcher 인터페이스는 Zookeeper Java API에 의해서 정의 되었습니다.
Zookeeper는 그것(Watcher 인터페이스)를 자신의 컨테이너에 커뮤니케이션 하기 위해 사용합니다.
Watcher 인터페이스는 꼴랑 process() 메소드 하나만 지원하고,
ZooKeeper는 이걸 일반적인 이벤트들과의 커뮤니케이션 하기위해 사용합니다.
일반적인 이벤트들이란, 메인 스레드가 연관이 되어 있는, 예를 들면, ZooKeeper 연결의 단계 또는 Zookeeper 세션과 같은 것입니다.

이 예제에서 Executor는 단순하게  이 이벤트들을 이 이벤트들과 함께 무언가를 하는 것을 결정하기 위해 DataMonitor에게 전달합니다.
일반적으로 Executor 나  비슷한 Executor와 같은 객체 "owns"  ZooKeeper 연결 포인트를  단순하게  설명하는 것입니다.
그러나 다른 객체들에게 이벤트들을 대리하는 것은 자유입니다.
또한 이것은 watch 이벤트들을 발생시키는  기본 채널로 사용합니다.(이것 이후에 보다 더)

    public void process(WatchedEvent event) {
        dm.process(event);
    }

한편으로 DataMonitorListener 인터페이스는 Zookeeper API 파트가 아닙니다.
샘플 어플리케이션을 위해 설계된 완벽하게 사용자 인터페이스 입니다. 
DataMonitor 객체는 또한 Executor 객체 인 자신의 컨테이너에 커뮤니케이션 하기 위해 사용됩니다.
DataMonitorListener 인터페이스는 아래와 같습니다.

public interface DataMonitorListener {
    /**
    * The existence status of the node has changed.
    */
    void exists(byte data[]);

    /**
    * The ZooKeeper session is no longer valid.
    * 
    * @param rc
    * the ZooKeeper reason code
    */
    void closing(int rc);
}

이 인터페이스는 DataMonitor에 정의되었고 Executor 클래스에서 구현되었습니다.

Executor.exist() 가 인보크되었을때, 요구사항 마다  Executor는 start up 할지 shut down 할지를 결정합니다.
요구들은 znode  생존하는 것을 중단할때, 실행가능한 것을 죽이기 위해 말하는 것을 재호출합니다.
(Recall that the requires say to kill the executable when the znode ceases to exist)

Executor.closing()이 인보크되었을 때 , Executor는 Zookeeper 연결이 불멸로 사라지는 것에 대한 응답으로 스스로가 셧다운 되는지 말지를 결정합니다. 
여러분이 추축하는 것 같이 DataMonitor는 Zookeeper의 상태에 따라 변경하기 위한 응답으로  이들 메소드들을 발생시키는 객체 입니다.
여기 DataMonitorListener.exists() 와 DAtaMonitorListener.closing 의 Executor의 구현이 있습니다.

public void exists( byte[] data ) {
    if (data == null) {
        if (child != null) {
            System.out.println("Killing process");
            child.destroy();
            try {
                child.waitFor();
            } catch (InterruptedException e) {
            }
        }
        child = null;
    } else {
        if (child != null) {
            System.out.println("Stopping child");
            child.destroy();
            try {
               child.waitFor();
            } catch (InterruptedException e) {
            e.printStackTrace();
            }
        }
        try {
            FileOutputStream fos = new FileOutputStream(filename);
            fos.write(data);
            fos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            System.out.println("Starting child");
            child = Runtime.getRuntime().exec(exec);
            new StreamWriter(child.getInputStream(), System.out);
            new StreamWriter(child.getErrorStream(), System.err);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

public void closing(int rc) {
    synchronized (this) {
        notifyAll();
    }
}


Executor의 job은 여러분이 커맨드 라인상에서 전달할 그 실행가능한 이름의 것을 시작하고 종료하기 위한 것으로 재호출합니다.
그것은 Zookeeper 객체에 의해 발생한 이벤트들에 대한 응답에서 이것을 합니다. 
위 코드에 보다 시피, Executor는 Zookeeper 생성자에 Watcher 인자를 자신에게 참조로 전달합니다.
또한 DataMoniotor 생성자에 DataMonitorListener 인자로써 자신에게 참조를 전달합니다.
Executor의 정의 마다, 이들 인터페이스 둘다 상속합니다. 

public class Executor implements Watcher, Runnable, DataMonitor.DataMonitorListener {
...

Watcher 인터페이스는 Zookeeper Java API에 의해서 정의 되어 집니다.
Zookeeper는 그것(Watcher 인터페이스)를 자신의 컨테이너에 커뮤니케이션 하기 위해 사용합니다.
Watcher 인터페이스는 단지 process() 메소드 하나만 지원하고,
ZooKeeper는 그것을 일반적인 이벤트들과의 커뮤니케이션 하기위해 사용합니다.
일반적인 이벤트들이란, 메인 스레드가 연관이 되어 있는, 예를 들면, ZooKeeper 연결의 단계 또는 Zookeeper 세션과 같은 것입니다.

이 예제에서 Executor는 단순하게  이 이벤트들을 이 이벤트들과 함께 무언가를 하는 것을 결정하기 위해 DataMonitor에게 전달합니다.
일반적으로 Executor 나  비슷한 Executor와 같은 객체 "owns"  ZooKeeper 연결 포인트를  단순하게  설명하는 것입니다.
그러나 다른 객체들에게 이벤트들을 대리하는 것은 자유입니다.
또한 이것은 watch 이벤트들을 발생시키는  기본 채널로 사용합니다.(이것 이후에 보다 더)

    public void process(WatchedEvent event) {
        dm.process(event);
    }

한편으로 DataMonitorListener 인터페이스는 Zookeeper API 파트가 아닙니다.
샘플 어플리케이션을 위해 설계된 완벽하게 사용자 인터페이스 입니다. 
DataMonitor 객체는 또한 Executor 객체 인 자신의 컨테이너에 커뮤니케이션 하기 위해 사용됩니다.
DataMonitorListener 인터페이스는 아래와 같습니다.

public interface DataMonitorListener {
    /**
    * The existence status of the node has changed.
    */
    void exists(byte data[]);

    /**
    * The ZooKeeper session is no longer valid.
    * 
    * @param rc
    * the ZooKeeper reason code
    */
    void closing(int rc);
}

이 인터페이스는 DataMonitor에 정의되었고 Executor 클래스에서 구현되었습니다.

Executor.exist() 가 인보크되었을때, 요구사항 마다  Executor는 start up 할지 shut down 할지를 결정합니다.
요구들은 znode  생존하는 것을 중단할때, 실행가능한 것을 죽이기 위해 말하는 것을 재호출합니다.
(Recall that the requires say to kill the executable when the znode ceases to exist)

Executor.closing()이 인보크되었을 때 , Executor는 Zookeeper 연결이 불멸로 사라지는 것에 대한 응답으로 스스로가 셧다운 되는지 말지를 결정합니다. 
여러분이 추축하는 것 같이 DataMonitor는 Zookeeper의 상태에 따라 변경하기 위한 응답으로  이들 메소드들을 발생시키는 객체 입니다.
여기 DataMonitorListener.exists() 와 DAtaMonitorListener.closing 의 Executor의 구현이 있습니다.

public void exists( byte[] data ) {
    if (data == null) {
        if (child != null) {
            System.out.println("Killing process");
            child.destroy();
            try {
                child.waitFor();
            } catch (InterruptedException e) {
            }
        }
        child = null;
    } else {
        if (child != null) {
            System.out.println("Stopping child");
            child.destroy();
            try {
               child.waitFor();
            } catch (InterruptedException e) {
            e.printStackTrace();
            }
        }
        try {
            FileOutputStream fos = new FileOutputStream(filename);
            fos.write(data);
            fos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            System.out.println("Starting child");
            child = Runtime.getRuntime().exec(exec);
            new StreamWriter(child.getInputStream(), System.out);
            new StreamWriter(child.getErrorStream(), System.err);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

public void closing(int rc) {
    synchronized (this) {
        notifyAll();
    }
}


The DataMonitor Class

DataMonitor 클래스는 ZooKeeper 로직의 핵심(meat)를 가지고 있습니다.
대부분은 비동기 이벤트 기반입니다. DataMonitor는 생성자에 의해서 시작합니다.

//맨 밑에 zk.exists 는 모니터링 할 노드에 watcher를 붙이는 작업, 이걸 해줘야 모니터링이 된다.
//이걸 해 두면, 변경 감지 시 StatCallback 인터페이스의 ProcessResult를 호출해 준다.

public DataMonitor(ZooKeeper zk, String znode, Watcher chainedWatcher,
        DataMonitorListener listener) {
    this.zk = zk;
    this.znode = znode;
    this.chainedWatcher = chainedWatcher;
    this.listener = listener;
   
    // Get things started by checking if the node exists. We are going
    // to be completely event driven
    zk.exists(znode, true, this, null);
}

Zookeeper.exists()에 호출은 znode가 존재하는지 체크하고, watch를 설정하고 자신에게 완벽한 callback 객체로써 참조를 전달합니다.
이 의미에서, watch가 트리거되어 질때  진짜 프로세싱이 일어난 이후로 시작하는 것이다.


Note
Don't confuse the completion callback with the watch callback.
The ZooKeeper.exists() completion callback, which happens to be the method StatCallback.processResult() implemented in the DataMonitor object,
is invoked when the asynchronous setting of the watch oepration (By ZooKeeper.exists()) completes on the server.
The Triggering of the watch, on the other hand, sends an event to the Executor object, since the Executor registered as Watcher of the ZooKeeper object.
As an aside, you might note that the DataMonitor could also register itself as the Watcher for this particular watch event.
This is new to ZooKeeper 3.0.0(the support of multiple Watchers).
In this example, however, DataMonitor does not regitster as the Watcher.

ZooKeeper.exists() 동작이 서버에서 완료될 때, Zookeeper API은 이 완성된 callback를 클라이언트에게 발생합니다.

// 실제로 수행할 비즈니스를 여기에 작성하면 된다.
// 여기서 node 포멧을 정의해 변경된 사항으로 맞는 이벤트를 작성해 준다면, Zookeeper를 매우 잘 활용한다고 불수 있다.
// 이전보다 먼저 호출되는 함수가 있긴 하다. --> Watcher 인터페이스의 process가 먼저 호출된다. 
public void processResult(int rc, String path, Object ctx, Stat stat) {
    boolean exists;
    switch (rc) {
    case Code.Ok:
        exists = true;
        break;
    case Code.NoNode:
        exists = false;
        break;
    case Code.SessionExpired:
    case Code.NoAuth:
        dead = true;
        listener.closing(rc);
        return;
    default:
        // Retry errors
        zk.exists(znode, true, this, null);
        return;
    }
 
    byte b[] = null;
    if (exists) {
        try {
            b = zk.getData(znode, false, null);
        } catch (KeeperException e) {
            // We don't need to worry about recovering now. The watch
            // callbacks will kick off any exception handling
            e.printStackTrace();
        } catch (InterruptedException e) {
            return;
        }
    }     
    if ((b == null && b != prevData)
            || (b != null && !Arrays.equals(prevData, b))) {
        listener.exists(b);
        prevData = b;
    }
}

코드는 처음 znode 존재, 치명적 에러들 그리고 재발생되는 에러들을 위한 에러 코드를 체크하는 것입니다. 
만약 파일(혹은 znode)가 존재하지 않는다면, znode로 부터 데이터를 얻고, 그리고 그때 상태가 변경되었다면, Executor의 exists() 콜백를 발생합니다.
getData 호출을 위해 기억하실것은 어떤 예외 처리를 하지 말아햐 합니다.  왜냐 하면, 에러가 유발할수 있는 어떤것에 대한 pendding을 보고있기 때문입니다 :
만약 노드가 ZooKeeper.getData()가 호출되기 전에 삭제된다면,  ZooKeeper.exists()에 의해 세팅하는  watch 이벤트는  콜백을 트리거합니다.
만약 커뮤니케이션 에러가 있다면,  connection watch event는 연결이 되돌아 왔을때, 발생 합니다.

마지막으로 DataMonitor가 watch 이벤트들을 어떻게 처리하는지 알아차립니다.

// 여기서는 서버 상태 변화처리를 담당한다.
// Event안에 보면, enum 클래스로 두 가지 이벤트 종류에 대한 정리된 값이 있다.
// 하나는 서버 상태 변화 다른 하나는 node의 변화 이다.
// DI와 Callback을 사용해 나를 호출한 쪽으로 비즈니스를 넘겨버린다.

    public void process(WatchedEvent event) {
        String path = event.getPath();
        if (event.getType() == Event.EventType.None) {
            // We are are being told that the state of the
            // connection has changed
            switch (event.getState()) {
            case SyncConnected:
                // In this particular example we don't need to do anything
                // here - watches are automatically re-registered with 
                // server and any watches triggered while the client was 
                // disconnected will be delivered (in order of course)
                break;
            case Expired:
                // It's all over
                dead = true;
                listener.closing(KeeperException.Code.SessionExpired);
                break;
            }
        } else {
            if (path != null && path.equals(znode)) {
                // Something has changed on the node, let's find out
                zk.exists(znode, true, this, null);
            }
        }
        if (chainedWatcher != null) {
            chainedWatcher.process(event);
        }
    }

만약에 말이죠... 클라이언트 사이드 ZooKeeper 라이브러리들이 세션 만료(만료된 이벤트) 전에 Zookeeper로 커뮤니케이션 채널(동기로 연결된 이벤트)을 재 구축한다면, 세션의 watches의 모두는 자동으로 서버(watches의 자동 리셋은 ZooKeeper 3.0.0.에 새로운것) 와 함게 재구축되어질 것입니다. 
프로그램 가이드에 ZooKeeper Watches에 대해 보다 더 많은 것이 나와 있답니다.
이 함수에 약간 낮게 , DataMoinitor 가 znode에 대한 이벤트를 얻을 때, 변경된 것을 찾아 내기 위해ZooKeeper.exists()를 호출 할것 입니다. 

DataMonitor 클래스는 ZooKeeper 로직의 meat를 가지고 있습니다.
대부분은 비동지 이벤트 기반입니다.
DataMonitor는 생성자에 의해서 시작합니다.

//맨 밑에 zk.exists 는 모니터링 할 노드에 watcher를 붙이는 작업, 이걸 해줘야 모니터링이 된다.
//이걸 해 두면, 변경 감지 시 StatCallback 인터페이스의 ProcessResult를 호출해 준다.

public DataMonitor(ZooKeeper zk, String znode, Watcher chainedWatcher,
        DataMonitorListener listener) {
    this.zk = zk;
    this.znode = znode;
    this.chainedWatcher = chainedWatcher;
    this.listener = listener;
   
    // Get things started by checking if the node exists. We are going
    // to be completely event driven
    zk.exists(znode, true, this, null);
}

Zookeeper.exists()에 호출은 znode가 존재하는지 체크하고, watch를 설정하고 자신에게 완벽한 callback 객체로써 참조를 전달합니다.
이 의미에서, watch가 트리거되어 질때  진짜 프로세싱이 일어난 이후로 시작하는 것이다.


Note
Don't confuse the completion callback with the watch callback.
The ZooKeeper.exists() completion callback, which happens to be the method StatCallback.processResult() implemented in the DataMonitor object,
is invoked when the asynchronous setting of the watch oepration (By ZooKeeper.exists()) completes on the server.
The Triggering of the watch, on the other hand, sends an event to the Executor object, since the Executor registered as Watcher of the ZooKeeper object.
As an aside, you might note that the DataMonitor could also register itself as the Watcher for this particular watch event.
This is new to ZooKeeper 3.0.0(the support of multiple Watchers).
In this example, however, DataMonitor does not regitster as the Watcher.

ZooKeeper.exists() 동작이 서버에서 완료될 때, Zookeeper API은 이 완성된 callback를 클라이언트에게 발생합니다.

// 실제로 수행할 비즈니스를 여기에 작성하면 된다.
// 여기서 node 포멧을 정의해 변경된 사항으로 맞는 이벤트를 작성해 준다면, Zookeeper를 매우 잘 활용한다고 불수 있다.
// 이전보다 먼저 호출되는 함수가 있긴 하다. --> Watcher 인터페이스의 process가 먼저 호출된다. 
public void processResult(int rc, String path, Object ctx, Stat stat) {
    boolean exists;
    switch (rc) {
    case Code.Ok:
        exists = true;
        break;
    case Code.NoNode:
        exists = false;
        break;
    case Code.SessionExpired:
    case Code.NoAuth:
        dead = true;
        listener.closing(rc);
        return;
    default:
        // Retry errors
        zk.exists(znode, true, this, null);
        return;
    }
 
    byte b[] = null;
    if (exists) {
        try {
            b = zk.getData(znode, false, null);
        } catch (KeeperException e) {
            // We don't need to worry about recovering now. The watch
            // callbacks will kick off any exception handling
            e.printStackTrace();
        } catch (InterruptedException e) {
            return;
        }
    }     
    if ((b == null && b != prevData)
            || (b != null && !Arrays.equals(prevData, b))) {
        listener.exists(b);
        prevData = b;
    }
}

코드는 처음 znode 존재, 치명적 에러들 그리고 재발생되는 에러들을 위한 에러 코드를 체크하는 것입니다. 
만약 파일(혹은 znode)가 존재하지 않는다면, znode로 부터 데이터를 얻고, 그리고 그때 상태가 변경되었다면, Executor의 exists() 콜백를 발생합니다.
getData 호출을 위해 기억하실것은 어떤 예외 처리를 하지 말아햐 합니다.  왜냐 하면, 에러가 유발할수 있는 어떤것에 대한 pendding을 보고있기 때문입니다 :
만약 노드가 ZooKeeper.getData()가 호출되기 전에 삭제된다면,  ZooKeeper.exists()에 의해 세팅하는  watch 이벤트는  콜백을 트리거합니다.
만약 커뮤니케이션 에러가 있다면,  connection watch event는 연결이 되돌아 왔을때, 발생 합니다.

마지막으로 DataMonitor가 watch 이벤트들을 어떻게 처리하는지 알아차립니다.

// 여기서는 서버 상태 변화처리를 담당한다.
// Event안에 보면, enum 클래스로 두 가지 이벤트 종류에 대한 정리된 값이 있다.
// 하나는 서버 상태 변화 다른 하나는 node의 변화 이다.
// DI와 Callback을 사용해 나를 호출한 쪽으로 비즈니스를 넘겨버린다.

    public void process(WatchedEvent event) {
        String path = event.getPath();
        if (event.getType() == Event.EventType.None) {
            // We are are being told that the state of the
            // connection has changed
            switch (event.getState()) {
            case SyncConnected:
                // In this particular example we don't need to do anything
                // here - watches are automatically re-registered with 
                // server and any watches triggered while the client was 
                // disconnected will be delivered (in order of course)
                break;
            case Expired:
                // It's all over
                dead = true;
                listener.closing(KeeperException.Code.SessionExpired);
                break;
            }
        } else {
            if (path != null && path.equals(znode)) {
                // Something has changed on the node, let's find out
                zk.exists(znode, true, this, null);
            }
        }
        if (chainedWatcher != null) {
            chainedWatcher.process(event);
        }
    }

만약에 말이죠... 클라이언트 사이드 ZooKeeper 라이브러리들이 세션 만료(만료된 이벤트) 전에 Zookeeper로 커뮤니케이션 채널(동기로 연결된 이벤트)을 재 구축한다면, 세션의 watches의 모두는 자동으로 서버(watches의 자동 리셋은 ZooKeeper 3.0.0.에 새로운것) 와 함게 재구축되어질 것입니다. 
프로그램 가이드에 ZooKeeper Watches에 대해 보다 더 많은 것이 나와 있답니다.
이 함수에 약간 낮게 , DataMoinitor 가 znode에 대한 이벤트를 얻을 때, 변경된 것을 찾아 내기 위해ZooKeeper.exists()를 호출 할것 입니다. 


전체 소스 


이번에는.... 이분의 블로그를 참고해서 로컬에서 만들어 보고 테스트 해봅시다.

이해하는데 도움이 많이 되었습니다. 


ZooKeeper Service는 Ensemble 이라고 불리는 Host 들의 집합들을 통해서 복제되며,
동일한 어플리케이션을 구성하는 서버들의 복제된 그룹을 Quorum 이라고 부른다. (Quorum : 정족수, 정수)
Quorum 내의 모든 서버는 동일한 설정 파일들의 복제본을 가지고 있다.
Zookeeper의 서버 구성의 수는 절반이 실패해도 기능을 수행할 수 있도록 홀수로 구성하는 것을 권장한다.
2대의 서버가 장애 상태가 되어도 나머지 서버들이 동작할 수 있도록 5대의 서버로 구성하는 것이다.
이중 한대는 Leader 이고, 최소한의 구성은 3대가 된다.

ZooKeeper 서버 구성의 수는 어떻게 할 것인가? - 홀수다
물리적인 서버의 수는 어떻게 할 것인가? - 물리적인 장애가 발생하는 경우를 대비하여, 물리적으로 존재하는 서버로 구성해라.
운영을 위한 포트의 구성은 어떻게 할 것인가?  - 별도의 서버로 존재하면 Port를 크게 신경쓰지 않아도 되지만,
                                                                                      테스트를 위해서 단일 서버에 구성하는 것이기 때문에 아래와 같이 구성한다.
 
Client Port : 클러스터에서 운영할 어플리케이션에 ZooKeeper로 접속하기 위한 포트를 위미한다.
Quorum Port :  클러스터내의 ZooKeeper 서버간에 통신을 위한 포트를 의미
Leader election Port :클러스터내의 ZooKeeper 서버간의 Leader를 선출하기 위한 통신 포트를 의미



list
SERVER ID / Client Port / Quorum Port / Leader Election Port
1 / 2181 / 2888 / 3888
2 / 2182 / 2889 / 3889
3 / 2183 / 2890 / 3890

ZooKeeper 서버의 dataDir 경로 : "D:\ZooKeeper\data" 와 같이 고정된 경로를 사용하여 데이터 구성을 위한 기본 폴더로 구성한다.(성능 문제)
ZooKeeper 서버의 dataLogDir  경로: "D:\ZooKeeper\logs" 와 같이 고정된 경로를 사용하여 데이터 구성을 위한 기본 폴더로 구성한다.(성능 문제)  
ZooKeeper에 대한 메모리 설정은? : 테스트를 위한 것이라면 기본 Heap size 를 사용해도 상관없다. 
                                                                   실제 운영이라면, ZooKeeper 설치 경로의 "/conf/java.env" 파일을 생성해서 관련된 메모리 설정을 조정해 주어야 한다. 

{ZooKeeper-HOME|\conf\java.env 파일을 생성하고 다음과 같이 설정 
export JVMFLAGS="-Xmx2048m"

Sample 테스트를 위해서 ZooKeeper 서버들을 구성한다.

  1. ZooKeeper 서버 운영을 위한 기본 폴더 구조를 작성한다.

Root 폴더는 "C:\ZooKeeper" 로 한다.

각 서버는 다음과 같이 3개의 폴더로 한다.
C:\Server\ZooKeeper\zk-server1
C:\Server\ZooKeeper\zk-server2
C:\Server\ZooKeeper\zk-server3

각 서버별 데이터 관리를 위해서 "data" 폴더 밑으로 구성한다. 

C:\Server\ZooKeeper\data\zk1
C:\Server\ZooKeeper\data\zk2
C:\Server\ZooKeeper\data\zk3

각 서버별 Log 관리를 위한 폴더를 "logs" 폴더 밑으로 구성한다.

C:\Server\ZooKeeper\logs\zk1
C:\Server\ZooKeeper\logs\zk2
C:\Server\ZooKeeper\logs\zk3


  1. 이제 각 서버별로 식별 할 수 있는 ID를 지정하기 위해서 위에서 설정한 "data" 폴더의 각 서버 밑에 "myid" 라는 파일을 생성한다.
    (ex C:\Server\ZooKeeper\data\zk1\myid) 파일의 내용에 서버를 식별 할 수 있는 ID 값을 (zk1 은 1 , zk2 는 2, zk3 은 3) 으로 설정한다.

  2. 다운로드 해 놓은 ZooKeeper3.4.6 폴더에서 전부를 복사해도 되고,
    아니면 필수적인 폴더들(bin,conf,lib)와 필요한 jar 파일을 각 서버 (zk-server1 , zk-server2 ,  zk-server3 ) 폴더에 복사해 넣는다. 

  3. ZooKeeper 서버를 구동 할 수 있는 환경 설정을 각 서버별로 구성한다.
    복사한 서버 (ex, zk1) 폴더의 conf를 보면 zoo-sample.cfg 파일이 존재 하므로 zoo.cfg 파일을 생성한다.

    1) zk1 서버 폴더의 conf 에 zoo-sample.cfg 파일이 존재 한다. 
        이 파일을 복사하여 zoo.cfg 파일을 생성한다.
    2) 파일의 내용 중에서 해당 서버와 연결되는 항목인 clientPort , dataDir ,dataLogDir 정보를 위에서 설정한 구조에 맞도록 설정한다.
        물론 포트도 역시 위에서 정리한 서버 별 포트 내용에 맞도록 설정한다.

    tickTime=2000
    initLimit=10
    syncLimit=5
    dataDir=C:\\Server\\ZooKeeper\\data\\zk1
    clientPort=2181
    dataLogDir=C:\\Server\\ZooKeeper\\logs\\zk1
    server.1=localhost:2888:3888
    server.2=localhost:2889:3889
    server.3=localhost:2890:3890

  4. 위에서 구성한 Log 폴더를 이용하기 위해서는 각 ZooKeeper 서버의 bin 폴더에 존재하는 zkEnv 파일에서 "ZOO_LOG_DIR"을 수정해 주면된다.
    (Log 설정을 위한 정보는 conf 폴더에 log4j.properties 파일이 존재하며, bin 폴더의 zkServer 스크립트 파일에서 Log 와 관련된 정보를 사용한다.)

    set ZOOCFGDIR=%~dp0%..\conf
    set ZOO_LOG_DIR=%~dp0%..\..\logs\zk1
    set ZOO_LOG4J_PROP=INFO,ROLLINGFILE

  5. 구동을 위한 설정이 준비되었으므로 각 ZooKeeper 서버를 구동하면 된다.
    각 서버의 bin 폴더에서 zkServer.cmd 를 실행하면 된다.

    Notes

    zk-server1 을 실행하면 Log 상에 2번과 3번 서버가 통신을 실패 했다는 Log를 볼수 있다.
    zk-server2를 실행하면 3번 서버로의 통신이 실패했다는 Log를 볼 수 있다.
    1번과 2번이 실행되었을 때 1번과 2번 모두 3번과 연결 할 수 없는 Log를 확인 할 수 있다.
    마지막으로 zk-serve3을 실행하면 연결이 모두 되어 동작하는 것을 확인 할 수 있다.

    다양한 명령 제공
    - start
    - start-foreground
    - stop
    - restart
    - status
    - upgrade
    - print-cmd

    그러나 windows 에서 사용할 zkServer.cmd 파일은 배포된 버전에서는 위와 같은 명령을 제공하지 않는다.
    따라서 "zkSErver.cmd start" 하면 안된다. zkServer.cmd 와 명령이 제공되는 버전은 별도 제공 된다.(그냥 리눅스에서 해라..)


Implementation

Zookeeper Components는 ZooKeeper 서비스의  높은수준의 컴포넌트들을 보여준다.
요청 프로세서의 제외와 함께, ZooKeeper 서비스를 구성하는  서버들의 각각은 컴포넌트들의 각각의 자신의 카피를 복제합니다.


복제된 데이터베이스는 완전한 데이타 트리를 포함하고 있는 인 메모리 데이터 베이스 입니다.
수정사항은 회복가능성을 위해 디스크에 로그되어 지고, 쓰기들은 그들이 인메모리 데이터베이스에 적용되어지기 전에 디스크에 나열되어 집니다.(serialized) 

모든 Zookeeper 서버는 클라이언트들을 서비스합니다.
클라이언트들은 정확하게 하나의 서버에 연결됩니다. 요청을 서브밋하기 위해서.
읽기 요청들은 각각의 서버 데이터베이스의 로컬 리플리카로 부터 서비스 되어 집니다.
서비스의 단계을 변경하는 요청들, 쓰기 요청들은 agreement 프로토콜에 의해 처리되어 집니다.

agreement 프로토콜의 파트로써 클라이언트에서의 모든 쓰기 요청들은 단일 서버에 포워드되어지고, leader 라고 부릅니다.
Zookeeper 서버들의 나머지는 followers 라고 불리고, 리더로 부터 메세지 제안들을 받습니다. 그리고 메세지 전달에 동의 합니다.
메세징 레이어는 실패와 leader들과 함께 syncing followers 상의  leaders에게 대처하는 것을 돌봅니다.

ZooKeeper은 custom atomic messaging protocol을 사용합니다.
메세징 레이어가 atomic 인 이후에, ZooKeeper는 로컬 리플리카는 절대 분기하지 않는 것을 승인할수있습니다.
리더가 write 요청을 받았을때, 그것은 시스템의 단계가 쓰기가 적용되어지고, 이것을 새로운 스테이지로 캡쳐하는 트랜젝션안으로 이전할 때 계산합니다. 

Uses

Zookeeper의 프로그래밍 인터페이스 은 고의로 단순합니다.
그것을 가지고, 그러나, 여러분은 좀더 높은 정렬 동작들을 구현 할수 있습니다.
예를 들어 동기화 원시, 그룹 멤버쉽, 소유권, 등등..
몇개의 분산 어플리케이션들은 그것을 사용하고 있습니다.

Performance

Zookeeper는 높은 성능으로 설계 되었습니다. 
그러나 그게 다여?
야후의 Zookeeper 개발팀 의 결과 Research은 아래와 같다고 합니다.
이건 특히 높은 성능입니다. 






- 분산 시스템에서 일치(consensus) 문제
- 일치 문제 해결을 위한 분산 알고리즘들
- 실제 적용된 ZooKeeper 사용 예
- ZooKeeper 사용시 애로점

Distributed Systems.
a distributed system is a collection of independent computers that appears to its users as  a single coherent system.

A distributed system is a software system in which components located on networked computers communicate and coordinate their actions by passing messages.
The components interact with each other in order to achieve a common goal.

Coordination system
분산 시스템의 consensus 문제를 위탁할 수 있는 빌딩 블록

분산 시스템 구현에서 다양하게 활용
- 분산 locking, 동적 configuration 관리, 마스터 election ,  message queue...

사용
- 2000년대 말부터 Yahoo 내부 web crawler 등에서 사용
- Hadoop에서 cluster 관리를 위해 사용


파일 시스템과 유사항 계층적 구조
- Znode는 data와 children 모두 가질 수 있음.
- Znode는 data 제한은 1MB

임시 노드
- Ephemeral 속성으로 znode 생성시, 노드를 만든 세션이 끊기면 노드도 같이 사라짐
 - 분산 lock을 잡고 있는 게임 서버가 죽어도 lock을 회수 할 수 있음.

순차 노드
- Sequence 속성으로 znode 생성시, 노드에 자동으로 일련 번호를 붙여줌
- Request queue 구현에 활용 가능

Watcher
- 특정 노드에 이벤트 발생시 알림을 받을 수 있음.
- 노드 삭제, 노드 데이터 변경을 처리해야 할 때 편리 함.



로비 서버가 유저에게 접근 가능한 게임 서버 리스트를 동적으로 전달 할 때.

  1. 게임 서버는 프로세스가 뜰 때 자신의 UID를 생성함.
  2. /servers/{UID} 형태로 노드를 생성하고, 노드 data 에 자신의 IP, port를 등록함.
  3. /servers/ 아래의 노드들을 순회하여 이미 뜬 게임 서버들이 등록해 둔 IP, port를 알아냄
  4. /servers/에 watcher를 등록해 이후에 등록되는 게임 서버가 있다면 이를 알림으로 받음.

빈번히 갱신되는 데이터 저장소로 사용하면 절대 안됨!

쓰기보다 읽기가 많다면 바람직
서버 대수의 증가는 동시 발생 장애 유연성을 위해서이지 처리 속도를 위한 것이 아님.

Slave가 죽는 것은 별 지장 없음.
Master가 죽더라도 200ms 이내에 복구함.

  1. Failure and recovery of a follower(slave 하나 죽였다가 살림)
  2. Failure and recovery of a different follower (다른 slave 하나 죽였다가 살림)
  3. Failure of the leader ( Master 죽임)
  4. Failure and recovery of two followers ( slave 두 개를 동시에 죽였다가 살림)
  5. Failure of another leader ( Master 죽임)


동적으로 ZooKeeper 서버를 추가/삭제하는 것이 안됨.

- Zookeeper cluster 안에서 다른 서버들 리스트는 사전에 설정 파일로 고정되어야 함.