Apache Kafka의 성능을 극대화하는 기술적 원리와 면접에서 자주 등장하는 핵심 질문들을 다룹니다.
1. Kafka는 왜 빠른가? (필수 질문!)
핵심 기술:- 순차 디스크 I/O: 로그 파일에 데이터를 순차적으로 추가하여 디스크 헤드의 무작위 탐색을 최소화합니다. 이는 테이프에 데이터를 쓰는 것과 유사한 방식입니다.
- Zero-Copy 기술:
sendfile시스템 호출을 사용하여 데이터가 커널의 페이지 캐시에서 직접 네트워크 인터페이스 카드로 전송됩니다. 이 과정에서 사용자 공간으로의 데이터 복사가 생략되어 I/O 효율성을 크게 높입니다.// Linux 시스템 호출을 활용한 Kafka의 내부 구현 (개념적) // 실제 구현은 C 언어로 되어 있으나, 원리는 다음과 같습니다. // int sendfile(int out_fd, int in_fd, off_t *offset, size_t count); - 배치 처리:
- Producer: 여러 메시지를 모아 배치로 전송하여 네트워크 I/O 횟수를 줄입니다.
- Consumer: 여러 메시지를 배치로 폴링(pull)하여 처리합니다.
- 파티션 병렬 처리: 각 파티션은 독립적으로 처리될 수 있으며, 여러 소비자가 각기 다른 파티션을 병렬로 처리하여 전체 처리량을 높입니다.
- 페이지 캐시 최적화: 운영체제의 페이지 캐시를 적극적으로 활용하여 JVM의 가비지 컬렉션(GC)으로 인한 성능 저하를 최소화합니다.
2. Rebalance란 무엇이며, 어떤 문제가 발생할 수 있는가?
Rebalance 발생 시나리오:- Consumer Group 내 멤버 변경 (새로운 Consumer 추가 또는 기존 Consumer 장애)
- 구독 중인 토픽의 파티션 수 변경
- Consumer의 Heartbeat 타임아웃 (기본값:
session.timeout.ms= 45초)
- 모든 Consumer의 메시지 소비 일시 중단
- 파티션 재할당 (Range 또는 Round Robin 전략 사용)
- 새로운 파티션 할당 계획을 모든 Consumer에게 동기화
- 소비 중단: Rebalance가 진행되는 동안 서비스가 일시적으로 중단됩니다.
- 메시지 중복 소비: Offset이 커밋되기 전에 Rebalance가 발생하면, 해당 메시지가 다시 처리될 수 있습니다.
- 데이터 불균형 (Data Skew): 파티션 할당 전략이 비효율적이거나 Consumer 간 처리 능력 차이로 인해 특정 Consumer에 부하가 집중될 수 있습니다.
// Consumer 설정 예시
Properties props = new Properties();
// ... 다른 설정 ...
// poll() 호출 간 최대 허용 간격 (기본값: 5분)
// Rebalance 발생 시 소비 중단 시간을 줄이려면 이 값을 조정해야 합니다.
props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, "300000");
// Consumer가 Broker에게 Heartbeat를 보내는 주기 (기본값: 3초)
props.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG, "3000");
// ...
3. 파티션 수는 어떻게 결정해야 하는가?
핵심 고려 사항:- 처리량 목표:
전체 처리량 = 파티션 수 × 단일 파티션 처리 능력
| 지표 | 계산 방식 | 예시 값 |
|---|---|---|
| Producer 처리량 | 최대 메시지 전송량 / 단일 파티션 처리 능력 (약 10MB/s) | 100MB/s → 10 파티션 |
| Consumer 처리량 | Consumer Group 내 Consumer 수 × 단일 스레드 처리 능력 | 3 Consumer × 5,000 메시지/s → 총 15,000 메시지/s 처리 능력 |
| 향후 확장성 | 현재 필요량의 20%~50% 여유분 확보 | 현재 8개 필요 → 실제 12개 생성 |
- 초기 배포 시에는 3~6개의 파티션으로 시작하는 것을 권장합니다.
- 단일 Broker 당 총 파티션 수는
2000개를 넘지 않도록 관리하여 파일 핸들 고갈 문제를 방지합니다.
4. 메시지 순서를 어떻게 보장할 수 있는가?
전체 순서 보장 (전체 Topic에 대한 순서, 비권장):- 단일 파티션 사용: 모든 메시지를 단일 파티션으로 라우팅합니다. 이는 병렬 처리를 저해하여 성능 병목 현상을 유발합니다.
- 동기 전송 및 재시도 비활성화: Producer 설정에서
acks=all을 사용하고 재시도를 비활성화합니다.
- 메시지 키(Key) 사용: 동일한 키를 가진 메시지는 항상 같은 파티션으로 라우팅되도록 합니다. 이를 통해 특정 키(예: 사용자 ID, 주문 ID)와 관련된 메시지들의 순서를 보장할 수 있습니다.
// 메시지 키를 지정하여 동일 파티션으로 라우팅 kafkaTemplate.send("order-topic", order.getUserId(), orderMsg);
// Producer 설정 예시
Properties props = new Properties();
// ... 다른 설정 ...
// 동일 커넥션 내에서 미확인 요청의 동시성을 제한 (기본값 5)
// 1로 설정하면 순차적 전송을 보장하여 단일 파티션 내 순서 보장을 강화합니다.
props.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, "1");
// 재시도 횟수를 최대로 설정하여 메시지 유실 방지
props.put(ProducerConfig.RETRIES_CONFIG, Integer.MAX_VALUE);
// ...
5. Exactly-Once 처리는 어떻게 구현하는가?
3단계 보장 메커니즘:- Idempotent Producer (멱등성 Producer): 동일한 메시지가 여러 번 전송되더라도 Broker에서 단 한 번만 처리되도록 합니다. Producer 설정에서
enable.idempotence=true로 설정하면 내부적으로acks=all및max.in.flight.requests.per.connection=5(Kafka 0.11 버전 이상)가 활성화됩니다.// Producer 설정 예시 Properties props = new Properties(); // ... // 멱등성 Producer 활성화 (Kafka 0.11 이상) props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, "true"); // ... - Transactional Messages (트랜잭션 메시지): 여러 토픽에 걸쳐 또는 Producer와 Consumer 간의 작업을 원자적으로 처리할 수 있게 합니다.
// Producer 측 트랜잭션 사용 예시 kafkaTemplate.executeInTransaction(transactionTemplate -> { transactionTemplate.send("topic1", msg1); transactionTemplate.send("topic2", msg2); return null; // 성공 시 null 반환 }); // Consumer 측 읽기 격리 수준 설정 Properties props = new Properties(); // ... // 읽기 격리 수준 설정: read_committed (트랜잭션 메시지 포함) 또는 read_uncommitted (기본값) props.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG, "read_committed"); // ... - External Deduplication (외부 저장소를 통한 중복 제거): 최종적인 데이터 정합성을 위해 데이터베이스 등의 외부 시스템에서 중복을 제거하는 메커니즘을 추가합니다.
-- 예시: 데이터베이스 고유 제약 조건을 활용한 중복 제거 CREATE TABLE processed_messages ( message_unique_id VARCHAR(255) PRIMARY KEY, processed_timestamp TIMESTAMP ); -- 메시지 처리 시 INSERT ... ON CONFLICT ... 등을 사용하여 중복 삽입 방지
6. Consumer Offset 커밋 방식에는 어떤 차이가 있는가?
| 커밋 방식 | 자동 커밋 (Auto Commit) | 수동 커밋 (Manual Commit) |
|---|---|---|
| 설정 파라미터 | enable.auto.commit=true |
enable.auto.commit=false |
| 커밋 시점 | 주기적으로 자동 커밋 (기본값: 5초) | Consumer가 명시적으로 commitSync() 또는 commitAsync() 호출 시 |
| 메시지 유실 위험 | 자동 커밋 후 Consumer 장애 발생 시, 커밋된 이후 메시지 유실 가능성 있음 | 커밋 시점 이전 Consumer 장애 발생 시, 해당 메시지가 다시 처리될 수 있음 (중복 소비) |
| 주요 사용 사례 | 약간의 메시지 중복을 허용할 수 있는 일반적인 애플리케이션 | 금융 거래 등 정확한 한 번 처리가 필수적인 미션 크리티컬한 애플리케이션 |
@KafkaListener(topics = "payment-events")
public void processPayment(@Payload String paymentData, Acknowledgment ack) {
try {
// 실제 결제 처리 로직
processPaymentLogic(paymentData);
// 성공적으로 처리 후 Offset을 동기적으로 커밋
ack.acknowledge();
} catch (Exception e) {
// 처리 실패 시, 일정 시간 후 재시도 (예: 1초 후)
// nack()은 Kafka Consumer API의 표준 메소드는 아니며,
// Spring Kafka의 Acknowledgment에서 제공하는 기능입니다.
// 내부적으로는 커밋을 지연시키거나 에러를 기록하는 방식으로 동작할 수 있습니다.
ack.nack(1000);
}
}
추가 면접 팁
- 개념 확장: "Kafka가 빠른 이유"에 대한 질문을 받으면, 아키텍처 외에도 Producer/Consumer 설정 튜닝(예: 배치 크기, linger.ms)에 대해 추가적으로 설명하여 깊이를 더하십시오.
- 상황 기반 답변: 실제 경험을 예로 들어 답변하십시오. "이전 전자상거래 프로젝트에서 주문 ID를 파티션 키로 사용하여 주문 관련 메시지의 순서를 성공적으로 보장했습니다." 와 같이 구체적인 사례를 제시하는 것이 좋습니다.
- 시각 자료 활용: 필요하다면 종이에 Producer, Broker, Consumer 간의 데이터 흐름과 주요 설정 파라미터를 직접 그려 설명하면 이해를 돕는 데 효과적입니다.
마지막 조언: 원리 이해가 암기보다 중요합니다. 모르는 질문에는 솔직하게 인정하고, 문제 해결을 위한 접근 방식이나 트러블슈팅 과정을 설명하는 능력을 보여주는 것이 좋습니다!