개요

쿠버네티스에서 사용하는 etcd에 대해서 간단하게 알아보고, Golang을 통해서 etcd를 사용하는 방법에 대해서 알아보려고 합니다.

etcd란 무엇인가?

etcd는 분산 시스템에서 데이터를 일관되고 신뢰성 있게 저장하기 위해 설계된 고가용성 분산 키-값 저장소입니다. Raft 합의 알고리즘을 통해 데이터의 일관성을 보장하고 Kubernetes와 같은 시스템의 핵심 구성 요소로 활용됩니다. 주로 쿠버네티스 상에서의 구성(Configuration) 관리와 서비스 발견(Service Discovery)에 사용됩니다.

img.png

쿠버네티스에서 Service B1, B2가 등록이 되면 etcd에 해당 정보를 저장하고 Service A는 etcd를 Watch 하면서 Service B1, B2의 정보를 받아올 수 있습니다.

Raft 합의 알고리즘

Raft는 분산 시스템에서 노드 간의 합의를 도와주는 알고리즘으로, etcd는 이 Raft 알고리즘을 통해 데이터의 일관성을 보장합니다. Raft는 Leader, Follower, Candidate 세 가지 역할로 구성되어 있으며, Leader가 클라이언트의 요청을 받아서 데이터를 저장하고, Follower와 Candidate는 Leader의 상태를 확인하고 Leader가 장애가 발생하면 새로운 Leader를 선출합니다.

특정 데이터가 업데이트 되면 Leader는 Follower와 Candidate에게 해당 데이터를 전파하고, Follower와 Candidate는 Leader의 데이터를 확인하여 일관성을 유지합니다. 만약 하나의 노드가 장애가 발생하면, 다른 노드가 Leader로 선출되어 데이터의 일관성을 유지합니다.

etcd 설치

etcd 설치는 docker-compose를 이용해서 작성하겠습니다.

 1version: '3.7'
 2
 3services:
 4  etcd-node1:
 5    image: quay.io/coreos/etcd:v3.4.13
 6    ports:
 7      - "2379:2379"
 8    container_name: etcd-node1
 9    platform: linux/amd64
10    environment:
11      - ETCD_NAME=node1
12      - ETCD_INITIAL_ADVERTISE_PEER_URLS=http://etcd-node1:2380
13      - ETCD_LISTEN_PEER_URLS=http://0.0.0.0:2380
14      - ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379
15      - ETCD_ADVERTISE_CLIENT_URLS=http://etcd-node1:2379
16      - ETCD_INITIAL_CLUSTER=node1=http://etcd-node1:2380,node2=http://etcd-node2:2380,node3=http://etcd-node3:2380
17      - ETCD_INITIAL_CLUSTER_TOKEN=etcd-cluster-1
18      - ETCD_INITIAL_CLUSTER_STATE=new
19
20  etcd-node2:
21    image: quay.io/coreos/etcd:v3.4.13
22    ports:
23      - "2380:2379"
24    container_name: etcd-node2
25    platform: linux/amd64
26    environment:
27      - ETCD_NAME=node2
28      - ETCD_INITIAL_ADVERTISE_PEER_URLS=http://etcd-node2:2380
29      - ETCD_LISTEN_PEER_URLS=http://0.0.0.0:2380
30      - ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379
31      - ETCD_ADVERTISE_CLIENT_URLS=http://etcd-node2:2379
32      - ETCD_INITIAL_CLUSTER=node1=http://etcd-node1:2380,node2=http://etcd-node2:2380,node3=http://etcd-node3:2380
33      - ETCD_INITIAL_CLUSTER_TOKEN=etcd-cluster-1
34      - ETCD_INITIAL_CLUSTER_STATE=new
35
36  etcd-node3:
37    image: quay.io/coreos/etcd:v3.4.13
38    ports:
39      - "2381:2379"
40    container_name: etcd-node3
41    platform: linux/amd64
42    environment:
43      - ETCD_NAME=node3
44      - ETCD_INITIAL_ADVERTISE_PEER_URLS=http://etcd-node3:2380
45      - ETCD_LISTEN_PEER_URLS=http://0.0.0.0:2380
46      - ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379
47      - ETCD_ADVERTISE_CLIENT_URLS=http://etcd-node3:2379
48      - ETCD_INITIAL_CLUSTER=node1=http://etcd-node1:2380,node2=http://etcd-node2:2380,node3=http://etcd-node3:2380
49      - ETCD_INITIAL_CLUSTER_TOKEN=etcd-cluster-1
50      - ETCD_INITIAL_CLUSTER_STATE=new

Golang을 통해서 etcd 사용하기

