Post

kubernetes etcd 장애 시나리오 분석

kubernetes etcd 장애 시나리오 분석

들어가며

이전 글에서 etcd의 구조와 Raft 알고리즘을 살펴봤다. 요약하면 다음과 같다.

graph LR
    B["⭐ ETCD B<br/>(Leader)"]:::leader
    A["ETCD A<br/>(Follower)"]:::follower
    C["ETCD C<br/>(Follower)"]:::follower
    B -->|heartbeat| A
    B -->|heartbeat| C
    A -->|AppendEntry ACK| B
    C -->|AppendEntry ACK| B
    classDef leader fill:#dcfce7,stroke:#16a34a,color:#14532d,font-weight:bold
    classDef follower fill:#dbeafe,stroke:#3b82f6,color:#1e3a5f
구분데이터설명
영속 (디스크)currentTerm현재 선거 회차
영속 (디스크)votedFor이번 회차에서 내가 찍은 후보
영속 (디스크)log[]클러스터에 적용된 모든 명령 기록 (멤버 설정 포함)
휘발성 (모든 노드)commitIndex과반수가 복제 완료해서 확정된 마지막 로그 번
  
휘발성 (모든 노드)lastApplied실제 상태 머신(DB)에 반영된 마지막 로그 번
  
휘발성 (리더만)nextIndex[]각 follower에게 다음에 보낼 로그 번호
휘발성 (리더만)matchIndex[]각 follower가 여기까지 받았다고 확인된 로그 번
  



ETCD의 장애

etcd는 Kubernetes의 단일 진실 공급원(single source of truth)이다. 모든 클러스터 상태가 etcd에 저장되기 때문에, etcd에 문제가 생기면 전체 클러스터에 영향을 준다.

graph TB
    subgraph normal["✅ 정상 상태"]
        direction LR
        e_ok["etcd 정상"]:::success
        api_ok["kube-apiserver<br/>정상 운영"]:::success
        ctl_ok["controller-manager"]:::success
        sched_ok["scheduler"]:::success
        kctl_ok["kubectl"]:::success
    end
    subgraph broken["⚡ etcd 장애 발생 후"]
        direction LR
        e_fail["🔴 etcd 장애"]:::failure
        api_fail["kube-apiserver<br/>Write 불가"]:::warning
        ctl_fail["❌ controller-manager<br/>상태 조정 불가"]:::impact
        sched_fail["❌ scheduler<br/>신규 Pod 배치 불가"]:::impact
        kctl_fail["❌ kubectl<br/>명령 실패"]:::impact
        kubelet["🟡 kubelet<br/>기존 Pod 자율 유지<br/>(단기간)"]:::note
        e_fail --> api_fail
        api_fail --> ctl_fail
        api_fail --> sched_fail
        api_fail --> kctl_fail
        api_fail --> kubelet
    end
    normal --> broken
    classDef success fill:#dcfce7,stroke:#22c55e,color:#14532d
    classDef failure fill:#fee2e2,stroke:#ef4444,color:#991b1b,font-weight:bold
    classDef warning fill:#fef3c7,stroke:#f59e0b,color:#92400e,font-weight:bold
    classDef impact fill:#fecaca,stroke:#dc2626,color:#7f1d1d
    classDef note fill:#f0fdf4,stroke:#22c55e,color:#14532d

이 글에서는 etcd에서 발생할 수 있는 장애 유형을 시나리오별로 분석하고, etcd의 메모리, 디스크 관리 방법과 백업, 복구 방법까지 함께 정리한다.



장애 시나리오 분석

etcd 장애는 발생 원인에 따라 크게 세 계층으로 나눌 수 있다.

1. Raft 합의 계층 — 노드 상태나 네트워크 문제로 합의 자체가 흔들리는 경우

2. 스토리지 계층 — 디스크 문제로 etcd 데이터 저장 자체가 불가해지는 경우

3. 운영/멤버십 계층 — 클러스터를 구성하거나 노드를 추가/제거하는 과정에서 발생하는 경우



Raft 합의 계층 장애

Raft 합의 계층의 장애는 달성 가능한 Quorom (정족수)가 전체 노드 중 몇 %냐에 따라 다르다.

1. ETCD node의 비정상 장애

1.1. 소수 Follower 장애 (서비스 영향 없음)

가장 흔하지만, 클러스터가 알아서 처리하는 유형이다.

정족수((N/2) + 1) 미만의 Follower가 장애 났을 때다.

graph TB
    B["⭐ ETCD B<br/>(Leader)"]:::leader
    A["ETCD A<br/>(Follower)"]:::follower
    C["ETCD C<br/>(Follower)"]:::follower
    D["❌ ETCD D<br/>(장애)"]:::failure
    E["❌ ETCD E<br/>(장애)"]:::failure
    B -->|"② AppendEntry"| A
    B -->|"③ AppendEntry"| C
    B -. "응답 없음" .- D
    B -. "응답 없음" .- E
    A -->|"① ACK"| B
    C -->|ACK| B
    classDef leader fill:#dcfce7,stroke:#16a34a,color:#14532d,font-weight:bold
    classDef follower fill:#dbeafe,stroke:#3b82f6,color:#1e3a5f
    classDef failure fill:#fee2e2,stroke:#ef4444,color:#991b1b

나머지 멤버들이 쿼럼을 유지하므로 클러스터는 계속 Write 요청을 처리한다. kube-apiserver는 장애 난 멤버와의 연결이 끊기지만, 클라이언트 라이브러리가 자동으로 정상 멤버로 재연결한다.


1.2. 다수 Follower 장애 (Quorom Loss)

정족수((N/2) + 1) 이상의 노드가 동시에 장애 나면 Write가 불가능해진다.

