Blog
kafkaavailabilitydurabilityreplicationtroubleshooting

[Kafka 운영 ⑤] Unclean Leader Election — 가용성을 위해 데이터를 버릴 것인가

파티션의 모든 in-sync 복제본이 죽었을 때 Kafka는 두 갈래 길에 선다. 오프라인을 감수하고 데이터를 지킬 것인가, 뒤처진 복제본을 리더로 올려 가용성을 되찾되 데이터 손실을 감수할 것인가. unclean.leader.election.enable의 의미와 운영 판단 기준을 정리한다.

Data Dynamics2026년 6월 4일23 min read

새벽 3시, 특정 파티션 하나가 통째로 멈췄습니다. 프로듀서는 메시지를 못 보내고, 컨슈머는 처리를 멈췄으며, 대시보드의 OfflinePartitionsCount는 0이 아닌 숫자를 가리킵니다. 원인을 추적해 보니 그 파티션의 in-sync 복제본이 전부 다운된 상태입니다. 이제 여러분 앞에는 잔인한 두 갈래 길이 놓입니다. 데이터를 지키며 기다릴 것인가, 아니면 뒤처진 복제본을 리더로 올려 서비스를 되살리되 일부 데이터를 영영 잃을 것인가. 바로 이 선택의 이름이 Unclean Leader Election입니다.

이 글에서 배우는 것

  • 리더/팔로워와 ISR이 무엇이고, 팔로워가 어떻게 ISR에서 탈락하는지
  • "모든 in-sync 복제본이 죽는" 시나리오에서 Kafka가 마주하는 두 선택지
  • unclean.leader.election.enable 설정값(true/false)이 운영상 무엇을 의미하는지
  • 오프라인 파티션·무음(silent) 데이터 손실·오프셋 절단을 탐지하는 방법
  • 이 옵션을 켜도 되는 (드문) 경우와 절대 켜면 안 되는 경우

이 글은 Kafka 운영 트러블슈팅 시리즈의 5편입니다. 복제·ISR의 기본기는 3편(ISR과 복제 지연)에서, acksmin.insync.replicas로 내구성을 보장하는 방법은 4편(프로듀서 내구성)에서 다뤘습니다. 이번 편은 그 모든 안전장치가 무너진 최악의 순간에 관한 이야기입니다.


1. 복습 — 리더, 팔로워, 그리고 ISR

본격적인 시나리오로 들어가기 전에, 이 글을 이해하는 데 꼭 필요한 개념 세 가지를 다시 짚습니다.

리더와 팔로워

Kafka의 각 파티션은 replication.factor만큼의 복제본(replica)을 가집니다. 그중 하나는 리더(leader), 나머지는 **팔로워(follower)**입니다.

  • 모든 읽기와 쓰기는 리더가 처리합니다. 프로듀서는 리더에게만 메시지를 보내고, 컨슈머도 (기본적으로) 리더에서만 읽습니다.
  • 팔로워는 리더의 로그를 그대로 복제(fetch)할 뿐입니다. 팔로워는 리더에게 주기적으로 fetch 요청을 보내 새 메시지를 따라갑니다.

리더가 죽으면, 컨트롤러(controller)가 살아 있는 복제본 중 하나를 새 리더로 선출합니다. 이때 "누구를 리더로 뽑을 자격이 있는가"를 결정하는 것이 바로 ISR입니다.

ISR (In-Sync Replicas)

ISR은 리더와 충분히 동기화된 복제본의 집합입니다. 리더 자신도 항상 ISR에 포함됩니다.

용어의미
AR (Assigned Replicas)해당 파티션에 할당된 전체 복제본
ISR (In-Sync Replicas)리더와 동기화 상태를 유지하는 복제본 집합
OSR (Out-of-Sync Replicas)ISR에서 탈락한, 뒤처진 복제본 (AR - ISR)

