왜 acks=all만으로는 부족한가

많은 팀이 "프로듀서에 acks=all을 걸었으니 메시지가 안전하다"고 믿는다. 하지만 이 설정만으로는 데이터 유실을 막지 못한다. acks=all은 "현재 ISR(In-Sync Replicas)에 속한 모든 리플리카가 메시지를 받으면 ack를 준다"는 의미일 뿐, ISR에 몇 개가 있어야 하는지는 규정하지 않는다.

문제는 장애 상황에서 드러난다. 리더 브로커 하나만 살아있고 팔로워들이 모두 ISR에서 빠진 상태라면, ISR 크기는 1이다. 이때도 acks=all은 정상적으로 ack를 반환한다. 리더 한 대에만 쓰인 메시지는 그 브로커의 디스크가 죽는 순간 함께 사라진다.

min.insync.replicas의 역할

min.insync.replicas는 "쓰기를 허용하기 위해 최소 몇 개의 리플리카가 ISR에 있어야 하는가"를 강제한다. acks=all과 함께 동작할 때만 의미가 있으며, ISR 크기가 이 값보다 작으면 프로듀서 쓰기는 NotEnoughReplicasException으로 거부된다.

핵심은 "쓰기를 실패시켜서라도 유실을 막는다"는 것이다. 조용히 데이터를 잃는 것보다 명시적으로 실패하는 편이 훨씬 다루기 쉽다.

replication.factor와의 조합

세 값을 함께 봐야 한다. 복제 팩터(RF), min.insync.replicas(minISR), 프로듀서 acks다. 실무 표준은 RF=3, minISR=2, acks=all이다.

RFminISR허용 가능한 브로커 장애 수내구성
321대 (쓰기 유지)권장 구성
330대 (1대만 죽어도 쓰기 중단)가용성 낮음
312대유실 위험
220대비권장

RF=3, minISR=2면 브로커 1대가 죽어도 ISR이 2를 유지하므로 쓰기가 계속되고, 최소 2벌의 복제본이 항상 보장된다. minISR을 RF와 같게 잡으면 단 한 대의 장애로 전체 쓰기가 멈추므로 가용성이 크게 떨어진다.

토픽·브로커 설정 예시

min.insync.replicas는 브로커 기본값으로 걸고, 중요한 토픽은 토픽 단위로 오버라이드하는 방식을 권장한다.

## server.properties (브로커 기본값)
default.replication.factor=3
min.insync.replicas=2
unclean.leader.election.enable=false

## 토픽 생성 시 명시적으로 지정
kafka-topics.sh --create \
  --topic payments \
  --bootstrap-server localhost:9092 \
  --replication-factor 3 \
  --config min.insync.replicas=2

## 기존 토픽 변경
kafka-configs.sh --alter \
  --topic payments \
  --bootstrap-server localhost:9092 \
  --add-config min.insync.replicas=2

unclean.leader.election.enable=false도 반드시 함께 꺼야 한다. 이 값이 true면 ISR에 없던 뒤처진 리플리카가 리더로 승격되면서 커밋된 데이터가 덮어써질 수 있다.

프로듀서 설정

브로커 쪽 설정만으로는 절반이다. 프로듀서가 acks=all이 아니면 minISR은 무시된다. 멱등성과 재시도까지 함께 켜야 중복 없이 안전하게 재전송된다.

from confluent_kafka import Producer

producer = Producer({
    'bootstrap.servers': 'localhost:9092',
    'acks': 'all',                    # ISR 전체 확인
    'enable.idempotence': True,       # 중복 방지 + 순서 보장
    'retries': 2147483647,            # 사실상 무한 재시도
    'max.in.flight.requests.per.connection': 5,
    'delivery.timeout.ms': 120000,
})

def on_delivery(err, msg):
    if err:
        # NotEnoughReplicasException 등은 여기로 온다
        raise RuntimeError(f'전송 실패: {err}')

producer.produce('payments', value=b'{"id": 1}', callback=on_delivery)
producer.flush()

enable.idempotence=True를 켜면 acks=all이 자동으로 강제되고, 재시도 시 시퀀스 번호로 중복을 걸러낸다.

운영 시 주의점

  • 쓰기 중단을 장애로 인식하라. minISR 미달로 쓰기가 막히는 것은 정상 동작이다. NotEnoughReplicasException이 발생하면 프로듀서가 죽는 게 아니라 브로커 복구 후 재전송되도록 처리해야 한다.
  • ISR 축소를 모니터링하라. UnderMinIsrPartitionCount와 UnderReplicatedPartitions 지표를 알람으로 걸어 ISR이 줄어드는 순간을 감지한다.
  • 롤링 재시작 순서. RF=3, minISR=2에서는 한 번에 브로커 1대씩만 내려야 한다. 2대를 동시에 내리면 해당 파티션의 쓰기가 즉시 멈춘다.
  • RF는 반드시 minISR보다 크게. RF=minISR이면 여유가 0이라 단일 장애로 서비스가 멈춘다.

정리

데이터 내구성은 프로듀서 한쪽 설정으로 완성되지 않는다. acks=all, min.insync.replicas=2, replication.factor=3, unclean.leader.election.enable=false가 하나의 세트로 맞물려야 "커밋된 메시지는 최소 2벌 존재하고, 조용히 사라지지 않는다"는 보장이 성립한다. 가용성과 내구성은 트레이드오프이며, minISR은 그 균형점을 명시적으로 선택하는 손잡이다.