KERBE.io

TECH / kafka

멱등성 프로듀서가 못 막는 중복, 애플리케이션 레벨 재전송

#Kafka#Producer#메시지큐

Kafka 3.0부터 enable.idempotence의 기본값이 true로 바뀌었습니다. 대부분의 신규 프로듀서는 별다른 설정 없이도 멱등성이 켜진 채로 동작한다는 뜻인데, 이 멱등성이 정확히 어느 범위까지 중복을 막아주는지는 소스를 확인하기 전까지 막연하게만 알고 있었습니다.

멱등성이 막는 것과 못 막는 것

멱등성 프로듀서가 막는 중복은 프로듀서 내부에서 일어나는 자동 재시도입니다. 네트워크 오류나 일시적인 브로커 문제로 배치 전송이 실패해서 프로듀서가 스스로 재시도할 때, 브로커가 시퀀스 번호로 이미 받은 배치인지 판단해서 중복 기록을 막아줍니다.

여기서 안 막아주는 게 두 가지입니다.

  • 애플리케이션 레벨 재전송: send() 호출이 실패를 반환했을 때, 애플리케이션 코드가 이를 감지하고 다시 send()를 호출하는 경우입니다. 이건 프로듀서 입장에서는 완전히 새로운 전송이라 멱등성 시퀀스 추적 범위 밖입니다.
  • 세션을 넘나드는 재시작: 멱등성은 하나의 프로듀서 세션 내에서만 보장됩니다. 프로듀서 프로세스가 재시작되면 새 세션이 시작되고, 재시작 전에 보낸 마지막 메시지가 실제로는 성공했는지 실패했는지 애매한 상태로 남을 수 있습니다.

공식 문서에서 send()가 (이미 무한 재시도를 거쳤음에도) 에러를 반환했다면 프로듀서를 종료하고 마지막으로 보낸 메시지의 중복 여부를 직접 확인하라고 권장하는 이유가 여기 있습니다. 재시도 로직을 애플리케이션 단에서 임의로 다시 만드는 순간 멱등성 보장이 깨질 수 있습니다.

max.in.flight.requests.per.connection이 5로 묶여 있는 이유

멱등성을 켜면 max.in.flight.requests.per.connection을 5 이하로 유지해야 합니다. 이건 임의로 정한 제약이 아니라, 브로커가 프로듀서별로 최근 배치 5개까지만 시퀀스 상태를 추적하기 때문입니다. ProducerStateManager가 유지하는 배치 추적 범위와 정확히 맞물려 있는 값이라, 5를 넘기면 순서 보장이 깨질 수 있는 조합이 됩니다.

멱등성을 끄고 max.in.flight.requests.per.connection을 1보다 크게 둔 채 재시도를 활성화하면, 먼저 보낸 배치가 재시도되는 동안 뒤에 보낸 배치가 먼저 성공해서 순서가 뒤바뀌는 경우가 생깁니다. 순서 보장이 필요하면 멱등성을 켜거나(권장), 재시도를 끄거나, in-flight를 1로 제한하는 것 중 하나를 선택해야 합니다.

트랜잭션이 필요한 경계

여러 파티션이나 여러 토픽에 걸쳐 원자적으로 쓰기가 이뤄져야 한다면, 혹은 세션을 넘나드는 exactly-once가 필요하다면 transactional.id를 써야 합니다. transactional.id를 설정하면 멱등성은 자동으로 함께 켜지고, initTransactions()beginTransaction() → 여러 send()commitTransaction() 흐름으로 씁니다.

producer.initTransactions();
try {
    producer.beginTransaction();
    producer.send(record1);
    producer.send(record2);
    producer.commitTransaction();
} catch (ProducerFencedException | OutOfOrderSequenceException | AuthorizationException e) {
    producer.close(); // 복구 불가, 종료해야 함
} catch (KafkaException e) {
    producer.abortTransaction(); // 재시도 가능
}

트랜잭션을 쓸 때 브로커 쪽에서 챙길 것도 있습니다. 트랜잭션 코디네이터 로그(__transaction_state)의 내구성을 위해 프로덕션에서는 transaction.state.log.replication.factor=3, min.isr=2 정도가 권장됩니다. 개발 환경에서 편의상 replication factor를 1로 낮춰 테스트하던 설정을 그대로 운영에 배포하는 실수가 은근히 흔합니다.

정리

멱등성 프로듀서는 "재시도했을 때 중복이 안 생긴다"는 좁은 범위의 보장입니다. 애플리케이션이 실패를 감지하고 스스로 다시 보내는 로직, 프로세스 재시작을 넘나드는 상황까지 커버하려면 트랜잭션이 필요합니다. enable.idempotence=true(기본값)와 acks=all을 그대로 두고 delivery.timeout.ms로 재시도 예산을 관리하는 정도가 일반적인 신뢰성 워크로드의 기본선이고, 그 이상의 원자성이 필요할 때만 트랜잭션으로 넘어가는 순서가 맞습니다.