에디블로그
Engineer's Field Notes

AI 자동화로 매일
한 편씩 쓰는
엔지니어 운영 노트

Claude Code · 자동화 파이프라인 · 사고 회고까지. 잘 굴러간 기록 + 깨진 흔적도 같이 남깁니다.

사람이 할 수 있는 일은,
AI도 할 수 있어야 합니다.
매일 한 편 쓰면서 검증 중.
— 이번 주 가장 많이 읽힌 글 TOP 3
백엔드

[Kafka] 프로듀서 acks=all, min.insync.replicas, 멱등 프로듀서: 메시지 유실 막고 순서 보장하기

반응형
[Kafka] 프로듀서 acks=all, min.insync.replicas, 멱등 프로듀서: 메시지 유실 막고 순서 보장하기

[Kafka] 프로듀서 acks=all, min.insync.replicas, 멱등 프로듀서: 메시지 유실 막고 순서 보장하기

지금까지 카프카 컨슈머의 신뢰성을 다뤘는데, 사실 메시지를 안전하게 받으려면 먼저 안전하게 보내야 해요. 프로듀서가 메시지를 어떻게 보내느냐에 따라 유실이 생기기도 하고, 순서가 뒤섞이기도 해요. 컨슈머를 아무리 잘 만들어도 프로듀서에서 메시지가 새면 소용없죠.

acks가 무엇을 보장하는지에서 시작해 min.insync.replicas와의 조합, 재시도와 순서 보장, max.in.flight.requests의 함정, 멱등 프로듀서, 배치와 압축 같은 성능 설정, 자주 만나는 문제까지 짚어요. 컨슈머 쪽은 @KafkaListener 컨슈머 설정부터 보면 좋아요. KafkaTemplate 전송 코드 구현 자체는 KafkaTemplate Producer 글에 따로 있어요. 이 글은 유실 없이 보내는 설정에 집중해요.

01. acks, 몇 명이 받아야 성공인가

acks는 프로듀서가 "전송 성공"으로 인정하기까지 몇 개의 복제본이 메시지를 받아야 하는지를 정해요. 세 가지예요.

카프카 프로듀서 acks 비교. acks=0은 응답을 안 기다려 가장 빠르지만 유실 통제가 안 되고, acks=1은 리더만 확인해 리더 장애 순간 유실되며, acks=all은 모든 ISR이 받아야 성공이라 가장 안전하다

  • acks=0 — 보내고 응답을 안 기다려요. 가장 빠르지만, 브로커가 못 받아도 성공으로 쳐서 유실 위험이 커요.
  • acks=1 — 파티션 리더가 받으면 성공. 리더가 받은 직후 복제 전에 죽으면 유실될 수 있어요.
  • acks=all (또는 -1) — 모든 in-sync 복제본(ISR)이 받아야 성공. 가장 안전해요.

각 단계는 안전성과 지연의 트레이드오프예요. 유실이 절대 안 되는 데이터(주문·결제·정산)는 acks=all이 기본이에요. 로그·메트릭처럼 일부 유실을 감수할 수 있는 대량 데이터는 acks=1이나 0으로 처리량을 올리기도 해요. 무엇을 보내느냐에 따라 고르는 거예요.

spring:
  kafka:
    producer:
      acks: all

02. acks=all만으로는 부족해요, min.insync.replicas

acks=all은 "모든 ISR이 받아야 한다"는 뜻인데, 함정이 있어요. ISR(In-Sync Replicas)은 리더와 보조를 맞춘 복제본들의 집합인데, 복제본이 밀리거나 죽으면 ISR에서 빠져요.

acks=all과 min.insync.replicas. 복제본이 ISR에서 탈락해 리더만 남으면 acks=all이 사실상 acks=1로 약해지므로, min.insync.replicas=2를 같이 걸어 ISR이 부족하면 쓰기를 거부하게 한다

만약 복제본이 다 빠지고 리더 하나만 ISR에 남았다면, "모든 ISR"이 리더 하나뿐이라 사실상 acks=1처럼 동작해요. 그 리더가 죽으면 유실이죠. acks=all을 믿었는데 보장이 사라지는 거예요.

