- Authors

- Name
- Youngju Kim
- @fjvbn20031
etcd Watch와 Lease 메커니즘 분석
etcd의 Watch와 Lease는 분산 시스템에서 핵심적인 기능을 제공합니다. Watch는 키 변경 사항의 실시간 감시를, Lease는 키의 자동 만료를 가능하게 합니다. 이 두 메커니즘의 내부 동작을 상세히 분석합니다.
1. Watch 내부 구조
1.1 Watchable Store 아키텍처
Watchable Store는 MVCC Store를 래핑하여 Watch 기능을 추가합니다:
// watchableStore 구조 (간략화)
type watchableStore struct {
*store // MVCC store
synced watcherGroup // 현재 revision에 동기화된 watcher
unsynced watcherGroup // 아직 따라잡지 못한 watcher
victims []watcherBatch // 전송 실패한 이벤트 대기열
}
1.2 Watcher 생명주기
- 클라이언트가 Watch 요청 전송
- Watcher 생성 및 시작 revision 결정
- 시작 revision이 현재 revision보다 오래되면 unsynced 그룹에 배치
- 히스토리에서 이벤트를 재생하여 따라잡기
- 현재 revision까지 따라잡으면 synced 그룹으로 이동
- 이후 새 이벤트를 실시간으로 수신
1.3 synced vs unsynced Watcher
synced watcher:
- 현재 store revision까지 모든 이벤트를 수신 완료
- 새 쓰기 작업 발생 시 즉시 이벤트 수신
- 성능 오버헤드가 적음
unsynced watcher:
- 아직 과거 이벤트를 재생 중
- 별도의 goroutine이 히스토리를 순회하며 이벤트 생성
- 따라잡기 완료 후 synced로 전환
2. 이벤트 생성과 전달
2.1 이벤트 생성 과정
MVCC Store에 쓰기가 발생하면:
// 트랜잭션 커밋 시 이벤트 생성 (간략화)
func (tw *storeTxnWrite) End() {
// 1. 변경사항을 BoltDB에 기록
tw.s.b.BatchTx().Commit()
// 2. 이벤트 생성
evs := make([]mvccpb.Event, len(tw.changes))
for i, change := range tw.changes {
evs[i] = mvccpb.Event{
Type: change.Type, // PUT or DELETE
Kv: change.Kv,
}
}
// 3. watchable store에 이벤트 전달
tw.s.notify(tw.rev, evs)
}
2.2 이벤트 필터링과 매칭
각 Watcher는 감시 대상을 정의합니다:
- 단일 키 Watch: 정확히 하나의 키만 감시
- 범위 Watch: 키 범위(prefix 포함)를 감시
- 필터: PUT 또는 DELETE 이벤트만 필터링 가능
이벤트 발생 시 watchable store는 영향받는 모든 watcher를 찾아 이벤트를 전달합니다. 효율적인 매칭을 위해 interval tree와 같은 자료구조를 사용합니다.
2.3 이벤트 순서 보장
etcd Watch는 이벤트의 순서를 보장합니다:
- 같은 키에 대한 이벤트는 revision 순서대로 전달
- Watch ID별로 이벤트가 순서대로 전달
- 클라이언트는 수신한 이벤트의 revision을 확인하여 누락을 감지
3. gRPC Watch 스트리밍
3.1 Watch 스트림 구조
클라이언트와 서버 간 Watch는 양방향 gRPC 스트리밍을 사용합니다:
service Watch {
rpc Watch(stream WatchRequest) returns (stream WatchResponse);
}
하나의 gRPC 연결에서 여러 Watch를 다중화(multiplexing)할 수 있습니다. 각 Watch는 고유한 Watch ID를 가집니다.
3.2 Watch 요청 타입
- WatchCreateRequest: 새 Watch 생성 (키, 범위, 시작 revision 등 지정)
- WatchCancelRequest: 기존 Watch 취소
- WatchProgressRequest: 현재 진행 상태 요청
3.3 Compacted Revision 처리
Watch의 시작 revision이 이미 compaction된 경우:
- 서버가 ErrCompacted 에러와 함께 compactRevision을 반환
- 클라이언트는 현재 데이터를 다시 로드 (Range 요청)
- 로드 완료 후 반환된 revision부터 새 Watch 시작
// 클라이언트 측 compaction 처리 예시
wch := client.Watch(ctx, "key", clientv3.WithRev(oldRev))
for resp := range wch {
if resp.CompactRevision > 0 {
// compaction 발생 - 데이터 재로드 필요
reloadData()
// 새 Watch 시작
wch = client.Watch(ctx, "key", clientv3.WithRev(resp.CompactRevision))
}
}
3.4 Watch 진행(Progress) 알림
클라이언트가 현재 Watch 진행 상태를 알고 싶을 때:
- WatchProgressRequest를 전송
- 서버가 현재 revision을 포함한 빈 WatchResponse를 반환
- 일정 간격으로 자동 progress 알림도 설정 가능
4. Lease 메커니즘
4.1 Lease 개요
Lease는 키에 TTL(Time-To-Live)을 부여하여 자동 만료를 가능하게 합니다:
- Lease를 생성하면 고유 ID와 TTL이 할당
- 키를 Lease에 연결하면 Lease 만료 시 키도 삭제
- 하나의 Lease에 여러 키를 연결 가능
4.2 Lease Grant
# Lease 생성 (TTL 300초)
etcdctl lease grant 300
# lease 694d71ddafb1e01a granted with TTL(300s)
# 키에 Lease 연결
etcdctl put --lease=694d71ddafb1e01a mykey myvalue
4.3 Lease 내부 구조
// Lessor 구조 (간략화)
type lessor struct {
leaseMap map[LeaseID]*Lease // 활성 lease 목록
leaseExpiredNotifier *LeaseExpiredNotifier
itemMap map[LeaseItem]LeaseID // 키 -> lease 매핑
b backend.Backend // 영속 저장소
}
type Lease struct {
ID LeaseID
ttl int64 // 원래 TTL (초)
remainingTTL int64 // 남은 TTL
expiryTime time.Time // 만료 시각
itemSet map[LeaseItem]struct{} // 연결된 키 목록
}
4.4 TTL 구현과 만료 처리
Lease의 만료는 다음과 같이 처리됩니다:
- Lessor가 주기적으로 만료된 Lease를 확인 (500ms 간격)
- 만료된 Lease 발견 시 Revoke 요청을 Raft에 제안
- Raft 합의 후 Lease와 연결된 모든 키를 삭제
- Lease 자체도 제거
4.5 Lease KeepAlive
클라이언트가 Lease를 갱신하려면 KeepAlive를 전송합니다:
// KeepAlive 사용 예시
resp, _ := client.Grant(ctx, 30) // 30초 TTL
_, _ = client.Put(ctx, "key", "value", clientv3.WithLease(resp.ID))
// 주기적으로 KeepAlive 전송
ch, _ := client.KeepAlive(ctx, resp.ID)
for ka := range ch {
// ka.TTL은 갱신된 TTL
fmt.Println("TTL renewed:", ka.TTL)
}
KeepAlive의 특성:
- 클라이언트 라이브러리가 자동으로 TTL/3 간격으로 전송
- 서버에서 TTL을 원래 값으로 리셋
- gRPC 스트리밍으로 효율적인 갱신
4.6 Lease Revoke
Lease를 명시적으로 취소:
etcdctl lease revoke 694d71ddafb1e01a
Revoke 시 해당 Lease에 연결된 모든 키가 즉시 삭제됩니다. Raft 합의를 거치므로 모든 멤버에서 일관적으로 처리됩니다.
5. Lease와 Kubernetes
5.1 리더 선출(Leader Election)
Kubernetes의 리더 선출은 etcd Lease를 활용합니다:
- 후보가 특정 키에 자신의 ID를 Lease와 함께 기록 (IfNotExists 조건)
- 성공하면 리더가 됨
- 리더는 KeepAlive로 Lease를 유지
- 리더 장애 시 KeepAlive가 중단되어 Lease 만료
- Lease 만료 후 다른 후보가 새 리더로 선출
5.2 노드 하트비트
Kubernetes에서 kubelet은 etcd의 Lease를 사용하여 노드 상태를 보고합니다:
- 각 노드에 대한 Lease 오브젝트가 kube-node-lease 네임스페이스에 존재
- kubelet이 주기적으로 Lease를 갱신하여 노드 활성 상태를 표시
- Lease 갱신 실패 시 노드가 NotReady 상태로 전환
5.3 API Server의 Lease 활용
kube-apiserver도 etcd Lease를 활용합니다:
- API server 인스턴스 등록
- 서비스 디스커버리
- 분산 락 구현
6. Watch와 Lease 결합 패턴
6.1 분산 구성 관리
Watch와 Lease를 결합한 일반적인 패턴:
- 설정값을 etcd에 저장
- 클라이언트가 설정 키를 Watch
- 설정 변경 시 이벤트를 수신하여 즉시 반영
- Lease로 임시 설정의 자동 정리
6.2 서비스 등록/발견
- 서비스 인스턴스가 Lease와 함께 자신을 등록
- 다른 서비스가 등록 키를 Watch하여 변화 감지
- 인스턴스 장애 시 Lease 만료로 자동 등록 해제
- Watch를 통해 다른 서비스가 즉시 인지
7. 정리
etcd의 Watch와 Lease는 분산 시스템의 핵심 프리미티브를 제공합니다. Watch는 실시간 변경 감지를, Lease는 TTL 기반 자동 만료를 가능하게 하며, 이 둘의 조합으로 리더 선출, 서비스 디스커버리 등 다양한 분산 패턴을 구현할 수 있습니다. 다음 글에서는 etcd와 Kubernetes API Server의 통합을 분석합니다.