메뉴

문서정보

목차

Zookeeper Tutorial

본격적으로 주키퍼(zookeeper)를 활용 해야 할 때가 왔다. 활용전에 주키퍼의 기본 기능들을 살펴보려 한다.

Zookeeper 설치

설치 환경은 다음과 같다. 아래와 같이 구성한다.

 주키퍼 테스트 환경 구성

3개의 주키퍼 노드로 주키퍼 클러스터를 만든다. 주키퍼 노드의 이름은 zk-01, zk-02, zk-03이다. 테스트를 위해서 두 개의 노드를 준비했다. 이 두 개의 노두를 하나의 클러스터로 관리하려 한다. 노드의 이름은 각각 node-01, node-02다. workstation은 내가 작업하는 공간이다. 간단한 와처(watcher) 프로그램과 zkCli.sh 등의 프로그램을 이용해서 주키퍼 클러스터가 제대로 굴러가는지를 테스트한다.

주키퍼 노드에 zookeeper 애플리케이션을 설치했다. 이 작업은 주키퍼 클러스터를 구성하는 모든 노드들에 대해서 수행해야 한다.
#  apt-get install zookeeper zookeeperd -y

각 주키퍼 노드에 /etc/hosts를 설정했다.
# cat /etc/hosts
......
192.168.56.50   zk-01
192.168.56.51   zk-02
192.168.56.52   zk-03
......

주키퍼 노드의 myid를 설정했다. zk-01은 1, zk-02는 2, zk-03은 3으로 설정한다. myid는 1에서 255까지의 값을 가질 수 있으며, 전체 주키퍼 클러스터에서 유일한 값이어야 한다.
# cat /var/lib/zookeeper/myid
1

주키퍼 설정파일을 수정한다.
# cat /etc/zookeeper/conf/zoo.cfg

server.1=zk-01:2888:3888
server.2=zk-02:2888:3888
server.3=zk-03:2888:3888

주키퍼 서버를 재시작 한다.
# service zookeeper restart

zkCli.sh로 주키퍼 기본 사용법 익히기

주키퍼 패키지를 설치하면, zkCli.sh라는 프로그램이 함께 설치된다. 이 프로그램을 이용하면 인터액티브하게 주키퍼 서버에 명령을 내릴 수 있다. Workstation에서 zkCli.sh를 이용 주키퍼 서버에 연결해 보자.

Workstation에도 zookeeper를 설치해야 한다. 서비스가 목적이 아니므로 zookeeperd는 설치하지 않는다.
# apt-get install zookeeper 
패키지를 설치하면 /usr/share/zookeeper/bin 에 zkCli.sh가 복사된다. zkCli.sh를 실행 할 수 있도록 .bashrc에 PATH 환경변수를 설정했다.
# cat ~/.bashrc
......
export PATH=$PATH:/usr/share/zookeeper/bin
......

zkCli.sh를 실행해보자.
# zkCli.sh -server zk-01 
[zk: zk-01(CONNECTED) 0] 

help 명령을 내리면 사용 할 수 있는 명령들이 나온다. 주키퍼의 기능들을 대략 확인 할 수 있다.
[zk: zk-01(CONNECTED) 2] help
ZooKeeper -server host:port cmd args
	connect host:port
	get path [watch]
	ls path [watch]
	set path data [version]
	rmr path
	delquota [-n|-b] path
	quit 
	printwatches on|off
	create [-s] [-e] path data acl
	stat path [watch]
	close 
	ls2 path [watch]
	history 
	listquota path
	setAcl path acl
	getAcl path
	sync path
	redo cmdno
	addauth scheme auth
	delete path [version]
	setquota -n|-b val path

테스트 시나리오

주키퍼의 활용에 대한 아이디어를 구체화하기 위해서 테스트 시나리오를 만들기로 했다. zkCli.sh를 이용해서 시나리오를 구현하려 한다.

나는 도커 클러스터를 관리하려고 한다. 도커 클러스터는 하나 이상의 도커 인스턴스로 구성이 된다. 각 도커 인스턴스는 도커 컨테이너를 실행하게 된다.

주키퍼를 이용해서 관리할 정보들은 다음과 같다.
  1. 클러스터에 몇 개의 도커 인스턴스가 있는지
  2. 클러스터에 총 몇 개의 컨테이너가 실행 중인지
  3. 각 도커 인스턴스에 몇 개의 컨테이너가 실행 중인지
  4. 도커 인스턴스가 제대로 작동하고 있는지 확인한다. 문제가 생긴 인스턴스에는 컨테이너를 할당하지 않는다.
이들 정보를 이용해서 어느 클러스터의 어느 인스턴스에 컨테이너를 만들어야 하는지, 어떤 인스턴스를 복구해야 할지를 결정 할 수 있을 것이다.

노드 읽기