Golang을 통해서 etcd를 사용하기 위해서는 go.etcd.io/etcd/clientv3 패키지를 사용하면 됩니다. 이 패키지를 통해서 etcd 클러스터에 연결하고, 데이터를 저장하거나 조회할 수 있습니다.

데이터 저장 및 조회

 1package main
 2
 3import (
 4	"context"
 5	"fmt"
 6	"log"
 7	
 8	clientv3 "go.etcd.io/etcd/client/v3"
 9)
10
11func main() {
12    cli, err := createClient()
13    if err != nil {
14        log.Fatal(err)
15    }
16    defer cli.Close()
17
18    err = putKey(cli, "key", "value")
19    if err != nil {
20        log.Fatal(err)
21    }
22}
23
24func createClient() (*clientv3.Client, error) {
25	cli, err := clientv3.New(clientv3.Config{
26		Endpoints:   []string{"http://localhost:2379", "http://localhost:2380", "http://localhost:2381"},
27		DialTimeout: 5 * time.Second,
28	})
29	if err != nil {
30		return nil, err
31	}
32	return cli, nil
33}
34
35func putKey(cli *clientv3.Client, key, value string) error {
36    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
37    defer cancel()
38    
39	_, err := cli.Put(ctx, key, value)
40    return err
41}
  • etcd cli를 통해서 데이터를 조회하면 다음과 같이 조회할 수 있습니다.
1docker exec -it etcd-node1 etcdctl get key
  • 조회결과
key
value

위에 결과는 etcd-node1가 아니라 etcd-node2, etcd-node3에서 조회해도 동일한 결과가 나옵니다.

Watch를 통해서 데이터의 모니터링

etcd에서 데이터가 변경되면 Watch를 통해서 변경된 데이터를 모니터링할 수 있습니다. 아래 예제는 고루틴을 통해서 Watch를 실행하고 데이터가 변경되면 Watch 이벤트를 출력합니다.

이벤트 순서

  1. PUT key value
  2. PUT key new_value
  3. DELETE key
 1package main
 2
 3import (
 4	"context"
 5	"fmt"
 6	"log"
 7	"time"
 8
 9	clientv3 "go.etcd.io/etcd/client/v3"
10)
11
12func main() {
13	cli, err := createClient()
14	if err != nil {
15		log.Fatal(err)
16	}
17	defer cli.Close()
18
19	// Watch
20	go watchKey(cli, "key")
21
22	// Simulate Create, Update, Delete operations
23	time.Sleep(1 * time.Second)
24	err = putKey(cli, "key", "value")
25	if err != nil {
26		log.Fatal(err)
27	}
28	time.Sleep(1 * time.Second)
29
30	err = putKey(cli, "key", "new_value")
31	if err != nil {
32		log.Fatal(err)
33	}
34	time.Sleep(1 * time.Second)
35
36	err = deleteKey(cli, "key")
37	if err != nil {
38		log.Fatal(err)
39	}
40
41	// Wait to observe the watch output
42	time.Sleep(3 * time.Second)
43}
44
45func createClient() (*clientv3.Client, error) {
46	cli, err := clientv3.New(clientv3.Config{
47		Endpoints:   []string{"http://localhost:2379", "http://localhost:2380", "http://localhost:2381"},
48		DialTimeout: 5 * time.Second,
49	})
50	if err != nil {
51		return nil, err
52	}
53	return cli, nil
54}
55
56func putKey(cli *clientv3.Client, key, value string) error {
57	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
58	defer cancel()
59	_, err := cli.Put(ctx, key, value)
60	return err
61}
62
63func deleteKey(cli *clientv3.Client, key string) error {
64	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
65	defer cancel()
66	_, err := cli.Delete(ctx, key)
67	return err
68}
69
70func watchKey(cli *clientv3.Client, key string) {
71	rch := cli.Watch(context.Background(), key)
72	for wresp := range rch {
73		for _, ev := range wresp.Events {
74			fmt.Printf("Watch: %s %q : %q\n", ev.Type, ev.Kv.Key, ev.Kv.Value)
75		}
76	}
77}
  • 출력 결과
Watch: PUT "key" : "value"
Watch: PUT "key" : "new_value"
Watch: DELETE "key" : ""

정리

etcd는 쿠버네티스와 같은 분산 시스템에서 데이터를 일관되고 신뢰성 있게 저장하기 위해 사용되는 고가용성 분산 키-값 저장소입니다. Raft 합의 알고리즘을 통해 데이터의 일관성을 보장합니다. 해당 포스팅에서는 Golang을 통해서 etcd의 데이터를 저장 및 조회하고, Watch를 통해서 데이터의 변경을 모니터링하는 예제를 작성해보았습니다.