"커밋되었다(committed)"는 말의 정의가 여기에 걸려 있습니다. 메시지는 ISR의 모든 복제본에 복제되었을 때 비로소 커밋된 것으로 간주되며, 컨슈머는 커밋된 메시지만 읽을 수 있습니다(High Watermark까지만 노출). 그래서 ISR 멤버는 "이 데이터를 확실히 가지고 있다고 신뢰할 수 있는" 복제본이고, 리더 선출의 1순위 후보입니다.

팔로워는 어떻게 ISR에서 탈락하는가

팔로워가 느려지거나 잠시 죽으면 리더의 로그를 제때 따라잡지 못합니다. 이 "뒤처짐"이 일정 시간을 넘기면 리더는 해당 팔로워를 ISR에서 제거합니다. 그 기준이 replica.lag.time.max.ms입니다.

# broker config (기본값 30초)
replica.lag.time.max.ms=30000
  • 팔로워가 마지막으로 리더의 로그 끝(log-end-offset)까지 따라잡은 시점으로부터 replica.lag.time.max.ms가 지나도록 다시 따라잡지 못하면 → ISR에서 제거(OSR로 강등).
  • 팔로워가 다시 따라잡으면 → ISR로 복귀.
Loading diagram…

핵심은 이것입니다. ISR은 고정된 집합이 아니라, 시시각각 늘었다 줄었다 하는 살아 있는 집합입니다. 부하가 몰리거나 디스크/네트워크가 느려지면 팔로워가 줄줄이 ISR에서 빠질 수 있고, 최악의 경우 ISR이 리더 단 하나만 남을 수도 있습니다. 바로 이 지점이 다음 시나리오의 출발점입니다.

ISR이 어떻게 늘었다 줄었다 하는지, under-replicated partitions를 어떻게 모니터링하는지는 3편에서 자세히 다뤘습니다.


2. 시나리오 — 모든 in-sync 복제본이 죽으면

이제 본론입니다. 어떤 파티션의 ISR에 속한 모든 복제본이 동시에(혹은 연쇄적으로) 다운되는 상황을 가정합니다.

예를 들어 replication.factor=3인 파티션에서:

  1. 처음엔 ISR = — 건강한 상태.
  2. Broker3이 GC 폭주로 느려져 ISR에서 탈락 → ISR = , OSR = .
  3. 그 직후 Broker1(리더)이 디스크 장애로 다운 → 컨트롤러가 Broker2를 새 리더로 선출. ISR = .
  4. 그런데 Broker2마저 정전으로 다운. 이제 ISR에 살아 있는 복제본이 하나도 없습니다.

남은 것은 OSR의 Broker3뿐입니다. Broker3은 살아는 있지만, 마지막에 ISR에서 빠진 시점 이후의 데이터를 가지고 있지 않습니다. Broker2가 커밋했지만 Broker3에는 복제되지 않은 메시지들이 분명히 존재합니다.

컨트롤러 입장에서 선택지는 정확히 두 가지뿐입니다.

선택지 (a) — 기다린다 (Clean): 파티션은 오프라인, 데이터는 보존

ISR 멤버(Broker1 또는 Broker2) 중 하나가 다시 살아 돌아올 때까지 기다립니다. ISR 멤버는 커밋된 모든 데이터를 확실히 가지고 있으므로, 그가 리더가 되면 데이터 손실이 전혀 없습니다.

대신 대가가 있습니다. 그가 돌아오기 전까지 이 파티션은 리더가 없는 오프라인 상태가 됩니다.

  • 프로듀서는 이 파티션에 쓰지 못합니다(메타데이터 갱신 후 NotLeaderOrFollowerException 등으로 재시도, 결국 타임아웃).
  • 컨슈머는 이 파티션을 더 읽지 못합니다.
  • 파티션은 가용하지 않지만(unavailable), 데이터는 단 한 건도 잃지 않습니다.

