근묵자흑
왜 etcd는 대규모 Kubernetes 클러스터에서 한계에 부딪히는가 본문
원문: Why etcd breaks at scale in Kubernetes — Daniele Polencic (LearnKube, February 2026)
번역 및 보강: 번역 + 최신 기술 동향 추가
목차
- etcd가 뭔지 모르고도 쓸 수 있다 — 하지만 대규모에서는?
- 컨트롤 플레인: API 서버만 etcd와 직접 대화한다
- 왜 etcd인가? SQLite나 PostgreSQL을 쓰면 안 되나?
- Raft 합의 알고리즘이란 무엇인가
- 합의의 비용: 스케일의 벽
- 데이터베이스는 단일 파일 하나에 저장된다
- MVCC: 모든 변경은 새로운 리비전을 만든다
- Watch 이벤트는 리더에게서 나온다
- 대규모에서 실제로 무엇이 깨지는가
- 첫 번째 탈출구: Kine과 k3s
- 하이퍼스케일러들이 만든 것: AWS와 Google의 해법
- 업스트림 Kubernetes는 왜 여전히 etcd와 결합되어 있나
- Kubernetes는 서서히 결합을 느슨하게 하고 있다
- 최신 기술 동향 (2024~2026)
- 전체 아키텍처 다이어그램
- 운영자를 위한 실전 체크리스트
- 결론: 그래서 Kubernetes는 여전히 etcd가 필요한가?
1. etcd가 뭔지 모르고도 쓸 수 있다 — 하지만 대규모에서는?
Kubernetes를 수년간 운영하면서도 etcd를 한 번도 신경 쓰지 않아도 되는 경우가 많습니다.
하지만 클러스터가 충분히 커지면 etcd는 순식간에 가장 중요한 걱정거리가 됩니다.
etcd는 왜 대규모에서 문제를 일으키는가?
- 무엇이, 왜 깨지는가?
- 세계 최대 규모의 Kubernetes 클러스터를 운영하는 팀들은 어떻게 이 문제를 해결했는가?
2. 컨트롤 플레인: API 서버만 etcd와 직접 대화한다
Kubernetes에서 etcd와 직접 대화하는 컴포넌트는 API 서버(kube-apiserver) 하나뿐입니다.
스케줄러, 컨트롤러 매니저, kubelet, kubectl, 그리고 여러분의 오퍼레이터들은 모두 API 서버를 통해 통신합니다.
오직 API 서버만이 etcd에 읽기/쓰기를 수행합니다.
💡 설명
Kubernetes 클러스터를 하나의 회사라고 생각해보세요.etcd = 회사 공식 데이터베이스 (계약서, 규정, 직원 명부)
- API 서버 = 데이터베이스 담당 비서 (다른 부서는 직접 DB에 접근 못하고 비서를 통해야 함)
- 스케줄러/컨트롤러 = 각 부서 (비서에게 요청해서 DB 정보를 얻음)
etcd는 API 서버의 전용 백엔드입니다. 다른 모든 것은 API를 통해서만 Kubernetes를 바라봅니다.
3. 왜 etcd인가? SQLite나 PostgreSQL을 쓰면 안 되나?
단일 API 서버, 단일 머신 환경이라면 사실 etcd가 없어도 됩니다.
SQLite, PostgreSQL, 심지어 디스크의 평문 파일에도 클러스터 상태를 저장할 수 있습니다.
그렇다면 왜 etcd를 쓰는가?
프로덕션 클러스터는 고가용성(HA) 이 필요하고, 이는 여러 API 서버를 동시에 실행하는 것을 의미합니다.
- API 서버 하나가 다운되거나 점검에 들어가더라도 → 다른 API 서버가 인수하여 클러스터가 계속 동작
- 하지만: API 서버가 3개라면, 셋 모두 동일한 데이터를 읽고 써야 합니다
각 API 서버에게 자체 DB를 주면 → 클러스터 상태에 대해 의견이 갈림 → 대혼란
💡 PersistentVolume 예시
사용 가능한 PersistentVolume(PV)이 하나 있다고 가정하겠습니다.
서로 다른 API 서버에 연결된 두 컨트롤러가 동시에 이 PV를 사용 가능하다고 읽고,
각각 다른 PVC에 바인딩하려고 합니다.
일관성이 없다면 둘 다 성공하고 → 두 파드가 같은 디스크를 소유했다고 착각합니다.
이 재앙을 막는 것이 바로 etcd입니다.
etcd는 Raft 합의 알고리즘을 사용해 여러 노드를 동기화 상태로 유지하는 분산 키-값 저장소입니다.
4. Raft 합의 알고리즘이란 무엇인가
💡 설명
Raft를 이해하는 가장 쉬운 비유는 국회 투표입니다.어떤 법안(=쓰기 요청)을 통과시키려면
- 과반수 의원(=etcd 노드)이 찬성(=확인)해야 합니다
- 과반수가 찬성하면 법이 공식 효력을 가집니다(=커밋)
Raft가 실제로 동작하는 방식:
- etcd 클러스터는 단 하나의 리더(leader) 노드를 선출합니다.
- 모든 쓰기 요청은 어느 노드에 들어오든 리더에게 전달됩니다.
- 리더는 쓰기를 로그에 추가하고, 팔로워(follower) 노드들에게 복제합니다.
- 과반수 노드가 영속화했다고 확인하면 → 쓰기가 커밋됩니다.
- 리더가 크래시하면 → 남은 노드들이 선거를 치르고 새 리더를 선출합니다.
과반수 노드가 온라인 상태인 한 클러스터는 계속 동작합니다 (3노드 클러스터는 1노드 장애에서도 생존).
이것이 제공하는 두 가지:
- 강한 일관성: 모든 클라이언트가 항상 동일한 데이터를 봄
- 고가용성: 노드 장애에서도 클러스터가 살아남음
5. 합의의 비용: 스케일의 벽
etcd는 강한 일관성을 제공하지만, 그 일관성을 보장하는 모든 설계 결정이 스케일 한계를 만들어냅니다.
단일 리더가 모든 쓰기를 처리한다
Raft 클러스터에는 항상 정확히 하나의 리더만 있습니다.
- 쓰기 요청은 무조건 리더에게 전달됩니다
- 리더는 로그에 추가 → 팔로워에게 복제 → 과반수 확인 → 커밋
- 쓰기 지연 시간 = 팔로워까지 네트워크 왕복 1회 + 각 노드의 디스크 fsync
- 쓰기 처리량 = 리더 1개 노드가 처리할 수 있는 양에 상한선이 있음
⚠️ 핵심 트레이드오프
etcd 노드를 늘려도 쓰기 용량이 늘어나지 않습니다.
오히려 리더가 더 많은 팔로워에게 복제해야 하므로 더 느려집니다.
Raft 클러스터에서는 쓰기를 수평으로 확장할 수 없습니다.
일반 Kubernetes 클러스터에서는 문제없습니다. API 서버가 쓰는 것은 파드 스펙, 디플로이먼트 정의, ConfigMap — 작고 상대적으로 드뭅니다.
하지만 수만 개의 오브젝트가 끊임없이 변경되는 클러스터에서는 단일 리더가 병목이 됩니다.
6. 데이터베이스는 단일 파일 하나에 저장된다
etcd는 모든 데이터를 bbolt에 저장합니다 — 단일 파일로 백업되는 B+ 트리 키-값 저장소입니다.
주요 제한:
| 항목 | 기본값 | 최대값 |
|------|--------|--------|
| 데이터베이스 크기 기본 쿼터 | 2 GiB | 8 GiB (권장 최대) |
| 단일 요청 최대 값 크기 | — | 1.5 MiB |
| 개별 키-값 페어 최대 크기 | — | 1 MiB |
💡 설명
이 제한 때문에 실제로 발생하는 일:
- 큰 ConfigMap이나 Secret을 만들면 에러가 나는 이유가 여기 있습니다.
(직렬화 후 1 MiB를 초과하면 거부됩니다)- 1 MiB = 약 100만 글자 (충분해 보이지만, 이미지나 바이너리를 Base64 인코딩하면 금방 찹니다)
왜 이 제한이 엄격한가?
Raft는 모든 것을 복제합니다.
팔로워가 뒤처지거나 새 노드가 클러스터에 합류하면 리더는 전체 데이터베이스의 스냅샷을 보내야 합니다.
DB가 8 GiB라면 → 8 GiB 스냅샷을 전송해야 합니다.
DB가 클수록 → 스냅샷이 느려지고 → 복구가 느려지고 → 전송 중 문제 발생 위험이 높아집니다.
DB 크기 제한은 임의적인 것이 아닙니다. Raft의 복제 및 복구 방식에서 직접 유래합니다.
7. MVCC: 모든 변경은 새로운 리비전을 만든다
etcd는 MVCC(다중 버전 동시성 제어)를 사용합니다.
키를 쓸 때마다 etcd는 이전 값을 덮어쓰지 않습니다. 전체 데이터셋의 새 리비전을 생성합니다.
💡 설명
구글 독스의 수정 이력과 같습니다.
문서를 수정할 때마다 새 버전이 생기고 이전 버전은 보존됩니다.
언제든 이전 버전으로 돌아갈 수 있습니다.
etcd도 마찬가지입니다 — 모든 쓰기마다 새 리비전이 쌓입니다.
이것이 Kubernetes에서 하는 역할:
resourceVersion— 모든 오브젝트에 리비전 번호가 있음- 컨트롤러가 변경 사항을 감시하고, 재시작 후 Watch를 재개하고, 업데이트 중 충돌을 감지하는 데 사용
- 롤백 가능: Kubernetes가 이전 리비전을 보고 변경된 것을 알 수 있음
문제: 오래된 리비전은 자동으로 사라지지 않는다
파드가 10,000개이고 각각 분당 한 번씩 업데이트된다면 → 분당 10,000개의 새 리비전이 쌓입니다.
시간 0: revision 1 - pod-a 생성
시간 1: revision 2 - pod-a 업데이트
시간 2: revision 3 - pod-a 업데이트
...
시간 n: revision n+1 - pod-a 업데이트 ← 이게 계속 쌓임
해결책: 컴팩션(Compaction)과 조각 모음(Defragmentation)
컴팩션(Compaction): 특정 시점 이전의 모든 리비전 버림 → DB 성장 억제
조각 모음(Defragmentation): 컴팩션 후에도 파일 크기는 줄지 않음 → 별도 작업으로 파일을 재빌드해 공간 회수
⚠️ 경고
변이 속도가 충분히 빠르면 컴팩션이 따라잡을 수 없습니다.
DB가 줄어드는 것보다 빠르게 자라고, 결국 백엔드 쿼터에 도달합니다.
쿼터에 도달하면 etcd가 알람 모드에 진입 → 더 이상 쓰기 불가
→ 전체 Kubernetes 컨트롤 플레인이 변경을 받지 않습니다.
→ 새 파드, 스케일링, 디플로이먼트 — 모두 불가.
8. Watch 이벤트는 리더에게서 나온다
Kubernetes 컨트롤러는 API 서버를 폴링하지 않습니다.
컨트롤러는 오래 지속되는 Watch 연결을 열고 오브젝트가 변경될 때 이벤트를 수신합니다.
내부적으로 API 서버는 etcd에 Watch 연결을 유지합니다.
키가 변경되면 etcd는 해당 키 범위에 관심 있는 모든 Watcher에게 이벤트를 스트리밍합니다.
💡 설명
신문 구독과 비슷합니다.
매일 신문사(etcd 리더)에 전화해서 뉴스를 확인하는 대신(폴링),
구독 신청을 하면 새 신문이 나올 때마다 자동으로 배달됩니다(Watch).
Watcher가 수천 명이면 신문사는 매번 수천 부를 인쇄해서 보내야 합니다.
Watch 팬아웃 문제
- Watcher 수천 개 × 파드 상태 업데이트 × 여러 네임스페이스
- 리더는 모든 쓰기마다 어떤 Watcher가 해당 키에 관심 있는지 평가하고 이벤트를 보내야 함
- 대규모에서 리더는 이벤트 배포에 더 많은 시간을 소비해 실제 쓰기 처리보다 이벤트 배포에 치우쳐집니다.
9. 대규모에서 실제로 무엇이 깨지는가
Raft 합의, 단일 파일 저장소, MVCC 리비전, Watch 팬아웃 — 이 설계 결정들은 각각 메타데이터 저장소로서 합리적인 트레이드오프입니다.
하지만 대규모에서 이것들이 복합적으로 작용합니다.
| 증상 | 원인 | 영향 |
|---|---|---|
| 쿼터 알람 | DB가 가득 참 | etcd가 읽기 전용으로 전환, 컨트롤 플레인 동결 |
| 느린 Watch | Watcher 폭증으로 리더 CPU/네트워크 포화 | 쓰기 확인 포함 모든 것이 느려짐 |
| 컴팩션 지연 | 변이 속도 > 컴팩션 속도 | DB가 줄어드는 것보다 빠르게 자라 → 쿼터 알람으로 수렴 |
| 스냅샷 압박 | DB가 크고 팔로워가 뒤처짐 | 리더가 수 GiB 스냅샷 전송 중 정상 운영 용량 감소 |
⚠️ 이 문제들은 버그가 아닙니다 — 시스템 설계의 결과입니다.
대부분의 Kubernetes 클러스터(수백 노드, 보통의 변경률)에서는 이 중 어느 것도 문제가 되지 않습니다.
10,000+ 노드를 운영하는 사람들에게는 이것이 부딪히는 벽입니다.
10. 첫 번째 탈출구: Kine과 k3s
etcd 의존성에 의문을 처음 진지하게 제기한 프로젝트는 Rancher의 경량 Kubernetes 배포판인 k3s입니다.
k3s는 엣지 하드웨어, IoT 기기, 단일 노드 설정에서 동작해야 했는데, 여기서 3노드 etcd 클러스터 운영은 비현실적입니다.
API 서버가 etcd와 통신하도록 만들어졌는데 어떻게 etcd를 제거할 수 있는가?
답은 Kine("Kine is not etcd")입니다.
Kine은 etcd API의 서브셋을 구현하고 요청을 관계형 DB로 변환하는 심(shim)입니다.
[Kubernetes API 서버]
↓ etcd gRPC API
[Kine 심]
↙ ↓ ↓ ↘
SQLite PostgreSQL MySQL NATS
핵심 통찰: Kubernetes API 서버는 etcd 내부가 아닌 etcd gRPC API와 통신합니다.
그 API 앞에 다른 DB를 구현하면 API 서버는 차이를 모릅니다.
단, Kine은 etcd API의 서브셋만 구현합니다:
- Watch 효율성은 백엔드의 폴링 구현에 의존
- 리비전 의미론이 근사치 (네이티브가 아님)
엣지 배포와 소규모 클러스터에서는 완벽하게 합리적인 트레이드오프입니다.
더 큰 통찰은 패턴입니다: Kubernetes 자체를 건드리지 않고 etcd API를 재구현해서 Kubernetes를 etcd로부터 분리할 수 있습니다.
하이퍼스케일 클라우드 프로바이더들도 이 패턴을 사용했습니다.
11. 하이퍼스케일러들이 만든 것: AWS와 Google의 해법
AWS EKS: 100,000 노드
2025년 AWS는 클러스터당 최대 100,000 워커 노드 지원을 발표했습니다.
표준 etcd로는 100,000 노드를 처리할 수 없습니다.
이것은 지금까지 논의한 모든 것의 결과입니다: 단일 리더 쓰기 경로, DB 크기 제한, Watch 팬아웃.
AWS가 기술 심층 분석에서 설명한 재구축 내용:
- Raft 합의를 내부 저널 서비스로 교체 (단일 리더 쓰기 병목 제거)
- bbolt 백엔드를 인메모리 설계로 교체 (단일 파일 크기 제한 제거)
- 키스페이스를 파티셔닝 (서로 다른 키 범위를 다른 노드가 처리할 수 있도록)
하지만 etcd API는 그대로 유지했습니다.
왜? 대안이 더 나쁘기 때문입니다.
Kubernetes API 서버 스토리지 인터페이스는 etcd 모델 위에 구축되어 있습니다. 이 인터페이스를 바꾸려면 API 서버를 포크해야 하고, Kubernetes 포크 유지 관리는 엄청난 지속적 비용이 발생합니다.
Kubernetes를 재작성하는 것보다 etcd를 재작성하는 것이 더 저렴합니다.
Google GKE: 130,000 노드
Google이 65,000노드 GKE 클러스터를 발표했을 때, 오픈소스 etcd를 Spanner 기반 키-값 백엔드로 교체했다고 설명했습니다.
Spanner는 Google의 전 세계적으로 분산된 데이터베이스입니다:
- bbolt의 단일 파일 크기 제한 없음
- 단일 리더 쓰기 병목 없음
- etcd가 한계에 도달하는 처리량과 내구성을 위해 설계됨
이후 130,000 노드까지 확장했습니다.
하지만 Spanner 백엔드를 사용해도 Google은 이 크기의 클러스터에 엄격한 제약을 둡니다:
- 클러스터 오토스케일러 없음
- Headless 서비스당 파드 100개 제한
- 노드당 파드 1개
왜? 스토리지 레이어는 병목 중 하나일 뿐이기 때문입니다.
Spanner로 DB 한계를 해결해도 API 서버 자체에는 여전히 처리 한계가 있습니다:
- 모든 오브젝트 직렬화/역직렬화
- 어드미션 컨트롤러 평가
- 모든 컨트롤러에 Watch 이벤트 배포
- 스케줄러는 파드를 하나씩 처리
- kubelet은 모든 상태 업데이트를 보고
- 네트워크 대역폭 제한
무한 확장 가능한 데이터베이스가 있어도 나머지 시스템에는 각자의 한계가 있습니다.
그것이 65,000 노드 클러스터에 다른 클러스터에는 없는 운영 제약이 있는 이유입니다.
12. 업스트림 Kubernetes는 왜 여전히 etcd와 결합되어 있나
Kine, EKS, GKE 모두 백엔드를 교체했는데 왜 업스트림 Kubernetes는 스토리지를 플러그인 가능하게 만들지 않는가?
API 서버의 스토리지 인터페이스를 보면:
type Interface interface {
Versioner() Versioner
Create(ctx context.Context, key string, obj, out runtime.Object, ttl uint64) error
Delete(ctx context.Context, key string, out runtime.Object,
preconditions *Preconditions, validateDeletion ValidateObjectFunc,
cachedExistingObject runtime.Object, opts DeleteOptions) error
Watch(ctx context.Context, key string, opts ListOptions) (watch.Interface, error)
Get(ctx context.Context, key string, opts GetOptions, objPtr runtime.Object) error
GetList(ctx context.Context, key string, opts ListOptions, listObj runtime.Object) error
GuaranteedUpdate(ctx context.Context, key string, destination runtime.Object,
ignoreNotFound bool, preconditions *Preconditions,
tryUpdate UpdateFunc, cachedExistingObject runtime.Object) error
RequestWatchProgress(ctx context.Context) error
GetCurrentResourceVersion(ctx context.Context) (uint64, error)
CompactRevision() int64
}
모든 연산이 리비전 인식입니다:
Watch: 재개할resourceVersion을 받음GetList: 리비전이 있는 오브젝트를 반환GuaranteedUpdate: 현재 리비전 기반의 compare-and-swap 사용CompactRevision: etcd 컴팩션 상태를 직접 노출
인터페이스는 etcd를 위해 설계되었습니다. 추상화로서 작동하지만 etcd 형태의 추상화입니다.
하지만 코드만의 문제가 아닙니다:
- kubeadm이 etcd를 부트스트랩
- 백업 도구들이 etcd 스냅샷 대상
- 모니터링 대시보드가 etcd 메트릭 추적
- 런북이 etcd 복구 절차 설명
스토리지 백엔드 변경은 단순한 코드 변경이 아닙니다. 전체 에코시스템이 변경을 지원해야 합니다.
클라우드 프로바이더들은 풀스택을 소유하고 있어서 자체 API 서버 바이너리를 컴파일하고 자체 스토리지 구현을 연결하고 자체 적합성 테스트를 실행할 수 있었습니다.
업스트림 Kubernetes를 실행하는 모든 사람에게 etcd는 유일한 지원 백엔드입니다.
13. Kubernetes는 서서히 결합을 느슨하게 하고 있다
커뮤니티는 이 압력을 무시하지 않았습니다.
Kubernetes 1.31: 캐시에서 일관된 읽기 (GA → Beta)
역사적으로 모든 강한 일관성 읽기는 etcd를 쳤습니다.
단순한 kubectl get pods도 etcd 트래픽을 발생시킬 수 있었습니다.
Kubernetes 1.31부터 API 서버는 캐시가 최신인지 확인할 수 있을 때 인메모리 Watch 캐시에서 일관된 읽기를 제공할 수 있습니다.
동일한 일관성 보장, 하지만 읽기가 etcd에 도달하지 않습니다.
5,000 노드 스케일러빌리티 테스트 결과: etcd 부하 크게 감소, 응답 지연 시간 향상.
Kubernetes 1.33: 스트리밍 LIST 응답
대형 LIST 연산은 전체 응답을 메모리에 버퍼링한 후 전송합니다.
50,000개 파드가 있는 클러스터에서는 엄청난 메모리 할당입니다.
Kubernetes 1.33부터 API 서버는 LIST 응답을 점진적으로 스트리밍할 수 있어 메모리 스파이크를 줄이고 동시성을 개선합니다.
테스트 결과: Watch List 기능 활성화 시 kube-apiserver 메모리 사용량이 ~20GB에서 ~2GB로 안정화 (10배 감소).
전반적인 패턴
Kubernetes는 API 서버와 etcd 사이에 더 두꺼운 레이어를 구축하고 있습니다.
각 릴리스마다 API 서버가 생성하는 직접적인 etcd 트래픽이 줄어듭니다.
etcd는 여전히 있지만 더 적은 부하를 처리합니다.
14. 최신 기술 동향 (2024~2026)
etcd v3.5/v3.6 개선사항
- gRPC 기반 Watch 부하 분산: etcd는 자동으로 Watch를 클러스터 노드 간에 비례 분산하도록 개선
- 느린 Watcher 메트릭:
etcd_debugging_mvcc_slow_watcher_total메트릭으로 kube-apiserver가 etcd 이벤트를 소비하지 못하는 상황 감지 가능 - DB 크기 제한 8 GiB: etcd v3.6에서 권장 최대값 유지, 초과 시 쿼터 알람
OpenAI 사태 (2024)
OpenAI(API, ChatGPT & Sora)는 2024년에 텔레메트리 서비스의 예기치 않은 부하가 리소스 집약적인 Kubernetes API 연산을 실행하면서 장애를 경험했습니다. 클러스터 크기에 따라 비용이 스케일링되어 전체 시스템을 다운시켰습니다. 이 사례는 컨트롤 플레인과 etcd 병목이 조직과 워크로드가 확장될수록 얼마나 흔해지는지 보여줍니다.
vCluster와 etcd 샤딩 (2024~2025)
단일 etcd에 대한 대안적 접근: 가상 클러스터(vCluster)를 사용해 etcd와 Kubernetes API 서버의 부하를 분산하는 방법이 주목받고 있습니다. 팀별로 vCluster를 두면 "noisy neighbor" 문제(한 팀의 CRD 스팸이 다른 팀에 영향)를 해결할 수 있습니다.
Kubernetes 1.32: WatchList 기능 (Beta)
Kubernetes 1.32에서 kube-controller-manager에 기본으로 활성화되었습니다. 클라이언트가 List 대신 특수한 Watch 요청으로 전환하면 Watch 캐시에서 서비스되어 메모리 사용이 대폭 줄어듭니다.
이벤트 TTL 최적화
etcd 크기를 관리하는 실용적인 방법 중 하나는 Kubernetes 이벤트 TTL 단축입니다:
- 기본 이벤트 TTL: 1시간
--event-ttl옵션 또는 OpenShift/OKD의eventTTLMinutes설정으로 조정 가능- 이벤트 오브젝트는 종종 etcd에서 다른 모든 타입의 오브젝트 수를 능가하며 상당한 저장 공간을 차지
15. 전체 아키텍처 다이어그램
15.1 Kubernetes 컨트롤 플레인과 etcd의 관계
graph TB
subgraph "사용자/도구"
kubectl["kubectl"]
Operator["Operators / CRDs"]
end
subgraph "컨트롤 플레인"
APIServer["API 서버\n(kube-apiserver)\n유일하게 etcd와 직접 통신"]
Scheduler["스케줄러\n(kube-scheduler)"]
CM["컨트롤러 매니저\n(kube-controller-manager)"]
WatchCache["Watch 캐시\n(인메모리, API 서버 내부)"]
end
subgraph "etcd 클러스터 (3노드)"
Leader["etcd 리더\n(모든 쓰기 처리)"]
Follower1["etcd 팔로워 1"]
Follower2["etcd 팔로워 2"]
end
subgraph "etcd 내부"
bbolt["bbolt (B+ 트리)\n단일 .db 파일"]
WAL["WAL\n(Write-Ahead Log)"]
MVCC["MVCC\n(다중 버전 동시성 제어)"]
end
kubectl -->|"gRPC/HTTPS"| APIServer
Operator -->|"gRPC/HTTPS"| APIServer
Scheduler -->|"gRPC/HTTPS"| APIServer
CM -->|"gRPC/HTTPS"| APIServer
APIServer --> WatchCache
APIServer -->|"etcd gRPC API"| Leader
Leader -->|"Raft 복제"| Follower1
Leader -->|"Raft 복제"| Follower2
Leader --> bbolt
Leader --> WAL
Leader --> MVCC
15.2 Raft 쓰기 흐름
sequenceDiagram
participant Client as API 서버 (클라이언트)
participant Leader as etcd 리더
participant F1 as 팔로워 1
participant F2 as 팔로워 2
Client->>Leader: 쓰기 요청 (예: pod 생성)
Leader->>Leader: 로그에 추가
Leader->>F1: 복제 (AppendEntries)
Leader->>F2: 복제 (AppendEntries)
F1-->>Leader: ACK (영속화 완료)
F2-->>Leader: ACK (영속화 완료)
Note over Leader: 과반수(2/2) 확인 → 커밋
Leader-->>Client: 쓰기 확인 (Success)
Leader->>F1: 커밋 알림
Leader->>F2: 커밋 알림
15.3 MVCC 리비전 누적과 컴팩션
graph LR
subgraph "MVCC 리비전 누적"
R1["Rev 1\npod-a: Running"]
R2["Rev 2\npod-a: Pending"]
R3["Rev 3\npod-b: Running"]
R4["Rev 4\npod-a: Running"]
R1 --> R2 --> R3 --> R4
end
subgraph "컴팩션 후"
C1["Rev 4 (현재)\n보존"]
C2["Rev 1~3\n삭제됨"]
end
subgraph "조각 모음 후"
D1["파일 재빌드\n실제 디스크 공간 회수"]
end
R4 -->|"컴팩션 실행"| C1
C1 -->|"조각 모음 실행"| D1
15.4 Watch 팬아웃 문제
graph TB
subgraph "etcd 리더"
Leader["etcd 리더\n키 변경 발생"]
end
subgraph "API 서버 Watch 캐시"
WC["Watch 캐시\n(1.31+ 인메모리 서비스)"]
end
subgraph "컨트롤러 (Watcher 수천 개)"
C1["Deployment 컨트롤러"]
C2["ReplicaSet 컨트롤러"]
C3["StatefulSet 컨트롤러"]
C4["Custom Operator A"]
C5["Custom Operator B"]
Cn["... (수천 개)"]
end
Leader -->|"Watch 이벤트"| WC
WC --> C1
WC --> C2
WC --> C3
WC --> C4
WC --> C5
WC --> Cn
style Leader fill:#ff6b6b
style WC fill:#4ecdc4
15.5 대규모 Kubernetes의 etcd 대안 아키텍처
graph TB
subgraph "upstream Kubernetes (표준)"
US_API["API 서버"] --> etcd["etcd (표준)\nRaft + bbolt\n최대 8GiB"]
end
subgraph "k3s / 엣지 환경 (Kine)"
K3_API["API 서버"] --> Kine["Kine 심\n(etcd API 구현)"]
Kine --> SQLite["SQLite"]
Kine --> PG["PostgreSQL"]
Kine --> MySQL["MySQL"]
end
subgraph "AWS EKS (100k 노드)"
EKS_API["API 서버"] --> EKS_BE["커스텀 스토리지 엔진\n(etcd API 구현)\n• 내부 저널 서비스\n• 인메모리 설계\n• 키스페이스 파티셔닝"]
end
subgraph "Google GKE (130k 노드)"
GKE_API["API 서버"] --> Spanner["Spanner 기반 KV\n(etcd API 구현)\n• 전 세계 분산\n• 단일 파일 제한 없음\n• 단일 리더 병목 없음"]
end
15.6 Kubernetes etcd 부하 감소 타임라인
timeline
title Kubernetes의 etcd 의존성 감소 노력
2015 : Watch 캐시 최초 도입
: API 서버 내 단일 Watch로 etcd 부하 분산
2024 : K8s 1.31 - 캐시에서 일관된 읽기 (Beta)
: 강한 일관성 읽기도 etcd 없이 서비스 가능
2024 : K8s 1.32 - WatchList 스트리밍 (Beta)
: LIST 요청 메모리 10배 감소
2025 : K8s 1.33 - 스트리밍 LIST 응답 (GA 예정)
: 점진적 스트리밍으로 메모리 스파이크 해결
2025 : AWS EKS - 100k 노드 지원 발표
: etcd 완전 교체 (API만 유지)
2026 : GKE - 130k 노드 (Spanner 기반)
: 추가 운영 제약 필요
16. 운영자를 위한 실전 체크리스트
etcd 모니터링 핵심 메트릭
# DB 크기 모니터링 (80% 이상이면 경보)
etcdctl endpoint status --write-out=table
# 느린 Watcher 확인
# etcd_debugging_mvcc_slow_watcher_total 메트릭 확인
# 엔드포인트 헬스 체크
etcdctl endpoint health -w table
# 리더 선출 지연 확인 (66ms 미만 권장)
컴팩션 및 조각 모음 설정
# 수동 컴팩션 (현재 리비전 기준)
REV=$(etcdctl endpoint status --write-out="json" | \
python3 -c "import sys,json; \
print(json.load(sys.stdin)[0]['Status']['header']['revision'])")
etcdctl compact $REV
# 조각 모음 (컴팩션 후 실행)
etcdctl defrag
# kube-apiserver 자동 컴팩션 주기 확인 (기본 5분)
# --etcd-compaction-interval 플래그
이벤트 TTL 최적화
# 이벤트 TTL 단축으로 etcd 크기 관리
kube-apiserver --event-ttl=30m # 기본 1시간에서 30분으로 단축
etcd 크기 제한 설정
# etcd 백엔드 쿼터 설정 (최대 8GiB)
etcd:
extraArgs:
quota-backend-bytes: "8589934592" # 8GiB
auto-compaction-mode: "periodic"
auto-compaction-retention: "1h"
알람 해제 절차
# etcd가 쿼터 알람으로 진입한 경우
# 1. 컴팩션
etcdctl compact $(etcdctl endpoint status --write-out=json | \
python3 -c "import sys,json; print(json.load(sys.stdin)[0]['Status']['header']['revision'])")
# 2. 조각 모음
etcdctl defrag --cluster
# 3. 알람 해제
etcdctl alarm disarm
17. 결론: 그래서 Kubernetes는 여전히 etcd가 필요한가?
이 모든 것을 고려한 실용적인 답:
| 환경 | etcd 필요 여부 | 비고 |
|---|---|---|
| 업스트림 Kubernetes | 예 | etcd가 유일한 지원 스토리지 백엔드, 가까운 미래에 바뀌지 않을 것 |
| GKE, EKS 등 관리형 서비스 | 아마도 아님 | 프로바이더가 etcd 내부를 더 스케일링 가능한 것으로 교체했을 수 있음 |
| k3s, 엣지 배포 | 선택적 | Kine이 SQLite/PostgreSQL 등으로 교체 옵션 제공 (서브셋 구현임을 인지) |
대부분의 클러스터에서 etcd는 워크로드를 충분히 처리합니다. 그 한계를 넘어서야 한다면 옵션이 있습니다 — 아직 업스트림에 있지 않을 뿐입니다.
💡 핵심 정리
etcd의 제한은 버그가 아니라 Raft 합의라는 설계 선택의 결과입니다.
- "etcd API를 유지하면서 내부를 교체한다"는 패턴이 Kine부터 AWS/GKE까지 일관되게 사용됩니다.
- Kubernetes는 매 릴리스마다 API 서버와 etcd 사이의 레이어를 두껍게 만들어 직접 etcd 접촉을 줄이고 있습니다.
- 스토리지 레이어만 고치면 되는 것이 아닙니다 — API 서버, 스케줄러, kubelet 모두 자체 한계가 있습니다.
참고 자료
'k8s > K8s Annotated' 카테고리의 다른 글
| Ingress-NGINX 지원 종료 완벽 가이드 — Gateway API 마이그레이션, 그 전에 알아야 할 모든 것 (0) | 2026.02.05 |
|---|---|
| Kubernetes를 배우기 전에 Linux를 먼저 배워야 하는 이유 (0) | 2025.12.24 |
| Kubernetes Churn (0) | 2025.12.22 |
| Kubernetes cgroups (2025) (0) | 2025.12.21 |