지난 글에서는 프로듀서, 컨슈머, 브로커, 토픽, 파티션 등 카프카의 핵심 개념과 아키텍처에 대해 알아보았습니다. 프로듀서가 producer.send()
를 통해 메시지를 발행하면, 해당 메시지는 토픽의 파티션으로 전송되고 컨슈머가 이를 가져가 처리하는 흐름을 이해했습니다.
그런데 여기서 한 가지 중요한 질문이 생깁니다. producer.send()
를 호출하고 나면, 우리는 “메시지가 안전하게 전송되었다”라고 언제 확신할 수 있을까요? 프로듀서의 요청이 리더 브로커에 도달하자마자일까요? 아니면 모든 복제본에 저장이 완료되었을 때일까요?
바로 이 신뢰도의 수준을 애플리케이션의 요구사항에 맞게 정밀하게 제어하는 핵심 장치가 바로 프로듀서의 acks
옵션입니다. 이번 글에서는 acks 옵션과 이를 뒷받침하는 ISR(In-Sync Replicas) 개념을 통해 카프카가 어떻게 메시지 전송의 신뢰도를 보장하는지, 그리고 개발자로서 우리는 어떤 트레이드오프를 고려해야 하는지 알아보겠습니다.
acks 옵션
acks는 “acknowledgments”의 약자로, 프로듀서가 보낸 메시지에 대해 브로커로부터 “잘 받았다”는 확인 응답을 얼마나 기다릴지 결정하는 설정 값입니다. 프로듀서 측에서 설정해주어야 합니다.
이 옵션은 크게 세 가지로 나뉩니다.
acks=0
lower latency / no strong durability
프로듀서는 메시지를 브로커로 전송하고, 어떠한 확인 응답도 기다리지 않습니다. (fire and forget) 그냥 네트워크 버퍼에 메시지를 쓰는 순간 “전송 성공”으로 간주합니다.
낮은 지연시간으로 인해 높은 처리량을 보장하지만, 메시지 유실 위험이 매우 높습니다.
일부가 유실되어도 전체 시스템에 큰 영향이 없는 데이터 수집 (로그, 메트릭 수집 등)에 적합합니다.
- 데이터 유실: 브로커가 메시지를 받기 전에 프로듀서에 장애가 발생하거나, 네트워크 문제, 브로커가 다운되는 경우
- 메시지 중복: 발생 X
acks=1
a little higher latency / a little bit better durability
프로듀서는 메시지를 보낸 후, 파티션의 리더 브로커가 자신의 로컬 디스크에 메시지를 기록했다는 확인 응답을 받을 때까지 기다립니다.
보통의 카프카 인프라에선 레플리카 옵션으로 리더 브로커의 데이터를 팔로워에서 주기적으로 복제해 리더 인스턴스의 장애에 대응합니다. 이 옵션은 프로듀서가 리더 브로커한테만 메시지 확인 유무에 대한 응답을 기다립니다. 만약 설정한 시간동안 응답이 오지 않으면, 메시지를 재발행합니다.
리더 브로커에 확인을 받는 만큼, 지연시간이 조금 더 생기지만, 그만큼 유실 위험성을 낮출 수 있습니다.
- 데이터 유실: 리더 파티션이 메시지를 받은 후 응답을 보냈으나, 팔로워 파티션들이 복제하기 전에 리더가 다운
- 메시지 중복: 프로듀서가 리더로부터 응답을 받지 못해 메시지를 재전송할 경우
- ex) 리더가 메시지를 받았지만 ack 응답을 보내기 전에 장애가 나, 프로듀서가 메시지가 전송되지 않았다고 판단하고 재시도한 경우
acks=all (OR -1)
higher latency / highest level of durability
프로듀서는 리더 브로커뿐만 아니라, 해당 파티션을 복제하고 있는 모든 ‘In-Sync’ 상태의 팔로워 브로커들(ISR)까지 메시지를 기록했다는 확인 응답을 모두 받을 때까지 기다립니다.
ISR(In-Sync Replicas)은 리더 파티션을 포함하여 거의 실시간으로 리더의 모든 메시지를 복제하고 있는 레플리카들의 집합입니다.
리더와 팔로워들의 응답을 모두 기다려야 하기 때문에, 지연시간이 가장 깁니다.
- 데이터 유실:
min.insync.replicas
설정보다 적은 수의 레플리카가 동기화되어 있을 때, 프로듀서의 요청이 실패하는 경우 혹은 모든 레플리카가 동시에 다운되는 경우 - 메시지 중복: 모든 ISR에 복제가 완료되었지만, 리더 브로커의 최종 응답을 받지 못하고 프로듀서가 재시도하는 경우
프로듀서 멱등성(Idempotence)
프로듀서 멱등성은 동일한 메시지를 여러 번 전송해도 카프카 브로커에 정확히 한 번만 기록되도록 보장하는 기능입니다. 이 기능은 acks=-1
설정과 결합되어 프로듀서의 재시도로 인한 메시지 중복 문제를 해결합니다.
설정 시 acks=-1을 강제하며, enable.idempotence=true
를 통해 설정합니다.
멱등성이 true로 설정된 프로듀서는 고유한 ID를 할당 받고 각 메시지에 자신의 ID와 Sequence Number를 부여해 브로커에게 전송합니다.
브로커는 각 프로듀서가 파티션에 마지막으로 기록한 메시지의 Sequence Number를 기억합니다. 프로듀서가 메시지를 재전송하면, 브로커는 메시지에 포함된 Sequence Number를 확인하여 이전에 이미 받은 메시지인지 판단합니다.
만약 수신된 메시지의 Sequence Number가 이미 기록된 Sequence Number와 같거나 더 작으면, 브로커는 해당 메시지를 중복으로 간주하고 무시합니다.
이를 통해 단일 세션 내에서의 메시지 중복 문제를 해결할 수 있습니다.
한계
멱등성(Idempotence)은 단일 프로듀서 세션 내에서만 작동합니다.
하지만 프로듀서 프로세스가 충돌하거나 재시작되면, 기존에 처리중이던 메시지도 유실될 수 있고, redis 등으로 재시도 로직을 엔지니어링 해도 새로운 인스턴스는 브로커로부터 완전히 새로운 고유 ID를 할당받기에 중복 발송을 해결할 수 없습니다.
이러한 문제를 해결하기 위해 카프카의 트랜잭션(Transactions) 기능이 필요합니다. 이는 추후 알아보겠습니다.
ISR(In-Sync Replicas)
앞에서 본 acks
의 의미는 결국 “어떤 지점까지 쓰기가 도달하면 성공으로 볼 것인가?”에 대한 합의입니다. 이 합의를 안전하게 이행하려면 리더 단독이 아니라, 리더와 거의 실시간으로 보조하는 복제본 집합이 필요합니다. 그 집합이 ISR(In-Sync Replicas) 입니다. ISR에는 항상 리더가 포함되며, 일정 기준 이상 뒤처지지 않는 팔로워들만 포함됩니다.
LEO, HW
ISR의 동작 원리를 살펴보기 위해 일부 용어를 정의하겠습니다.
LEO(Log End Offset)
각 브로커(리더/팔로워)가 가진 로그의 “마지막” 위치입니다. 새 레코드가 추가되면 해당 브로커의 LEO가 증가합니다.
HW(High Watermark)
파티션의 모든 ISR 구성원이 “공통적으로 도달한” 오프셋입니다. 컨슈머는 기본적으로 HW를 넘어서 읽지 않습니다. 즉, HW를 기준으로 “커밋된 데이터”가 정의됩니다.
프로듀서 쓰기 흐름은 대략 다음과 같습니다.
- 리더가 레코드를 자신의 로그에 append하여 리더 LEO 증가
- 팔로워들이 Fetch 요청으로 리더로부터 레코드를 복제하여 각자 LEO 증가
- ISR 모든 구성원의 LEO가 특정 오프셋 이상으로 정렬되면, 리더가 그 오프셋까지의 HW를 전진
자세한 내용은 위 링크에서 확인할 수 있습니다.
replica.lag.time.max.ms
팔로워가 리더를 일정 시간 이상 따라가지 못하면 ISR에서 제외됩니다. 반대로 충분히 따라잡으면 다시 포함됩니다.
replica.lag.time.max.ms
설정을 통해 관리됩니다. 팔로워가 이 시간 이상 리더 데이터를 가져가지 못하면 ISR에서 제외됩니다.
과거에는 replica.lag.max.messages
라는 메시지 개수 기준이 있었지만, 버전에 따라 비권장/제거 추세입니다.
이 메커니즘 덕분에 ISR은 “지금 이 순간 리더와 의미 있게 동기화된 집합”이라는 성질을 유지합니다. 즉, acks=all
이 기대하는 복제 보장은 ISR의 질에 달려 있습니다.
min.insync.replicas
acks=all
의 의미를 보장하기 위해, ISR의 최소 크기를 강제해야 합니다. min.insync.replicas
설정을 통해 관리됩니다.
acks=all
, replication factor=3
, min.insync.replicas=2
라면,
프로듀서가 메시지를 보내면, 리더가 메시지를 기록하고 팔로워에게 복제합니다. ISR이 3이기에 ack를 받을 수 있습니다.
팔로워 하나가 장애가 난 상황입니다. ISR이 2이지만, min.insync.replicas가 2이기에 리더는 ack를 보내게 됩니다.
만약 min.insync.replicas가 3이였다면, ISR이 존재해서 대응은 여전히 가능한 상황이지만, 리더가 쓰기를 거절해 클러스터가 마비될 수 있겠죠?
따라서 주로 repication factor=3이면, min.insync.replicas=2로 설정이 권장됩니다.
unclean.leader.election.enable
unclean.leader.election.enable=true
면, ISR에 없는 노드도 리더로 승격될 수 있습니다.
가용성은 올라가지만, 새 리더의 로그가 뒤쳐져 있으면 데이터 손실이 발생합니다. 강한 내구성이 요구되는 환경에서는 반드시 false
로 두어야 합니다.
메시지의 key 와 순서 보장
Kafka에서 메시지는 키(Key) 와 값(Value) 으로 구성됩니다. 여기서 키는 메시지의 값에 대한 메타데이터 역할을 하며, Kafka 브로커가 메시지를 어느 파티션에 저장할지 결정하는 중요한 기준이 됩니다.
키가 null
인 경우, 메시지는 별도의 커스텀 파티셔너를 만들지 않는 한 라운드 로빈 방식으로 각 파티션에 균등하게 분배됩니다.
카프카는 파티션 내에서의 순서만 보장합니다. 만약 동일 객체에 관련된 메시지가 순서 보장이 필요할 때, 키 값을 지정하지 않으면 여러 파티션에 분산되기에 메시지의 순서가 보장되지 않습니다.
반면, 키가 null
이 아닌 경우에는 동일한 키를 가진 메시지들은 항상 동일한 파티션에 기록됩니다. 이 덕분에 메시지들은 파티션 내에서 기록된 순서대로 정렬되며, 컨슈머도 그 순서대로 메시지를 읽게 됩니다.
그러나 키가 너무 편향되어 있다면 특정 파티션에 메시지가 몰려 부하 불균형이 발생할 수 있습니다. 예를 들어, 웹사이트에서 ‘Guest’라는 키를 사용하는 메시지가 대부분이라면, 해당 파티션에만 트래픽이 집중되어 성능 문제가 발생할 수 있습니다. 따라서 키를 설계할 때는 메시지를 파티션에 고르게 분산시킬 수 있도록 카디널리티가 높은 값을 사용하는 것이 중요합니다.
마치며
카프카의 메시지 전송 신뢰도는 단순히 “보장된다” 또는 “안된다”의 이분법적인 개념이 아닙니다. acks와 min.insync.replicas 같은 옵션들을 통해 개발자가 서비스의 요구사항에 맞춰 지연 시간, 처리량, 내구성 사이의 관계를 능동적으로 조절할 수가 있죠.
우리가 만드는 서비스의 데이터가 얼마나 중요한지, 약간의 유실이 치명적인지, 아니면 속도가 더 중요한지를 명확히 이해하고 그에 맞는 최적의 옵션을 선택하는 것이 중요하겠습니다.
WAS - 프로듀서 쪽 개념은 이걸로 마무리하고, 다음 글에서는 컨슈머 쪽에서의 순서 보장, 재처리, 오프셋 커밋 전략을 통해 e2e 신뢰도를 완성하는 방법을 살펴보겠습니다.