graph TB
    Client["🚫 Client<br/>Write 불가"]:::blocked
    B["ETCD B<br/>(Follower/Candidate 반복)"]:::unstable
    A["ETCD A<br/>(Follower/Candidate 반복)"]:::unstable
    C["❌ ETCD C<br/>(장애)"]:::failure
    D["❌ ETCD D<br/>(장애)"]:::failure
    E["❌ ETCD E<br/>(장애)"]:::failure
    Client -. "❌" .-> B
    B --- A
    B -.- C
    B -.- D
    B -.- E
    classDef blocked fill:#fee2e2,stroke:#ef4444,color:#991b1b,font-weight:bold
    classDef unstable fill:#fef3c7,stroke:#f59e0b,color:#92400e
    classDef failure fill:#fecaca,stroke:#dc2626,color:#7f1d1d

노드 개수에 따른 정속수와 허용 장애 노드 수는 다음과 같다. 위 케이스는 5 nodes에 3대 이상 동시 장애에 해당한다.

노드 개수정족수허용 장애쿼럼 손실 조건
1개10개1대 장애
3개21개2대 이상 동시 장애
5개32개3대 이상 동시 장애



Network 계층 장애

네트워크 스위치 장애, 방화벽 정책 변경, 클라우드 AZ 간 회선 단절 등이 원인으로, ETCD node 간 네트워크 경로가 끊어져 서로 통신할 수 없게 될 수 있다.

1. 단절된 Network Partition 장애

예룰 들어 단절된 두 Network Partition이 있다고 가정한다.

graph LR
    subgraph partA ["🟢 파티션 A (쿼럼 달성)"]
        N1["Node1<br/>(Leader)"]:::leader
        N2[Node2]:::normal
        N3[Node3]:::normal
    end
    subgraph partB ["🔴 파티션 B (고립)"]
        N4[Node4]:::isolated
        N5[Node5]:::isolated
    end
    N1 <--> N2
    N2 <--> N3
    N4 <--> N5
    N3 -. "❌ 단절" .- N4
    classDef leader fill:#dcfce7,stroke:#16a34a,color:#14532d,font-weight:bold
    classDef normal fill:#dbeafe,stroke:#3b82f6,color:#1e3a5f
    classDef isolated fill:#fee2e2,stroke:#ef4444,color:#991b1b

양쪽 파티션이 독립적으로 Write를 처리하여 데이터 일관성이 파괴되는 Split-Brain가 발생할 수 있다. 그러나 etcd는 네트워크 파티션에서 Split-Brain이 발생하지 않는다. ETCD 멤버 추가/제거 자체가 과반수 합의를 통해서만 가능하기 때문이다. 즉, 소수 파티션이 독립적인 클러스터로 분리되는 상황이 구조적으로 차단된다.

파티션의 동작은 Leader가 어느 쪽에 있느냐에 따라 두 가지로 나뉜다.

1.1. 안전한 파티션 장애 - 정족수 유지

1
2
3
# Leader가 다수 쪽에 있는 경우
[Node1 Leader] ←→ [Node2] [Node3]   |   [Node4] [Node5]
        다수 쪽 (쿼럼 달성)                  소수 쪽
  • 다수 쪽: 쿼럼 유지 → 정상 운영 계속
  • 소수 쪽: election timeout → Candidate → 투표 요청 → 쿼럼 미달로 Leader 선출 실패 → Write 불가 대기
1
2
3
# Leader가 소수 쪽에 있는 경우
[Node1 Leader]   |   [Node2] [Node3] [Node4] [Node5]
    소수 쪽              다수 쪽 (쿼럼 달성)
graph TB
    subgraph cb1["① 파티션 발생 — Node1(Leader) 소수 쪽에 고립"]
        direction LR
        cb_n1a["🔴 Node1<br/>(Leader, 소수쪽)"]:::minority
        cb_n2a["Node2"]:::follower
        cb_n3a["Node3"]:::follower
        cb_n4a["Node4"]:::follower
        cb_n5a["Node5"]:::follower
        cb_n1a -. "❌ 단절" .- cb_n2a
        cb_n2a --- cb_n3a
        cb_n3a --- cb_n4a
    end
    subgraph cb2["② Node1: Commit 불가 / 다수 쪽: 새 Leader 선출"]
        direction LR
        cb_n1b["🔴 Node1<br/>Write 요청 거부<br/>(쿼럼 미달)"]:::blocked
        cb_n2b["⭐ Node2<br/>(새 Leader)"]:::leader
        cb_n3b["Node3"]:::follower
        cb_n2b -->|heartbeat| cb_n3b
    end
    subgraph cb3["③ 파티션 복구 — Node1 자동 Follower 전환"]
        direction LR
        cb_n2c["⭐ Node2<br/>(Leader)"]:::leader
        cb_n1c["✅ Node1<br/>(Follower 전환)"]:::success
        cb_n3c["Node3"]:::follower
        cb_n2c -->|heartbeat| cb_n1c
        cb_n2c -->|heartbeat| cb_n3c
    end
    cb1 --> cb2 --> cb3
    classDef minority fill:#fef3c7,stroke:#f59e0b,color:#92400e,font-weight:bold
    classDef follower fill:#dbeafe,stroke:#3b82f6,color:#1e3a5f
    classDef leader fill:#dcfce7,stroke:#16a34a,color:#14532d,font-weight:bold
    classDef blocked fill:#fee2e2,stroke:#ef4444,color:#991b1b,font-weight:bold
    classDef success fill:#dcfce7,stroke:#22c55e,color:#14532d


1.2. 안전하지 않은 파티션 장애 - 정족수 미달

그러나 파티션 내에서 리더를 정하지 못하면 장애가 발생할 수 있다. 예를 들어 2:2로 Partition이 나뉘게 되는 경우, Leader 선출 실패로 장애가 발생할 수 있다.



스토리지 계층 장애

ETCD는 DB SIZE가 기본 쿼터인 2GB에 도달하면 etcd는 mvcc: database space exceeded 에러를 반환하며 read-only 모드로 전환된다.

