메뉴

문서정보

목차

etcd

etcd는 분산 key-value store다. CoreOS에서 coreos 인스턴스의 클러스터를 관리하기 위해서 사용했으며, 구글의 클러스터 컨테이너 관리 소프트웨어인 Kubernetes의 백엔드 시스템으로 사용하면서 더 유명해 졌다. 오픈소스로 GitHub에서 다운로드 해서 사용 할 수 있다. etcd는 네트워크로 연결된 노드들 중 리더를 선정해서 클러스터를 관리한다. 데이터는 분산 저장되기 때문에 리더를 포함한 시스템의 오류에 대한 내성을가진다.

응용 프로그램은 etcd에 데이터를 읽고 쓸 수 있다. 간단한 예제는 데이터베이스 연결 정보와 플래그 값을 etcd에 저장하는 것이다. etcd는 값 변경에 대한 감시 기능을 제공하므로 설정이 변경될 경우 앱을 재구성할 수있다. 좀 더 나아가면 데이터에 대한 일관성을 보장하는 점을 활용해서 리더를 선출하고, 작업 클러스터 전체에 대한 분산 잠금을 수행 할 수 있다.

etcd는 하나의 마스터(master)와 하나 이상의 팔로워(follower)로 구성된다.

 Etcd 클러스터 구성

테스트를 위한 etcd cluster 구성 환경

VirtualBox를 이용해서 etcd-01, etcd-02, etcd-03 3개의 테스트 운영체제를 실행 했다. 게스트 운영체제로는 우분투리눅스 18.04를 사용했다.

 etcd 환경

노드 이름 주소 호스트 이름
etcd-01 192.168.56.101 etcd-01.example.com
etcd-02 192.168.56.102 etcd-02.example.com
etcd-03 192.168.56.103 etcd-03.example.com

etcd 설치

Coreos Github에서 tar.gz을 다운로드 받아서 설치했다. 2018년 7월 30일 현재 최신버전은 v3.3.9다. 압축을 푼 다음 etcd와 etcdctl을 /usr/local/bin에 복사한다. /usr/local/bin을 환경변수 PATH에 등록했다.
$ wget https://github.com/coreos/etcd/releases/download/v3.3.9/etcd-v3.3.9-linux-amd64.tar.gz
$ cd etcd-v3.3.9-linux-amd64
$ cp cp etcd etcdctl /usr/local/bin/
$ echo $PATH
/usr/local/sbin:/usr/local/bin:...

$ etcd --version
etcd Version: 3.3.9
Git SHA: fca8add78
Go Version: go1.10.3
Go OS/Arch: linux/amd64

Static cluster 구성

etcd-01,etcd-02,etcd-03 3개 노드로 이루어진 클러스터를 만들기로 했다. 각 노드에서 아래의 명령을 실행했다.
etcd --name etcd-01 \
--initial-advertise-peer-urls http://192.168.56.101:2380 \
--listen-peer-urls http://192.168.56.101:2380 \
--listen-client-urls http://192.168.56.101:2379,http://127.0.0.1:2379 \
--advertise-client-urls http://192.168.56.101:2379 \
--initial-cluster-token "etcd-cluster-1" \
--initial-cluster etcd-01=http://192.168.56.101:2380,etcd-02=http://192.168.56.102:2380,etcd-03=http://192.168.56.103:2380 \
--initial-cluster-state new

etcd --name etcd-02 \
--initial-advertise-peer-urls http://192.168.56.102:2380 \
--listen-peer-urls http://192.168.56.102:2380 \
--listen-client-urls http://192.168.56.102:2379,http://127.0.0.1:2379 \
--advertise-client-urls http://192.168.56.102:2379 \
--initial-cluster-token "etcd-cluster-1" \
--initial-cluster etcd-01=http://192.168.56.101:2380,etcd-02=http://192.168.56.102:2380,etcd-03=http://192.168.56.103:2380 \
--initial-cluster-state new