선택지 (b) — Unclean Election: 뒤처진 복제본을 리더로, 데이터는 손실

OSR의 Broker3을 강제로 새 리더로 선출합니다. 파티션은 즉시 온라인으로 복구되어 다시 읽고 쓸 수 있게 됩니다.

대신 치명적인 대가가 있습니다. Broker3은 뒤처져 있었으므로:

  • Broker2까지만 커밋되었고 Broker3에 도달하지 못한 메시지들은 영원히 사라집니다. 그 메시지를 ack 받았던 프로듀서 입장에선 "성공했다고 응답받은 데이터가 증발"한 셈입니다.
  • 새 리더(Broker3)의 log-end-offset이 이전 리더보다 작기 때문에, 파티션의 오프셋이 **거꾸로 절단(truncate)**됩니다. 나중에 Broker1/Broker2가 돌아오면 자신이 가진 더 긴 로그를 새 리더(Broker3)의 더 짧은 로그에 맞춰 잘라내야 합니다. 즉, 살아 돌아온 멀쩡한 복제본의 데이터까지 버려집니다.
구분(a) 기다린다 (Clean)(b) Unclean Election
파티션 상태오프라인 (unavailable)온라인 (available)
데이터 손실없음있음 (커밋된 메시지 유실)
오프셋보존절단(truncate) 가능
복구 시점ISR 멤버가 돌아와야즉시
우선하는 가치내구성(Durability)가용성(Availability)

이것이 분산 시스템의 고전적인 트레이드오프입니다. 일관성·내구성을 택하면 가용성을 잃고, 가용성을 택하면 일관성·내구성을 잃습니다. Kafka는 이 선택을 한 줄짜리 설정으로 여러분에게 위임합니다.


3. 설정 — unclean.leader.election.enable

위 두 갈래 길 중 무엇을 택할지 결정하는 설정이 바로 unclean.leader.election.enable입니다.

# broker(server.properties) 또는 토픽 레벨에서 설정
unclean.leader.election.enable=false
동작의미
false (기본값)ISR이 빌 때 기다린다데이터 손실 없음, 그러나 파티션은 오프라인. 내구성 우선.
trueOSR 복제본을 리더로 선출즉시 복구, 그러나 데이터 손실 가능. 가용성 우선.

기본값은 false다 — 그리고 그게 옳다

과거(0.11.0.0 이전) Kafka의 기본값은 true였습니다. 가용성을 위해 조용히 데이터를 버리는 동작이 기본이었던 셈이죠. 이는 "내가 ack를 받았는데 데이터가 사라졌다"는 운영자들의 악몽으로 이어졌고, Kafka는 기본값을 false로 전환했습니다. 현대의 Kafka는 기본적으로 내구성을 가용성보다 우선합니다. 특별한 이유 없이 이 값을 true로 바꾸지 마세요.

설정 레벨 — 브로커 vs 토픽

  • 브로커 레벨 (server.propertiesunclean.leader.election.enable): 클러스터 전체의 기본 정책. 재시작이 필요합니다.
  • 토픽 레벨 (동적 설정): 특정 토픽만 정책을 다르게 가져갈 수 있습니다. 재시작 없이 즉시 적용됩니다.
# 특정 토픽만 unclean election 허용 (가용성 우선 토픽)
kafka-configs.sh --bootstrap-server localhost:9092 \
  --entity-type topics --entity-name clickstream-raw \
  --alter --add-config unclean.leader.election.enable=true
 
# 다시 내구성 우선으로 되돌리기
kafka-configs.sh --bootstrap-server localhost:9092 \
  --entity-type topics --entity-name clickstream-raw \
  --alter --delete-config unclean.leader.election.enable

주의: 브로커 레벨에서 이 값을 동적으로 true로 바꾸면, 그 순간 ISR이 비어 오프라인 상태로 대기 중이던 파티션들이 즉시 unclean election을 트리거해 데이터를 잃을 수 있습니다. 장애 한복판에서 "일단 살리고 보자"며 이 값을 켜는 것은 되돌릴 수 없는 데이터 손실을 부르는 행위임을 명심하세요.


