Kafka 기본 구조
이 이미지는 카프카의 전체 구성도 이다.
하나의 물리적(논리적) 서버에 하나의 카프카 서버가 설치되고 이것이 브로커이다.
위 이미지에서는 3개의 카프카 서버를 1개의 클러스터로 묶어서 운영하는 상황이다.
카프카 클러스터로 묶인 브로커들은 프로듀서가 보낸 데이터를 안전하게 분산 저장하고 복제하는 역할을 수행한다.
카프카의 데이터 복제는 파티션 단위로 이루어진다. 복제된 파티션은 리더와 팔로워로 구성된다.
프로듀서나 컨슈머가 통신하는 토픽의 파티션은 리더 파티션이라고 보시면 됩니다.
팔로워 파티션은 프로듀서로부터 리더 파티션으로 전달된 데이터를 복제(replication)하여 복제된 데이터를 저장한다. 팔로워 파티션은 리더 파티션의 데이터를 복사하여 보관하는 역할을 하고 있지만, 리더 파티션이 속해있는 브로커에 장애가 발생한다면, 팔로워 파티션에서 리더 파티션의 지위를 가지게 될 수 있다.
주키퍼는 카프카의 상태를 관리한다고 보면 된다.
브로커를 여러 개 두는 이유
하나의 브로커가 존재하고 이 하나의 브로커로 프로듀서와 컨슈머가 통신을 하게 되면 프로듀서와 컨슈머가 늘어날수록 브로커는 부하가 심해지게 되고 장애가 발생할 수 있다. 그래서 여러개의 브로커를 두고 거기에 리더 파티션과 팔로워 파티션을 둬서 프로듀서와 컨슈머의 요청을 분산시켜 부하를 줄인다. 하나의 브로커에 장애가 발생한다면 장애가 발생한 브로커의 리더 파티션의 역할이 다른 브로커의 팔로워 파티션으로 옮겨져서 빠르게 서비스 정상화가 가능하다.
Kafka 내부 구조
카프카 내부를 좀 더 자세히 알아보자.
우선 브로커 그다음 토픽 파티션 레코드 순서대로 알아본다.
브로커는 카프카의 데이터를 담는 애플리케이션이다.
토픽을 생성하면 브로커 애플리케이션이 파일 시스템을 생성하여 토픽에 레코드가 저장된다.
토픽은 카프카에서 데이터를 구분하기 위해 사용하는 단위다.
토픽은 1개 이상 파티션으로 이루어져 있으며 파티션이 존재하지 않는 토픽은 존재할 수 없다.
쉽게 생각해서 토픽은 RDB의 테이블과 같은 개념이라고 보면 된다.
파티션은 큐와 비슷한 구조다.
FIFO 구조와 같이 먼저 들어가 레코드는 컨슈머가 먼저 가져가게 된다.
다만 일반적인 큐는 데이터를 가져가면 해당 데이터를 삭제하지만 카프카에서는 삭제 하지 않는다.
파티션의 레코드는 컨슈머가 가져가는 것과 별개로 관리된다.
이러한 특징 때문에 토픽의 레코드는 다양한 목적을 가진 여러 컨슈머 그룹들이 토픽의 데이터를 여러 번 가져갈 수 있다.
파티션에는 프로듀서가 보낸 데이터들이 들어가 저장되는데 이 데이터를 레코드라고 부른다.
레코드는 타임스탬프, 메시지 키, 메시지 값, 오프셋, 헤더로 구성되어 있다.
이미지 맨 왼쪽을 보면 레코드 구조가 있는데 여기서는 제일 중요한 오프셋과 메시지 키 메시지 값에 대해서 얘기해 보겠다.
프로듀서가 우선 레코드를 생성하고 브로커로 전송한다.
브로커로 전송되면 브로커가 해당 레코드의 오프셋과 타임스탬프를 지정 후 파티션에 저장한다.
여기서 주의할 점은 프로듀서는 해당 레코드에 대한 오프셋을 지정하지 않는다.
메시지 키는 메시지 값을 순서대로 처리하거나 메시지 값의 종류를 나타내기 위해 사용된다.
메시지 키에 대해서는 뒤에서 좀 더 얘기하겠다.
메시지 값에는 실질적으로 처리할 데이터가 들어 있다.
메시지 키와 메시지 값은 직렬화되어 브로커로 전송되기 때문에 컨슈머가 이용할 때는 직렬화한 형태와 동일한 형태로 역직렬화를 수행해야 한다. 즉 직렬화, 역직렬화할 때는 반드시 동일한 형태로 처리해야 한다.
레코드의 오프셋은 0 이상의 숫자로 이루어져 있다.
레코드의 오프셋은 직접 지정할 수 없고 브로커에 저장될 때 이전에 전송된 레코드의 오프셋+1 값으로 생성한다.
예를 들어 이전에 전송된 레코드의 오프셋 값이 19이면 새로 들어온 레코드의 오프셋 값은 20이 된다.
오프셋은 카프카 컨슈머가 데이터를 가져갈 때 사용된다. 오프셋을 사용하면 컨슈머 그룹으로 이루어진 카프카 컨슈머들이 파티션의 데이터를 어디까지 가져갔는지 커밋을 하여 명확히 지정할 수 있다.
파티션이 여러 개가 존재하는 이유
카프카에서 파티션은 카프카의 병렬처리의 핵심이다.
그룹으로 묶인 컨슈머들이 레코드를 병렬로 처리할 수 있도록 매칭이 된다.
컨슈머의 처리량이 한정된 상황에서 많은 레코드를 병렬로 처리하는 가장 좋은 방법은 컨슈머의 개수를 늘려 스케일 아웃하는 것다. 컨슈머 개수를 늘림과 동시에 파티션 개수도 늘리면 처리량이 증가하는 효과를 볼 수 있다.
레코드가 파티션에 저장되는 방식
메시지 키 사용 안 하는 경우
- 첫 번째는 프로듀서가 메시지 키 없이 레코드를 보내는 경우다.
- 위와 같이 메시지 키가 없는 경우 라운드로빈 방식으로 파티션에 한 번씩 메시지를 할당한다.
- 예를 들어 0,1,2,3 값을 가진 레코드가 들어오면 0은 파티션 0번에 1은 파티션 2번에 2는 파티션 0번 이런 식으로 저장이 된다.
메시지 키 사용하는 경우
- 두 번째는 프로듀서가 메시지 키를 포함하여 레코드를 보내는 경우다.
- 위와 같이 메시지 키가 있는 경우 메시지 키의 해시값을 토대로 파티션을 지정한다. 즉 동일한 메시지 키라면 동일 파티션에 들어간다.
- 다만 맨 처음 메시지 키가 왔을 때 해당 키가 어느 파티션에 지정될지 알 수 없다.
- 메시지 키를 사용한다면 하나의 파티션에 특정 종류의 데이터의 연속성을 보장할 수 있다.
메시지 키와 데이터가 저장되는 순서
메시지 키 사용 여부는 데이터가 저장되는 순서와 밀접한 연관이 있다.
앞에서 메시지 키를 사용하여 레코드를 전송하면 특정 파티션으로 레코드를 순서대로 저장할 수 있게 되어 이를 통해 데이터의 저장 순서를 보장할 수 있다고 얘기했다. 하지만 메시지 키가 반드시 저장 순서를 보장해 주는 것은 아니다.
만약 파티션 개수가 달라지면 이미 매칭된 파티션과 메시지 키의 매칭이 깨지고 전혀 다른 파티션에 데이터가 할당될 수 있다. 왼쪽 상황에서는 액션이라는 메시지 키가 파티션 0번에 매칭이 되고 코믹 키가 파티션 1번에 매칭이 되는 상황이다. 이때 파티션이 하나 늘어나게 되면 기존 메시지 키와 파티션 매칭이 깨지고 액션과 코믹 키가 파티션 2번으로 매칭될 수 있다.
그러므로 파티션 개수 변환 이전과 이후 메시지 키와 파티션 매칭이 달라지기 때문에 파티션 개수가 달라지는 순간에는 메시지 키에 매칭된 특정 파티션을 사용하는 컨슈머는 데이터의 순서를 더는 보장받지 못한다.
데이터 삭제
카프카는 다른 메시징 플랫폼과 다르게 컨슈머가 데이터를 가져가더라도 토픽의 데이터는 삭제되지 않는다.
또한 컨슈머나 프로듀서가 데이터 삭제를 요청할 수도 없다. 오직 브로커만이 데이터를 삭제할 수 있다.
데이터 삭제는 파일 단위로 이루어지는데 이 단위를 ‘로그 세그먼트’ 혹은 세그먼트라고 부릅니다.
그러면 로그 세그먼트란 뭘까?
로그 세그먼트는 프로듀서가 전송한 실제 레코드가 브로커의 로컬 디스크에 저장되는 파일을 말한다.
여러 개의 레코드들이 파일에 저장되는데 이때 파일을 로그 세그먼트라고 생각하면 된다.
로그 세그먼트는 데이터가 쌓이는 동안 파일 시스템으로 열려있으며 카프카 브로커에 log.segement.bytes 또는 log.segment.ms 옵션의 설정된 값에 따라 세그먼트 파일이 닫힌다.
세그먼트 파일이 닫히게 되는 기본값은 1GB 용량에 도달했을 때다.
닫힌 세그먼트 파일은 log.retention.bytes 또는 log.retention.ms 옵션의 설정값을 넘으면 삭제된다.
세그먼트 파일의 크기 제한으로 삭제
파티션 A1의 상황을 예시로 들어 좀 더 자세히 얘기해보겠다.
처음 상황은 파티션 A1에 레코드들이 쌓인다. 그리고 실제 레코드들이 저장되는 파일을 로그 세그먼트라고 얘기한다.
하나의 로그 세그먼트에 계속 레코드를 저장하다가 세그먼트 파일의 크기가 1GB에 도달하면 해당 세그먼트 파일을 닫은 후 새로운 로그 세그먼트 파일을 생성한다.
시간 설정으로 삭제
닫힌 세그먼트 파일은 log.retention.bytes 또는 log.retension.{ms|minutes|hours} 옵션의 설정값을 넘으면 삭제된다. log.retention.bytes을 2GB로 설정 해놓으면 닫힌 세그먼트 파일의 크기의 합이 2GB일 경우 삭제된다. 또는 log.retension.{ms|minutes|hours} 옵션을 설정할 경우 설정한 시간이 지나면 삭제 된다.
컨슈머 그룹이란
Consumer Group 이란 컨슈머 인스턴스들을 대표하는 그룹이고
Consumer Instance란 하나의 프로세스 또는 하나의 서버라고 할 수 있다.
이 그림은 2개의 컨슈머 그룹을 나타내고 있다.
2개의 컨슈머 그룹은 서로를 구분하기 위한 이름으로 consumer-01, consumer-02이라고 되어 있고, consumer-01 그룹의 구성원은 4개의 서버로 구성되어 있으며, consumer-02 그룹의 구성은 6개의 서버로 구성되어 있다.
Group Coordinator
클러스터의 다수 브로커 중 한대는 코디네이터의 역할을 수행한다.
만약 브로커가 한대이면 해당 브로커가 코디네이터 역할도 수행한다.
코디네이터는 컨슈머 그룹의 상태를 체크하고 파티션을 컨슈머와 매칭되도록 분배하는 역할을 수행한다.
컨슈머 운영 방식
토픽의 파티션으로부터 데이터를 가져가기 위해 컨슈머를 운영하는 방법은 크게 2가지가 있다.
첫 번째는 1개 이상의 컨슈머로 이루어진 컨슈머 그룹을 운영하는 것이고
두 번째는 토픽의 특정 파티션만 구독하는 컨슈머를 운영하는 것이다.
컨슈머 그룹으로 운영하는 방법
먼저 컨슈머 그룹으로 운영하는 방법에 대해 알아보자.
우선, 컨슈머 그룹으로 운영하는 방법은 컨슈머를 각 컨슈머 그룹으로부터 격리된 환경에서 안전하게 운영할 수 있도록 도와주는 카프카의 독특한 방식이다. 컨슈머 그룹으로 묶인 컨슈머들은 토픽의 1개 이상 파티션들에 할당되어 데이터를 가져갈 수 있다. 이미지는 한 개의 컨슈머가 파티션 3개에 할당되어 데이터를 가져오는 구조다.
컨슈머 그룹으로 묶인 컨슈머가 토픽을 구독해서 데이터를 가져갈 때 1개의 파티션은 최대 1개의 컨슈머에 할당 가능하다. 그리고 1개 컨슈머는 여러 개의 파티션에 할당될 수 있다.
3개의 파티션을 가진 토픽을 효과적으로 처리하기 위해서는 3개 이하의 컨슈머로 이루어진 컨슈머 그룹으로 운영해야 한다. 왼쪽 이미지와 같이 4개의 컨슈머로 이루어진 컨슈머 그룹에 3개의 파티션을 가진 토픽이 할당하면 1개의 컨슈머는 파티션을 할당받지 못해 스레드만 차지하고 실질적인 데이터 처리를 하지 못하므로 불필요한 스레드로 남게 된다.
이러한 특징으로 컨슈머 그룹의 컨슈머 개수는 가져가고자 하는 토픽의 파티션 개수보다 같거나 작아야 한다.
앞에서 말했다시피 같은 컨슈머 그룹일 때는 1개의 파티션은 최대 1개의 컨슈머에 할당 가능하다.
오른쪽 이미지와 같이 한 개의 파티션이 같은 그룹에 있는 2개 이상의 컨슈머에 할당은 불가능하다.
컨슈머 그룹은 다른 컨슈머 그룹과 격리되는 특징을 가지고 있다.
이 말은 카프카 프로듀서가 보낸 데이터를 각기 다른 컨슈머 그룹끼리 영향을 받지 않게 처리할 수 있다는 소리다.
이처럼 컨슈머 그룹은 다른 컨슈머 그룹과 격리되는 특징으로 특정 그룹의 장애 발생 시, 서로의 그룹 간 장애가 격리되는 구조이므로 유연하게 대처가 가능한 장점이 있다.
컨슈머 그룹의 컨슈머에 장애가 발생할 경우
컨슈머 그룹으로 이루어진 컨슈머들 중 일부 컨슈머에 장애가 발생하면, 장애가 발생한 컨슈머에 할당된 파티션은 장애가 발생하지 않은 컨슈머에 소유권이 넘어간다.
컨슈머 중 1개에 이슈가 발생하여 더는 동작을 안 하고 있다면 이슈가 발생한 컨슈머에 할당된 파티션은 더는 데이터 처리를 하지 못하고 있으므로 데이터 처리에 지연이 발생할 수 있다.
이를 해소하기 위해 이슈가 발생한 컨슈머를 컨슈머 그룹에서 제외하여 모든 파티션이 지속적으로 데이터를 처리할 수 있도록 가용성을 높인다.
이러한 과정을 ‘리밸런싱’이라고 부르며 주로 아래 네 가지 상황에서 발생한다.
- 컨슈머 그룹에 새로운 컨슈머가 추가될 때
- 기존 컨슈머가 그룹에서 나갈 때
- 구독하는 토픽에 새로운 파티션이 생길 때
- 컨슈머가 구독하는 토픽이 변경될 때
리밸런싱이 발생할 때 파티션의 소유권을 컨슈머로 재할당하는 과정에서 해당 컨슈머 그룹의 컨슈머들이 토픽의 데이터를 읽을 수 없기 때문에 자주 일어나서는 안된다.
리밸런싱은 무거운 작업이기에 자주 발생하게 되면 시스템에 부담이 될 수 있다.
이를 최소화하기 위한 다양한 방법들이 있고 그중 '파티션 할당 전략'에 대해 알아보자.
Partition Assignment Strategy(파티션 할당 전략)이란?
'Partition Assignment Strategy'는 카프카 컨슈머가 토픽의 어떤 파티션을 소비할 것인지 결정하는 방식을 의미한다. 이 전략에 따라 컨슈머와 파티션 간의 관계가 결정되며, 데이터 처리 효율성과 성능에 큰 영향을 미친다.
카프카의 파티션 할당 전략은 크게 '적극적 리밸런싱'과 '협력적 리밸런싱'으로 나뉘며, 이에 따라 아래의 4가지의 파티션 할당 전략이 존재한다.
- 레인지(Range) 파티션 할당 전략
- 라운드 로빈(RoundRobin) 파티션 할당 전략
- 스티키(Sticky) 파티션 할당 전략
- 협력적 스티키(CooperativeSticky) 파티션 할당 전략
우선 적극적 리밸런싱과 협력적 리밸런싱에 대해 알아보자.
적극적 리밸런싱(Eager Rebalance)
'레인지(Range), 라운드 로빈(RoundRobin), 스티키(Sticky) 파티션 할당 전략'이 해당하는 방식이다.
리밸런싱이 일어나게 되면 모든 컨슈머는 카프카로부터 데이터 수신을 중단하고, 자신들이 가지고 있던 파티션의 그룹 구성을 포기한다. 이 과정에서 모든 컨슈머가 동시에 작업을 멈추게 되고 이로 인해 전체 컨슈머 그룹의 데이터 처리가 일시적으로 중단된다.
그림에서는 revoke all 이 발생한 이후의 상황으로 보면 된다.
이 시간 동안 컨슈머 그룹은 유휴 상태가 되어 메시지를 소비하거나 오프셋 커밋을 허용하지 않는다.
이에 반해 프로듀서의 경우에는 리밸런싱과 무관하게 파티션에 데이터를 계속해서 쓰기 때문에 대기 시간 동안에는 LAG가 급격하게 증가하게 된다. 이로 인해 컨슈머 그룹의 데이터 처리 성능에 일시적인 영향을 미칠 수 있다.
리밸런싱 이후 즉 Assign All 이후에는 컨슈머들이 그룹에 다시 참여하고, 새로운 파티션을 할당받게 된다.
이때 주의할 점은, 컨슈머들이 이전에 가졌던 파티션을 반드시 다시 받는다는 보장이 없다.
즉 리밸런싱 후에는 컨슈머들이 새로운 파티션을 할당받을 가능성이 있다.
이러한 특성들로 인해, '적극적 리밸런싱'은 그룹의 안정성이나 데이터 처리 성능에 영향을 미칠 수 있어 신중한 사용이 필요하다.
협력적 리밸런싱(Cooperative Rebalance, Incremental Rebalance)
그다음으로는 협력적 리밸런싱에 대해 알아보겠습니다.
'협력적 스티키(CooperativeSticky) 파티션 할당 전략'이 해당하는 방식으로, 협력적 리밸런싱은 Kafka v2.4부터 도입된 꽤나 최근에 나온 진보된 리밸런싱 방식이다. 이 방식은 파티션의 일부만이 그니까 그림에서는 파티션 2번이 이에 해당한다.
파티션의 일부만이 한 컨슈머에서 다른 컨슈머로 이동하는 특징을 가지며, 이는 리밸런싱과 직접적인 관련이 없는 다른 카프카 컨슈머들이 데이터 처리를 계속 진행할 수 있게 하여, 전체 그룹의 처리 성능에 미치는 영향을 최소화한다.
'협력적 리밸런싱'은 안정적인 파티션 할당 상태를 점진적으로 찾아가는 과정으로, 여러 차례의 리밸런싱을 거친다.
첫 번째 단계에서는 리더 파티션이 모든 컨슈머에게 일부 파티션의 소유권을 잃게 될 것임을 알리고, 컨슈머는 이러한 파티션의 소유권을 포기한다. 그 후 단계에서 조정자는 이 고아가 된 파티션을 새로운 컨슈머에게 할당한다.
이미지를 보면 파티션 0,1,2 중에 리더 파티션이 존재하는데 이 리더 파티션이 컨슈머 그룹에 파티션 2번의 소유권을 잃게 될 거라고 알린다. 그러면 컨슈머 2번은 파티션 2에 대한 소유를 포기한다. 그 후 코디네이터가 파티션 2번을 컨슈머 3번에 할당한다.
이 방식은 안정적인 파티션 할당 상태가 될 때까지 몇 번의 반복이 필요하지만, 적극적 리밸런싱에서 발생하는 서비스 중단 상황을 피할 수 있어, 리밸런싱에 많은 시간이 소요되는 큰 규모의 컨슈머 그룹에서 특히 중요하다.
따라서 협력적 리밸런싱은 전체 그룹의 성능을 유지하면서 파티션 할당을 유연하게 조절할 수 있는 장점이 있다.
하지만 이 방식을 활용할 때에도 신중하게 접근해야 하며, 그룹의 상황에 따라 적절한 전략을 선택하는 것이 중요하다.
4가지의 파티션 할당 전략 중 레인지와 협력적 스티키 파티션 할당 전략에 대해 알아보자.
1. 레인지 파티션 할당 전략
'레인지(Range) 파티션 할당 전략'은 아파치 카프카 v2.4 이전까지는 기본으로 설정된 '적극적 리밸런싱' 방식의 파티션 할당 전략이다.
이 전략은 다음과 같은 순서로 진행된다.
- 먼저 구독 중인 토픽의 파티션과 컨슈머를 순서대로 나열한다.
- 이후 각 컨슈머가 받아야 할 파티션의 수를 결정하는데, 이는 해당 토픽의 전체 파티션 수를 컨슈머 그룹의 총 컨슈머 수로 나눈 값이다.
- 만약 컨슈머 수와 파티션 수가 정확히 일치한다면, 모든 컨슈머는 파티션을 균등하게 할당받는다.
- 그러나 파티션 수가 컨슈머 수로 균등하게 나누어지지 않는다면, 순서상 앞에 있는 컨슈머들이 추가로 파티션을 할당받게 된다.
위 그림으로 예시를 보이면 각 토픽에 존재하는 파티션의 수는 총 2개, 컨슈머 수는 총 3개다.
따라서 2를 3으로 나눈 결과인 2/3가 되어 한 컨슈머는 파티션을 할당받지 못하게 된다.
만약 파티션 수가 3개 컨슈머 수가 3개라면 모든 컨슈머는 파티션을 균등하게 할당받는다.
이 전략의 큰 장점 중 하나는 특정 도메인에 대한 데이터 처리를 한 컨슈머에서 일관되게 관리할 수 있다.
그러나 이 전략의 단점은 파티션의 분배가 균등하지 않을 수 있어 특정 컨슈머가 과부하를 겪을 수 있다.
4. 협력적 스티키 파티션 할당 전략
이 전략은 전체 컨슈머 그룹이 아닌 개별 컨슈머에 초점을 맞춰 더욱 유연하고 효율적인 리밸런싱을 가능하게 한다.
카프카 v2.4부터 default로 지정되었다.
이미지 위쪽은 새로운 컨슈머인 컨슈머 3번이 추가될 때이고, 아래는 기존의 컨슈머인 컨슈머 3이 제외될 때를 나타낸다.
협력적 리밸런싱은 리밸런싱이 필요한 특정 파티션에만 집중하며, 그 외의 나머지 파티션들은 그대로 유지되는 방식이다. 여기서는 특정 파티션을 파티션 3번으로 보면 된다.
즉, 컨슈머 재할당이 필요한 특정 파티션의 소비만 중지하고, 다른 나머지 파티션에서는 계속해서 데이터를 소비한다.
전체 컨슈머 그룹이 아닌 개별 컨슈머의 작업 중단을 최소화하므로, 전체적인 데이터 처리 성능에 더 적은 영향을 미친다.
따라서, 협력적 스티키 파티션 할당 전략은 컨슈머가 재할당되지 않은 파티션에서 계속 데이터를 소비할 수 있도록 지원하며, 이로 인해 리밸런싱의 영향을 최소화하면서 효율적인 리밸런싱을 가능하게 한다.
컨슈머 주요 옵션
다음으로 컨슈머의 주요 옵션에 대해 알아보자.
컨슈머 애플리케이션을 실행할 때 설정해야 하는 필수 옵션과 선택 옵션이 있다.
필수 옵션은 사용자가 반드시 설정해야 하는 옵션이고 선택 옵션은 사용자의 설정을 필수로 받지 않는다.
필수 옵션
필수 옵션으로는 프로듀서가 데이터를 전송할 대상 카프카 클러스터에 속한 브로커의 호스트 이름:포트 설정
레코드의 메시지 키와 값을 역직렬화하는 클래스를 지정하는 옵션이 있다
선택 옵션
선택 옵션으로는 컨슈머 그룹 아이디 지정 이것은 특정 컨슈머 그룹에 속할 수 있게 하는 옵션이다.
또 다른 것은 자동 커밋으로 할지 수동 커밋으로 선택하는 옵션 등 수많은 선택 옵션들이 존재한다.
auto.offset.reset
여러 선택 옵션 중에 auto.offset.reset 옵션에 대해 좀 더 자세히 얘기해보겠다.
auto.offset.reset 옵션은 컨슈머 그룹이 특정 파티션을 읽을 때 저장된 컨슈머 오프셋이 없는 경우 어느 오프셋부터 읽을지 선택하는 옵션이다.
오프셋 옵션 종류 특징
auto.offset.reset=latest | 가장 최신 오프셋부터 읽기 시작 |
auto.offset.reset=earliest | 가장 오래전에 넣은 오프셋부터 읽기 시작 |
auto.offset.reset=none | 컨슈머 그룹이 커밋한 기록이 있는지 찾아서 없으면 오류를, 있다면 기존 커밋 기록 이후 오프셋부터 읽기 시작 |
맨 위 이미지는 earliest 옵션일 때의 상황이다.
처음에는 컨슈머 그룹 A가 파티션 0번에 붙어서 레코드를 5번까지 처리하고 컨슈머 그룹 B가 새로 등장했는데 earliest 옵션일 경우 컨슈머 그룹 B는 0번 이후부터의 레코드를 가져온다.
그다음은 latest 옵션일 때의 상황이다.
컨슈머 그룹 A가 파티션 0번에 붙어서 레코드를 5번까지 처리하고 컨슈머 그룹 B가 새로 등장했는데 latest 옵션일 경우 컨슈머 그룹 B는 5번 이후부터의 레코드를 가져온다.
auto.offset.reset = latest
그렇다면 컨슈머 그룹 C가 새로 등장했을 때 컨슈머 그룹 C는 몇 번 오프셋부터 레코드를 가져올까?
정답은 9번이다. lastest 옵션은 최신 오프셋부터 읽어 오는 옵션이고 현재 토픽에서의 최신 오프셋은 9다.
따라서 컨슈머 그룹 C는 최신 오프셋인 9번부터 레코드를 가져오게 된다.
주의할 점은 latest 옵션의 가장 최신 오프셋은 커밋이 완료된 최신 오프셋이 아니다.
둘을 헷갈리지 말자
오프셋 커밋
가용성을 높이면서도 안정적인 운영을 도와주는 리밸런싱은 유용하지만 자주 일어나서는 안 된다.
리밸런싱이 발생할 때 파티션의 소유권을 컨슈머로 재할당하는 과정에서 해당 컨슈머 그룹의 컨슈머들이 토픽의 데이터를 읽을 수 없기 때문이다. 그룹 조정자(group coordinator)는 리밸런싱을 발동시키는 역할을 하는데 컨슈머 그룹의 컨슈머가 추가되고 삭제될 때 감지한다. 카프카 브로커 중 한 대가 그룹 조정자의 역할을 수행한다.
컨슈머는 카프카 브로커로부터 데이터를 어디까지 가져갔는지 커밋(commit)을 통해 기록한다.
특정 토픽의 파티션을 어떤 컨슈머 그룹이 몇 번째 가져갔는지 카프카 브로커 내부에서 사용되는 내부 토픽(consumer_offsets)에 기록된다. 컨슈머 동작 이슈가 발생하여 consumer_offsets 토픽에 어느 레코드까지 읽어갔는지 오프셋 커밋이 기록되지 못했다면 데이터 처리의 중복이 발생할 수 있다. 그러므로 데이터 처리의 중복이 발생하지 않게 하기 위해서는 컨슈머 애플리케이션이 오프셋 커밋을 정상적으로 처리했는지 검증해야만 한다. (컨슈머는 처리 완료한 레코드의 오프셋을 커밋한다)
오프셋 커밋은 컨슈머 애플리케이션에서 명시적, 비명시적으로 수행할 수 있다.
기본 옵션은 poll() 메서드가 수행될 때 일정 간격마다 오프셋을 커밋하도록 enable.auto.commit=true 설정되어 있다. 이렇게 일정 간격마다 자동으로 커밋되는 것을 비명시 '오프셋 커밋'이라고 부른다. 이 옵션은 auto.commit, interval.ms에 설정된 값과 함께 사용되는데, poll() 메서드가 auto.commit.interval.ms에 설정된 값 이상이 지났을 때 그 시점까지 읽은 레코드의 오프셋을 커밋한다. poll() 메서드를 호출할 때 커밋을 수행하므로 코드상에서 따로 커밋 관련 코드를 작성할 필요가 없다. 비명시 오프셋 커밋은 편리하지만 poll() 메서드 호출 이후에 리밸런싱 또는 컨슈머 강제종료 발생 시 컨슈머가 처리하는 데이터가 중복 또는 유실될 수 있는 가능성이 있는 취약한 구조를 가지고 있다. 그러므로 데이터 중복이나 유실을 허용하지 않는 서비스라면 자동 커밋을 사용해서는 안 된다.
명시적으로 오프셋을 커밋하려면 poll() 메서드 호출 이후에 반환받은 데이터의 처리가 완료되고 commitSync() 메서드를 호출하면 된다. commitSync() 메서드는 poll() 메서드를 통해 반환된 레코드의 가장 마지막 오프셋을 기준으로 커밋을 수행한다. commitSync() 메서드는 브로커에 커밋 요청을 하고 커밋이 정상적으로 처리되었는지 응답하기까지 기다리는데 이는 컨슈머의 처리량에 영향을 끼친다.
데이터 처리 시간에 비해 커밋 요청 및 응답에 시간이 오래 걸린다면 동일 시간당 데이터 처리량이 줄어들기 때문이다. 이를 해결하기 위해 commitAsync()메서드를 사용하여 커밋 요청을 전송하고 응답이 오기 전까지 데이터 처리를 수행할 수 있다. 하지만 비동기 커밋은 커밋 요청이 실패했을 경우 현재 처리 중인 데이터의 순서를 보 장하지 않으며 데이터의 중복 처리가 발생할 수 있다.
컨슈머의 내부 구조에 대해 알아보자. 컨슈머는 poll() 메서드를 통해 레코드들을 반환받지만 poll() 메서드를 호출하는 시점에 클러스터에서 데이터를 가져오는 것은 아니다. 컨슈머 애플 리케이션을 실행하게 되면 내부에서 Fetcher 인스턴스가 생성되어 poll() 메서드를 호출하기 전에 미리 레코드들을 내부 큐로 가져온다. 이후에 사용자가 명시적으로 poll() 메서드를 호출하면 컨슈머는 내부 큐에 있는 레코드들을 반환받아 처리를 수행한다.
명시적/비명시적 오프셋 커밋 방법이 있다.
비명시 오프셋 커밋(자동 커밋) 방식은 enable.auto.commit=true와 auto.commitl.inverval.ms옵션을 통해 일정 시간마다 자동으로 오프셋 커밋이 발생되도록 설정할 수 있다.
오프셋 커밋 방식 구현 방법 특징 데이터 유실 가능 여부
비명시적 커밋(자동 커밋) | enable.auto.commit=true 와 auto.commitl.inverval.ms옵션을 통해 설정 | 로그성 데이터 처리 같이 사용자가 오프셋, 파티션 관리를 하지 않아도 되는 경우 사용됨.구현하기는 쉽지만 리밸런싱이나 컨슈머 강제 동료과 같은 상황이 발생했을 경우, 데이터 유실/중복이 발생할 수 있다. | O |
명시적 커밋(수동 커밋) | (poll() 메서드 호출 후)commitSync() 호출 | 메시지 처리 완료 때 까지 메시지를 가져온 것으로 간주되면 안되는 경우에 사용이전 커밋이 실패하더라도 다음 커밋이 처리되긴 하지만 수동 커밋임으로 이러한 상황이 발생할 경우에 대해 대응하는 코드를 짜둠으로써(ex. consumerRecord.seek() or retry 설정) 처리가 가능한 커밋 방식이다(이전 커밋에 실패했을 때 해당 커밋을 재시도하기 위한 설정이 아님)브로커에 커밋 요청 후 응답을 받을 때 까지 기다렸다가 다음 커밋을 진행하기 때문에 속도가 느려 처리량이 줄어든다. | X |
(poll() 메서드 호출 후)commitAsync() 호출 | 커밋 요청 후 응답 받기 전에 비동기적으로 다음 커밋을 요청한다. 때문에 이전 커밋 요청이 실패했더라도 다음 커밋을 처리하는 과정에서 데이터의 순서 보장이 안되거나 중복 처리가 발생할 수 있다. | O |
'Kafka' 카테고리의 다른 글
Kafka KRaft Mode (0) | 2024.07.12 |
---|---|
kafka가 왜 필요할까? (0) | 2024.07.07 |