2011년 5월 1일 일요일

ZooKeeper 데이터모델

일반 파일 시스템은 파일에만 데이터를 저장할 수 있지만 주키퍼는 모든 노드에 데이터를 저장할 수 있다. 모든 디렉토리 구조(/dir1, /dir2)등에도 데이터를 저장한다. (무슨 차이지? ㅡㅡa)

파일 시스템은 로컬에 저장돼 있거나 마운트돼 있는 파일과 디렉토리에 대한 접근을 수행하지만 주키퍼는 클라이언트 라이브러리를 이용해 네임스페이스에 대한 조회나 노드에 저장된 데이터를 원격 클라이언트에서도 접근할 수 있는 분산 시스템이다.

주키퍼의 모든 데이터는 서버의 메모리에 존재하기 때문에 주키퍼에 저장할 수 있는 전체 데이터의 용량은 주키퍼 데본 서버가 할당받은 힙 메모리 영역을 넘지 못한다. 따라서 주키퍼에서 저장되는 데이터는 일반 파일과 같은 데이터가 아닌 시스템 관리, 모니터링, 락 관리 등에 필요한 메타 정보만 저장하는 것이 일반적이다. 데이터는 메모리에 존재하지만 트랜잭션 로그와 스냅샷 파일은 로컬 디스크에 저장돼 주키퍼 서버를 재시작 했을때 에도 데이터는 영속적으로 존재한다.

* 패스
: 노드를 식별하는 식별자는 파일 시스템의 디렉토리 구조와 동일하게 '/'로 구분된다.

패스 문자 규약
- null 문자(0x000)
- \u001 ~ \u0019, \u007F ~ \u009F
- \ud800 ~ \uF8FFF, \uFFF0 ~ \uFFFF, \uXFFFE ~ \uXFFFF, \uF0000 ~ \uFFFFF
- zookeeper : 시스템에서 사용하는 예약어이기 때문에 사용할 수 없음.
- 상대 경로를 나타내는 '.', '..' 는 파일시스템 처럼 현재 또는 상위 디렉토리 의미가 아닌 문자자체로 인식한다.

* 시간
: 주키퍼는 하나 이상의 서버에서 수행되기 때문에 클라이언트의 처리 요청에 따른 버전정보나 시간정보등에 대해 모든 주키퍼 서버가 공유해야한다. 아래와 같은 방법들로 시간 버전정보를 관리한다.

- 트랜잭션 아이디(Zxid) : 주키퍼에 저장된 노드 상태변경에 부여된 트랜잭션ID로 모든 변경 요청에 대해서 순서적으로 부여되고 zxid1은 zxid2보다 먼저 요청되었음을 보장한다.
- 버전 번호 : 노드 데이터 변경시 마다 버전값이 증가한다. 버전종류로는 z노드 데이터 변경, 자식노드 데이터 변경, ACL변경에 대해 각각 다른 버전이 부여된다.
- 경과시간(Tick time) : 세션 타임아웃, 커넥션 타임아웃등과같은 이벤트의 경과 시간을 의미.
- 시스템 시간 : 대부분 사용하지 않으나 노드 생성, 수정 일시에 대한 정보는 시스템 시간이용.

* Z 노드
: 계층적인 네임스페이스에서 각 노드를 z노드라고 한다. '/', '/dir1', '/dir2/a' 모두 z노드다. 파일시스템의 디렉토리나 파일에 해당하지만 다음과 같은 특징이있다.
1. 부모 노드에도 데이터를 저장할 수 있다.
2. 크기가 작은 데이터 위주로 저장 : 서버의 상태, 락 정보, 환경설정 정보등과 같은 크기가 작은 메타 데이터를 주로 저장한다.
3. 버전 : z노드에 저장되는 모든 데이터는 버전을 갖고 있다.
4. 접근권한 : z노드 단위로 권한(ACL)을 관리할 수 있다.

주키퍼 클라이언트 라이브러리
: 대부분 z노드 연산과 관련돼 있으며, 단순기능의 API만제공한다. 자바, C, 파이썬, 펄등과 같은 다양한 언어를 제공한다. 아래는 라이브러리 API에 대한 설명이다.

