홈으로 돌아가기

Node.js에서의 Kafka: 안정적인 컨슈머와 스케일링

이 글은 Node.js에서 안정적이고 고성능 Kafka 컨슈머 설계에 바쳐집니다. partitions를 통한 스케일링, 병렬 처리, 오프셋 관리, 리밸런스 최소화 문제들이 다뤄집니다.

Node.js에서의 Kafka: 컨슈머 오류 방지
Advertisement 728x90

# Node.js에서 효과적인 Kafka 메시지 처리: 컨슈머, 리밸런싱 및 스케일링

Node.js에서 Kafka 컨슈머를 제대로 구현하려면 기본 API뿐 아니라 이 분산 큐의 아키텍처적 특징까지 이해해야 합니다. 파티션 설정, 읽기 전략, 오프셋 관리에서 실수하면 메시지 손실, 과도한 리밸런싱, 처리량 저하가 발생합니다. 이 글에서는 KafkaJS를 사용해 안정적이고 고성능 컨슈머를 만드는 핵심 요소를 자세히 살펴봅니다.

스케일링의 기반이 되는 파티션

Kafka 토픽에서 메시지 소비를 스케일링하는 데는 파티션 수가 제한 요인입니다. 아무리 많은 인스턴스를 실행해도 그룹 내 활성 컨슈머 수는 토픽의 파티션 수를 초과할 수 없습니다. 공식은 간단합니다: N_consumers ≤ N_partitions. 이 비율을 초과하면 일부 인스턴스가 할당 없이 유휴 상태로 남아 그룹 코디네이터에 불필요한 부하를 줍니다.

초기 파티션 수 선택은 전략적 결정입니다. Kafka는 파티션을 동적으로 늘릴 수 있지만, 이는 키에 따른 메시지 순서 보장을 깨뜨립니다: 동일 키의 새 메시지가 이전 메시지와 다른 파티션에 배치될 수 있습니다. 유연성을 위해 1부터 24까지의 컨슈머 수에 고르게 분배되는 숫자(예: 24)를 선택하세요.

Google AdInline article slot

주의할 점: 전체 클러스터의 총 파티션 수는 200,000개를 초과하지 마세요. 트래픽이 적은 토픽에는 과도한 파티션은 무의미합니다.

병렬 처리와 복제본 선택

하나의 컨슈머에 여러 파티션이 할당되어도 기본적으로 처리는 순차적입니다. 인스턴스 내 병렬 처리를 위해 partitionsConsumedConcurrently 옵션을 사용하세요:

consumer.run({
    partitionsConsumedConcurrently: 3,
    eachMessage: async ({ topic, partition, message }) => {
        // processing
    },
});

이 설정은 단일 파티션 내 순서 보장은 유지하지만 파티션 간에는 하지 않습니다. 처리 로직을 깨뜨리지 않으면서 처리량을 안전하게 높여줍니다.

Google AdInline article slot

지리적으로 분산된 클러스터에서는 네트워크 지연이 중요합니다. 기본적으로 컨슈머는 쓰기도 담당하는 리더 복제본에서 읽어 병목이 발생합니다. 해결책은 랙 인식 복제입니다. 브로커에 broker.rack 매개변수를 설정하고 클라이언트에서 rackId를 지정하세요:

const consumer = kafka.consumer({
  groupId: 'user-service',
  rackId: 'sibir_tomsk'
});

그러면 Kafka가 같은 "랙 도메인"의 복제본으로 요청을 라우팅해 지연을 최소화합니다. 대부분의 시나리오에서 약간의 복제 지연은 허용 가능하며, Kafka는 엄격한 실시간 처리를 위해 설계되지 않았습니다.

오프셋 관리와 초기 읽기

새 컨슈머 그룹은 흔히 이런 문제를 겪습니다: 과거 메시지를 보지 못합니다. 원인은 auto.offset.reset 매개변수로, 기본값이 latest이기 때문입니다. __consumer_offsets에 커밋된 오프셋이 없으면 파티션 끝부터 시작합니다.

Google AdInline article slot

대안은 earliest지만, 안전장치 없이 사용하면 수년치 이력 처리가 서비스와 하류 시스템을 과부하할 위험이 있습니다. 더 안전한 방법:

  • 전체 이력이 필요하면 구독 시 fromBeginning: true 사용:
await consumer.subscribe({ topic: 'user_events', fromBeginning: true });

이 옵션은 첫 연결 시에만 작동합니다. 첫 오프셋 커밋 후 Kafka는 fromBeginning을 무시합니다.

  • 정확한 복구를 위해 컨슈머를 중지한 후 CLI나 UI 도구(KafkaUI, Kadeck)로 오프셋을 수동 설정하세요.

이 제어로 우발적인 오프셋 되감기를 막고 처리 무결성을 보장합니다.

리밸런싱 전략과 함정

리밸런싱은 그룹 멤버십 변경 시 파티션을 재분배합니다. KafkaJS는 Eager 전략만 지원하며, 모든 컨슈머가 처리를 일시 중지("stop-the-world")하고 현재 작업을 마무지른 후 오프셋을 커밋하고 새 할당을 기다립니다.

빈번한 리밸런싱은 가용성을 떨어뜨리고 지연을 증가시킵니다. 주요 원인:

  • 하나의 프로세스에 혼합 기능—HTTP 서버, 백그라운드 작업, 컨슈머가 같은 인스턴스에 있음. 메시지 처리 외 치명적 오류가 전체 프로세스를 크래시시켜 리밸런싱을 유발합니다.
  • 잘못된 타임아웃sessionTimeoutheartbeatInterval을 맞춰야 합니다. 경험칙: heartbeatInterval ≤ sessionTimeout / 3.
  • 느린 메시지 처리—한 메시지 처리 시간이 rebalanceTimeout을 초과하면 그룹에서 추방됩니다.

진단을 위해 REBALANCING 이벤트에 구독하세요:

consumer.on(consumer.events.REBALANCING, ({ payload }) => {
  console.log('Rebalancing started for group:', payload.groupId);
});

이렇게 메트릭을 수집해 비정상 리밸런싱 빈도를 파악할 수 있습니다.

Kafka 2.3+는 Static MembershipCooperativeStickyAssignor를 제공하지만 KafkaJS는 아직 지원하지 않습니다. 안정성을 위해 아키텍처적 수정을 해야 합니다.

주요 요약

  • 파티션 수는 컨슈머 수평 스케일링의 상한을 정합니다.
  • 병렬 처리(partitionsConsumedConcurrently)는 파티션별 순서를 잃지 않으면서 속도를 높입니다.
  • 로컬 복제본 읽기(rackId)는 지리 분산 환경에서 네트워크 지연을 줄입니다.
  • 새 컨슈머 그룹은 기본적으로 이력을 건너뜁니다—fromBeginning이나 수동 오프셋 설정을 사용하세요.
  • 불필요한 리밸런싱을 피하기 위해 컨슈머를 다른 앱 구성 요소와 분리하세요.

— Editorial Team

Advertisement 728x90

다음 읽기