본문 바로가기

Data/Kafka

[Kafka] 카프카(Kafka)는 어떻게 메시지를 잃지 않는가

Kafka가 메시지를 잃지 않는 핵심은 ISR(In-Sync Replica) 이다.
ISR이 무엇인지, 어떻게 동작하는지 이해하면 왜 특정 설정이 필요한지도 자연스럽게 납득된다.

이번 편에서는 ISR 원리를 중심으로, 그것을 실제로 작동시키는 설정까지 함께 다룬다.

 


1. 배경 — Kafka가 데이터를 저장하는 방식

본격적인 ISR 이야기 전에 Kafka의 저장 구조를 간단히 짚고 넘어간다.

Kafka는 메시지를 Commit Log에 저장한다. 일반적인 메시지 큐가 Consumer가 소비하면 메시지를 삭제하는 것과 달리, Kafka는 소비 여부와 상관없이 디스크에 그대로 보존하고 새 메시지는 항상 뒤에만 추가(Append-Only)한다.

 

 

이 구조 덕분에 Consumer가 다운됐다 재시작해도, 마지막으로 읽은 오프셋 위치부터 다시 이어서 읽을 수 있다. 그러나 Commit Log는 단일 브로커 안에서의 이야기다. 그 브로커 자체가 죽으면 데이터에 접근할 수 없다. 이 문제를 해결하는 것이 바로 ISR이다.

 


2. 핵심 — ISR이란 무엇인가

2-1. ISR의 정의

ISR(In-Sync Replica) 은 "Leader와 동기화가 완료된 Replica의 집합"이다.

 

Kafka는 하나의 파티션을 여러 브로커에 복제해둔다. 이때 복제본들이 모두 동등한 게 아니다. Leader를 따라잡은 Replica만 ISR에 포함되고, 뒤처진 Replica는 ISR에서 제외된다.

 

ISR에 속한 Replica는 Leader와 동일한 데이터를 갖고 있다는 보장이 있다. 이것이 유실 방지의 핵심이다.

 

2-2. 복제 흐름과 Failover

Follower는 Leader에게 주기적으로 데이터를 Pull해서 따라잡는다. Leader는 ISR 목록을 관리하면서 일정 시간(replica.lag.time.max.ms) 이상 따라오지 못하는 Follower를 ISR에서 제외한다.

 

Leader 장애가 발생하면 Kafka는 ISR 안에 있는 Replica 중 하나를 새 Leader로 선출한다. ISR 안에 있다는 건 Leader와 데이터가 동일하다는 의미이므로, 장애 직전까지 기록된 메시지가 고스란히 보존된다.

 

반대로 말하면, ISR 밖에 있는 Replica가 Leader로 선출되면 그 Replica가 따라잡지 못한 만큼의 메시지가 유실된다. 이를 막는 설정이 unclean.leader.election.enable=false다.

 

2-3. ISR이 유실 방지의 핵심인 이유

정리하면 이렇다.

메시지 유실의 주요 원인
  → 브로커 장애 시 복제되지 않은 메시지가 사라짐

ISR의 역할
  → 항상 Leader와 동기화된 Replica 집합을 유지
  → Leader 장애 → ISR 내 Replica가 즉시 승계
  → 유실 없이 서비스 지속

Commit Log가 "단일 브로커 안에서 메시지를 보존하는 구조"라면, ISR은 "브로커 장애가 나도 메시지를 보존하는 메커니즘"이다. 이 둘이 합쳐져야 진짜 내구성이 생긴다.

 


3. ISR을 제대로 작동시키는 설정

ISR은 설정이 맞아야 실제로 유실을 막는다. 관련 설정은 Producer와 Broker 두 곳에 걸쳐 있다.

 

3-1. acks=all — ISR과 연동되는 핵심 설정

acks는 Producer가 메시지를 보낸 뒤 몇 개의 브로커에게 확인을 받을지 결정한다.

  • acks=0: 확인 없이 전송. 브로커가 받았는지조차 모름
  • acks=1: Leader만 확인. Leader가 ack를 보낸 직후 Replica에 복제되기 전에 죽으면 유실
  • acks=all: ISR 전체가 복제를 완료한 뒤 ack. 이 시점 이후에는 Leader가 죽어도 ISR 내 Replica가 이어받으므로 유실 없음

acks=1이 위험한 이유를 명확히 이해하는 게 중요하다. "Leader가 받았으면 된 것 아닌가?" 싶지만, Leader가 ack를 보낸 시점과 Replica에 복제된 시점 사이에 장애가 나면 그 메시지는 어디에도 없다.

acks=all

 

3-2. min.insync.replicas — ISR의 안전망

acks=all은 "현재 ISR 전체에 복제"를 의미한다. 그런데 ISR이 1개만 남아 있다면(= Leader 혼자), acks=all이어도 사실상 acks=1과 다를 바 없다.

 

min.insync.replicas쓰기를 허용하는 최소 ISR 수를 강제한다. ISR이 이 값보다 적으면 NotEnoughReplicasException을 발생시켜 쓰기 자체를 거부한다. 데이터를 잃느니 쓰기를 멈추는 쪽을 선택하는 것이다.