- exists : 특정 노드 존재 여부 확인. 리턴타입은 boolean이 아닌 Stat객체반환하고 값이 null이면 존재하지 않음을 의미한다.
- create : 노드를 생성한다. 생성시 노드 데이터도 같이 입력할 수 있다. 최대 1MB까지 가능하고 이미 존재하는 노드면 KeeperException.NodeExistsException을 발생.
- delete : 특정노드를 삭제한다. 존재하지 않으면 KeeperException.NoNodeException을 발생.
- getData : 노드 데이터를 가져고 존재하지 않으면 KeeperException.NoNodeException을 발생.
- setData : 노드 데이터를 설정하고 버전이 맞지 않으면 Exception이 발생하며 버전값이 -1이면 버전에 상관없이 값을 설정한다.
- getChildren : 노드의 자식 노드 목록을 가져온다.
- sync : 주키퍼는 모든 서버에 복제본이 저장될 때까지 기다린다.

* 주키퍼 서버목록 : 주키퍼 서버가 여러 대 이면 ','로 구분해 입력한다.(예:server1:2181, server2:2181) 서버정보에 루트 패스를 지정할 수도 있다. 'server1:2181, server2:2181/app/myapp1' 같이 정보를 주면 server2에서는 app/myapp1을 루트 노드로 인식한다.
* 세션 타임아웃 : 주키퍼 서버와 클라이언트 연결에 대한 타임아웃 시간이고 밀리초단위.
* 와처 : 이벤트 리스너를 등록한다.


주키퍼의 용도
: 클라이언트 애플리케이션은 주키퍼 객체를 생성한 후 주키퍼 클래스에서 제공하는 메소드를 이용해 노드를 관리한다. 노드에 저장하는 정보에 따라 용도가 달라진다. 노드에 서버정보를 저장하면 클러스터 멤버십 관리 용도가 될 수 있고, 메시지를 저장하면 메시지 큐로 활용할 수 있다.

API의 동기 및 비동기
: 비동기 API를 사용하면 클라이언트는 주키퍼 서버로 요청만 보내고 반환 값을 기다리지 않고 바로 다음 처리를 진행한다. 서버로 부터의 응답은 백그라운드 스레드에 의해 요청 시 등록된 콜백 메소드를 통해 받는다. 비동기 API는 병렬로 주키퍼 서버에 많은 작업을 처리하는 클라이언트에서 활용할 수 있다. 비동기 요청에 대해서 서버로 부터의 응답은 요청한 순서에 따라 받는다.
비동기 호출을 하더라도 처리되는 순서는 주키퍼 서버에 의해 보장된다.
서버측 결과를 기다리지않고 바로 다음 코드를 실행할 수 있고 문제 발생시 콜백 메소드에서 받아서 처리할 수 있기 때문에 주키퍼에 많은 요청을 보내야하는 클라이언트의 경우 성능을 최대로 높일 수 있다.
비동기 호출에서 클라이언트 라이브러리는 내부적으로 멀티 스레드로 동작한다. 비동기 호출뿐만 아니라 주키퍼 서버로의 heartbeat메시지 전송, 서버로부터의 이벤트 수신 등이 추가로 스레드를 생성해서 실행된다.

// 비동기 API 코드==========================================
package kr.co.jaso.zk.basic;

import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.AsyncCallback.StringCallback;
import org.apache.zookeeper.ZooDefs.Ids;

public class AsyncAPITest implements Watcher {
private ZooKeeper zk;
private Object monitor = new Object();
class CreateCallBack implements StringCallback {
@Override // create이후 "do something"이 실행되고 난뒤에 processResult가 실행됨.
public void processResult(int rc, String path, Object ctx, String name) {
if (rc == 0) {
System.out.println("콜백 수신 성공");
} else {
System.out.println("콜백 수신 실패");
}
//callback 받으면 테스트 프로그램을 종료시키기 위해 main 흐름으로 알려준다.
synchronized (monitor) {
monitor.notifyAll();
}
}
}

public void start() {
try {
zk = new ZooKeeper("127.0.0.1:2181", 5000, this);

//callback 함수와 같이 호출하 async 호출이 된다.
zk.create("/async_test101", "test".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT, new CreateCallBack(), null);

//이 부분은 create 호출 후 서버로부터 응답이 오지 않아도 바로 실행된다.
System.out.println("do something");

//아래 코드가 없으면 테스트 프로그램이 바로 종료되기 때문에
// callback이 실행되기 전에는 종료 안되게 하기 위한 코드
synchronized (monitor) {
monitor.wait();
}
} catch (Exception e) {
e.printStackTrace();
}
}

@Override
public void process(WatchedEvent event) {
}

public static void main(String[] args) {
(new AsyncAPITest()).start();
}
}

