들어가며

Kafka는 고가용성 분산 시스템이지만, 극한 상황에서는 예상치 못한 동작이 발생한다. 각 시나리오를 이해하고 방어 전략을 갖추는 것이 프로덕션 운영의 핵심이다.


시나리오 1: 브로커 장애 시 리더 선출과 데이터 유실

상황 설정

초기 상태:
Broker 1 (Leader)   Broker 2 (Follower)   Broker 3 (Follower)
┌─────────────┐     ┌─────────────┐       ┌─────────────┐
│ P0 Leader   │     │ P0 Replica  │       │ P0 Replica  │
│ offset: 100 │────►│ offset: 98  │       │ offset: 95  │
└─────────────┘     └─────────────┘       └─────────────┘

ISR = {Broker1, Broker2, Broker3}
(모두 ISR에 포함, 단 복제 지연 있음)

장애 발생과 데이터 유실 경로

acks=1 설정 시:

1. Producer → Broker1에 offset 99, 100 전송 (ACK 수신)
2. Broker1 장애! (offset 99, 100은 팔로워에 미복제)
3. Controller: ISR 중 Broker2를 새 Leader로 선출
4. Broker2의 최신 offset = 98

결과:
┌─────────────────────────────────────────┐
│ offset 99, 100 영구 유실!               │
│ Producer는 ACK를 받았지만 데이터 없음   │
└─────────────────────────────────────────┘

acks=all 설정 시 시나리오

acks=all + min.insync.replicas=2:

1. Producer → Broker1(Leader) 전송
2. Broker1 → Broker2, Broker3 복제 대기
3. 모든 ISR 복제 완료 후 ACK

Broker1 장애 시:
→ Broker2 또는 Broker3 중 하나가 새 Leader
→ 두 팔로워 모두 최신 데이터 보유
→ 데이터 유실 없음!

단, ISR이 {Broker1}만 남은 상태에서 min.insync.replicas=2:
→ Producer에게 NotEnoughReplicasException 발생
→ 쓰기 거부 (데이터 유실보다 가용성 포기)

Unclean Leader Election 위험

극단적 시나리오:
Broker 1 (ISR, Leader, offset: 100) → 장애
Broker 2 (ISR, offset: 100)         → 장애
Broker 3 (ISR에서 제외됨, offset: 80) → 살아있음

unclean.leader.election.enable=true (기본값: false):
→ Broker3를 Leader로 선출 (ISR 아님)
→ offset 81~100 영구 유실!
→ 하지만 가용성은 유지됨

unclean.leader.election.enable=false (권장):
→ ISR 멤버가 복구될 때까지 해당 파티션 불가용
→ 데이터 무결성 우선

방어 전략

// Producer 설정
props.put(ProducerConfig.ACKS_CONFIG, "all");
props.put(ProducerConfig.RETRIES_CONFIG, Integer.MAX_VALUE);
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);

// 브로커 설정 (server.properties)
// min.insync.replicas=2        ← 최소 2개 ISR 동기화 보장
// unclean.leader.election.enable=false  ← ISR 외 선출 금지
// default.replication.factor=3          ← 복제 3개

시나리오 2: Consumer 리밸런싱 중 중복 처리

리밸런싱 타이밍 문제

초기 상태:
Consumer A → [P0, P1]
Consumer B → [P2, P3]

Consumer A가 처리 중:
1. poll() → [msg_offset_50, msg_offset_51, msg_offset_52] 수신
2. msg_offset_50 처리 완료
3. msg_offset_51 처리 완료
4. msg_offset_52 처리 중...
   ↓
   리밸런싱 발생! (Consumer C 합류)
   ↓
5. Consumer A: P0 해제 (offset_51까지만 커밋)
6. Consumer C: P0 할당 받음 → offset_52부터 읽기 시작
7. Consumer C: msg_offset_52 처리 ← 중복 처리!
   (Consumer A도 처리했었음)

상세 타임라인

시간축:

t=0:  Consumer A: poll() → [off50, off51, off52, off53]
t=1:  Consumer A: off50 처리 완료
t=2:  Consumer A: off51 처리 완료
t=3:  Consumer A: off52 처리 시작 (무거운 작업, DB 저장)
t=5:  리밸런싱 시작 (Consumer C 합류)
t=5:  Consumer A: onPartitionsRevoked() 호출
      → 커밋: offset=52 (off51까지 처리 완료이므로 next=52)
t=6:  Consumer C: P0 할당
t=6:  Consumer C: poll() → [off52, off53, ...]
t=7:  Consumer C: off52 처리 ← 중복!
      (Consumer A가 처리 중이었거나 완료했을 수도 있음)

해결 방법 1: ConsumerRebalanceListener 활용

@Service
public class SafeConsumerService {

    private final KafkaConsumer<String, OrderEvent> consumer;
    private final Map<TopicPartition, OffsetAndMetadata> pendingOffsets
        = new ConcurrentHashMap<>();

    public void startConsuming() {
        consumer.subscribe(List.of("order-events"), new ConsumerRebalanceListener() {

            @Override
            public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
                // 리밸런싱 전: 처리 완료된 offset 즉시 커밋
                log.info("파티션 반환 전 커밋: {}", pendingOffsets);
                consumer.commitSync(pendingOffsets);
                pendingOffsets.clear();
            }

            @Override
            public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
                log.info("새 파티션 할당: {}", partitions);
            }
        });

        while (true) {
            ConsumerRecords<String, OrderEvent> records =
                consumer.poll(Duration.ofMillis(100));

            for (ConsumerRecord<String, OrderEvent> record : records) {
                processOrder(record.value());
                // 처리 완료 offset 누적
                pendingOffsets.put(
                    new TopicPartition(record.topic(), record.partition()),
                    new OffsetAndMetadata(record.offset() + 1)
                );
            }

            consumer.commitAsync(pendingOffsets, (offsets, ex) -> {
                if (ex != null) log.error("비동기 커밋 실패", ex);
            });
        }
    }
}

해결 방법 2: 멱등성 처리 (Idempotent Consumer)

@Service
public class IdempotentOrderService {

    private final OrderRepository orderRepository;
    private final ProcessedEventRepository processedEventRepo;

    @KafkaListener(topics = "order-events")
    @Transactional
    public void handleOrder(ConsumerRecord<String, OrderEvent> record) {
        String eventId = record.value().getEventId();

        // 이미 처리된 이벤트인지 확인
        if (processedEventRepo.existsByEventId(eventId)) {
            log.info("중복 이벤트 무시: {}", eventId);
            return;
        }

        // 처리
        orderRepository.save(record.value().toOrder());

        // 처리 완료 기록 (같은 트랜잭션)
        processedEventRepo.save(new ProcessedEvent(eventId));
        // → 처리 + 완료기록이 원자적으로 수행됨
    }
}

해결 방법 3: Cooperative Rebalancing

// 리밸런싱 중 처리 중단을 최소화
props.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG,
    CooperativeStickyAssignor.class.getName());
// → 이동하지 않는 파티션은 계속 처리
// → 중복 처리 시간 창이 줄어듦

시나리오 3: 파티션 수 변경 시 키 기반 라우팅 깨짐

문제 원리

변경 전: 파티션 4개
키 "user-123" → hash("user-123") % 4 = 1 → Partition 1

파티션 6개로 증가:
키 "user-123" → hash("user-123") % 6 = 3 → Partition 3!

결과:
Partition 1: [user-123의 과거 메시지들]
Partition 3: [user-123의 새 메시지들]

순서 보장 파괴!
동일 키인데 서로 다른 파티션 = 병렬 처리 가능 = 순서 보장 불가

실제 영향

주문 처리 시스템 예시:

파티션 증가 전:
order-123 생성  → P1 (offset 100)
order-123 수정  → P1 (offset 101)
order-123 취소  → P1 (offset 102)
→ Consumer가 P1만 처리하면 순서 보장