etcd --name etcd-03 \
--initial-advertise-peer-urls http://192.168.56.103:2380 \
--listen-peer-urls http://192.168.56.103:2380 \
--listen-client-urls http://192.168.56.103:2379,http://127.0.0.1:2379 \
--advertise-client-urls http://192.168.56.103:2379 \
--initial-cluster-token "etcd-cluster-1" \
--initial-cluster etcd-01=http://192.168.56.101:2380,etcd-02=http://192.168.56.102:2380,etcd-03=http://192.168.56.103:2380 \
--initial-cluster-state new
실행하고 나면 실행 디렉토리 밑에 {name}.etcd파일이 만들어진다. 이 디렉토리 밑에 etcd의 데이터파일들이 저장된다.

클러스터 관리

etcd는 HTTP 기반의 REST API를 제공한다. 이 API를 이용해서 클러스터를 관리 할 수 있다.

모니터링

/health API로 etcd 노드의 상태를 확인 할 수 있다.
# curl -L http://192.168.56.101:2379/health
{"health":"true"}
# curl -L http://192.168.56.102:2379/health
{"health":"true"}
# curl -L http://192.168.56.103:2379/health
{"health":"true"}

etcdctl로 전체 클러스터의 상태를 확인 할 수도 있다.
# etcdctl cluster-health
member 11796896800e7f4d is healthy: got healthy result from http://192.168.56.101:2379
member 4a84c8889f5d7a35 is healthy: got healthy result from http://192.168.56.103:2379
member b3a64181bea306fb is healthy: got healthy result from http://192.168.56.102:2379
cluster is healthy