주키퍼는 디렉터리형식으로 자원을 배치하고 관리한다. ls명령으로 파일시스템을 탐색하듯 자원을 탐색할 수 있다.
[zk: zk-01(CONNECTED) 3] ls /
[zookeeper]
지금은 단지 하나의 디렉터리만 있다. zookeeper 디렉터리 밑에 있는 자원을 살펴보자.
[zk: zk-01(CONNECTED) 5] ls /zookeeper
[quota]
quota라는 자원이 보인다.

노드 추가하기

주키퍼는 디렉터리형식으로 자원을 배치하고 관리한다. 도커 클러스터를 관리하기 위한 노드를 만든다. 노드의 이름은 docker-cluster로 했다. docker-cluster는 하나 이상의 클러스터를 관리 할 수 있다. 그래서 0001, 0002 두 개의 노드를 docker-cluster 밑에 추가했다.
[zk: zk-01(CONNECTED) 16] create /docker-cluster docker_cluster
[zk: zk-01(CONNECTED) 17] create /docker-cluster/0001 cluster_0001
[zk: zk-01(CONNECTED) 18] create /docker-cluster/0002 cluster_0002

주키퍼의 데이터관리 시스템은 파일 시스템과 아주 유사하다. 하지만 파일시스템과 다르게 각 노드는 하위 노드 뿐만 아니라 자신과 관련된 데이터를 가질 수 있다. create 명령을 실행 할때 노드의 이름과 노드의 데이터를 설정할 수 있다. 노드에 대한 메타정보로 볼 수 있겠다.

노드는 2가지 타입이 있다.
  1. Ephemeral : 세션이 끊기면(클라이언트와의 연결이 끊어지면)삭제되는 노드다.
  2. Persistent : 세션이 끊기더라도 여전히 남아있는 노드다.
각 타입은 Sequential 성질을 가질 수 있다. Sequential을 설정하면 노드 이름뒤에 4바이트 크기의 유일한 숫자를 붙는다. 이 숫자는 0부터 증가를 한다. 따라서 2147483647개의 노드에 대해서 유일한 이름을 줄 수 있다.

/docker-cluster/0001 밑에 mynode 라는 이름으로 시작되는 sequential 노드를 만들어보자.
[zk: 192.168.56.50(CONNECTED) 8] create -s /docker-cluster/0001/mynode node1
Created /docker-cluster/0001/mynode0000000000
[zk: 192.168.56.50(CONNECTED) 9] create -s /docker-cluster/0001/mynode node2
Created /docker-cluster/0001/mynode0000000001

노드 정보 가져오기

get을 이용해서 노드의 정보를 가져올 수 있다.
[zk: 192.168.56.50(CONNECTED) 10] get /docker-cluster/0001/mynode0000000000
node1
cZxid = 0x30000000b
ctime = Mon Mar 21 00:47:33 KST 2016
mZxid = 0x30000000b
mtime = Mon Mar 21 00:47:33 KST 2016
pZxid = 0x30000000b
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 5
numChildren = 0
[zk: 192.168.56.50(CONNECTED) 11] 

노드 삭제

delete 명령을 이용해서 노드를 지울 수 있다.
[zk: 192.168.56.50(CONNECTED) 13] delete /docker-cluster/0001/mynode0000000000

데이터 변경

set으로 노드의 데이터를 변경 할 수 있다.
[zk: 192.168.56.50(CONNECTED) 3] set /docker-cluster/0001/node0000000005 newdata
[zk: 192.168.56.50(CONNECTED) 4] get /docker-cluster/0001/node0000000005
newdata
cZxid = 0x30000000f
ctime = Mon Mar 21 00:59:47 KST 2016
mZxid = 0x300000012
......

Watches

주키퍼는 각 노드들에 대한 변경이벤트의 발생을 기다릴 수 있다. help를 보면 wath 옵션을 사용 할 수 있는 명령이 있다.
[zk: 192.168.56.50(CONNECTED) 12] help
    ......
	get path [watch]
	ls path [watch]
	stat path [watch]
    ......
	ls2 path [watch]
    ......
watch 옵션을 주면 노드에 변경정보가 있을 때, 이를 알려준다.
[zk: 192.168.56.50(CONNECTED) 15] ls /docker-cluster/0001 watch
[mynode0000000004]
다른 터미널에서 zkCli.sh를 이용해서 /docker-cluster/0001/node3를 만들어보자. 아래와 같이 노드 정보가 변경됐다는 이벤트 메시지를 확인 할 수 있을 것이다.
[zk: 192.168.56.50(CONNECTED) 16] 
WATCHER::
WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/docker-cluster/0001

주키퍼는 getData(), getChildren(), exists()에 대한 watch를 제공한다. 주키퍼의 watch 이벤트는 one-time-trigger방식으로 작동한다. 즉 watch로 이벤트를 가져왔다면, 다시 watch를 수행해야지만 다음 이벤트를 가지고 올 수 있다.