파티션 증가 후 (4→6):
order-123 생성  → P1 (이전에 저장됨)
order-123 수정  → P3 (라우팅 변경!)
order-123 취소  → P3

Consumer-1이 P1 처리: 생성만 보임
Consumer-2가 P3 처리: 수정 → 취소 (생성 없이 취소!)
→ 처리 불가 또는 데이터 불일치

방어 전략

// 전략 1: 충분히 큰 파티션 수로 처음부터 설계
// (파티션 줄이기는 불가, 늘리기만 가능)
// 예상 최대 처리량 기준으로 여유 있게 설정

// 전략 2: 파티션 변경 시 마이그레이션 계획
// Phase 1: 새 토픽 생성 (더 많은 파티션)
// Phase 2: 프로듀서를 새 토픽으로 전환
// Phase 3: 구 토픽 메시지 모두 소비 후 구 컨슈머 종료
// Phase 4: 구 토픽 삭제

// 전략 3: 커스텀 파티셔너로 파티션 수 변경 대응
public class StableHashPartitioner implements Partitioner {
    @Override
    public int partition(String topic, Object key, byte[] keyBytes,
                         Object value, byte[] valueBytes, Cluster cluster) {
        // 고정 해시 함수 사용 (파티션 수 변경 전후 동일 매핑 유지)
        // 단, 새 파티션으로의 라우팅은 의도적으로 제어
        int fixedPartitionCount = 4; // 논리적 파티션 수 고정
        int physicalPartitions = cluster.partitionCountForTopic(topic);
        int logicalPartition = Math.abs(murmur2(keyBytes)) % fixedPartitionCount;
        return logicalPartition % physicalPartitions;
    }
}
권장 운영 원칙:
┌────────────────────────────────────────────┐
│ 파티션 수는 처음부터 넉넉하게 설정          │
│ (일반적으로 브로커 수의 2~4배)             │
│                                            │
│ 불가피하게 변경 시:                        │
│ 1. 키 기반 순서 보장 요구사항 재검토        │
│ 2. 새 토픽으로 마이그레이션                 │
│ 3. 컨슈머 멱등성 보장 후 변경              │
└────────────────────────────────────────────┘

시나리오 4: 디스크 가득 참 시 동작

브로커 디스크 100% 상황

디스크 사용량 증가:
80% → 90% → 95% → 99% → 100%

100% 도달 시:
┌──────────────────────────────────────────────────────┐
│ 1. 새 메시지 쓰기 실패                               │
│    → KafkaStorageException                            │
│    → 해당 파티션 리더가 오류 상태                    │
│                                                      │
│ 2. 프로듀서: TimeoutException / NotLeaderException   │
│                                                      │
│ 3. 컨슈머: 읽기는 가능하지만 lag 급증               │
│                                                      │
│ 4. 복제 중단: 팔로워가 리더를 따라잡지 못함          │
│    → ISR 축소                                        │
│    → acks=all이면 프로듀서 쓰기 더 느려짐            │
└──────────────────────────────────────────────────────┘

단계별 장애 확산

시간 흐름:

t=0:  디스크 100% 도달
t=1:  Partition 0 리더 (Broker 1): 쓰기 실패
t=2:  Producer: 재시도 시작
t=10: 재시도 소진 → ProducerException
t=15: Broker 2, 3 팔로워: 리더로부터 복제 못 받음
      → replica.lag.time.max.ms 초과 → ISR 제외
t=30: ISR = {Broker1} (리더만)
t=31: min.insync.replicas=2 설정 시
      → NotEnoughReplicasException 발생
      → 클러스터 쓰기 완전 불가!

방어 전략

