본문 바로가기

카테고리 없음

Kafka

카프카 구조

  • 클러스터: 여러 대의 서버(브로커)로 구성된 카프카 시스템이다.
  • 브로커: 카프카 시스템을 구성하는 개별 서버이다.
  • 토픽: 메시지들의 특정 카테고리 또는 피드를 나타낸다. 토픽은 데이터를 카테고리화하여 관리할 수 있게 한다. 예를 들어, 다양한 종류의 이벤트나 메시지들을 서로 다른 토픽으로 분류할 수 있다. 또한 하나의 토픽은 여러 소비자가 구독할 수 있다.
  • 파티션: 토픽을 구성하는 하위 단위로, 여러 개의 토픽은 여러 개의 파티션으로 나누어질 수 있다. 이를 통해 카프카의 확장성과 병렬 처리 능력을 향상시킨다.
  • 세그먼트: 파티션의 데이터를 실제로 저장하는 물리적 파일이다. 카프카는 데이터를 파티션에 순차적으로 기록하지만, 이 데이터는 여러 세그먼트 파일로 나누어 저장된다.

파티셔너

프로듀서가 데이터를 보내면 파티셔너를 통해 브로커로 데이터가 전달되는데, 이때 파티셔너는 데이터를 어떤 파티션에 넣을지 결정하는 역할을 한다.

 

프로듀서를 사용할 때 파티셔너를 따로 설정하지 않으면, UniformStickyPartitioner로 설정된다. 

  • 메시지 키가 있는 경우
    • 메시지에 특정 키를 할당하면 프로듀서는 해당 키의 해시값을 사용해 특정 파티션을 결정한다.
    • 동일한 메시지 키 값을 가진 레코드는 동일한 해시값을 만들기 때문에 항상 동일한 파티션에 들어가는 것을 보장한다. 
    • 이로 인해 메시지 순서를 보장하는데 유용한다.
  • 메시지 키가 없는 경우
    • 라운드로빈 방식으로 파티션에 데이터가 전달된다.

컨슈머 파티션 할당 전략

파티션 할당 전략이란, 카프카 컨슈머가 구독하는 대상 토픽 중 어느 파티션의 레코드를 읽을지 결정하는 방식을 의미한다. 컨슈머 그룹에 설정된 전략에 따라 컨슈머-파티션 간의 매칭이 결정된다.

레인지 파티션 할당 전략

구독 중인 토픽의 파티션과 컨슈머를 순서대로 나열한 후, 각 컨슈머가 받아야 할 파티션 수를 결정한다. 이때 각 컨슈머가 받는 파티션 수는 해당 토픽의 전체 파티션 수를 컨슈머 그룹의 컨슈머 수로 나눈 값이다.

 

따라서 파티션 수와 컨슈머 수가 균등하게 나눠지지 않으면, 앞 순서의 컨슈머들이 더 많은 파티션을 할당받는다. 즉, 파티션이 한쪽으로 몰리는 문제가 발생할 수 있다. 

라운드 로빈 파티션 할당 전략

파티션을 컨슈머 그룹 내의 모든 컨슈머에게 균등하게 분배한다. 따라서 레인지 할당 전략과 달리 균등한 파티션 분배가 가능하다. 따라서 컨슈머를 효과적으로 활용해 성능을 향상시킬 수 있다.

 

그러나 리밸런싱이 발생할 때 모든 컨슈머가 중단된다. 또한 모든 파티션을 균등하게 분배하려 하기 때문에 하나의 컨슈머만 다운되더라도 모든 컨슈머의 리밸런싱이 발생한다.

스티키 파티션 할당 전략

앞선 두 전략은 리밸런싱 발생 시 기존 매핑정보와 전혀 다른 매핑이 맺어진다. 반면, 스티키 파티션 할당 전략은 리밸런싱이 발생하더라도 기존 매핑 정보를 유지하려는 전략이다.

 

스티키 파티션 할당 전략은 2가지 목적으로 컨슈머에 파티션을 할당한다.

  1. 가능한 균형 잡힌 파티션 할당
  2. 리밸런싱 발생 시, 되도록 기존의 할당된 파티션 정보를 보장

협력적 스티키 파티션 할당 전략

카프카 버전 2.4부터 디폴트 파티션 할당 전략으로 사용된다. 스티키 파티션 할당 전략과 결과적으로 동일하지만 컨슈머 그룹 내부의 리밸런싱 동작은 한층 더 고도화되었다. 

 

앞서 설명한 3가지 전략은 내부적으로 EAGER 프로토콜을 사용하여 리밸런싱 발생 시 모든 컨슈머가 메시지를 구독할 수 없는 Stop the world 현상이 발생했다. 이와 달리 협력적 스티키 파티션 할당 전략은 COOPERATIVE 프로토콜을 사용하여 리밸런싱이 필요한 특정 파티션에만 집중하여 그 외의 나머지 파티션들은 컨슈머와 매핑을 그대로 유지한다. 즉, 문제가 있는 파티션의 메시지 컨슈밍만 중단되고, 이외의 파티션은 모두 정상적으로 메시지 컨슈밍이 동작하기 때문에 전체적인 데이터 처리 성능을 크게 저해하지 않는다. 따라서 컨슈머 그룹의 구성 변경이 자주 변경되는 환경에 유용하며, 효율적인 리밸런싱을 수행할 수 있다.

커밋과 오프셋