# replication.factor=3일 때 권장
min.insync.replicas=2

min.insync.replicasreplication.factor와 같게 설정하면 브로커 1대만 죽어도 쓰기 전체가 불가해진다. replication.factor - 1이 적절한 값이다.

 

3-3. unclean.leader.election.enable=false — ISR 외부 선출 차단

unclean.leader.election.enable=false

ISR에 없는 Replica가 Leader로 선출되는 것을 막는 설정이다. true이면 ISR이 모두 죽었을 때 뒤처진 Replica라도 Leader로 올려 가용성을 유지하지만, 그 Replica가 따라잡지 못한 메시지는 영구 유실된다. 데이터 정합성이 중요한 서비스에서는 반드시 false로 설정한다.

 

3-4. replication.factor — 복제본 수

default.replication.factor=3

ISR이 제대로 작동하려면 복제본이 충분해야 한다. 프로덕션에서는 최소 3이 권장된다. 브로커 1대가 장애나도 ISR에 2개가 남아 min.insync.replicas=2 조건을 유지할 수 있기 때문이다.

 

3-5. enable.idempotence=true — 재시도 중복 방지

acks=all과 함께 retries를 높게 설정하면 네트워크 순간 장애 시 같은 메시지가 두 번 전송될 수 있다.

enable.idempotence=true

이 설정은 Producer가 각 메시지에 Sequence Number를 부여하고, Broker가 이미 받은 번호의 메시지를 자동으로 무시하게 한다. acks=allretries > 0이 자동으로 강제되므로, 이 옵션 하나면 재시도로 인한 중복 없이 유실 방지가 완성된다.

retries=2147483647          # 사실상 무한 재시도
retry.backoff.ms=100
delivery.timeout.ms=120000  # 전체 전송 제한 시간 (2분)

 


4. Consumer 측 유실 방지 — 오프셋 커밋

ISR 덕분에 Broker에 메시지가 안전하게 보존되더라도, Consumer가 "어디까지 처리했는지"를 잘못 관리하면 유실이 생긴다.

 

Kafka에서 Consumer는 오프셋(offset) 번호로 자신의 위치를 관리하고, 처리가 끝난 위치를 Broker에 기록하는 것을 오프셋 커밋이라 한다.

 

자동 커밋(enable.auto.commit=true)은 일정 시간마다 알아서 오프셋을 커밋한다. 메시지를 처리하기 전에 커밋이 먼저 되면, 그 사이에 Consumer가 crash될 경우 재시작 시 해당 메시지를 건너뛰어 유실된다.

 

수동 커밋(enable.auto.commit=false)은 처리 완료 후 직접 커밋하므로 유실이 없다. 중복 처리가 생길 수 있지만, 이는 Consumer에서 멱등성 처리로 대응할 수 있다.

 

try {
    while (running) {
        ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
        for (ConsumerRecord<String, String> record : records) {
            process(record);       // 1. 처리 먼저
        }
        consumer.commitAsync();    // 2. 평소: 비동기 커밋 (성능 우선)
    }
} finally {
    consumer.commitSync();         // 3. 종료 시: 동기 커밋 (안전하게 마무리)
    consumer.close();
}

commitAsync()는 실패 시 재시도하지 않는다. 종료 시점에는 반드시 commitSync()로 마무리해야 마지막 오프셋이 안전하게 저장된다.

 


5. 실전 설정 예시

Producer

acks=all
enable.idempotence=true         # acks=all + retries 자동 강제
retries=2147483647
retry.backoff.ms=100
delivery.timeout.ms=120000

# 배치 성능 (선택)
linger.ms=5
batch.size=16384
compression.type=snappy

Broker

default.replication.factor=3
min.insync.replicas=2
unclean.leader.election.enable=false
log.retention.hours=168         # 7일

Consumer

enable.auto.commit=false
auto.offset.reset=earliest      # 오프셋 없을 때 처음부터 소비
max.poll.interval.ms=300000     # 처리 시간 여유 (5분)
session.timeout.ms=30000

 


6. 정리 & 체크리스트

Kafka가 메시지를 잃지 않는 구조를 한 줄로 요약하면 이렇다.

Commit Log로 단일 브로커 안에서 보존하고, ISR 복제로 브로커 장애에서 살아남는다.
그리고 오프셋 커밋으로 Consumer가 처리한 위치를 정확히 기록한다.

계층 핵심 원리 설정
Broker ISR 복제 — 장애 시 데이터 보존 replication.factor,
min.insync.replicas,
unclean.leader.election.enable
Producer acks=all — ISR 전체 복제 확인 후 전송 완료 acks=all,
enable.idempotence
Consumer 수동 커밋 — 처리 완료 후 오프셋 기록 enable.auto.commit=false

 

Producer

  • acks=all
  • enable.idempotence=true
  • retries 충분히 설정
  • delivery.timeout.ms 설정

Broker

  • replication.factor >= 3
  • min.insync.replicas = replication.factor - 1
  • unclean.leader.election.enable=false

Consumer

  • enable.auto.commit=false
  • 처리 완료 후 커밋
  • auto.offset.reset=earliest