graph TB
    subgraph db1["① etcd DB 2GB 한계 도달"]
        direction LR
        db_etcd1["⚠️ etcd<br/>DB: ~2GB (한계 도달)"]:::trigger
        db_note1["Compaction 미설정으로<br/>revision 데이터 누적"]:::note
    end
    subgraph db2["② etcd read-only 전환"]
        direction LR
        db_etcd2["🔴 etcd<br/>read-only 모드<br/>'mvcc: database space exceeded'"]:::failure
        db_api2["kube-apiserver<br/>Write 요청 전부 실패"]:::failure
        db_etcd2 --> db_api2
    end
    subgraph db3["③ 클러스터 제어 마비"]
        direction LR
        db_ctl["❌ controller-manager<br/>조정 불가"]:::impact
        db_sch["❌ scheduler<br/>배치 불가"]:::impact
        db_kctl["❌ kubectl<br/>명령 실패"]:::impact
    end
    db1 --> db2 --> db3
    classDef trigger fill:#fef3c7,stroke:#f59e0b,color:#92400e,font-weight:bold
    classDef note fill:#dbeafe,stroke:#3b82f6,color:#1e3a5f
    classDef failure fill:#fee2e2,stroke:#ef4444,color:#991b1b,font-weight:bold
    classDef impact fill:#fecaca,stroke:#dc2626,color:#7f1d1d

1. I/O 지연 장애

1.1. 리더 재선출

WAL fsync가 지연되면 Leader가 heartbeat를 제때 보내지 못하게 된다. 그 결과로 불필요한 리더 재선출이 발생한다.

graph TB
    subgraph io1["① Leader 디스크 I/O 포화 — fdatasync 지연"]
        direction LR
        io_l1["⚠️ Leader<br/>fdatasync 지연 중"]:::io
        io_d1["💾 디스크 I/O 포화"]:::disk
        io_fa1["Follower A<br/>정상"]:::follower
        io_fb1["Follower B<br/>정상"]:::follower
        io_l1 --- io_d1
    end
    subgraph io2["② heartbeat 누락 → Follower election timeout"]
        direction LR
        io_l2["⚠️ Leader<br/>heartbeat 지연"]:::warning
        io_fa2["Follower A<br/>Election Timeout ⏰"]:::timeout
        io_fb2["Follower B<br/>Election Timeout ⏰"]:::timeout
    end
    subgraph io3["③ 리더 재선출 — 구 Leader Follower로 전환"]
        direction LR
        io_fa3["⭐ Follower A<br/>(새 Leader)"]:::leader
        io_l3["구 Leader<br/>(Follower 전환)"]:::demoted
        io_fb3["Follower B"]:::follower
        io_fa3 -->|heartbeat| io_l3
        io_fa3 -->|heartbeat| io_fb3
    end
    subgraph io4["④ 모든 lease revoke"]
        direction LR
        io_result["🔴 모든 lease 일괄 revoke<br/>분산 락 해제<br/>서비스 등록 전부 해제"]:::critical
    end
    io1 --> io2 --> io3 --> io4
    classDef io fill:#fef3c7,stroke:#f59e0b,color:#92400e
    classDef disk fill:#f3e8ff,stroke:#a855f7,color:#581c87
    classDef follower fill:#dbeafe,stroke:#3b82f6,color:#1e3a5f
    classDef warning fill:#ffe4e6,stroke:#f43f5e,color:#881337,font-weight:bold
    classDef timeout fill:#fef3c7,stroke:#f59e0b,color:#92400e
    classDef leader fill:#dcfce7,stroke:#16a34a,color:#14532d,font-weight:bold
    classDef demoted fill:#f3e8ff,stroke:#a855f7,color:#581c87
    classDef critical fill:#fecaca,stroke:#dc2626,color:#7f1d1d,font-weight:bold

그래서 ETCD는 control plane과 동일한 디스크를 사용하지 않는 것이 권장된다.

  • 속도가 빠른 SSD 사용
  • External ETCD Cluster 구성

추가 사례

  • Issue #13648에서는 Pod 약 50개, 초당 7개 key 변경이라는 매우 낮은 부하의 클러스터에서도 I/O 경합으로 인해 Range request took too long, WAL 타임아웃, 리더 재선출이 연쇄적으로 발생한 사례가 보고됐다.

  • Issue #15247에서는 Leader가 fdatasync에 묶이면서 heartbeat를 놓쳐 리더십을 잃었고, 이 과정에서 클러스터의 모든 lease가 일괄 revoke됐다. 분산 락, 서비스 등록 등 lease 기반 기능을 사용하는 애플리케이션이 동시에 전부 영향을 받는 상황이다.


1.2. 장애 복구 중 발생하는 추가 부하

장애 멤버가 복구되면 Follower는 Leader로부터 누락된 log를 동기화한다. etcd는 snapshot-count(기본 100,000 entries) 이상이 쌓이면 메모리의 log를 파일로 truncate하는데, 만일 truncate된 구간이 있으면, Leader는 log 대신 snapshot 파일 전체를 복구 멤버에게 전송한다.

그런데 많은 log를 복구하는 과정에서 Leader의 I/O 지연이 발생하여 1.1과 같은 문제가 발생할 수 있다.