그래서 min.insync.replicas를 같이 설정해요. 이건 "최소 이만큼의 복제본이 살아 있어야 쓰기를 허용한다"는 브로커·토픽 설정이에요. 표준 조합은 복제 팩터 3 + acks=all + min.insync.replicas=2예요. 복제본 하나가 죽어도 2개가 받으니 쓰기가 계속되고, 2개 미만으로 떨어지면 쓰기를 거부해 유실을 막아요. 거부는 보이는 장애지만, 유실은 안 보이는 사고예요. acks와 min.insync.replicas는 한 쌍으로 봐야 해요.

03. 재시도, 일시 장애를 넘기기

전송이 실패하면 프로듀서는 재시도해요. 네트워크 순단이나 리더 선출 같은 일시적 장애는 재시도로 넘어가요. 이게 있어서 짧은 장애에는 애플리케이션이 신경 쓰지 않아도 돼요.

예전에는 retries 횟수를 직접 정했지만, 지금은 delivery.timeout.ms(기본 2분)로 "이 시간 안에서 알아서 재시도"하게 두는 게 권장이에요. 그 시간을 넘기면 최종 실패로 콜백에 에러가 와요. 횟수를 세는 것보다 "언제까지 시도할지"가 운영에 더 직관적이에요.

spring:
  kafka:
    producer:
      acks: all
      properties:
        delivery.timeout.ms: 120000

전송 결과는 콜백으로 받아요.

kafkaTemplate.send("orders", key, event)
    .whenComplete((result, ex) -> {
        if (ex != null) {
            log.error("send 실패: {}", key, ex);  // 실패 처리 필수
        }
    });
이 콜백을 비워두는 게 흔한 함정이에요. 재시도까지 다 소진하고 최종 실패한 메시지는 콜백에만 알려지는데, 거기서 아무것도 안 하면 메시지가 조용히 사라져요. "성능 문제"가 아니라 "유실 사고"로 나타나는 거죠. 최종 실패는 최소한 로깅·알림을 붙이고, 잃으면 안 되는 데이터면 재발행 경로까지 설계해야 해요.

04. 재시도가 순서를 뒤섞을 수 있어요

여기가 중요한 함정이에요. 재시도가 순서를 깨뜨릴 수 있어요.

카프카 재시도 순서 역전. max.in.flight가 1보다 클 때 메시지 A가 실패해 재시도되는 사이 B가 먼저 성공하면 파티션에 B, A 순서로 쌓인다. 멱등 프로듀서를 켜면 시퀀스 번호로 순서를 보장한다

max.in.flight.requests.per.connection은 응답을 안 받은 채 동시에 날릴 수 있는 요청 수예요(기본 5). 메시지 A가 실패해 재시도되는 사이, 뒤에 보낸 B가 먼저 성공하면 파티션 안에서 B가 A보다 앞에 쌓여요. 순서가 뒤집히는 거죠.

해결책은 두 가지예요. 옛날 방식은 max.in.flight.requests.per.connection=1로 두는 거예요. 한 번에 하나씩만 보내니 순서가 안 깨지지만, 처리량이 떨어져요. 더 나은 방식은 다음에 볼 멱등 프로듀서를 켜는 거예요.

05. 멱등 프로듀서, 중복도 막고 순서도 지키고

멱등 프로듀서(enable.idempotence=true)를 켜면 두 가지를 한 번에 얻어요. 재시도로 인한 중복 전송을 막고, max.in.flight가 5여도 파티션 안 순서를 보장해요.

원리는 프로듀서마다 고유 ID(PID)와 시퀀스 번호를 붙이는 거예요. 브로커가 파티션별로 마지막 시퀀스를 기억하다가, 같은 번호가 또 오면 중복으로 버리고, 순서가 어긋난 번호가 오면 거부해요. 그래서 처리량을 유지하면서 순서와 무중복을 얻을 수 있어요.

spring:
  kafka:
    producer:
      acks: all
      properties:
        enable.idempotence: true

최신 카프카(3.0+)에서는 이게 기본으로 켜진 경우가 많아요. 멱등 프로듀서는 acks=all을 전제로 동작하고, 카프카 트랜잭션(Exactly-once)의 기반이기도 해요. 사실상 "켜두는 게 기본"이라고 봐도 돼요.

06. 순서는 "파티션 안에서만" 보장돼요