# 모니터링 알림 설정 (Prometheus + AlertManager)
groups:
  - name: kafka_disk
    rules:
      - alert: KafkaDiskUsageHigh
        expr: (node_filesystem_size_bytes - node_filesystem_free_bytes)
              / node_filesystem_size_bytes > 0.80
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Kafka 브로커 디스크 80% 초과"

      - alert: KafkaDiskUsageCritical
        expr: (node_filesystem_size_bytes - node_filesystem_free_bytes)
              / node_filesystem_size_bytes > 0.90
        for: 1m
        labels:
          severity: critical
# 브로커 설정: 디스크 보호
log.retention.bytes=107374182400   # 파티션당 최대 100GB
log.retention.hours=72             # 3일 보관
log.segment.bytes=536870912        # 세그먼트 500MB (빠른 삭제 단위)

# 디스크 임계값 도달 시 자동 조치
# log.cleaner.enable=true          # 로그 컴팩션 활성화
// 디스크 부족 시 자동 보존 기간 단축 (운영 자동화)
@Scheduled(fixedDelay = 60000)
public void adjustRetentionPolicy() {
    double diskUsage = getDiskUsagePercent();
    if (diskUsage > 0.85) {
        adminClient.alterConfigs(Map.of(
            new ConfigResource(ConfigResource.Type.TOPIC, "order-events"),
            new Config(List.of(
                new ConfigEntry("retention.ms", "86400000") // 1일로 단축
            ))
        ));
        log.warn("디스크 {}% → 보존 기간 1일로 단축", diskUsage * 100);
    }
}

시나리오 5: ISR 축소 → Unclean Leader Election

ISR 점진적 축소 시나리오

초기: ISR = {B1(Leader), B2, B3}

Step 1: B3 네트워크 불안정
        replica.lag.time.max.ms=30s 경과
        → ISR = {B1, B2}

Step 2: B2 GC 멈춤 (Full GC 60초)
        → ISR = {B1}
        (리더만 ISR에 남음)

Step 3: min.insync.replicas=2 설정 시
        → 쓰기 거부! (가용성 ↓, 안전성 ↑)

Step 4: B1 (리더) 장애!
        ISR가 비어있음
        unclean.leader.election.enable=true:
        → B3 (ISR 아님, offset: 100 뒤처짐) 리더로 선출
        → 100개 메시지 영구 유실!

unclean.leader.election.enable=false:
        → 해당 파티션 완전 불가용
        → B1 또는 B2 복구 시까지 대기

ISR 모니터링

# ISR 상태 확인
kafka-topics.sh --bootstrap-server kafka1:9092 \
  --describe --topic order-events

# 출력 예시:
Topic: order-events  Partition: 0  Leader: 1  Replicas: 1,2,3  Isr: 1,2
#                                                                       ↑
#                                                               Broker 3 빠짐!
// ISR 축소 감지 및 알림
@Scheduled(fixedDelay = 30000)
public void checkISRHealth() {
    DescribeTopicsResult result = adminClient.describeTopics(
        List.of("order-events", "payment-events")
    );

    result.all().get().forEach((topic, desc) -> {
        desc.partitions().forEach(partition -> {
            int replicaCount = partition.replicas().size();
            int isrCount = partition.isr().size();

            if (isrCount < replicaCount) {
                alertService.sendAlert(String.format(
                    "ISR 축소! 토픽=%s 파티션=%d ISR=%d/%d",
                    topic, partition.partition(), isrCount, replicaCount
                ));
            }
        });
    });
}

방어 전략

ISR 축소 방지:
1. 브로커 JVM GC 튜닝 (G1GC 사용, pause time 최소화)
2. 네트워크 대역폭 충분히 확보
3. 브로커간 복제 전용 네트워크 분리
4. replica.lag.time.max.ms 현실적으로 설정

설정 조합:
unclean.leader.election.enable=false  ← 데이터 우선
min.insync.replicas=2                 ← 최소 2개 보장
default.replication.factor=3         ← 여유 복제본

시나리오 6: Consumer Lag 폭증 시 대응

Lag 발생 원인과 탐지

