본문으로 건너뛰기

2.3 etcd

kube-apiserver가 클러스터의 유일한 관문이라면, etcd는 그 관문 뒤에 있는 유일한 기억 장치다. Deployment 정의도, Pod의 현재 상태도, ConfigMap과 Secret도, 클러스터가 아는 모든 것이 etcd에 key-value 쌍으로 저장된다. apiserver는 무상태 프로세스라 전부 재시작해도 잃는 것이 없지만, etcd의 데이터가 사라지면 클러스터는 자신이 무엇을 실행 중이었는지조차 잊는다. Kubernetes에서 백업이라는 말은 사실상 etcd 스냅샷을 뜻한다.

무엇이 어떻게 저장되는가

etcd는 Kubernetes 전용 저장소가 아니라 독립된 오픈소스 프로젝트다. Raft 합의 알고리즘으로 여러 멤버가 같은 데이터를 복제해 유지하는 분산 key-value 저장소이고, Kubernetes는 그 가장 큰 사용자다. apiserver는 모든 오브젝트를 /registry/<리소스>/<네임스페이스>/<이름> 형태의 key 아래에 직렬화해 저장한다.

# control plane 노드에서 실행 (인증서 옵션은 환경에 맞게 지정)
etcdctl get /registry/pods/default/ --prefix --keys-only

2.2에서 본 대로 이 key들을 읽고 쓰는 클라이언트는 apiserver 하나뿐이다. 스케줄러도 kubelet도 etcd의 존재 자체를 모른 채 동작한다. DBA에게 익숙한 그림으로 옮기면, etcd는 단 하나의 애플리케이션(apiserver)만 접속을 허용하는 전용 데이터베이스다.

Raft: 리더 하나와 과반의 동의

etcd 멤버들은 Raft로 하나의 리더를 선출하고, 모든 쓰기를 리더로 모은다. 리더는 변경을 자기 로그에 기록한 뒤 팔로워들에게 복제하고, 전체 멤버의 과반이 수신을 확인해야 그 변경이 확정(commit)된다. 3멤버 클러스터라면 리더 자신을 포함해 2개의 확인이 필요하다.

    flowchart TD
  API["kube-apiserver"] -->|"쓰기 요청"| L["etcd 리더"]
  L -->|"로그 복제"| F1["팔로워 1"]
  L -->|"로그 복제"| F2["팔로워 2"]
  F1 -->|"확인"| L
  F2 -->|"확인"| L
  L -->|"과반 확정 후 응답"| API
  

리더가 죽으면 남은 멤버들이 새 리더를 선출하는데, 이때도 과반의 표가 필요하다. 그래서 etcd 클러스터는 과반을 이루지 못하는 순간 쓰기가 전면 중단된다. 남은 멤버가 데이터를 갖고 있어도 그것이 최신이라고 보증할 방법이 없기 때문이다. 네트워크가 갈라져도 과반을 확보한 쪽 하나만 쓰기를 계속하므로, 두 리더가 서로 다른 데이터를 쓰는 split brain은 구조적으로 차단된다.

멤버 수를 홀수로 두는 관례도 이 과반 계산에서 나온다.

멤버 수과반허용 가능한 멤버 장애
110
220
321
431
532

멤버를 3에서 4로 늘려도 허용 가능한 장애 수는 1로 그대로다. 복제할 곳만 하나 늘어 쓰기는 오히려 느려지고, 네트워크가 2:2로 갈라지면 어느 쪽도 과반이 되지 못한다. 짝수 구성은 비용만 늘고 얻는 것이 없으므로 3이나 5로 구성한다. 대부분의 클러스터에는 3이면 충분하고, 5는 멤버 2개의 동시 상실까지 견뎌야 하는 환경에서 선택한다.

watch: 변경 전파의 원천