한 가지 분명히 해둘 게 있어요. 카프카의 순서 보장은 같은 파티션 안에서만이에요. 파티션이 다르면 순서가 보장되지 않아요.

그래서 순서가 중요한 데이터는 같은 키를 줘서 같은 파티션으로 보내요. 예를 들어 한 주문의 이벤트들은 주문 ID를 키로 주면 같은 파티션에 순서대로 쌓여요. 멱등 프로듀서로 프로듀서 쪽 순서를 지키고, 키로 같은 파티션에 모으는 두 가지가 함께 가야 순서가 끝까지 보장돼요. 키와 파티셔닝 이야기는 파티셔닝 편에서 더 자세히 다뤄요.

07. 프로듀서 성능, 배치와 압축

안전성을 챙겼으면 처리량도 봐야 해요. 프로듀서는 메시지를 한 건씩 보내지 않고 모아서 배치로 보내요. 이 배치를 어떻게 묶느냐가 처리량을 좌우해요.

  • batch.size — 한 배치의 최대 크기(바이트). 차면 바로 보내요.
  • linger.ms — 배치가 덜 찼어도 이 시간만큼 기다렸다 보내요(기본 0). 살짝 늘리면 배치가 커져 처리량이 올라가요.
  • buffer.memory — 보내기 전 메시지를 담는 버퍼 크기. 가득 차면 max.block.ms 동안 send가 블록돼요.

처리량이 중요하면 linger.ms를 5~10ms 정도 줘서 배치를 키우고, 압축을 함께 켜요. compression.typelz4zstd로 두면 네트워크·디스크 사용이 줄어 처리량이 올라가요.

spring:
  kafka:
    producer:
      acks: all
      properties:
        linger.ms: 10
        compression.type: lz4

다만 linger.ms는 지연을 약간 늘리는 거라, 실시간성이 중요한 메시지엔 과하게 주지 않아요. 안전성(acks)·순서(멱등)·처리량(배치·압축)의 균형을 데이터 성격에 맞춰 잡는 거예요.

08. 자주 만나는 문제

가끔 메시지가 사라져요

acks0이나 1인 경우, 또는 send 실패 콜백을 처리 안 한 경우예요. 유실이 안 되는 데이터면 acks=all + min.insync.replicas=2로 바꾸고, 실패 콜백을 반드시 처리해요.

순서가 뒤바뀌어요

재시도와 max.in.flight의 조합 때문이에요. 멱등 프로듀서를 켜면 순서가 보장돼요. 그래도 안 맞으면 같은 키로 같은 파티션에 보내는지 확인해요.

쓰기가 갑자기 거부돼요

ISR이 min.insync.replicas 밑으로 떨어진 거예요. 브로커가 죽었거나 복제가 밀린 상황이니, 클러스터 상태부터 확인해요. 이건 유실을 막으려고 일부러 거부하는 정상 동작이에요.

send가 가끔 느리게 블록돼요

buffer.memory가 가득 찬 거예요. 브로커가 느리거나 유입이 버퍼보다 빠른 상황이니, 버퍼를 늘리거나 유입 속도·브로커 상태를 점검해요.

안전하게 보내는 체크

프로듀서에서 안전하게 보내는 공식은 결국 acks=all + min.insync.replicas=2 + 멱등 프로듀서예요. 보내기 전에 이 항목들을 점검해요.

  • 유실 방지acks로 몇 명이 받아야 성공인지 정하고, min.insync.replicas로 그 의미를 지킨다.
  • 중복·순서 — 멱등 프로듀서로 재시도의 중복과 순서 역전을 한 번에 해결한다.
  • 순서가 중요한 데이터 — 같은 키로 같은 파티션에 보낸다.
  • 처리량 — 배치(linger.ms)와 압축으로 올리되 지연과 균형을 맞춘다.

컨슈머 쪽 @KafkaListener 설정, 수동 커밋과 멱등성, 그리고 키 기반 분배를 다루는 파티셔닝 편과 함께 보면 좋아요.

출처: Kafka — Producer acks · Kafka — enable.idempotence · Spring for Apache Kafka — Sending Messages

반응형

📚 같이 보면 좋은

"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 일정액의 수수료를 제공받습니다."