graph TB
    subgraph rb1["① Follower B 복구 — log 동기화 요청"]
        direction LR
        rb_l1["⭐ Leader"]:::leader
        rb_fa1["Follower A"]:::follower
        rb_fb1["🔧 Follower B<br/>(복구, 재연결)"]:::recovery
        rb_fb1 -->|"log 동기화 요청"| rb_l1
    end
    subgraph rb2["② log 구간이 truncate됨 → snapshot 전체 전송"]
        direction LR
        rb_l2["⭐ Leader<br/>📦 snapshot 전송 중<br/>(수십~수백 MB)"]:::heavy
        rb_fb2["Follower B<br/>snapshot 수신 중"]:::recovery
        rb_fa2["Follower A"]:::follower
        rb_l2 -->|"대용량 snapshot"| rb_fb2
        rb_l2 -. "heartbeat 지연" .- rb_fa2
    end
    subgraph rb3["③ Leader I/O 부하 → heartbeat 누락 → 리더 재선출"]
        direction LR
        rb_l3["⚠️ Leader<br/>heartbeat 지연"]:::warning
        rb_fa3["Follower A<br/>Election Timeout ⏰"]:::timeout
        rb_fa3new["⭐ Follower A<br/>(새 Leader)"]:::leader
        rb_fa3 --> rb_fa3new
    end
    subgraph rb4["④ 클러스터 Write 일시 중단"]
        direction LR
        rb_result["🔴 Write 중단<br/>(새 리더 선출 중)"]:::critical
    end
    rb1 --> rb2 --> rb3 --> rb4
    classDef leader fill:#dcfce7,stroke:#16a34a,color:#14532d,font-weight:bold
    classDef follower fill:#dbeafe,stroke:#3b82f6,color:#1e3a5f
    classDef recovery fill:#e0f2fe,stroke:#0284c7,color:#0c4a6e
    classDef heavy fill:#ffe4e6,stroke:#f43f5e,color:#881337,font-weight:bold
    classDef warning fill:#fef3c7,stroke:#f59e0b,color:#92400e
    classDef timeout fill:#fef3c7,stroke:#f59e0b,color:#92400e
    classDef critical fill:#fecaca,stroke:#dc2626,color:#7f1d1d,font-weight:bold

이는 etcd 저장 log가 너무 많은 것이 문제인 것으로, 주기적으로 etcd DB 크기를 확인하고, 가능하면 Compaction + Defrag로 이전 revision을 지우는 것이 좋다.

추가 사례

  • Issue #13913에서는 DB 크기가 35MB 이상인 클러스터에서 Follower 복구 중 대형 snapshot 전송으로 인해 Leader가 heartbeat를 놓쳐 의도치 않은 리더 재선출이 반복된 사례가 보고됐다.
  • Issue #2662에서는 Follower 복구 중 Leader의 메모리가 최대 40배까지 치솟은 케이스도 있다.
  • Issue #14501에서는 단일 Follower의 fsync 지연이 클러스터 전체 Write latency를 끌어올린 사례가 보고됐다. 심한 경우 “slow follower livelock”이 발생한다. (Follower가 snapshot을 받는 속도보다 쓰기가 더 빠르게 쌓여 영원히 따라잡지 못하는 상태)
  • 실제 운영 포스트모텀 사례 중
  1. etcd 멤버 1대 교체 → 신규 멤버 기동 → snapshot 동기화 시작
  2. 대형 snapshot 전송으로 Leader 네트워크 포화
  3. Leader → 나머지 Follower heartbeat 누락 → 리더 재선출
  4. 구 멤버는 제거됐고 신규 멤버는 아직 동기화 중 → 쿼럼 손실
  5. control plane 전체 다운

이 발생한 케이스도 있다.

2. WAL / 데이터 손상 장애 (Corruption)

가장 드물지만 복구가 어려운 유형이다.

2.1. 발생 원인

  • 노드 강제 종료 중 WAL fsync 미완료
  • 스토리지 하드웨어 장애 (bad sector)
  • 잘못된 etcd 버전 다운그레이드

etcd가 기동 시 다음과 같은 에러를 출력하고 시작을 거부한다.

1
2
wal: crc mismatch
mvcc: db file is corrupt

2.2. 복구 방법

3대 클러스터 기준, 1대가 손상되면 나머지 2대로 쿼럼을 유지하면서 손상된 멤버를 교체한다.

1
2
3
4
5
6
7
8
9
10
# 1. 손상된 멤버 제거
etcdctl member remove <member-id>

# 2. 해당 노드의 etcd 데이터 디렉토리 삭제
rm -rf /var/lib/etcd/

# 3. 기존 클러스터에 새 멤버로 재가입
etcdctl member add <new-member-name> --peer-urls=<peer-url>

# 4. etcd 재시작 (INITIAL_CLUSTER_STATE=existing 으로 설정)


Leaner 도입으로 해결된 장애

etcd v3.4부터 Learner 상태가 도입됐다.

Learner는 투표권 없이 클러스터에 참여하는 비투표 멤버다. 쿼럼 크기에 영향을 주지 않으면서 Leader의 log를 복제받아 따라잡을 수 있다.

graph TB
    subgraph lr1["① Learner로 참여 — 쿼럼 크기 변화 없음 (안전)"]
        direction LR
        lr_l1["⭐ Leader"]:::leader
        lr_fa1["Follower A"]:::follower
        lr_new1["🎓 new-node<br/>(Learner, 투표권 없음)"]:::learner
        lr_note1["쿼럼 = Leader + A = 2<br/>안전하게 유지"]:::note
    end
    subgraph lr2["② log 복제 — 데이터 따라잡기"]
        direction LR
        lr_l2["⭐ Leader"]:::leader
        lr_fa2["Follower A"]:::follower
        lr_new2["🎓 new-node<br/>(Learner, 동기화 중)"]:::learner
        lr_l2 -->|"log 복제"| lr_new2
    end
    subgraph lr3["③ member promote — 투표 멤버로 승격"]
        direction LR
        lr_l3["⭐ Leader"]:::leader
        lr_fa3["Follower A"]:::follower
        lr_new3["✅ new-node<br/>(Follower, 투표권 있음)"]:::success
        lr_l3 -->|heartbeat| lr_new3
        lr_note3["쿼럼 크기 증가 시점<br/>(동기화 완료 후 — 안전)"]:::note
    end
    lr1 --> lr2 --> lr3
    classDef leader fill:#dcfce7,stroke:#16a34a,color:#14532d,font-weight:bold
    classDef follower fill:#dbeafe,stroke:#3b82f6,color:#1e3a5f
    classDef learner fill:#e0f2fe,stroke:#0284c7,color:#0c4a6e,font-weight:bold
    classDef success fill:#dcfce7,stroke:#22c55e,color:#14532d,font-weight:bold
    classDef note fill:#f0fdf4,stroke:#22c55e,color:#14532d