2.2의 list-watch는 apiserver가 새로 만든 메커니즘이 아니라 etcd의 watch를 이어받은 것이다. etcd는 모든 변경에 단조 증가하는 revision 번호를 부여하고, 클라이언트는 특정 revision 이후의 key 변경을 스트림으로 구독한다. apiserver는 리소스 종류별로 etcd watch를 하나씩 걸어 두고, 받은 이벤트를 watch cache를 통해 수천 개의 컴포넌트 watch로 분배한다. 오브젝트마다 붙어 있던 resourceVersion 값은 이 etcd revision에서 온다. 스케줄러가 새 Pod를 지체 없이 발견하고 kubelet이 자기 배정을 즉시 아는 반응성은 결국 etcd의 revision 스트림에서 출발한다.

DBA 시선에서 본 etcd

관계형 데이터베이스를 다루던 감각으로 보면 etcd는 개성이 뚜렷한 저장소다.

우선 작은 값을 지향한다. 요청 하나의 기본 상한이 1.5MiB이고, 공식 문서는 값을 작게 유지하라고 권고한다. 대용량 데이터를 저장하는 용도가 아니라 메타데이터를 저장하고 그 변경을 빠르게 전파하는 용도이며, ConfigMap이나 Secret 하나의 데이터가 1MiB를 넘지 못하는 제한도 여기서 나온다.

MVCC로 동작한다는 점도 특징이다. 값을 덮어써도 이전 버전이 바로 지워지지 않고 revision 이력으로 남는다. watch를 과거 revision부터 재생하는 것도 이 이력 덕분이다. 대신 이력이 계속 쌓이므로 오래된 revision을 지우는 compaction이 필요하고, Kubernetes에서는 apiserver가 기본 5분 간격으로 compaction을 요청한다. compaction은 논리적 정리라 디스크 파일 크기는 줄지 않으며, 공간을 파일시스템에 실제로 돌려주려면 defragmentation을 별도로 실행해야 한다. PostgreSQL의 MVCC와 vacuum 관계를 아는 DBA라면 익숙한 구도다.

데이터베이스 크기에는 quota가 있다. 기본 2GB이고 공식 문서가 권장하는 상한은 8GB다. 수백 GB를 다루는 관계형 데이터베이스의 감각으로는 의아할 만큼 작지만, 전체 상태를 빠르게 복제하고 스냅샷하는 저장소라는 목적에 맞춘 크기다.

quota를 초과하면 etcd는 NOSPACE 알람을 발생시키고 유지보수 모드로 전환된다. 이 상태에서는 읽기와 삭제만 허용되어 클러스터 전체가 사실상 읽기 전용이 된다. compaction과 defragmentation으로 공간을 회수한 뒤 알람을 해제해야 쓰기가 재개된다.

마지막으로 디스크 fsync에 극도로 민감하다. Raft 로그의 모든 항목이 fsync를 거쳐 디스크에 내려가야 확정되므로, 디스크 쓰기 지연이 그대로 모든 API 쓰기의 지연이 된다. etcd가 느려지면 kubectl 응답만 늦어지는 것이 아니다. watch 이벤트 전파가 지연되어 컨트롤러의 반응이 늦어지고, kubelet의 heartbeat 갱신이 제때 확정되지 않으면 멀쩡한 노드가 응답 없음으로 오판될 여지도 생긴다. 공식 문서가 하드웨어 요건에서 디스크 성능을 가장 앞에 두고 SSD를 권고하는 이유다.

정리

  • 클러스터의 모든 상태는 etcd에 있고, 읽고 쓰는 경로는 apiserver 하나다. etcd 백업이 곧 클러스터 백업이다.
  • 쓰기는 Raft 리더를 거쳐 과반 확인 후 확정된다. 짝수 멤버는 내결함성을 늘리지 못하므로 3이나 5로 구성한다.
  • 작은 값 지향, MVCC와 compaction, 기본 2GB quota, 그리고 fsync 민감성. etcd의 디스크 지연은 클러스터 전체로 전파된다.