one-time-trigger 방식으로 작동하기 때문에, 다음 watch를 수행하기 전에 발생한 이벤트는 잃어버릴 수 있다. 이를 감안해서 watch전략을 짜야 한다. 예를 들어서 여러 개의 자식 노드를 가지고 있는 노드에 대한 watch는 하지 말자. 동시에 여러 개의 하위 노드들의 값이 변할 수 있는데, 이들을 처리하기가 만만치 않을 것이다.

주키퍼를 이용한 분산 코디네이터

컨테이너 기반의 SaaS 인프라를 관리하기 위한 분산 코디네이터를 만들어보자. 이 분산 코디네이터는 컨테이너를 실행하는 인스턴스들의 목록을 유지하며, 각 인스턴스의 가용 자원을 모니터링한다. 관리자가 SaaS 애플리케이션 실행을 요청하면, 적당한 인스턴스를 찾아서 컨테이너를 실행하는 일을 한다. 또한 코디네이터는 각 인스턴스가 실행하고 있는 애플리케이션의 목록들도 가지고 있어야 한다.

나는 다음과 같이 분산 코디네이터 환경을 만들기로 했다.

 분산 코디네이터 환경 구성

이 환경은 Jsaas-Master과 Jsaas-Slave로 구성된다. Jsaas-Master는 인스턴스를 관리하는 역할을 하며, 주키퍼로 관리를 한다. 가용성을 위해서 3개 이상의 Jsaas-Master로 클러스터를 구성한다. 이 클러스터는 Active-Standby 방식으로 작동한다. 요청의 분산과 가용성을 위해서 Active-Active로 운영하고 싶겠지만 좋은 생각이 아니다.

주키퍼를 데이터베이스로 사용 할 수 있을 것이란 생각에 Active-Active하게 구성할 생각을 한다. 관리하는 모든 인스턴스와 애플리케이션 정보를 주키퍼에 두면, Jsaas-Master들이 모두 공유해서 서비스 할 수 있을 것 처럼 보이기 때문이다. 주키퍼는 Mysql처럼 빠른 읽기/쓰기를 위한 목적으로 만들어진 소프트웨어가 아니다. 빠른 읽기/쓰기가 필요한 정보들을 서로 공유하기 위해서 주키퍼를 사용하는 것은 좋은 생각이 아니다.

그래서 주키퍼는 Jsaas-Master를 관리하는 목적으로만 사용하기로 했다. Jsaas-Slave의 정보들은 Active 상태에 있는 Jsaas-Master만 관리한다. Standby 상태의 Jsaas-Master는 아무런 정보도 가지지 않는데, 어차피 Jsaas-Slave들은 Active 상태의 Jsaas-Master에만 연결할 것이기 때문에 문제될 게 없다.

중요한건 3개의 Jsaas-Master 중에서 리더를 결정하는 것이다. 분산 코디네이터네에서는 bully 알고리즘을 이용해서 리더를 선출하는데, 이 경우 주키퍼를 이용해서 리더를 선출 할 수 있다. 대략 적인 알고리즘은 다음과 같다.

 리더 선출 알고리즘

  1. 각 노드(Jsaas-Master)들은 일련번호를 갖는다. Sequence 타입으로 znode를 만드는 것으로 일련번호를 부여 할 수 있다. 이 일련번호는 계속 증가한다.
  2. 낮은 일련 번호를 가지는 노드가 리더로 선출될 확률이 높다.
  3. 처음 주키퍼에 접속한 노드-1은 가장 낮은 일련번호를 가지고 있을 거다.
  4. 처음에는 노드-1만 있기 때문에 자동으로 리더로 선출 된다.
  5. 노드-2, 3, 4 가 붙는다. 새로 붙는 노드들은 이미 붙어있는 노드에 누가 리더인지를 묻는다. 이 경우 노드-1이 리더가 되고, 나머지 뒤에 붙은 노드들은 follower가 된다.
  6. 노드-1이 빠져나갔다고 가정해보자. 리더가 사라졌으니, 새로운 리더를 선출해야 한다. 노드-2,3,4는 watch로 노드-1의 연결이 끊겼다는 것을 알아챈다. 노드-2는 자신의 일련번호가 가장 낮다는 것을 알고 있으므로, 주변 노드에게 내가 리더가 되겠으니 승인 해달라는 메시지를 전송한다.
  7. 노드-2가 요청 메시지를 늦게보내거나 아예 보내지 못 할 수도 있다. 다른 노드들이 무한정 기다릴 수는 없으므로 일정 시간동안 리더 승인 요청이 없으면, 다른 노드들이 리더 승인 요청 메시지를 보낸다. 만약 노드-2가 이 메시지를 받는다면, 아냐 내가 리더이니 승인해줘라는 응답 메시지를 보내고 리더가 될 수 있을 것이다. 노드-2로 부터 응답이 없다면, 일련번호가 가장 낮은 노드-3이 리더가 된다.
  8. 노드-1이 다시 주키퍼에 붙으면 새로 (가장 높은)일련번호로 붙는다. 주변 노드에 리더 확인 요청을 보내는 것으로 리더를 확인하고 자신은 follower가 된다.
