반응형
개요
- 이번 글에서는 카프카의 컨슈머에 대한 동작 원리와 컨슈머 오프셋, 컨슈머들의 파티션 할당 정책과 프로듀서의 정확히 한 번 전송과 결합해 트랜잭션 컨슈머의 내부 동작에 대해서 글을 작성하도록 해보겠습니다.
컨슈머 오프셋 관리
- 컨슈머의 동작 중 가장 핵심은 오프셋 관리입니다.
- 컨슈머의 오프셋의 역할은 메시지의 위치를 나타내는것입니다. 이러한 오프셋이 중요한 이유는 컨슈머가 일시적으로 동작을 멈추고 재시작 하는 경우에 새로운 컨슈머가 기존 컨슈머의 역할을 대신하는 경우에 기존 컨슈머의 마지막 메시지 위치부터 새로운 컨슈머가 메시지를 가져오기 때문입니다.
- 오프셋 정보는 숫자형태로 나타내며 컨슈머 그룹이 오프셋 정보를 카프카에서 가장 안전한 저장소인 토픽에 저장하여 관리하고 있습니다.
- 컨슈머 그룹은
_consumer_offsets
파일에 오프셋 정보, 토픽, 파티션, 컨슈머 그룹 등의 내용을 통합해서 기록합니다.
- 컨슈머 그룹은
_consumer_offsets
저장된 오프셋값을 활용하여 어디까지 메시지를 읽었는지 확인합니다.
- 여기서 저장되는 오프셋값은 컨슈머가 다음으로 읽어야할 위치를 의미합니다.
그룹 코디네이터
- 컨슈머 그룹이란 컨슈머들이 모여있는 집합체를 의미합니다. 모든 컨슈머는 한 개의 컨슈머 그룹에 포함되어 있으며 자신의 정보를 공유하면서 하나의 공통체로 동작합니다.
- 그룹 내의 컨슈머들은 언제든지 자신이 속합 그룹을 떠날수 있으며 새로운 컨슈머가 합류할 수도 있습니다.
- 따라서 그룹 컨슈머는 각 컨슈머에게 작업을 균등하게 분배해야하는데 컨슈머들에게 작업을 균등하게 분해하는 동작을 컨슈머 리벨런싱(Consumer Rebalancing)이라고 합니다.
- 이전에 프로듀서를 설명하는 글에서 정확히 한 번 전송을 관리하기 위해 트랜잭션 코디네이터가 있는 것을 확인했었는데 컨슈머 또한 안정적인 컨슈머 그룹 관리를 위해 별도의 코디네이터가 존재합니다. 이를 카프카에서는 그룹 코디네이터라고 호칭합니다.
- 그룹 코디네이터의 목적은 컨슈머 그룹이 구독한 토픽의 파티션들과 그룹의 맴버들을 트래킹하는 것입니다. 따라서 파티션 또는 그룹의 맴버에 변화가 생길 경우 작업을 균등하게 재분배하기 위해 리밸런싱 동작이 발생하게 됩니다.
- 그룹 코디네이터는 각 컨슈머 그룹에 그룹별로 존재하며 그룹 코디네이터는 카프카 클러스터내의 브로커 중 하나에 위치합니다.
- 아래는 컨슈머 그룹과 그룹 코디네이터가 어떻게 동작하는지에 대한 절차입니다.
- 컨슈머는 컨슈머 설정값중에서 bootstrap.brokers 리스트에 있는 브로커에게 컨슈머 클라이언트와 초기 커넥션을 연결하기 위한 요청을 보냅니다.
- 해당 요청을 받은 브로커는 그룹 코디네이터를 생성하고 컨슈머에 응답을 보냅니다.
- 그룹 코디네이터는 group.initial.reablance.delay.ms의 시간 동안 컨슈머 요청을 기다립니다.
- 컨슈머는 컨슈머 등록 요청을 그룹 코디네이터에 요청합니다. 이때 가장 먼저 요청을 보내는 컨슈머가 컨슈머 그룹의 리더가 됩니다.
- 컨슈머 등록 요청을 받은 그룹 코디네이터는 해당 컨슈머 그룹이 구독하는 토픽 파티션 리스트 등 리더 컨슈머의 요청에 응답을 보냅니다.
- 리더 컨슈머는 정해진 컨슈머 파티션 할당 전략에 따라 그룹 내 컨슈머들에게 파티션 할당을 한 뒤 그룹 코디네이터에게 전달합니다.
- 그룹 코디네이터는 해당 정보를 캐시하고 각 그룹 내 컨슈머들에게 성공을 알립니다.
- 각 컨슈머들은 각자 지정된 토픽 파티션으로부터 메시지를 가져옵니다.
- 컨슈머가 중간에 이탈하는것을 방지하는것을 감지하기위해 컨슈머와 그룹 코디네이터는 서로 하트비트라는 수치를 주고 받습니다.
- 하트비트를 주기적으로 주고받으면서 그룹 코디네이터는 컨슈머가 잘 살아 있는지 확인하는것입니다.
- 하트비트와 관련된 옵션은 아래와 같습니다.
컨슈머 옵션 | 값 | 설명 |
heartbeat.interval.ms | 3000 | 그룹 코디네이터와 하트비트 인터벌 시간을 의미함. 기본값은 3000이며 session.timeout.ms보다 낮게 설정해야한다. 평균적으로 session.timeout.ms의 1/3이 적당함 |
session.timeout.ms | 10000 | 어떤 컨슈머가 특정 시간 안에 하트비트를 받지 못하면 문제가 발생했다고 판단하는 기준. 만약 하트 비트를 받지 못했다고 판단하는 경우 컨슈머 그룹에서 해당 컨슈머는 제거되고 리밸런싱 동작이 일어남. 기본값은 10000 |
max.poll.interval.ms | 300000 | 컨슈머 poll()을 활용해 컨슈머가 문제가 있는지를 판단하는 기준. 컨슈머는 주기적으로 poll()을 호출하여 토픽으로부터 레코드를 가져오는데 poll() 호출 후 최대 5분간 응답값이 없으면 리밸런싱 동작이 일어남. |
- 위 표를 확인해보면 그룹 코디네이터와 컨슈머간의 상태를 확인하는 수단은 poll()을 호출한 시간과 하트비트 두 가지를 활용해 상태를 체크한다는것을 알 수 있습니다.
- 컨슈머 리밸런싱 동작은 경우에 따라 매우 높은 비용이 지출되므로 리밸런싱이 자주 발생하지 않도록 주의해야합니다.
- 위 옵션 부분은 기본 설정을 유지하기를 권장하며 반드시 필요한 경우에만 옵션값을 변경하기를 권장합니다.
스태틱 멤버십
- 스태틱 맴버십이란 컨슈머 그룹 내에서 컨슈머가 재시작 등으로 컨슈머 그룹에서 나갔다가 다시 합류하더라도 리밸런싱이 일어나지 않게 하는 기능입니다.
- 스태틱 맴버십 기능을 적용하기 위해서는 기본값이 null로 설정되어있는 group.instance.id값을 컨슈머를 식별하기 위한 고유값을 입력하고 session.timeout.ms를 기본값보다는 큰값으로 입력해야합니다.
- session.timeout.ms를 기본값보다 작게 설정할 경우 지정 시간동안 그룹 코디네이터가 하트비트를 받지 못할 경우 강제로 리벨런싱이 일어나기 때문입니다.
- 스태틱 맴버십은 불필요한 리밸런싱을 막기 위해 등장한 개념입니다. 예를 들어 하드웨어 점검이나 소프트웨어 업데이트와 같은 작업으로 인해 재시작을 해야할 때 컨슈머는 결과적으로 사용자에 의해 재시작 되는데 리밸런싱 작업이 매번 일어나게 됩니다.
- 이러한 문제를 해결할 때 스태틱 맴버십을 사용하면 유용합니다. session.timeout.ms보다 큰 시간으로 설정하여 리밸런싱이 일어나는 시간을 미룰수 있기 때문입니다.
- 스태틱 맴버십 또한 session.timeout.ms 시간을 넘어갈 경우 리밸런싱이 똑같이 일어나기 때문에 자신인 진행하는 작업 시간을 고려하여 리밸런싱 시간을 연기하는 수치를 입력하면 됩니다.
컨슈머 파티션 할당 전략
- 컨슈머 파티션 할당 전략은 프로듀서의 파티셔너가 어떤 파티션으로 메시지를 전송할 지 결정했던 것처럼 컨슈머가 어떤 파티션의 메시지를 읽어올지를 결정하는 정책입니다.
- 파티션 할당 전략에는 레인지 전략(RangeAssignor), 라운드 로빈 전략(RoundRobinAssignor), 스티키 전략(StickyAssignor), 협력적 스티키 전략(CooperativeStickyAssignor) 4가지 가 존재합니다.
레인지 파티션 할당 전략
- 레인지 파티션 할당 전략은 파티션 할당 전략의 기본값입니다.
- 전략의 진행 절차는 먼저 구독하는 토픽에 대한 파티션을 순서대로 나열한 후 컨슈머를 순서대로 나열합니다.
- 이후 각 컨슈머가 몇 개의 파티션을 할당해야 하는지 한 토픽의 전체 파티션 수를 컨슈머 그룹의 총 컨슈머 수로 나눈 개수 만큼 분배합니다.
- 컨슈머 수와 파티션 수가 일치하면 균등하게 할당 되지만 균등하게 나눠지지 않는 경우 앞쪽 컨슈머들이 추가로 파티션을 할당 받습니다.
- 레인지 파티션 할당 전략은 불균형하게 할당되는 전략이라 왜 사용하는지 의문이 들 수 있지만 동일한 레코드 키를 생산하는 별도의 토픽 파티션이 2개 이상일 때 2개 이상의 파티션을 한 개의 컨슈머가 소비할 때 유용할 수 있습니다. 이런 상황이 아니라면 균등하게 컨슈머에 파티션이 할당되지 않으므로 레인지 파티션 할당 전략은 추천하지 않습니다.
라운드 로빈 파티션 할당 전략
- 라운드 로빈 파티션 할당 전략은 말 그대로 라운드 로빈 형식으로 파티션을 컨슈머에 할당하는 정책입니다.
- 위와 같은 그림을 아래의 테이블 같은 순서대로 하나씩 컨슈머에 매핑하게 됩니다.
파티션 | 매핑된 컨슈머 |
토픽1-파티션0 | 컨슈머1 |
토픽1-파티션1 | 컨슈머2 |
토픽1-파티션2 | 컨슈머1 |
토픽2-파티션0 | 컨슈머2 |
토픽2-파티션1 | 컨슈머1 |
토픽2-파티션2 | 컨슈머2 |
스티키 파티션 할당 전략
- 스티키 파티션 할당 전략은 리밸런싱 작업이 일어나기 전의 컨슈머의 파티션 정보를 우선으로 매핑해주는 전략입니다.
- 무슨 의미이냐면 라운드 로빈 파티션 할단 전략의 그림에 리밸런싱이 일어날 경우 토픽1의 파티션0이 컨슈머1에 매핑되리라는 보장은 없습니다. 컨슈머2에 매핑이 될 수도 있기 때문입니다.
- 스티키 파티션 할당 전략은 위와 같은 상황에서 기존에 매핑됐던 파티션과 컨슈머를 최대한 유지하려고 하는 전략입니다.
- 스키마 파티션 할당 전략은 두 가지 목적으로 컨슈머에 파티션을 할당합니다.
- 첫 번째 목적은 가능한 균형 잡힌 파티션을 할당하는 것
- 두 번째 목적은 재할당이 발생할 때 되도록 기존의 할당된 파티션 정보를 보장하는 것
- 첫 번째 목적이 우선순위가 더 높습니다.
- 스티키 파티션 할당 전략이라고 해서 무조건 기존의 파티션과 컨슈머를 유지하지는 않습니다. 균형 잡힌 파티션을 할당하는 것이 최우선순위이기 때문입니다.
- 아래에서는 스키마 파티션 할당 전략이 어떻게 동작하고 라운드 로빈 전략과 어떻게 다른지 그림을 보면서 비교해보겠습니다.
- 만약 위와 같은 상황에서 컨슈머2가 다운되었다고 가정해보겠습니다.
- 위와 같은 경우는 라운드 로빈 전략으로 리밸런싱이 진행된 경우입니다.
- 위와 같은 경우는 스티키 파티션 할당 전략으로 재할당된 경우입니다.
- 라운드 로빈 전략과 다르게 최초 컨슈머2가 다운되었을때의 컨슈머1과 컨슈머2 정보가 그대로 유지된채 리밸런싱이 일어난것을 확인할 수 있습니다.
- 스티키 파티션 할당 전략이 동작하는 절차는 아래와 같습니다.
- 컨슈머들끼리 최대 할당된 파티션 수의 차이는 1을 유지함
- 기존에 존재하는 파티션 할당은 최대한 유지함
- 재할당 동작시 유효하지 않은 모든 파티션 할당은 제거함
- 할당되지 않은 파티셔들은 균형을 맞추는 방법으로 컨슈머들에게 할당함
- 위와 같은 절차로 움직이기에 최소한의 움직임으로 컨슈머를 할당하기 때문에 스티키 파티션 전략은 라운드 로빈 전략보다 효율적입니다.
협력적 스티키 파티션 할당 전략
- 협력적 스티키 파티션 할당 전략은 스티키 파티션 할당 전략과 동일합니다. 즉 리밸런싱이 일어나도 기존의 컨슈머와 파티션 매핑은 유지하고 최소한의 파티션만 컨슈머와 매핑합니다.
- 협력적 스티키 파티션 할당 전략은 스티키 파티션보다 한 가지 차이점이 존재하는데 내부 리밸런싱 동작이 한층 더 고도화됐다는 부분입니다.
- 협력적 스티키 파티션 할당 전략의 이전 전략들은 모두 EAGER라는 리밸런스 프토콜을 사용했습니다.
- EAGER 리밸런스 프로토콜은 리밸런스시 모든 파티션과 컨슈머의 연결을 끊는것입니다.
- 이러한 이유는 크게 두 가지가 있는데 첫 번째로는 컨슈머들의 파티션 소유권을 변경해야하기 때문입니다.
- 하나의 컨슈머 그룹내에서는 둘 이상의 컨슈머가 동일한 파티션을 소유할 수 없는데 A컨슈머가 B컨슈머에게 0번 파티션의 할당을 이전해야하는 경우에 모든 컨슈머가 0번 파티션에 소유권을 가지고 있으면 안되기 때문입니다.
- 두 번째로는 그룹 내에서 여러 파티션들에 대한 소유권 변경 작업을 동시에 이뤄져야 하므로 이러한 로직을 단순하게 구현하기 위해서입니다.
- 하지만 이런 EAGER 리밸런싱 프로토콜은 모든 파티션 할당을 취소하기 때문에 리소스를 많이 사용하는 컨슈머 그룹에서 컨슈머들의 다운 타임에 문제가 생기게 되었습니다.
- 위 그림은 리밸런싱의 전체 동작과정을 나타낸 그림으로서 크게 감지, 중지, 재시작으로 구분됩니다.
- 첫 번째 단계에서 컨슈머가 다운되었음을 감지 두 번째 단계에서 컨슈머에 할당된 모든 파티션을 제거하면서 다운타임이 발생하게 됩니다. 컨슈머의 동작과 프로듀서의 동작은 완벽하게 분리되어 있으므로 리밸런싱하는 동중에도 프로듀서는 해당 토픽의 파티션에 메시지를 전송하게 되고 이는 컨슈머의 다운타임동안 LAG이 급격하게 증가되는 현상을 만들게 됩니다.
- 이러한 문제점이 지나간후 구독한 파티션이 컨슈머들에게 재할당 됩니다.
- 이러한 문제점을 해결하고자 협력적 스티키는 내부 리밸런싱 프로토콜인 EAGER가 아닌 COOPERATIVE(협력적) 프로토콜을 적용하기 시작했습니다.
- COOPERATIVE 리밸런싱 프로토콜은 동작 중인 커너슈머들에게 영향을 주지 않는 상태에서 몇 차례에 걸쳐 리벨런싱이 이뤄지게 됩니다.
- 위 그림을 통해 협렵적 스티키 파티션 할당전략의 동작 과정을 요약했습니다.
- 위 그림은 peter-consumer01 컨슈머 그룹에 peter-kafka-2, peter-kafka03이라는 2개의 컨슈머가 3개의 파티션을 컨슘하고 있는 상태에서 새로운 컨슈머(peter-kafka01)이 합류하면서 리밸런싱이 일어나는 과정을 볼 수 있습니다. 위 상항의 절차를 살펴보겠습니다.
- 컨슈머 그룹의 peter-kafka01이 합류하면서 리밸런싱이 동작합니다.(1.감지)
- 컨슈머 그룹 내 컨슈머들은 그룹 합류 요청과 자신들이 컨슘하는 토픽의 파티션정보를 그룹 코디네이터로 전송합니다.(1.감지)
- 그룹 코디네이터는 해당 정보를 조합해 컨슈머 그룹의 리더에게 전송합니다.(1.감지)
- 컨슈머 그룹의 리더는 현재 컨슈머들이 소유한 파티션 정보를 활용해 제외해야할 파티션 정보를 담은 새로운 파티션 할당 저보를 컨슈머 그룹 맴버들에게 전달합니다.(2.첫 번째 리밸런싱 단계)
- 새로운 파티션 할당 정보를 받은 컨슈머 그룹 맴버들은 현재의 파티션 할당 전략과 차이를 비교해보고 필요 없는 파티션을 골라 제외합니다. 이전의 파티션 할당 정보와 새로운 파티션 할당 정보가 동일한 파티션들에 대해서는 어떤 작업도 수행할 필요가 없기 때문입니다.(2.첫 번째 리밸런싱 단계)
- 제외된 파티션 할당을 위해 컨슈머들은 다시 합류를 요청합니다. 여기서 두 번째리밸런싱이 트리거됩니다.(3-두 번째 리밸런싱 단계)
- 컨슈머 그룹의 리더는 제외된 파티션ㄴ을 적절한 컨슈머에게 할당합니다.(3-두 번째 리밸런싱 단계)
- 위 과정을 통해 협력적 스티키 파티션 할당 전략은 파티션 재배치가 필요하지 않은 컨슈머들은 다운타임 없이 계속 동작하며 이를 위해서 여러 번의 리밸런싱이 일어날수 있단든 점입니다.
- 첫 번째 리벨런싱 동작에서 peter-kafka02가 소유하고 있던 2번 파티션만 제외하고 다른 파티션들의 메시지는 소비되었기 때문입니다.
- 컨플루언트에 의하면 EAGER 리밸런싱 프로토콜 보다 COOPERATIVE 리밸런싱 프로토콜 방식이 성능 적으로 더 우세하다고 하니 COOPERATIVE 리밸런싱 프로토콜을 사용하면 될 거 같습니다.
정확히 한 번 컨슈머 동작
- 이전 글에서 트랙잭션 프로듀서와 트랜잭션 코디네이터를 활용하여 메시지를 정확히 한 번 보내는것을 알아보았습니다. 트랜잭션 코디네이터는 프로듀서의 정확히 한 번 전송이 성공하면 해당 레코드에 트랙잭션 성공을 표시하는 특수한 메시지(컨트롤러 메시지)를 추가합니다. 트랜잭션 프로듀서가 있듯이 컨슈머에도 트랜잭션 컨슈머가 존재하는데 트랜잭션 컨슈머가 컨트롤러 메시지만 읽는다면 정확히 한 번 읽을 수 있습니다.
- 트랜잭션 컨슈머를 활용하기 위해서는 내부 컨슈머 설정에서
ISOLATION_LEVEL_CONFIG
정보를read_committed
값으로 변경해주면 됩니다. 기본값은read_uncommitted
로 되어있는데 메시지를 committed만 된 값만 메시지를 읽겠다는 옵션입니다.
- 위와 같이 트랜잭션 컨슈머가 정확히 한 번만 메시지를 가져오는 것처럼 설명했지만 사실은 트랜잭션 컨슈머라고 해서 정확히 한 번만 메시지를 가져오는것을 보장하지는 않습니다. 프로듀서의 경우 트랜잭션 코디네이터와 통신하면서 해당 트랜잭션이 정확하게 처리되는것을 보장했지만, 컨슈머의 경우 트랜잭션 프로듀서가 보낸 컨트롤러 메시지만 가져올 수 있는지에 대해서 옵션으로 선택할 수 있습니다. 즉 컨슈머는 트랜잭션 코디네이터와 통신하는 부분이 없으므로 정확하게 메시지를 한 번 가져오는지 보장할 수 없습니다.
- 이로 인해 컨슈머에 의해 컨슘된 메시지가 다른 싱크 저장소로 중복 저장될 수 있기 때문입니다. 또한 컨슈머가 가져온 메시지를 다른 애플리케이션에 저장하는 과정에서 중복 처리되는 경우도 존재합니다.
- 컨슈머의 동작까지 정확히 한 번 처리가 가능해지려면 ‘컨슘-메시지 처리-프로듀싱’동작이 하나의 트랜잭션으로 처리되어야 하는데 sendOffsetToTransaction 메소드를 사용하면 컨슈머 그룹의 오프셋 커밋을 트랜잭션에 포함시킵니다.
- 일부 컨슈머 애플리케이션에서 ‘정확히 한 번’을 지원하는 경우도 존재하므로 애플리케이션 가이드 문서를 읽어보고 원하는 기능이 있는지 확인해봐야 압니다.
반응형
'Kafka' 카테고리의 다른 글
Kafka에서 정확한 한 번을 지원하는 방법 (1) | 2024.11.12 |
---|---|
카프카 스키마 레지스트리 (0) | 2023.06.01 |
프로듀서의 내부 동작 원리(파티셔너, 배치) (0) | 2023.06.01 |
카프카의 내부 동작 원리(리플리케이션,리더, 팔로워, 리더에포크, 컨트롤러,로그) (1) | 2023.06.01 |
카프카 기본 개념과 구조 (0) | 2023.06.01 |