이전까지는 다음과 같은 장애가 발생할 수 있었다.

1. I/O 지연 장애 CASE

1.1. 신규 멤버가 Leader를 과부하시키는 경우

신규 멤버는 데이터가 전혀 없는 상태로 시작한다. 그래서 Follower 복구보다 따라잡아야 할 양이 더 많다. Leader로부터 모든 log를 받아야 하는데, 장애 기간이 길수록 받아야 할 양이 많아지고, 이미 truncate된 구간은 snapshot 전체를 전송받아야 한다. 이로 인해 I/O 지연 장애가 발생할 가능성이 높아진다.


2. 네트워크 장애

이전에는 신규 멤버가 추가되면 클러스터 크기와 정족수가 바뀌었다. 예를 들어, 다음과 같은 상황이 발생할 수 있었다.

2.1. 신규 멤버 추가 후 네트워크 파티션 장애 발생

Case 1: 3대 클러스터에 1대 추가 → 총 4대 (쿼럼=3) Case 2: 파티션 장애 발생 상태에서의 멤버가 추가 (쿼럼=3) | 파티션 구성 | 결과 | |———–|——| | 3+1 (Leader가 3쪽) | 쿼럼 3 유지 → 정상 운영 | | 2+2 | 양쪽 모두 쿼럼(3) 미달 → 리더 선출 불가 |

기존 3대일 때는 2+1 파티션이어도 2쪽에서 쿼럼을 유지할 수 있었지만, 4대 클러스터에서 2+2로 갈리면 양쪽 모두 쿼럼이 없어 클러스터 전체가 마비된다.


2.3. 잘못된 peer URL로 멤버 추가

etcdctl member add 명령은 peer URL의 유효성을 검증하지 않고 즉시 적용된다. 잘못된 URL이 추가되면 신규 etcd 프로세스 자체가 기동되지 못하고, 이미 쿼럼이 바뀐 상태가 된다.

graph TB
    subgraph wu1["① member add 실행 — URL 검증 없이 즉시 쿼럼 크기 증가"]
        direction LR
        wu_l1["⭐ Leader"]:::leader
        wu_fa1["Follower A"]:::follower
        wu_new1["➕ new-node<br/>--peer-urls=wrong-ip:2380"]:::newmember
        wu_note1["쿼럼 크기 즉시 2→3"]:::warning
    end
    subgraph wu2["② new-node 기동 실패 — 쿼럼은 이미 3"]
        direction LR
        wu_l2["⭐ Leader"]:::leader
        wu_fa2["Follower A"]:::follower
        wu_new2["❌ new-node<br/>(기동 실패)"]:::failure
        wu_note2["정상 멤버=2, 쿼럼=3<br/>→ 쿼럼 미달"]:::critical_note
    end
    subgraph wu3["③ Write 불가 — member remove도 불가 (쿼럼 없음)"]
        direction LR
        wu_stuck["🔴 쿼럼 미달로 Write 불가<br/>member remove도 쿼럼 필요<br/>→ 되돌릴 수 없음"]:::critical
    end
    subgraph wu4["④ 강제 복구 필요"]
        direction LR
        wu_force["etcd --force-new-cluster<br/>강제 재구성"]:::action
    end
    wu1 --> wu2 --> wu3 --> wu4
    classDef leader fill:#dcfce7,stroke:#16a34a,color:#14532d,font-weight:bold
    classDef follower fill:#dbeafe,stroke:#3b82f6,color:#1e3a5f
    classDef newmember fill:#e0f2fe,stroke:#0284c7,color:#0c4a6e
    classDef warning fill:#fef3c7,stroke:#f59e0b,color:#92400e,font-weight:bold
    classDef failure fill:#fee2e2,stroke:#ef4444,color:#991b1b
    classDef critical_note fill:#fef3c7,stroke:#f59e0b,color:#92400e
    classDef critical fill:#fecaca,stroke:#dc2626,color:#7f1d1d,font-weight:bold
    classDef action fill:#f3e8ff,stroke:#a855f7,color:#581c87

잘못된 멤버 추가 하나가 클러스터 전체를 불능 상태로 만들 수 있었다.



ETCD node의 장애 복구

1. 자동 장애 복구

과반수 멤버가 다시 정상화되면 클러스터가 자동으로 새 Leader를 선출하고 정상 복귀한다.


2.1. Leader 장애 & 재선출

Leader node에 장애가 발생하는 경우이다.

graph TB
    subgraph step1["① 정상 운영"]
        direction LR
        B1["⭐ ETCD B<br/>(Leader)"]:::leader
        C1a["ETCD C<br/>(Follower)"]:::follower
        C1b["ETCD D<br/>(Follower)"]:::follower
        B1 -->|heartbeat| C1a
        B1 -->|heartbeat| C1b
    end

    subgraph step2["② Leader 장애"]
        direction LR
        B2["❌ ETCD B<br/>(장애)"]:::failure
        C2a["ETCD C<br/>Election Timeout ⏰"]:::timeout
        C2b["ETCD D<br/>heartbeat 대기 중..."]:::timeout
    end

    subgraph step3["③ 선거 시작"]
        direction LR
        C3a["🗳️ ETCD C<br/>(Candidate)"]:::candidate
        B3["ETCD B"]:::normal
        C3b["ETCD D<br/>Election Timeout ⏰"]:::timeout
        C3a -->|"RequestVote RPC"| B3
        C3a --> C3b
    end

    subgraph step4["④ 새 Leader 선출"]
        direction LR
        C4a["⭐ ETCD C<br/>(새 Leader)"]:::leader
        B4["ETCD B<br/>(Follower)"]:::follower
        C4b["ETCD D<br/>(Follower)"]:::follower
        C4a -->|heartbeat| B4
        C4a -->|heartbeat| C4b
    end

    step1 --> step2 --> step3 --> step4

    classDef leader fill:#dcfce7,stroke:#16a34a,color:#14532d,font-weight:bold
    classDef follower fill:#dbeafe,stroke:#3b82f6,color:#1e3a5f
    classDef failure fill:#fee2e2,stroke:#ef4444,color:#991b1b,font-weight:bold
    classDef timeout fill:#fef3c7,stroke:#f59e0b,color:#92400e
    classDef candidate fill:#fef3c7,stroke:#f59e0b,color:#92400e,font-weight:bold
    classDef normal fill:#dbeafe,stroke:#3b82f6,color:#1e3a5f