Consumer Lag:
┌────────────────────────────────────────────────────────┐
│  Producer가 쓰는 속도 > Consumer가 읽는 속도            │
│                                                        │
│  Log End Offset (최신 메시지 위치):  ────────────────► │
│  Committed Offset (처리 완료 위치):  ──────►           │
│                                     ↑                  │
│                              Lag = 차이                │
└────────────────────────────────────────────────────────┘

현재 Lag: Log End Offset - Committed Offset

Lag 폭증 원인별 분류

원인 1: Consumer 처리 속도 저하
┌───────────────────────────────────────────┐
│ - 외부 DB 응답 지연                        │
│ - GC 멈춤                                 │
│ - 처리 로직 CPU 집약적                    │
│ - 다운스트림 서비스 장애                  │
└───────────────────────────────────────────┘

원인 2: Producer 급격한 유입량 증가
┌───────────────────────────────────────────┐
│ - 트래픽 급증 (이벤트, 세일 등)           │
│ - 업스트림 서비스 배치 처리               │
│ - DDoS 또는 비정상 트래픽                 │
└───────────────────────────────────────────┘

원인 3: Consumer 인스턴스 감소
┌───────────────────────────────────────────┐
│ - 배포 중 인스턴스 순차 종료              │
│ - OOM으로 인한 프로세스 종료              │
│ - 리밸런싱 중 처리 중단                   │
└───────────────────────────────────────────┘

Lag 모니터링 및 자동 스케일링

// Lag 모니터링 서비스
@Service
public class KafkaLagMonitor {

    @Scheduled(fixedDelay = 10000)
    public void checkAndAlertLag() {
        Map<String, Map<TopicPartition, Long>> lagMap = calculateLag();

        lagMap.forEach((groupId, partitionLags) -> {
            long totalLag = partitionLags.values().stream()
                .mapToLong(Long::longValue).sum();

            // Prometheus 메트릭 노출
            meterRegistry.gauge("kafka.consumer.lag",
                Tags.of("group", groupId), totalLag);

            if (totalLag > 100_000) {
                log.warn("Consumer Group {} Lag 폭증: {}", groupId, totalLag);
                triggerAutoScaling(groupId, totalLag);
            }
        });
    }

    private Map<String, Map<TopicPartition, Long>> calculateLag() {
        // AdminClient로 Log End Offset 조회
        // ConsumerGroupDescription으로 Committed Offset 조회
        // 차이 계산
        ...
    }
}
# Kubernetes HPA (Consumer Pod 자동 확장)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: order-consumer-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: order-consumer
  minReplicas: 2
  maxReplicas: 10  # 파티션 수 이상으로 늘려도 의미 없음
  metrics:
    - type: External
      external:
        metric:
          name: kafka_consumer_group_lag
          selector:
            matchLabels:
              group: order-processing-group
        target:
          type: AverageValue
          averageValue: "10000"  # 평균 lag이 10000 초과 시 스케일아웃

긴급 대응 절차

Lag 폭증 감지 시 대응 순서:

Step 1: 원인 파악 (5분 이내)
  kafka-consumer-groups.sh --describe --group order-group
  → Lag이 특정 파티션에 집중? → 해당 파티션 처리 병목
  → 전체 파티션 균등? → 처리 속도 전반적 저하

Step 2: 빠른 완화
  - 처리 느린 컨슈머 재시작
  - max.poll.records 줄이기 (처리 단위 축소)
  - 컨슈머 인스턴스 추가 (파티션 수 미만까지)

Step 3: 근본 원인 해결
  - DB 인덱스 최적화
  - 다운스트림 서비스 복구
  - 처리 로직 비동기화

Step 4: Lag 해소 모니터링
  - 정상화까지 5분 간격 lag 추적

시나리오 7: 네트워크 파티션 시나리오

Split-Brain 상황

정상 상태:
Broker 1 (Leader) ←→ Broker 2 ←→ Broker 3