컨슈머가 poll()을 호출할 때마다 컨슈머 그룹은 카프카에 저장되어 있는 아직 읽지 않은 메시지를 가져온다. 컨슈머 그룹의 컨슈머들은 파티션에 자신이 가져간 메시지의 위치 정보(오프셋)을 기록한다. 그리고 각 파티션에 현재 위치를 업데이트하는 동작을 커밋한다고 한다.

 

만약 커밋된 오프셋이 컨슈머가 실제 마지막으로 처리한 오프셋보다 작으면 마지막 처리된 오프셋과 실제 오프셋 사이의 메시지가 중복으로 처리되고, 커밋된 오프셋이 컨슈머가 실제 마지막으로 처리한 오프셋보다 크면 마지막 처리된 오프셋과 커밋된 오프셋 사이의 메시지는 누락된다.

 

오프셋 관리는 컨슈머 그룹 단위로 오프셋을 관리한다. 따라서 토픽 1을 구독한 컨슈머 그룹이 여러개가 있을 때 각 그룹은 각각의 오프셋을 가지며 각자 데이터를 오프셋에 맞게 처리한다. 참고로 컨슈머 그룹에 컨슈머 1, 2, 3이 있을 때, 컨슈머 1에 장애가 발생하더라도, 동일 컨슈머 그룹 내의 2, 3, 4 컨슈머가 읽을 수 있도록 리밸런싱 된다

자동 커밋

자동 커밋을 사용하려면 컨슈머 옵션 중 enable.auto.commit을 true로 설정하면 된다. 자동 커밋 모드에서는 auto.commit.interval.ms(default: 5s) 설정값만큼 주기적으로 마지막 오프셋이 커밋된다.

 

그런데 자동 커밋은 메시지 중복 처리와 메시지 손실 문제가 발생할 수 있다. 

 

예를 들어, 2번 오프셋까지 커밋된 상황에서 8번 오프셋을 처리하는 도중 컨슈머 리밸런싱이 일어났다고 가정하자. 리밸런싱 후 해당 파티션에 할당된 컨슈머는 2번 오프셋까지 커밋되었기 때문에 7번 오프셋부터 컨슈밍한다. 즉, 7번 오프셋을 중복 처리하게 된다. 

그리고 컨슈머가 메시지를 처리 중이더라도, auto.commit.interval.ms로 설정된 시간 간격마다 마지막으로 읽은 오프셋이 커밋되는데, 이때 아직 처리되지 않은 메시지에 대한 오프셋이 커밋되는 상황이 발생할 수 있다. 따라서 아직 해당 오프셋의 메시지 처리가 끝나지 않았으며 해당 컨슈머에 장애가 발생하면 해당 메시지는 손실될 수 있다. 

수동 커밋

경우에 따라 수동 커밋을 사용해야 하는 경우도 있는데, 주로 메시지 처리가 완료될 때까지 메시지를 가져온 것으로 간주해서는 안되는 경우이다. 수동 커밋 모드로 설정하고, 적절하게 구현하면 된다. 스프링 카프카에서는 사용자가 사용할 만한 커밋 종류를 7가지로 세분화하고 미리 로직을 만들어 놓았다. 따라서 사용자는 커밋 방식만 설정하면 되고 커밋을 구현할 필요가 없다. 

 

참고로 스프링 카프카에서는 커밋이라고 부르지 않고, AckMode라고 부른다. 프로듀서에서 사용하는 acks와 혼동되지 않도록 주의해야 한다.

스프링 카프카 프로듀서

스프링 카프카 프로듀서는 KafkaTemplate 클래스를 사용하여 데이터를 전송할 수 있다. 카프카 템플릿을 사용하는 방법은 크게 두 가지가 있다.

  1. 스프링 카프카에서 제공하는 기본 카프카 템플릿을 사용하는 방법
  2. 직접 사용자가 카프카 템플릿을 프로듀서 팩토리로 생성하는 방법

기본 카프카 템플릿

사용자가 직접 선언하지 않은 빈이지만, 스프링 카프카에서 제공하는 기본 KafkaTemplate 객체로 주입되고, application.yml에 선언한 옵션 값은 자동으로 주입된다.

커스텀 카프카 템플릿

프로듀서 팩토리를 통해 만든 카프카 템플릿 객체를 빈으로 등록하여 사용하는 방법이다. 한 스프링 카프카 애플리케이션 내부에 다양한 종류의 카프카 프로듀서 인스턴스를 생성하고 싶다면 이 방식을 사용하면 된다. 예를 들어, A 클러스터로 전송하는 카프카 프로듀서와 B 클러스터로 전송하는 카프카 프로듀서를 동시에 사용하고 싶은 경우, 2개의 카프카 템플릿을 빈으로 등록해 사용하면 된다. 

스프링 카프카 컨슈머

스프링 카프카 컨슈머는 기존 컨슈머를 2개 타입으로 나누고 커밋을 7가지로 나누어 세분화했다.

 

컨슈머 타입은 두 가지가 있다.

  1. 레코드 리스너(MessageListener): 한 번 호출되는 메서드에서 단 1개의 레코드를 처리한다. (default)
  2. 배치 리스너(BatchMessageListener): 기존 카프카 클라이언트 라이브러리의 poll() 메서드로 리턴받은 ConsumerRecords처럼 한 번에 여러 개의 레코드를 처리할 수 있다.

Reference