4. 증상과 탐지

이 문제는 두 얼굴을 가집니다. false(기본값)일 때는 시끄러운 장애(파티션 오프라인)로, true일 때는 조용한 데이터 손실로 나타납니다. 둘 다 탐지법을 알아야 합니다.

false일 때 — 오프라인 파티션 (시끄러운 장애)

ISR이 비고 unclean election이 비활성화되어 있으면, 파티션은 리더 없는 오프라인 상태가 됩니다.

① 핵심 메트릭: OfflinePartitionsCount

이 메트릭은 반드시 0이어야 하며, 0이 아니면 즉시 알람을 걸어야 하는 1순위 지표입니다.

# JMX MBean
kafka.controller:type=KafkaController,name=OfflinePartitionsCount
메트릭정상값의미
OfflinePartitionsCount0리더가 없는(오프라인) 파티션 수. 컨트롤러에서 수집.
ActiveControllerCount1 (클러스터 합)활성 컨트롤러 수
UnderMinIsrPartitionCount0min.insync.replicas 미만으로 떨어진 파티션 수

② CLI로 확인 — 리더가 없는 파티션

kafka-topics.sh --bootstrap-server localhost:9092 \
  --describe --topic orders
Topic: orders  Partition: 7  Leader: none  Replicas: 1,2,3  Isr:

Leader: none과 **텅 빈 Isr:**가 결정적 증거입니다. 리더가 없으니 이 파티션은 읽기/쓰기가 모두 막혀 있습니다.

③ 클라이언트 증상

  • 프로듀서: 해당 파티션 대상 전송이 멈추고, 결국 TimeoutException(메타데이터를 못 받거나 리더가 없어서).
  • 컨슈머: 해당 파티션의 lag이 더 이상 줄지 않고 정체.

true일 때 — 무음 데이터 손실 (조용한 장애)

unclean election이 켜져 있으면 파티션은 멀쩡히 살아납니다. 겉보기엔 아무 문제가 없어 보이는 것이 더 무섭습니다. 하지만 내부에서는:

① log-end-offset이 거꾸로 간다

뒤처진 복제본이 리더가 되면서 파티션의 끝 오프셋(LEO)이 이전보다 작아집니다. 시계열로 LEO를 그려 보면 단조 증가해야 할 그래프가 순간적으로 뒤로 점프합니다.

# 시간순 log-end-offset
12:00:00  LEO=1,000,000
12:00:05  LEO=1,000,420   <- Broker2가 리더, 정상 증가
12:00:10  LEO=  998,800   <- Broker3으로 unclean election! 1,620건 증발 + 오프셋 후퇴

② 브로커 로그의 절단(truncation) 경고

이후 Broker1/Broker2가 복구되면, 자신의 더 긴 로그를 새 리더의 짧은 로그에 맞춰 잘라냅니다. 브로커 로그에 다음과 같은 메시지가 남습니다.

WARN [ReplicaFetcher ...] Truncating partition orders-7 to offset 998800
      because the leader's log start/end offset is smaller ...
INFO Truncating log orders-7 to offset 998800, discarding 1620 records

③ 탐지 전략

신호어디서 보는가
LEO가 감소브로커별 LogEndOffset 메트릭 시계열, 또는 토픽 끝 오프셋 모니터링
로그 절단브로커 로그의 Truncating ... to offset (WARN/INFO)
ISR 변동 + 리더 변경컨트롤러 로그, LeaderElectionRateAndTimeMs
ack 받은 데이터 누락애플리케이션 레벨 검증(메시지 키/시퀀스 갭)

무음 데이터 손실은 메트릭만으로 100% 잡기 어렵습니다. 데이터 무결성이 중요하다면 애플리케이션 레벨에서 메시지 시퀀스/키의 갭을 검증하는 방어선을 함께 두세요.