네트워크 파티션 발생:
┌──────────────────┐      X      ┌──────────────────┐
│   Zone A         │  (단절)     │   Zone B         │
│   Broker 1       │────────────X│   Broker 2       │
│   (리더라고 생각) │             │   Broker 3       │
└──────────────────┘             └──────────────────┘

Zone B에서 새 리더 선출:
→ Broker 2 또는 3이 새 Leader로 선출
→ Broker 1도 여전히 자신이 Leader라고 생각 (zombie leader)

Zombie Leader 문제

네트워크 분리 후:

Producer (Zone A) → Broker 1 (zombie) 에 쓰기
Producer (Zone B) → Broker 2 (new leader) 에 쓰기

두 리더 모두 쓰기 수락!

네트워크 복구 후:
Broker 1이 복구 → 새 리더(Broker 2)의 로그와 충돌
Broker 1의 독립적으로 쓴 메시지 폐기!

Kafka의 방어 메커니즘

Epoch (Generation Number):

각 리더 선출마다 epoch 증가:
Broker 1: Leader epoch 5 (구 리더)
Broker 2: Leader epoch 6 (새 리더)

Broker 1이 쓰기 시도:
Producer → Broker 1 (epoch 5)
Broker 1 → 자신이 리더인지 ZooKeeper/Controller 확인
→ "당신은 구 리더(epoch 5), 현재 리더는 epoch 6"
→ NotLeaderOrFollowerException 반환!
→ Producer: 새 리더 메타데이터 갱신 후 재시도

네트워크 파티션 시나리오별 영향

시나리오 A: Producer와 Leader가 같은 Zone
Producer ──► Leader(격리됨) ──X──► Follower들
→ acks=1: 쓰기 가능, ISR 축소 위험
→ acks=all: ISR 멤버 감소로 쓰기 거부 가능

시나리오 B: Producer와 Leader가 다른 Zone
Producer ──X──► Leader(Zone A)
Producer ──► 새 Leader(Zone B)에 메타데이터 갱신 후 재연결

시나리오 C: 소수 Zone에 리더
Zone A (2브로커): 리더 보유
Zone B (1브로커): 팔로워만

min.insync.replicas=2 + acks=all:
→ Zone B 분리 시: Zone A에서 계속 쓰기 가능
→ Zone A 분리 시: Zone B 쓰기 불가 (가용성 포기, 안전성 유지)

방어 전략

# 다중 AZ 배포 시 권장 설정

# 복제 설정
default.replication.factor=3      # AZ당 하나씩
min.insync.replicas=2             # 과반수

# 타임아웃 설정 (네트워크 복구 시간 고려)
replica.lag.time.max.ms=30000    # 30초
zookeeper.session.timeout.ms=18000
// 클라이언트 메타데이터 갱신 설정
props.put(ProducerConfig.METADATA_MAX_AGE_CONFIG, 300000);    // 5분
props.put(ProducerConfig.RECONNECT_BACKOFF_MS_CONFIG, 50);
props.put(ProducerConfig.RECONNECT_BACKOFF_MAX_MS_CONFIG, 1000);

시나리오 8: Producer 타임아웃 + 재시도 시 중복

중복 발생 메커니즘

멱등성 없는 Producer의 재시도:

1. Producer → msg(A) → Broker
2. Broker: 메시지 저장 완료
3. Broker → ACK 전송
4. 네트워크 지연으로 ACK 손실!
5. Producer: 타임아웃 → msg(A) 재전송
6. Broker: msg(A) 또 저장 (중복!)

                   ┌──────────────────────────────┐
Partition 0:       │ [A][B][A_dup][C]             │ ← A가 두 번!
                   └──────────────────────────────┘

타임아웃 관련 설정들

props.put(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG, 30000);    // 요청 타임아웃 30초
props.put(ProducerConfig.DELIVERY_TIMEOUT_MS_CONFIG, 120000);  // 전체 전달 타임아웃 2분
props.put(ProducerConfig.RETRIES_CONFIG, 3);                    // 재시도 3회
props.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG, 100);        // 재시도 간격 100ms