최적화할 구석이 있지만 위의 방식으로 알고리즘을 대략 구현 할 수 있을 것이다.

구현

실제 구현을 해보려 한다. 단 코드는 만들지 않는다(귀찮아서). 구성은 아래와 같다.

 분산 코디네이터 구현 환경

Jsaas-master들이 사용할 자료구조를 설계한다. 이들 마스터는 /jsaas디렉토리 밑에 저장하기로 했다. 노드의 타입은 emphasis, sequence이다. 노드의 이름은 node.info.json.으로 했다. 마스터는 node.info.json.00000000009 형식의 이름으로 연결된다.

 znode 구성

node.info.json. 파일에는 노드의 상세 정보가 들어있다. 마스터들은 이 정보로 다른 마스터들을 확인 할 수 있다.
{
  "hostname":"mast-01.joinc.priv",
  "ip":"192.168.56.155",
  "port":"8081"
}
  1. hostname : 마스터의 호스트 이름
  2. ip : 마스터의 IP
  3. port : 마스터의 Port
테스트용 코드다. 주키퍼에 연결해서 znode를 등록하는 일을 한다.
package main

import (
    "encoding/json"
    "fmt"
    "github.com/samuel/go-zookeeper/zk"
    "time"
)

func main() {
    cli, _, err := zk.Connect([]string{"192.168.56.155", "192.168.56.156", "192.168.56.157"}, time.Second * 5)
    if err != nil {
        panic(err)
    }

    data := struct {
        Hostname string  `json:"hostname"` 
        Ip       string  `json:"ip"`
        Port     int     `json:"port"` 
    }{
        "jsaas-01.joinc.priv",
        "192.168.56.155",
        8888,
    }
    jdata, _ := json.Marshal(data)

    path, err := cli.Create("/jsaas/node.infojson.", jdata,
        zk.FlagSequence|zk.FlagEphemeral,
        zk.WorldACL(zk.PermAll))
    if err != nil {
        panic(err)
    }

    fmt.Println("Create ", path)
    time.Sleep(time.Second * 1000)
}

	
		
/jsaas는 미리 만들어 놓았다. 실제 작동하는 코드에서는 jsaas를 검사해서 없으면 새로 만들어야 할 것이다. 그리고 hostname, ip, port 정보를 가지는 구조체를 만들어서 /jsaas/node.info.json. 노드의 데이터로 설정했다. 역시 실제 코드에서는 hostname을 읽어서 자신의 ip를 설정해야 겠으나 테스트인 만큼 (귀찮아서)하드코딩 했다. 이 프로그램을 실행한 결과다.
# go run jsaas-master.go 
2016/04/13 19:06:19 Connected to 192.168.56.155:2181
2016/04/13 19:06:19 Authenticated: id=95718048050708488, timeout=5000
Create  /jsaas/node.infojson.0000000006
하나 더 실행 했다.
# go run goconnect.go 
2016/04/13 19:07:19 Connected to 192.168.56.155:2181
2016/04/13 19:07:19 Authenticated: id=95718048050708489, timeout=5000
Create  /jsaas/node.infojson.0000000007
zkCli.sh로 노드의 정보를 확인했다.
[zk: 192.168.56.155(CONNECTED) 0] ls /
[jsaas, zookeeper]
[zk: 192.168.56.155(CONNECTED) 2] ls /jsaas
[node.infojson.0000000007, node.infojson.0000000006]
[zk: 192.168.56.155(CONNECTED) 2] ls /jsaas/
[node.infojson.0000000007, node.infojson.0000000006]
[zk: 192.168.56.155(CONNECTED) 5] get /jsaas/node.infojson.0000000006
{"hostname":"jsaas-01.joinc.priv","ip":"192.168.56.155","port":8888}
cZxid = 0xb00000053

bully 알고리즘은 그룹내 모든 프로세스들이 다른 프로세스들의 정보를 미리 알고 있어야 하기 때문에 동적으로 시스템을 확장하기기 쉽지 않다. 이 일을 주키퍼가 도맡아 처리하기 때문에 쉽게 분산 코디네이터를 만들 수 있다.

마치며

주키퍼 구성에서 분산 코디네이터 구현까지 간단히 살펴봤다. 분산 코디네이터 내용의 대부분은 Mesos 아키텍처를 참고했다.