5. 판단 기준 — 언제 켜고, 언제 끄는가

결국 질문은 하나입니다. "이 토픽에서 데이터 한 건의 가치가, 다운타임 몇 분의 가치보다 큰가?"

그대로 두어야 하는 경우 (false 유지) — 대부분

데이터 무결성이 조금이라도 중요하다면 **무조건 false**입니다.

  • 금융 거래, 결제, 주문, 정산 — 한 건의 유실도 용납 불가.
  • 이벤트 소싱(Event Sourcing) / CDC — 로그가 곧 진실의 원천. 오프셋 절단은 곧 상태 불일치.
  • acks=all + min.insync.replicas>=2로 어렵게 보장한 내구성 — unclean election은 이 보장을 한 방에 무효화합니다. 4편에서 공들여 세운 내구성 보장을 스스로 허무는 셈이죠.
  • 데이터를 잃느니 차라리 파티션이 잠시 멈추는 편이 낫다고 판단되는 모든 경우.

(드물게) 켜도 괜찮은 경우 (true)

데이터 한 건의 가치가 낮고, 가용성·실시간성이 절대적인 경우에 한해 토픽 단위로 고려합니다.

  • 저가치·고볼륨 텔레메트리/메트릭 — 몇 초치 데이터가 빠져도 추세 분석에 영향이 미미.
  • 유실되어도 무방한 클릭스트림/로그 수집의 일부 — 어차피 샘플링·근사로 다루는 데이터.
  • 다운타임이 데이터 손실보다 비싼 실시간 모니터링 피드 — "최신 상태"가 "완전한 이력"보다 중요한 경우.

이런 경우에도 클러스터 전체가 아니라 해당 토픽에만 per-topic override로 적용하는 것이 원칙입니다. 클러스터 기본값은 false로 두고, 명시적으로 가용성을 택한 토픽만 예외로 여세요.

의사결정 흐름

Loading diagram…

마치며

Unclean Leader Election은 "버그"가 아니라 여러분에게 위임된 결정입니다. Kafka는 데이터와 가용성을 동시에 100% 보장할 수 없는 순간이 오면, 그 선택을 한 줄의 설정으로 여러분의 손에 쥐여 줍니다.

  • 파티션의 모든 ISR 멤버가 죽으면, Kafka는 (a) 오프라인으로 기다리거나 (b) 뒤처진 복제본을 리더로 올리는 두 길 사이에 섭니다.
  • unclean.leader.election.enablefalse(내구성, 기본값)true(가용성) 사이의 트레이드오프 스위치입니다. 현대 Kafka의 기본값 false는 합리적인 선택이며, 함부로 바꾸지 마세요.
  • false일 때의 증상은 오프라인 파티션입니다. OfflinePartitionsCount를 1순위로 알람하고, kafka-topics.sh --describeLeader: none·빈 Isr:을 확인하세요.
  • true일 때의 증상은 무음 데이터 손실입니다. LEO가 거꾸로 가는지, 브로커 로그에 **Truncating ... to offset**이 찍히는지 감시하세요.
  • 판단 기준은 단순합니다. 데이터 무결성이 중요하면 끄고, 한 건의 손실보다 다운타임이 더 비싼 저가치 토픽에서만 per-topic으로 켜세요.
  • 가장 좋은 방어는 애초에 ISR이 비지 않게 하는 것입니다. replication.factor>=3, 랙(rack) 분산 배치, min.insync.replicas>=2, 그리고 4편의 프로듀서 내구성 설정으로 이 극단적 상황 자체의 확률을 낮추세요.

다음 편에서는 이 모든 복제 동작의 무대인 컨슈머 그룹 리밸런싱과 stuck consumer 문제를 다룹니다.

참고 자료


— Data Dynamics 엔지니어링 팀