//==================================================


* 비동기 호출을 위한 콜백 클래스를 정의해야한다. 메소드별 콜백 클래스는 다음과 같다.
- create : StringCallback
- delete : VoidCallback
- exists : StatCallback
- getACL : ACLCallback
- getChildren :ChildrenCallback
- getData : DataCallback


* Z노드 속성
: 주키퍼가 분산 코디네이터의 역할을 수행할 수 있게 하는 z노드의 중요한 몇가지 속성이있다.
1. Stat
: z노드의 상태 정보를 저장한다. z노드 생성 시에는 byte[]정보만 전달하지만 주키퍼 내부적으로는 사용자가 전달한 데이터 이외에 부가적인 정보를 Stat객체로 저장한다.
-----------------------------------------------------------------
- czxid z노드의 생성 트랜잭션에서 사용된 zxid
- mzxid z노드의 마지막 수정 트랜잭션에서 사용된 zxid
- ctime z노드가 생성된 시스템 시간
- mtime z노드가 마지막 수정된 시스템 시간
- version z노드가 수정된 횟수
- cversion z노드의 자식의 갯수가 수정된 횟수
- aversion z노드의 ACL이 수정된 횟수
- ephemeralOwner 임시노드이면 z노드를 생성한클라이언트의 세션아이디이고, 임시노드아니면0
- dataLength 데이터 필드의 길이
- numChildren z노드의 자식수
-----------------------------------------------------------------
2. 와처(Watcher)
: 클라이언트는 모든 z노드에 모니터링 객체(callback object)를 등록할 수 있다. z노드에 데이터 수정, 삭제, 자식 노드 추가/삭제, ACL변경 등의 연산이 수행되면 z노드에 등록된 모니터링 객체의 콜백 메소드인 process()메소드를 호출해준다.
3. 원자적 데이터 처리(atomic)
: z노드에 대한 데이터의 조회와 저장은 원자성을 가진다.데이터의 일부만 저장 또는 조회되지않는다.
4. 영속성 노드(Persistent Node)
: 'Persistent'옵션으로 생성된 z노드는 주키퍼 서버의 로컬 디스크에 영구히 저장되고 클라이언트의 삭제 요청에 의해서만 삭제된다. 주키퍼 서버(클러스터)가 재시작 돼도 존재한다.
로컬 디스크에 저장된다는 말은 커밋 로그나 스냅샷 파일에 저장된다는 의미이며, 모든z노드는 생성 옵션에 상관없이 메모리에 존재한다. 따라서 주키퍼 서버에 저장할 수 있는 용량은 주키퍼 서버의 모모리 용량에 제한된다.(참조:서버/인프라를 지탱하는 기술 24시간 365일)
5. 임시노드(Ephemeral Node)
: 노드를 생성한 클라이언트와의 세션이 끊기면 해당 노드도 자동으로 삭제되는 노드다. 임시노드는 자식노드를 가질 수 없으며 임시노드의 부모는 반드시 영속성(Persistent로 생성된)노드여야됨
6. 시퀀스 노드(Sequence Node)
:프로그램 개발시 유일한 값이 필요한 경우가 있는데 싱글 프로세스나 동일 머신에서는 스레드 락이나 파일락을 이용할 수 있지만 분산시스템에서는 쉽지가않다. 가장 많이사용하는 방법은 DB테이블에서 자동으로 증가하는 칼럼을 이용하는 방법이다. 그러나 클라이언트가 많을시 서버에 많은 락을 유발함으로써 성능에 좋지않다. 시퀀스노드는 유일한 값을 쉽게 만들 수 있는데 자식노드의 이름값에 순차적으로 증가하는 번호를 부여하는 방식이다. 예를 들면 '/test'에 시퀀스 노드 옵션으로 생성하면 '/test0000000001'과 같이 만들어준다. 시퀀스 숫자의 범위는 '2147483647'이며 오버 되면 '-2147483647'로 된다.