Leader 선출 소요 시간은 수백 ms ~ 수 초 정도로, 실제로는 거의 실시간이라고 한다. (election timeout 기반 감지)

graph TB
    subgraph uw1["① Client → Leader write 요청 / AppendEntry 전송 중"]
        direction LR
        uw_c1["Client"]:::client
        uw_l1["⭐ Leader<br/>WAL 기록 완료"]:::leader
        uw_fa1["Follower A<br/>(복제 중...)"]:::pending
        uw_fb1["Follower B<br/>(복제 중...)"]:::pending
        uw_c1 -->|"write(x=1)"| uw_l1
        uw_l1 -. "AppendEntry" .-> uw_fa1
        uw_l1 -. "AppendEntry" .-> uw_fb1
    end
    subgraph uw2["② ⚡ 쿼럼 달성 전 Leader 장애"]
        direction LR
        uw_l2["❌ Leader (장애)"]:::failure
        uw_fa2["Follower A<br/>(복제 미완료)"]:::pending
        uw_fb2["Follower B<br/>(복제 미완료)"]:::pending
    end
    subgraph uw3["③ 새 Leader 선출 — uncommitted log 덮어씀"]
        direction LR
        uw_fa3["⭐ Follower A<br/>(새 Leader)"]:::leader
        uw_fb3["Follower B"]:::follower
        uw_l3["구 Leader<br/>(Follower 전환)"]:::demoted
        uw_fa3 -->|heartbeat| uw_fb3
        uw_fa3 -->|heartbeat| uw_l3
    end
    subgraph uw4["④ 결과: write(x=1) 유실"]
        direction LR
        uw_c4["Client<br/>timeout"]:::client
        uw_lost["❌ uncommitted log 유실<br/>쿼럼 달성 전 장애로<br/>새 Leader가 덮어씌움"]:::lost
    end
    uw1 --> uw2 --> uw3 --> uw4
    classDef client fill:#f3e8ff,stroke:#a855f7,color:#581c87
    classDef leader fill:#dcfce7,stroke:#16a34a,color:#14532d,font-weight:bold
    classDef follower fill:#dbeafe,stroke:#3b82f6,color:#1e3a5f
    classDef pending fill:#fef3c7,stroke:#f59e0b,color:#92400e
    classDef failure fill:#fee2e2,stroke:#ef4444,color:#991b1b,font-weight:bold
    classDef demoted fill:#f3e8ff,stroke:#a855f7,color:#581c87
    classDef lost fill:#fecaca,stroke:#dc2626,color:#7f1d1d,font-weight:bold


2.2. Follower 장애 & 복구

Follower node에 장애가 발생하는 경우이다.

쿼럼 이하의 Follower 장애라면 클러스터는 계속 정상 운영된다. Leader는 장애 난 Follower에게 AppendEntry를 계속 시도하고, 나머지 멤버로 쿼럼을 채워 Write를 처리한다.

graph TB
    subgraph fw1["① Follower B 장애 — 쿼럼은 유지됨"]
        direction LR
        fw_l1["⭐ Leader"]:::leader
        fw_fa1["Follower A"]:::follower
        fw_fb1["❌ Follower B<br/>(장애)"]:::failure
        fw_l1 -->|AppendEntry| fw_fa1
        fw_l1 -. "응답 없음" .- fw_fb1
        fw_note1["쿼럼 = Leader + A = 유지<br/>클러스터 정상 운영 지속"]:::note
    end
    subgraph fw2["② Follower B 복구 — log 동기화 요청"]
        direction LR
        fw_l2["⭐ Leader"]:::leader
        fw_fa2["Follower A"]:::follower
        fw_fb2["🔧 Follower B<br/>(복구, 재연결)"]:::recovery
        fw_l2 -->|AppendEntry| fw_fa2
        fw_fb2 -->|"log 동기화 요청"| fw_l2
    end
    subgraph fw3["③ Leader → 누락 log 전송 (nextIndex[] 기준)"]
        direction LR
        fw_l3["⭐ Leader"]:::leader
        fw_fa3["Follower A"]:::follower
        fw_fb3["🔧 Follower B<br/>(log 수신 중)"]:::recovery
        fw_l3 -->|AppendEntry| fw_fa3
        fw_l3 -->|"누락 log 전송"| fw_fb3
    end
    subgraph fw4["④ 동기화 완료 — 정상 멤버 복귀"]
        direction LR
        fw_l4["⭐ Leader"]:::leader
        fw_fa4["Follower A"]:::follower
        fw_fb4["✅ Follower B<br/>(복귀)"]:::success
        fw_l4 -->|AppendEntry| fw_fa4
        fw_l4 -->|AppendEntry| fw_fb4
    end
    fw1 --> fw2 --> fw3 --> fw4
    classDef leader fill:#dcfce7,stroke:#16a34a,color:#14532d,font-weight:bold
    classDef follower fill:#dbeafe,stroke:#3b82f6,color:#1e3a5f
    classDef failure fill:#fee2e2,stroke:#ef4444,color:#991b1b,font-weight:bold
    classDef recovery fill:#e0f2fe,stroke:#0284c7,color:#0c4a6e
    classDef success fill:#dcfce7,stroke:#22c55e,color:#14532d,font-weight:bold
    classDef note fill:#f0fdf4,stroke:#22c55e,color:#14532d


추가 개념: Split Vote (동시에 Candidate가 되는 경우)