etcd 클러스터의 멤버리스트를 확인해 보자.
root@etcd-01:~# etcdctl member list
11796896800e7f4d: name=etcd-01 peerURLs=http://192.168.56.101:2380 clientURLs=http://192.168.56.101:2379 isLeader=true
4a84c8889f5d7a35: name=etcd-03 peerURLs=http://192.168.56.103:2380 clientURLs=http://192.168.56.103:2379 isLeader=false
b3a64181bea306fb: name=etcd-02 peerURLs=http://192.168.56.102:2380 clientURLs=http://192.168.56.102:2379 isLeader=false
멤버의 이름 peer URL, client URL, 리더를 확인 할 수 있다. etcd-01이 리더인데, 이녀석을 죽이면 어떻게 될까 ? 테스트해봤다.
$ etcdctl cluster-health
member 11796896800e7f4d is unreachable: [http://192.168.56.101:2379] are all unreachable
member 4a84c8889f5d7a35 is healthy: got healthy result from http://192.168.56.103:2379
member b3a64181bea306fb is healthy: got healthy result from http://192.168.56.102:2379
리더가 바뀐걸 확인 할 수 있다.
$ etcdctl member list
11796896800e7f4d: name=etcd-01 peerURLs=http://192.168.56.101:2380 clientURLs=http://192.168.56.101:2379 isLeader=false
4a84c8889f5d7a35: name=etcd-03 peerURLs=http://192.168.56.103:2380 clientURLs=http://192.168.56.103:2379 isLeader=false
b3a64181bea306fb: name=etcd-02 peerURLs=http://192.168.56.102:2380 clientURLs=http://192.168.56.102:2379 isLeader=true
etcd는 raft알고리즘으로 리더를 설정한다. 언젠가 이쪽도 한번 살펴봐야 겠다.

적절한 클러스터 사이즈

권장되는 클러스터 크기는 3, 5, 7이며, 내결함성 요구 사항에 따라서 크기가 결정된다. 크기가 7인 경우 거의 완전한 내결함성을 제공 할 수 있다. 클러스터가 클 수록 내결함성이 향상되지만 데이터 복제가 많아지기 대문에 쓰기 성능이 저하된다. 아래표는 클러스터 크기에 따른 결함 허용을 나타내고 있다.
클러스터 크기 과반 결함 허용
1 1 0
2 2 0
3 2 1
4 3 1
5 3 2
6 4 2
7 4 3
8 5 3
9 5 4

클러스터 크기 변경

etcdctl 툴을 이용해서 멤버를 추가하거나 삭제할 수 있다.

etcd REST API

etcdctl 대신 curl로 직접 API를 호출해보기로 했다.

version

/version API로 etcd의 버전을 확인 할 수 있다.
$ curl -L http://etcd-01:2379/version
{"etcdserver":"3.3.9","etcdcluster":"3.3.0"}

Key & Value 설정

key & value를 설정해보자. key 이름은 /message, 값은 "Hello world"를 저장했다.
$ curl -L http://etcd-01:2379/v2/keys/message -XPUT -d value="Hello world"
{"action":"set","node":{"key":"/message","value":"Hello world","modifiedIndex":13,"createdIndex":13}}
/message key에 다른 값을 설정해보자.
# curl -L http://etcd-01:2379/v2/keys/message -XPUT -d value="My name is yundream"
{
   "prevNode" : {
      "value" : "Hello world",
      "modifiedIndex" : 13,
      "createdIndex" : 13,
      "key" : "/message"
   },
   "action" : "set",
   "node" : {
      "createdIndex" : 17,
      "modifiedIndex" : 17,
      "key" : "/message",
      "value" : "My name is yundream"
   }
}
"prevNode"라는 새로운 필드가 보인다. 이 필드는 해당 키에 있던 이전 값을 보여준다. 이전 값이 없다면 생략된다. /message key 값을 가져와보자.

key에 대한 값 가져오기

앞서 설정한 /message key의 값을 읽어보자.
# curl http://etcd-01:2379/v2/keys/message | json_pp
{
   "node" : {
      "value" : "My name is yundream",
      "key" : "/message",
      "modifiedIndex" : 17,
      "createdIndex" : 17
   },
   "action" : "get"
}

Key 삭제하기

DELETE 메서드로 /message API를 호출하면 된다.
# curl http://etcd-01:2379/v2/keys/message  -XDELETE
{
   "node" : {
      "key" : "/message",
      "modifiedIndex" : 18,
      "createdIndex" : 17
   },
   "action" : "delete",
   "prevNode" : {
      "modifiedIndex" : 17,
      "createdIndex" : 17,
      "value" : "My name is yundream",
      "key" : "/message"
   }
}

# curl http://etcd-01:2379/v2/keys/message 
{
   "index" : 18,
   "errorCode" : 100,
   "cause" : "/message",
   "message" : "Key not found"
}

TTL 설정

일정 시간 시간 이후에 삭제되도록 TTL(Time To Live)를 설정 할 수 있다.
# curl http://etcd-02:2379/v2/keys/foo -XPUT -d value=bar -d ttl=60 
{
   "action" : "set",
   "node" : {
      "expiration" : "2018-08-02T14:06:12.728999386Z",
      "ttl" : 60,
      "createdIndex" : 21,
      "value" : "bar",
      "modifiedIndex" : 21,
      "key" : "/foo"
   }
}
GET 을 하면 남은 TTL을 확인 할 수 있다.
# curl http://etcd-02:2379/v2/keys/foo
{
   "node" : {
      "expiration" : "2018-08-02T14:06:12.728999386Z",
      "value" : "bar",
      "key" : "/foo",
      "createdIndex" : 21,
      "ttl" : 5,
      "modifiedIndex" : 21
   },
   "action" : "get"
}
TTL이 지난 후 GET을 하면 "Key not found"를 반환한다.
# curl http://etcd-02:2379/v2/keys/foo
{
   "errorCode" : 100,
   "index" : 22,
   "cause" : "/foo",
   "message" : "Key not found"
}

TTL 재 설정

wather에 이벤트를 전송하지 않고 TTL을 재 설정 할 수 있다.
# curl http://etcd-02:2379/v2/keys/foo
{
   "action" : "get",
   "node" : {
      "key" : "/foo",
      "expiration" : "2018-08-02T14:17:31.888343014Z",
      "createdIndex" : 23,
      "modifiedIndex" : 23,
      "ttl" : 71,
      "value" : "bar"
   }
}

# curl http://etcd-02:2379/v2/keys/foo -XPUT -d  ttl=120 -d refresh=true -d prevExist=true
{
   "prevNode" : {
      "value" : "bar",
      "ttl" : 56,
      "modifiedIndex" : 23,
      "createdIndex" : 23,
      "expiration" : "2018-08-02T14:17:31.888343014Z",
      "key" : "/foo"
   },
   "action" : "update",
   "node" : {
      "value" : "bar",
      "ttl" : 120,
      "modifiedIndex" : 24,
      "createdIndex" : 23,
      "expiration" : "2018-08-02T14:18:36.490309241Z",
      "key" : "/foo"
   }
}

데이터 변경 이벤트를 기다리기

key 값의 변화를 long polling으로 기다릴 수 있다. recusive=true를 설정하면 자식 키의 변화도 기다릴 수 있다. GET 메서드를 호출 할 때, wait=true를 함께 전송하면 된다.
# curl http://etcd-02:2379/v2/keys/joinc?wait=true
지금 joinc 키는 존재하지 않는다. 존재하지 않는 키를 기다릴 수도 있다. PUT으로 key를 설정했다.
# curl http://etcd-02:2379/v2/keys/foo?wait=true
{
   "node" : {
      "value" : "var",
      "modifiedIndex" : 26,
      "createdIndex" : 26,
      "key" : "/foo"
   },
   "action" : "set"
}
wait=true로 기다리고 있던 요청이 반환된다.
{
   "node" : {
      "modifiedIndex" : 27,
      "createdIndex" : 27,
      "key" : "/joinc",
      "value" : "yundream"
   },
   "action" : "set"
}
존재하는 key에 대해서 기다릴 경우 prevNode정보도 함께 출력된다.
{
   "node" : {
      "modifiedIndex" : 28,
      "createdIndex" : 278
      "key" : "/joinc",
      "value" : "Hello world"
   },
   "prevNode" : {
      "modifiedIndex" : 27,
      "createdIndex" : 27,
      "key" : "/joinc",
      "value" : "yundream"
   },
   "action" : "set"
}
인덱스 값을 이용해서 과거에 실행한 명령을 확인 할 수도 있다. 이 기능은 watch 명령 사이에 발생한 이벤트를 놓치지 않도록 하는데 사용 할 수 있다. 보통 가장 최근 명령의 modifiedIndex+1 시점에서 watch를 수행한다.
# curl http://etcd-02:2379/v2/keys/foo?wait=true&waitIndex=26

Automically Creating In-Order Keys

POST를 이용하면, 유일하게 증가하는 일련의 key를 만들 수 있다.
# curl http://etcd-01:2379/v2/keys/queue -XPOST -d value=Job1
{
   "action" : "create",
   "node" : {
      "modifiedIndex" : 21,
      "createdIndex" : 21,
      "key" : "/queue/00000000000000000021",
      "value" : "Job1"
   }
}
다시 명령을 내리면, 이전보다 증가한 유일한 key 값을 얻을 수 있다.
$ curl http://etcd-01:2379/v2/keys/queue -XPOST -d value=Job2
{
   "node" : {
      "modifiedIndex" : 22,
      "createdIndex" : 22,
      "key" : "/queue/00000000000000000022",
      "value" : "Job2"
   },
   "action" : "create"
}
GET메서드로 /queue 밑에 있는 모든 key들을 가져올 수 있다. "sorted"파라메터를 이용하면 정렬된 목록을 얻을 수 있다.
$ curl -s http://etcd-01:2379/v2/keys/queue?recurisive=true\&sorted=true 
{
   "action" : "get",
   "node" : {
      "dir" : true,
      "key" : "/queue",
      "createdIndex" : 18,
      "modifiedIndex" : 18,
      "nodes" : [
         {
            "value" : "Job1",
            "key" : "/queue/00000000000000000018",
            "createdIndex" : 18,
            "modifiedIndex" : 18
         },
         {
            "modifiedIndex" : 19,
            "createdIndex" : 19,
            "value" : "Job2",
            "key" : "/queue/00000000000000000019"
         }
    }
}
In-Order 키는 선입선출(FIFO) 방식의 작업큐를 만들 때 유용하게 사용 할 수 있다.

Atomic Compare-and-Swap

분산 시스템에서 어떤 Key의 값을 제대로 업데이트하는게 쉬운일이 아니다. 특정 Key에 두 개 이상의 클라이언트가 값을 읽어서 업데이트를 할 수 있기 때문이다. etcd는 분산 잠금 기능을 이용한 compareAndSwap(CAS)를 제공한다. 이 명령을 이용하면 key의 값이 현재 조건과 동일 할 때만 key 값을 변경 할 수 있다.

CAS는 디렉토리에 대해서는 작동하지 않는다.

3가지 비교 조건을 사용 할 수 있다. foo key가 존재하지 않는 상태에서 테스트를 진행했다.
$ curl http://etcd-01:2379/v2/keys/foo?prevExist=false -XPUT -d value=one
{
   "action" : "set",
   "node" : {
      "modifiedIndex" : 30,
      "value" : "one",
      "key" : "/foo",
      "createdIndex" : 30
   }
}
아직 key가 존재하지 않기 때문에 성공한다. 동일한 옵션으로 값 변경을 시도했다.
$ curl http://etcd-01:2379/v2/keys/foo?prevExist=false -XPUT -d value=two 
{
   "index" : 35,
   "errorCode" : 105,
   "message" : "Key already exists",
   "cause" : "/foo"
}
key 값이 "one"이면 업데이트 하도록 명령을 내렸다. 이전 값이 "one" 이므로 성공할 것이다.
$ curl http://etcd-01:2379/v2/keys/foo?prevValue=one -XPUT -d value=two 
{
   "node" : {
      "key" : "/foo",
      "modifiedIndex" : 36,
      "value" : "two",
      "createdIndex" : 35
   },
   "action" : "compareAndSwap",
   "prevNode" : {
      "value" : "one",
      "key" : "/foo",
      "modifiedIndex" : 35,
      "createdIndex" : 35
   }
}
동일한 명령을 다시 내려보자.
$ curl http://etcd-01:2379/v2/keys/foo?prevValue=one -XPUT -d value=two 
{
   "index" : 36,
   "cause" : "[one != two]",
   "errorCode" : 101,
   "message" : "Compare failed"
}
이전 key 값이 "one"이 아니므로 에러코드를 반환한다. "two"이면 "three"로 업데이트하도록 명령을 내렸다. 이번엔 성공 할 것이다.
$ curl http://etcd-01:2379/v2/keys/foo?prevValue=two -XPUT -d value=three
{
   "prevNode" : {
      "createdIndex" : 35,
      "value" : "two",
      "modifiedIndex" : 36,
      "key" : "/foo"
   },
   "action" : "compareAndSwap",
   "node" : {
      "createdIndex" : 35,
      "value" : "three",
      "modifiedIndex" : 37,
      "key" : "/foo"
   }
}

Atomic Compare-and-Delete

CAS의 삭제버전이다. key 조건을 검사해서 삭제할 수 있다. 두 가지 조건을 제공한다. foo=one인 새로운 key를 만들었다.
$ curl http://etcd-01:2379/v2/keys/foo -XPUT -d value=one
foo의 값이 two 인 경우 삭제하도록 명령을 내렸다.
$ curl http://etcd-01:2379/v2/keys/foo?prevValue=two -XDELETE
{
   "errorCode" : 101,
   "message" : "Compare failed",
   "index" : 39,
   "cause" : "[two != one]"
}
실패했다. one인 경우 삭제하도록 하면 성공할 것이다.
$ curl http://etcd-01:2379/v2/keys/foo?prevValue=one -XDELETE
{
   "action" : "compareAndDelete",
   "node" : {
      "createdIndex" : 39,
      "key" : "/foo",
      "modifiedIndex" : 40
   },
   "prevNode" : {
      "value" : "one",
      "modifiedIndex" : 39,
      "key" : "/foo",
      "createdIndex" : 39
   }
}

디렉토리 만들기
etcd는 필요하면 자동으로 디렉토리를 만든다. 유저 정보를 저장하기 위해서 /user 디렉토리를 만들고 여기에 /yundream 을 저장해봤다.
$ curl http://etcd-01:2379/v2/keys/user/yundream -XPUT -d value="joinc" 
{
   "node" : {
      "key" : "/user/yundream",
      "modifiedIndex" : 45,
      "value" : "joinc",
      "createdIndex" : 45
   },
   "action" : "set"
}
/user 정보를 가져와 보자.
$ curl http://etcd-01:2379/v2/keys/user
{
   "action" : "get",
   "node" : {
      "modifiedIndex" : 45,
      "createdIndex" : 45,
      "dir" : true,
      "nodes" : [
         {
            "value" : "joinc",
            "key" : "/user/yundream",
            "modifiedIndex" : 45,
            "createdIndex" : 45
         }
      ],
      "key" : "/user"
   }
}
"dir": true 를 확인 할 수 있다. 주키퍼는 디렉토리 타입의 노드에도 값을 입력 할 수 있는데, etcd는 어떨지 테스트를 해봤다.
$ curl http://etcd-01:2379/v2/keys/user -XPUT -d value="user information" 
{
   "errorCode" : 102,
   "index" : 45,
   "message" : "Not a file",
   "cause" : "/user"
}
파일 타입이 아니라고 실패한다. etcd는 디렉토리 타입 key에는 값을 저장 할 수 없다.

디렉토리 삭제
앞서만든 /user 디렉토리를 지워보자. 디렉토리 타입의 key는 dir=true 파라메터를 설정해야 한다.
$ curl http://etcd-01:2379/v2/keys/user?dir=true -XDELETE
{
   "message" : "Directory not empty",
   "cause" : "/user",
   "index" : 45,
   "errorCode" : 108
}
디렉토리가 비지 않아서 실패했다. recurisve=true파라메터를 이용해서 디렉토리 밑에 있는 파일 타입 key들을 모두 지우고 디렉토리를 지우도록 할 수 있다. 리눅스의 rm -rf dir과 비슷한 일을 한다.
$ curl http://etcd-01:2379/v2/keys/user/yundream -XPUT -d value="joinc" 
{
   "node" : {
      "key" : "/user/yundream",
      "modifiedIndex" : 45,
      "value" : "joinc",
      "createdIndex" : 45
   },
   "action" : "set"
}

통계 및 모니터링

리더 통계

/stats/leader로 etcd 클러스터의 리더와 팔로워 통계 정보를 확인할 수 있다.
$ curl http://etcd-01:2379/v2/stats/leader
{
   "followers" : {
      "b3a64181bea306fb" : {
         "latency" : {
            "average" : 0.0272181886792453,
            "standardDeviation" : 0.179779028718072,
            "maximum" : 1.864959,
            "current" : 0.002855,
            "minimum" : 0.001844
         },
         "counts" : {
            "fail" : 0,
            "success" : 106
         }
      },
      "4a84c8889f5d7a35" : {
         "latency" : {
            "minimum" : 0.001542,
            "current" : 0.00274,
            "standardDeviation" : 0.00603894694624668,
            "maximum" : 0.05214,
            "average" : 0.00754414423076923
         },
         "counts" : {
            "success" : 104,
            "fail" : 1
         }
      }
   },
   "leader" : "11796896800e7f4d"
}

자신의 통계 정보 화인

/stats/self API로 자신의 통계정보를 확인 할 수 있다. etcd-01의 정보를 확인해봤다.
$ curl http://etcd-01:2379/v2/stats/self
{
   "sendAppendRequestCnt" : 211,
   "recvAppendRequestCnt" : 0,
   "state" : "StateLeader",
   "startTime" : "2018-08-04T02:49:27.947923609Z",
   "name" : "etcd-01",
   "id" : "11796896800e7f4d",
   "leaderInfo" : {
      "startTime" : "2018-08-04T02:49:29.050278739Z",
      "leader" : "11796896800e7f4d",
      "uptime" : "4h49m25.913009175s"
   }
}
state가 "StateLeader"이다. 즉 리더임을 알 수 있다. 아래는 etcd-02 내용이다.
$ curl http://etcd-02:2379/v2/stats/self
{
   "sendAppendRequestCnt" : 0,
   "startTime" : "2018-08-03T10:56:48.553970613Z",
   "id" : "b3a64181bea306fb",
   "leaderInfo" : {
      "uptime" : "4h50m57.251822063s",
      "startTime" : "2018-08-03T10:56:48.779424664Z",
      "leader" : "11796896800e7f4d"
   },
   "recvAppendRequestCnt" : 106,
   "name" : "etcd-02",
   "state" : "StateFollower"
}

앞으로 할 것