// 재시도로 인한 순서 역전 방지
props.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, 5);
// → 멱등성 활성화 시: 최대 5개 동시 요청 허용 (순서 보장됨)
// → 멱등성 비활성화 시: 1로 설정해야 순서 보장

순서 역전 시나리오

MAX_IN_FLIGHT_REQUESTS=5, 멱등성 없음:

1. msg1 전송 (flight 중)
2. msg2 전송 (flight 중)
3. msg1 전송 실패 → 재시도 대기
4. msg2 전송 성공
5. msg1 재시도 성공
→ Partition: [msg2, msg1] ← 순서 역전!

MAX_IN_FLIGHT_REQUESTS=1:
1. msg1 전송 → 완료
2. msg2 전송 → 완료
→ Partition: [msg1, msg2] ← 순서 보장, 단 처리량 저하

완전한 해결: Idempotent Producer

// 멱등성 활성화 시
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
// 자동으로:
// acks=all
// max.in.flight.requests.per.connection=5 (순서 보장하면서 성능도 유지)
// retries=Integer.MAX_VALUE

// 브로커는 PID + Sequence Number로 중복 감지:
// seq=1 저장 → seq=1 재수신 → "이미 처리됨" → 무시 (ACK 반환)
// seq=3 수신 후 seq=2 수신 → "seq 2 누락" → OutOfOrderSequenceException

재시도와 중복 처리 요약

상황별 권장 설정:

데이터 유실 절대 불허 (금융):
  acks=all
  enable.idempotence=true
  retries=MAX_INT
  transactional.id=unique-id  (EOS 필요 시)

고처리량 우선 (로그 수집):
  acks=1
  retries=3
  enable.idempotence=false
  compression.type=snappy

균형 (일반 서비스):
  acks=all
  enable.idempotence=true
  retries=10
  delivery.timeout.ms=120000

극한 시나리오 종합 방어 체크리스트

프로듀서 설정:
□ acks=all (데이터 무결성)
□ enable.idempotence=true (중복 방지)
□ retries=충분히 크게
□ delivery.timeout.ms > request.timeout.ms * retries

브로커 설정:
□ replication.factor >= 3
□ min.insync.replicas = replication.factor - 1
□ unclean.leader.election.enable=false
□ auto.leader.rebalance.enable=true
□ log.retention.bytes 설정 (디스크 보호)

컨슈머 설정:
□ enable.auto.commit=false (수동 커밋)
□ max.poll.interval.ms > 최대 처리 시간
□ CooperativeStickyAssignor 사용
□ ConsumerRebalanceListener 구현

토픽 설계:
□ 파티션 수를 처음부터 넉넉하게
□ 키 기반 순서 보장 요구사항 명확화
□ 컴팩션 vs 삭제 정책 결정

운영:
□ Consumer Lag 모니터링 및 알림
□ ISR 상태 모니터링
□ 디스크 사용량 80% 알림
□ 브로커 JVM 튜닝 (G1GC)
□ 네트워크 파티션 대비 다중 AZ 배포

시나리오별 빠른 참조

시나리오 핵심 위험 방어 방법
브로커 장애 미복제 메시지 유실 acks=all + min.insync.replicas=2
리밸런싱 중 중복 offset 재처리 ConsumerRebalanceListener + 멱등성 처리
파티션 수 변경 키 라우팅 깨짐 처음부터 충분한 파티션 수, 새 토픽 마이그레이션
디스크 풀 쓰기 완전 중단 80% 알림, 보존 기간 자동 조정
ISR 축소 Unclean 선출 위험 unclean.leader.election=false, ISR 모니터링
Consumer Lag 폭증 실시간성 파괴 자동 스케일링, Lag 알림
네트워크 파티션 Split-Brain Leader Epoch, 다중 AZ 배포
재시도 중복 메시지 중복 저장 enable.idempotence=true

카테고리:

업데이트: