[Kafka 운영 ④] acks · min.insync.replicas · 복제 인수 — 내구성의 삼위일체
acks, 복제 인수(RF), min.insync.replicas가 어떻게 맞물려 Kafka의 데이터 내구성과 가용성을 결정하는지 정리합니다. RF=3·min.insync.replicas=2·acks=all 안전 설정의 원리와 트레이드오프, 설정 조합표, ISD 동작을 다룹니다.
장애가 났을 때 "데이터가 사라졌다"는 말을 듣는 것만큼 운영자를 식은땀 나게 하는 순간도 없습니다. Kafka는 분산 로그라 데이터를 여러 브로커에 복제하지만, 복제만 켜둔다고 안전한 것은 아닙니다. 프로듀서의 acks, 토픽의 복제 인수(replication factor), 브로커의 min.insync.replicas — 이 세 가지가 정확히 맞물려야 비로소 "메시지가 정상 응답을 받았으면 절대 잃지 않는다"는 보장이 성립합니다. 하나라도 어긋나면 다른 둘이 아무리 보수적이어도 무너집니다.
이 글에서는 이 세 설정을 따로따로가 아니라 하나의 시스템으로 묶어 이해합니다. 흔히 정답처럼 인용되는 RF=3, min.insync.replicas=2, acks=all이 왜 안전한지, 그리고 그 안전을 위해 무엇을 포기하는지를 끝까지 따라가 봅니다.
이 글에서 배우는 것
acks세 가지 모드(0/1/all)의 정확한 의미와 흔한 오해- 복제 인수와 ISR(In-Sync Replicas)의 동작 원리
min.insync.replicas가acks=all과 만났을 때 일어나는 일RF=3 · min.insync.replicas=2 · acks=all이 안전한 이유와 한계- 내구성과 가용성의 트레이드오프를 한눈에 보는 설정 조합표
이 글은 Kafka 운영 트러블슈팅 시리즈의 4편입니다. 어떤 조건에서 메시지가 유실되는지는 [3편 — 메시지 유실 시나리오]에서 다뤘고, 본문에서 잠깐 등장하는 unclean leader election의 위험성은 [5편 — Unclean Leader Election]에서 깊이 파헤칩니다. 이 글은 그 사이를 잇는 "정상 운영 시 내구성을 어떻게 설계하는가"에 해당합니다.
1. 내구성의 삼위일체 개관
Kafka에서 "쓰기 한 건"이 안전하게 보관되기까지는 세 개의 서로 다른 계층이 관여합니다. 각 설정이 누가 소유하고, 무엇을 결정하는지를 먼저 분리해 두는 것이 핵심입니다.
| 설정 | 소유 주체 | 적용 범위 | 결정하는 것 |
|---|---|---|---|
acks | 프로듀서 | 프로듀서별 / 요청별 | 프로듀서가 언제 응답을 받았다고 간주하는가 |
replication.factor | 토픽 | 파티션별 | 데이터 사본을 몇 개 유지하는가 |
min.insync.replicas | 브로커 / 토픽 | 토픽(또는 브로커 기본) | acks=all에서 쓰기를 허용할 최소 동기 사본 수 |
세 가지가 모두 보수적으로 맞물려야 보장이 생깁니다. 비유하자면 다음과 같습니다.
- 복제 인수는 "사본을 몇 부 만들 것인가" — 안전 금고의 개수.
- **
min.insync.replicas**는 "몇 부가 실제로 채워졌을 때 거래를 승인할 것인가" — 거래 승인의 정족수(quorum). - **
acks**는 "프로듀서가 그 정족수를 기다릴 것인가" — 영수증을 언제 받을 것인가.
복제 인수를 3으로 늘려도 프로듀서가 acks=0이면 사본이 채워지길 기다리지 않으니 무의미합니다. 반대로 acks=all을 켜도 min.insync.replicas=1이면 사본 하나만 있어도 응답이 나가버립니다. 세 개가 한 팀이라는 점을 처음부터 못 박아 둡시다.
2. acks — 프로듀서가 무엇을 기다리는가
acks는 프로듀서가 브로커에게 "이 메시지를 받았다고 인정하기 전에 무엇을 확인할 것인가"를 정합니다. 세 가지 값이 있습니다.
acks | 기다리는 대상 | 내구성 | 처리량/지연 | 유실 위험 |
|---|---|---|---|---|
0 | 아무것도 안 기다림 | 매우 낮음 | 최고 처리량 | 전송만 하고 잊음 — 네트워크 드롭도 모름 |
1 | 리더만 기록 | 중간 | 빠름 | 팔로워 복제 전 리더 장애 시 유실 |
all(=-1) | 모든 ISR이 기록 | 높음 | 가장 느림 | ISR 정의가 충분하면 사실상 무손실 |
acks=0 — 보내고 잊기
프로듀서는 메시지를 소켓에 써넣고 곧바로 다음으로 넘어갑니다. 브로커의 응답을 기다리지 않으므로 네트워크에서 패킷이 사라져도, 브로커가 거부해도 프로듀서는 성공으로 간주합니다. 지연 시간이 가장 짧고 처리량이 가장 높지만, 메트릭·로그 수집처럼 일부 유실을 감내할 수 있는 경우에만 씁니다.
acks=1 — 리더만 확인
리더 브로커가 자신의 로그에 메시지를 기록하면 즉시 응답합니다. 문제는 리더가 응답한 직후, 팔로워가 아직 복제를 끝내기 전에 리더가 죽는 순간입니다. 새 리더는 그 메시지를 본 적이 없으므로 데이터는 사라집니다. 프로듀서는 이미 "성공" 응답을 받았기 때문에 재전송하지 않습니다. 이것이 3편에서 다룬 전형적인 유실 시나리오입니다.
acks=all — 그런데 "all"이 전부는 아니다
가장 중요한 오해를 먼저 풀고 갑니다. acks=all은 "모든 복제본(replica)"이 아니라 "모든 ISR(In-Sync Replicas)"을 기다립니다.
RF=3 이고 팔로워 1대가 뒤처져 ISR에서 빠진 상황:
복제본(replica) 집합 = { leader, follower-A, follower-B } (3개)
ISR 집합 = { leader, follower-A } (2개) ← follower-B 탈락
acks=all 은 ISR(2개)만 기다린다 → follower-B 를 기다리지 않는다.즉 뒤처진 팔로워는 응답 경로에서 제외됩니다. 만약 acks=all이 정말로 "모든 복제본"을 기다린다면, 팔로워 한 대만 느려져도 전체 쓰기가 멈출 것입니다. Kafka는 그렇게 동작하지 않습니다. 대신 "몇 개의 ISR이 남아야 쓰기를 허용할지"의 하한선은 별도 설정인 min.insync.replicas로 통제합니다. 바로 다음 절들의 주제입니다.
# 프로듀서 설정 (producer.properties 또는 코드)
acks=all
enable.idempotence=true # 중복 없는 정확히 한 번 전송 (acks=all 강제)
retries=2147483647 # 재시도로 일시적 장애 흡수
max.in.flight.requests.per.connection=5 # 멱등 프로듀서에서 순서 보장 상한
enable.idempotence=true를 켜면 프로듀서는acks=all을 강제하고 재시도 시 중복을 제거합니다. 내구성을 진지하게 다룬다면 사실상 기본값처럼 켜 두는 것을 권합니다.
3. 복제 인수와 ISR
복제 인수(replication factor)
복제 인수는 각 파티션의 사본을 몇 개 유지할지를 정하는 토픽 단위 설정입니다. RF=3이면 한 파티션이 브로커 3대에 걸쳐 존재합니다. 그중 하나가 **리더(leader)**로 모든 읽기·쓰기를 처리하고, 나머지 둘은 **팔로워(follower)**로 리더의 로그를 계속 따라 복제합니다.
# RF=3 으로 토픽 생성
kafka-topics.sh --create \
--topic orders \
--partitions 6 \
--replication-factor 3 \
--bootstrap-server localhost:9092RF는 "동시에 몇 대의 브로커 장애를 견딜 수 있는가"의 상한을 정합니다. RF=3이면 이론상 최대 2대 손실까지 데이터 자체는 살아남습니다. 다만 "데이터가 살아남는 것"과 "쓰기를 계속 받을 수 있는 것"은 다른 문제이며, 후자를 결정하는 것이 다음의 ISR과 min.insync.replicas입니다.
ISR(In-Sync Replicas)
ISR은 현재 리더와 충분히 동기화된 복제본의 집합입니다. 리더 자신은 항상 ISR에 포함되고, 팔로워는 replica.lag.time.max.ms(기본 30초) 안에 리더의 최신 로그를 따라잡고 있으면 ISR에 머뭅니다. 이 시간 안에 따라잡지 못하면 ISR에서 제거되고, 다시 따라잡으면 복귀합니다.
ISR이 중요한 이유는 두 가지입니다.
acks=all이 기다리는 대상이 바로 ISR입니다(전체 복제본이 아니라).- 리더가 죽으면 새 리더는 원칙적으로 ISR 안에서만 선출됩니다. ISR 밖의 뒤처진 복제본이 리더가 되는 것을 허용하면 — 이것이 unclean leader election이며, 데이터 유실로 직결됩니다(5편 참고).
# ISR 상태 확인 — Isr 컬럼이 Replicas 보다 적으면 뒤처진 복제본이 있다는 뜻
kafka-topics.sh --describe --topic orders \
--bootstrap-server localhost:9092
# 예시 출력
# Topic: orders Partition: 0 Leader: 1 Replicas: 1,2,3 Isr: 1,2
# ^^^ broker 3 이 ISR 에서 빠짐위 출력에서 Replicas: 1,2,3이지만 Isr: 1,2라면, 브로커 3의 복제본이 30초 이상 뒤처져 ISR에서 빠진 상태입니다. 이 시점에 동기 사본은 2개뿐입니다.
4. min.insync.replicas — 쓰기의 정족수
min.insync.replicas(줄여서 min.isr)는 acks=all 쓰기를 받아들이기 위해 ISR에 최소 몇 개의 복제본이 있어야 하는지를 정합니다. 토픽 또는 브로커 단위로 설정합니다.
핵심 규칙은 단순합니다.
acks=all일 때, ISR 크기 <min.insync.replicas이면 쓰기는 거부된다.
거부될 때 프로듀서가 받는 예외는 다음 두 가지입니다.
| 예외 | 발생 시점 |
|---|---|
NotEnoughReplicasException | 쓰기 시도 시작 시점에 이미 ISR이 부족 — 로그에 추가되기 전 거부 |
NotEnoughReplicasAfterAppendException | 리더 로그에는 추가됐지만, 필요한 만큼의 ISR이 복제를 확인해 주지 못함 |
두 번째 예외가 미묘합니다. 메시지가 리더 로그에는 들어갔을 수 있지만 정족수를 못 채웠으므로 프로듀서 관점에서는 실패로 처리되어 재시도됩니다(멱등 프로듀서면 중복 없이). 즉 Kafka는 이런 경우 "성공했다고 거짓말하느니 차라리 실패를 알린다"는 쪽을 택합니다.
# 브로커 기본값 (server.properties)
min.insync.replicas=2
# 또는 토픽별로 (운영 권장 — 토픽마다 내구성 요구가 다르므로)
kafka-configs.sh --alter --topic orders \
--add-config min.insync.replicas=2 \
--bootstrap-server localhost:9092중요:
min.insync.replicas는acks=all일 때만 효력이 있습니다. 프로듀서가acks=1로 보내면 이 설정은 무시되고 리더 혼자 응답합니다. 그래서 세 설정을 반드시 함께 맞춰야 합니다 — 토픽에min.insync.replicas=2를 걸어 두어도 프로듀서가acks=1이면 아무 효과가 없습니다.
5. 세 설정의 상호작용 — 왜 RF=3 · min.isr=2 · acks=all 인가
이제 셋을 합쳐 봅니다. 가장 널리 인용되는 안전 설정은 다음과 같습니다.
replication.factor = 3
min.insync.replicas = 2
acks = all이 조합이 왜 "황금 비율"로 불리는지 단계별로 따라가 봅시다.
정상 상태 (브로커 3대 정상)
ISR = = 3개. min.isr=2 충족. 모든 쓰기는 리더 + 최소 1개 팔로워가 받으면 응답됩니다(acks=all은 ISR 전체를 기다리므로 실제로는 3개 모두지만, 한 대가 뒤처져도 2개만 남으면 그대로 통과).
브로커 1대 장애 (ISR이 2로 감소)
한 대가 죽으면 ISR = 2개. min.isr=2를 정확히 만족하므로 쓰기는 계속됩니다. 동시에 모든 메시지가 최소 2개 사본에 안전하게 기록됩니다. 즉 1대 장애를 견디면서도 여전히 쓰기 가능합니다. 이것이 이 조합의 핵심 가치입니다.
브로커 2대 장애 (ISR이 1로 감소)
이제 ISR = 1개로 min.isr=2를 못 채웁니다. Kafka는 쓰기를 거부하고 프로듀서는 NotEnoughReplicasException을 받습니다. 아프지만 이것이 올바른 동작입니다. 사본 1개만 남은 상태에서 쓰기를 계속 받았다가 그 1개마저 죽으면 데이터는 영영 사라집니다. Kafka는 조용한 유실 대신 **빠른 실패(fail fast)**를 선택합니다. 프로듀서는 에러를 보고 재시도하거나 알림을 울릴 수 있습니다 — 데이터를 잃는 것보다 훨씬 낫습니다.
위 다이어그램의 흐름을 한 줄로 요약하면: 프로듀서가 보낸 메시지를 리더가 받아 ISR 팔로워들에 복제하고, ISR 크기가 min.insync.replicas 이상임을 확인한 뒤에야 프로듀서에 ack를 돌려준다. 이 "확인" 단계가 바로 무손실 보장의 핵심입니다.
6. 트레이드오프 — 내구성과 가용성은 시소다
min.insync.replicas를 높일수록 내구성은 올라가지만 가용성은 내려갑니다. 극단적으로 **min.insync.replicas = RF**로 설정하면 모든 복제본이 동기 상태여야 쓰기가 되므로 내구성은 최대지만, 단 한 대라도 뒤처지거나 죽는 순간 전체 쓰기가 멈춥니다. 한 대 재시작·롤링 업그레이드만 해도 쓰기가 막히는 셈이라 운영상 거의 쓰지 않습니다.
반대로 min.insync.replicas=1은 acks=all을 켜도 사본 하나만 있으면 응답이 나가므로, 사실상 acks=1과 비슷한 유실 위험으로 떨어집니다.
핵심 공식은 다음과 같습니다.
견딜 수 있는 브로커 장애 수(쓰기 유지) =
RF−min.insync.replicas
RF=3, min.isr=2이면 3 − 2 = 1, 즉 1대 장애까지 쓰기를 유지합니다. 이 공식을 기준으로 설정 조합을 비교해 봅시다.
| RF | min.insync.replicas | acks | 내구성 | 가용성(쓰기 유지) | 견디는 장애 | 평가 |
|---|---|---|---|---|---|---|
| 1 | 1 | 1 | 매우 낮음 | 단일 장애에 전면 중단 | 0대 | 개발/테스트 전용 |
| 3 | 1 | all | 낮음 | 2대 장애까지 쓰기 가능 | 2대(쓰기), 단 유실 가능 | acks=all 무색 — 권장 안 함 |
| 3 | 2 | 1 | 낮음 | 높음 | — | min.isr 무력화(acks=1) — 권장 안 함 |
| 3 | 2 | all | 높음 | 1대 장애까지 쓰기 가능 | 1대 | 표준 안전 설정 ✅ |
| 3 | 3 | all | 최고 | 1대만 빠져도 쓰기 중단 | 0대(쓰기) | 가용성 희생, 특수 용도 |
| 5 | 3 | all | 매우 높음 | 2대 장애까지 쓰기 가능 | 2대 | 미션 크리티컬·고가용 |
표에서 보듯 대부분의 운영 환경에서는 **RF=3 · min.insync.replicas=2 · acks=all**이 내구성과 가용성의 균형점입니다. 금융·결제처럼 더 높은 가용성과 내구성이 동시에 필요하면 **RF=5 · min.isr=3**으로 올려 2대 동시 장애까지 쓰기를 유지하는 구성을 검토합니다.
흔한 실수 체크리스트
acks를 프로듀서에서 안 바꿈: 토픽에min.insync.replicas=2만 걸고 프로듀서는 기본acks=1(또는 라이브러리 기본값)로 두면 정족수 보장이 통째로 사라집니다. 셋은 항상 함께 설정합니다.min.insync.replicas를 RF와 같게 설정: 내구성에 눈이 멀어min.isr=RF로 두면 롤링 재시작 때마다 쓰기가 막힙니다.- 단일 브로커/단일 AZ에 복제본 몰림: RF=3이어도 사본 3개가 같은 랙·같은 가용 영역에 있으면 그 영역 장애 시 동시에 사라집니다.
broker.rack으로 rack-aware 배치를 켜 사본을 분산하세요. unclean.leader.election.enable=true방치: 이 값이 켜져 있으면 ISR 밖 복제본도 리더가 될 수 있어, 위에서 쌓은 모든 보장이 무너집니다. 자세한 메커니즘과 위험은 [5편 — Unclean Leader Election]에서 다룹니다.
마치며
- 데이터 내구성은 하나의 설정이 아니라 세 설정의 합주입니다.
acks(프로듀서가 기다리는 것),replication.factor(사본 수),min.insync.replicas(쓰기 정족수)가 함께 맞물려야 보장이 생깁니다. acks=all은 "모든 복제본"이 아니라 "모든 ISR"을 기다립니다. 뒤처진 팔로워는 응답 경로에서 빠지고, 최소 동기 사본 수의 하한은min.insync.replicas가 통제합니다.- **
RF=3 · min.insync.replicas=2 · acks=all**은 1대 브로커 장애를 견디면서 쓰기를 유지하고, 2대가 죽으면 조용한 유실 대신NotEnoughReplicasException으로 빠르게 실패합니다. - 내구성과 가용성은 시소입니다. **견디는 장애 수 =
RF − min.insync.replicas**라는 공식으로 토픽의 요구사항에 맞춰 균형점을 고르세요. - 세 설정을 아무리 잘 잡아도
unclean.leader.election.enable=true하나면 무너집니다 — 그 이야기는 다음 5편에서 이어집니다.
참고 자료
- Apache Kafka. "Broker Configs" (
min.insync.replicas,replica.lag.time.max.ms,unclean.leader.election.enable) — https://kafka.apache.org/documentation/#brokerconfigs- Apache Kafka. "Producer Configs" (
acks,enable.idempotence,retries) — https://kafka.apache.org/documentation/#producerconfigs- Apache Kafka. "Replication" — https://kafka.apache.org/documentation/#replication
— Data Dynamics 엔지니어링 팀