만일 Leader node에 장애가 발생해서 여러 Follower 중 하나가 Leader가 되야하는 상황이라 가정하자. 만일 둘 다 Candidate가 되는 경우에는 어떻게 해야할까 ?

ETCD는 이 문제를 방지하기 위해 Raft 알고리즘의 Election Timeout을 150ms~300ms 랜덤으로 설계하였다. 그래서 보통 한 Follower가 먼저 timeout되어 빠르게 리더가 선출된다. 하지만 드물게 두 Follower가 거의 동시에 timeout되면 Split Vote가 발생한다.

graph TB
    subgraph sv1["① Leader 다운"]
        direction LR
        N1["❌ Node1<br/>(Leader 장애)"]:::failure
        N2a["Node2<br/>(Follower)"]:::follower
        N3a["Node3<br/>(Follower)"]:::follower
    end

    subgraph sv2["② 동시에 Election Timeout — Split Vote 발생"]
        direction LR
        N2b["🗳️ Node2<br/>(Candidate, Term:2)<br/>자기 자신에게 투표"]:::candidate
        N3b["🗳️ Node3<br/>(Candidate, Term:2)<br/>자기 자신에게 투표"]:::candidate
        N2b -. "투표 요청 거절<br/>(이미 투표함)" .- N3b
    end

    subgraph sv3["③ 결과: 쿼럼 미달 — 아무도 Leader 없음"]
        direction LR
        N2c["Node2<br/>쿼럼(2) 미달"]:::blocked
        N3c["Node3<br/>쿼럼(2) 미달"]:::blocked
        retry["⏰ 다음 Term에서<br/>랜덤 timeout 재시도"]:::retry
    end

    sv1 --> sv2 --> sv3

    classDef failure fill:#fee2e2,stroke:#ef4444,color:#991b1b,font-weight:bold
    classDef follower fill:#dbeafe,stroke:#3b82f6,color:#1e3a5f
    classDef candidate fill:#fef3c7,stroke:#f59e0b,color:#92400e,font-weight:bold
    classDef blocked fill:#f3e8ff,stroke:#a855f7,color:#581c87
    classDef retry fill:#e0f2fe,stroke:#0284c7,color:#0c4a6e

아무도 리더가 되지 못하면, 두 Candidate 모두 다시 랜덤한 Election Timeout을 기다린다. 다음 라운드에서 한 쪽이 먼저 timeout되면 상대방이 아직 Follower 상태여서 투표해준다.

상황결과
한 Follower만 먼저 timeout빠르게 리더 선출 (일반적)
동시에 timeout (Split Vote)이번 Term은 리더 없음 → 다음 Term에서 재시도
Split Vote 반복극히 드묾, 랜덤 timeout이 확률적으로 해소

Split Vote는 Safety(데이터 일관성)를 깨지 않는다. 아무도 쿼럼을 달성하지 못했기 때문에 데이터 Write도 발생하지 않는다.



2. 수동 장애 복구

Quorom Loss과반수 멤버가 복구 불가한 경우에는 스냅샷을 이용한 수동 복원이 필요하다.

2.1. 스냅샷을 이용한 복원

etcd 복구를 위해 주기적인 스냅샷 백업이 필요하다.

1
2
3
4
5
6
7
8
9
# 스냅샷 백업
etcdctl snapshot save /backup/etcd-snapshot-$(date +%Y%m%d%H%M).db \
  --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt \
  --key=/etc/kubernetes/pki/etcd/healthcheck-client.key

# 스냅샷 상태 확인
etcdctl snapshot status /backup/etcd-snapshot.db --write-out=table

etcdctl snapshot save과 단순 파일 복사의 차이

  • etcdctl snapshot save로 만든 파일에는 무결성 해시(hash)가 포함된다.
  • etcdctl snapshot restore 시 파일 변조 여부를 자동으로 검증한다.
  • 단순 복사로 만든 파일로 복구할 때는 --skip-hash-check 옵션이 필요하다.

CompactionDefrag를 수행한 후 백업하면, 불필요한 revision 데이터가 제거되어 백업 파일 크기를 줄일 수 있다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 1. kube-apiserver 중지
#    /etc/kubernetes/manifests/kube-apiserver.yaml 임시 이동

# 2. 기존 etcd 데이터 백업
mv /var/lib/etcd /var/lib/etcd.bak

# 3. 스냅샷으로 데이터 복원
etcdctl snapshot restore /backup/etcd-snapshot.db \
  --name=<member-name> \
  --initial-cluster=<member-name>=https://<ip>:2380 \
  --initial-advertise-peer-urls=https://<ip>:2380 \
  --data-dir=/var/lib/etcd

# 4. etcd 재시작

# 5. kube-apiserver 복구
#    /etc/kubernetes/manifests/kube-apiserver.yaml 복원

복구 시 새로운 클러스터 메타데이터 사용이 중요하다.

etcd 메타데이터에는 클러스터 UUID와 멤버 UUID가 저장된다. 이전 메타데이터를 그대로 사용하면, 장애 났던 서버가 다시 살아날 경우 충돌이 발생할 수 있다. etcdctl snapshot restore 명령은 자동으로 새 클러스터 메타데이터를 생성하므로, 단순 파일 복사가 아닌 이 명령을 사용해야 한다.

kubeadm 클러스터에서는 /etc/kubernetes/manifests/ 아래 Static Pod manifest를 이동시키는 것으로 컴포넌트를 중지/재시작할 수 있다.


추가 개념: 자동 백업 구성 방법

수동 백업은 누락될 수 있기 때문에 CronJob으로 주기적 백업을 자동화하는 것을 권장한다고 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# etcd-backup-cronjob.yaml (예시)
apiVersion: batch/v1
kind: CronJob
metadata:
  name: etcd-backup
  namespace: kube-system
spec:
  schedule: "0 2 * * *"   # 매일 새벽 2시
  jobTemplate:
    spec:
      template:
        spec:
          hostNetwork: true
          containers:
          - name: etcd-backup
            image: bitnami/etcd:latest
            command:
            - /bin/sh
            - -c
            - |
              etcdctl snapshot save /backup/etcd-$(date +%Y%m%d).db
              # 오래된 백업 삭제 (5일치 유지)
              find /backup -name "etcd-*.db" -mtime +5 -delete
            env:
            - name: ETCDCTL_API
              value: "3"
            - name: ETCDCTL_ENDPOINTS
              value: "https://127.0.0.1:2379"
            # cacert, cert, key 마운트 필요
          restartPolicy: OnFailure

주요 고려사항

  • 모든 클러스터가 동시에 백업하면 네트워크 부하가 집중된다. 클러스터마다 다른 시간을 설정하는 것이 좋다.
  • 백업 파일은 오브젝트 스토리지(S3 등)에 저장하여 노드 장애에도 안전하게 보존한다.
  • 백업 전 Compaction + Defrag를 수행하면 백업 크기를 줄일 수 있다. (오래된 revision 제거)


2.2. 디스크 관리 - Revision & Compaction

etcd는 하나의 key에 대한 모든 변경사항을 파일시스템에 기록한다. 이것을 revision이라고 한다.

1
2
3
4
5
key "x"에 대한 write 이력:

revision 3: x = "apple"
revision 7: x = "banana"
revision 12: x = "cherry"   ← 현재 최신

이 구조 덕분에 특정 시점의 데이터를 조회할 수 있지만, 별도 관리 없이 계속 쌓이면 디스크 공간이 고갈된다. Compaction으로 오래된 revision을 삭제할 수 있으며, 삭제된 revision은 더 이상 조회할 수 없다.

Auto Compaction

etcd는 두 가지 Auto Compaction 모드를 제공한다. (etcd 공식 운영 가이드 — Auto Compaction)

항목Revision 모드Periodic 모드
설정auto-compaction-mode: revisionauto-compaction-mode: periodic
retention 예시auto-compaction-retention: 1000auto-compaction-retention: 8h
동작 주기5분마다 실행retention을 10으로 나눈 간격 (8h → 1h마다)
삭제 기준최신 revision - retention 값 이하 삭제지정 시간 이전의 revision 삭제
예시현재 revision이 5000이면 → 4000 이하 삭제1시간마다 1시간 전 revision 이하 삭제


2.3. Defragmentation (단편화 제거)

Compaction으로 revision을 삭제해도 파일시스템의 디스크 공간이 자동으로 반환되지 않는다.

etcd는 내부적으로 BoltDB(페이지 기반 파일 DB) 를 사용한다. Compaction은 해당 페이지를 “비었다”고 표시만 할 뿐 파일 크기를 줄이지 않는다. 이는 RDB에서 DELETE를 해도 디스크 공간이 확보되지 않는 것과 같은 원리다. (삭제할 때마다 파일을 shrink하면 뒤의 모든 페이지를 앞으로 당겨야 해서 비용이 너무 크기 때문)

Defragmentation은 유효 데이터만 새 파일에 순서대로 복사 → 원본 파일과 교체하는 방식으로 실제 디스크 공간을 반환한다. 이 과정에서 해당 멤버의 read/write가 잠깐 block된다.

항목CompactionDefragmentation
목적오래된 revision 논리적 삭제디스크 공간 실제 반환
자동화가능 (auto compaction)없음 (수동만 가능)
처리 중 영향없음해당 멤버 read/write 일시 block
block 시간-보통 수 ms (DB 크기에 따라 다름)
1
2
3
4
5
6
7
8
9
10
11
12
13
# 1. 현재 revision 확인
CURRENT_REV=$(etcdctl endpoint status --write-out="json" \
  | jq '.[] | .Status.header.revision')

# 2. Compaction (오래된 revision 정리)
etcdctl compact $CURRENT_REV

# 3. Defrag (디스크 공간 실제 반환)
#    한 번에 하나씩 순서대로 진행 (멤버별 순차 실행 권장)
etcdctl defrag --endpoints=<etcd-endpoint>

# 4. DB 사이즈 확인
etcdctl endpoint status --write-out=table



장애 시나리오 요약

#시나리오자동 복구서비스 영향핵심 대응
1소수 Follower 장애가능 (자동 재연결)없음 (부하 증가)빠른 복구로 쿼럼 여유 유지
2Quorum Loss과반수 복구 시 자동클러스터 제어 전체 마비노드 복구 또는 스냅샷 복원
3Leader 장애가능 (수 초 내 재선출)Write 큐 대기, uncommitted 유실 가능모니터링으로 반복 여부 확인
5Network Partition가능 (파티션 복구 시 자동)소수 파티션 Write 불가네트워크 복구
6디스크 고갈불가Write 거부 → 제어 마비Compaction + Defrag
7WAL / 데이터 손상부분 (멤버 재가입)개별 멤버 기동 불가멤버 제거 후 재가입



함께 사용하면 좋을 모니터링 지표

메트릭설명임계값
etcd_server_has_leader리더 존재 여부== 0 이면 Alert
etcd_server_leader_changes_seen_total리더 변경 횟수1시간에 3회 이상이면 이상 징후
etcd_server_proposals_failed_total합의 실패 횟수증가 추세이면 Alert
etcd_disk_wal_fsync_duration_secondsWAL fsync 지연p99 > 10ms 이상이면 경고
etcd_disk_backend_commit_duration_secondsBoltDB commit 지연p99 > 25ms 이상이면 경고
etcd_mvcc_db_total_size_in_bytesDB 사이즈쿼터의 80% 초과 시 경고
etcd_network_peer_round_trip_time_seconds피어 간 RTTp99 > 100ms 이상이면 경고

I/O 지연 지표(wal_fsync, backend_commit)는 단순 장애 감지를 넘어 하드웨어 성능 저하나 디스크 경합의 조기 신호로 활용할 수 있다.



참고 자료

This post is licensed under CC BY 4.0 by the author.