개발 일지/Kafka

[Kafka] 컨슈머의 리밸런싱

배발자 2023. 10. 25.
반응형

개요

지금까지 카프카의 내부 원리부터 프로듀서가 어떻게 신뢰성있게 메시지를 전송하는지에 대해서 자세히 살펴보았다. 이번 포스팅은 드디어 컨슈머라는 주제를 가지고 내부적으로 어떻게 동작하는지 자세히 살펴보려고 한다. 이전에 "정확히 한 번 메시지 전송" 포스팅에서 트랜잭션 코디네이터가 해당 로직에서 필수적인 존재라는 것을 알아본 적이 있었다. 컨슈머와 관련돼서도 비슷한 코디네이터가 존재하는데 이에 대해서 알아보고 추가적으로, 리밸런싱이 무엇인지도 학습해보자.  

 

컨슈머 오프셋 관리

컨슈머에서 중요하게 처리하는 부분은 오프셋 관리인데 컨슈머가 컨슘하고 있는 토픽에서 메시지를 어디까지 들고왔는지 기록을 하고 있어야한다. 이때 활용하는 것이 오프셋인데 만약, 컨슈머가 일시적으로 장애가 발생하거나 일시적으로 동작을 멈췄을 때 새로운 컨슈머가 기존의 컨슈머를 대신하는 경우가 발생할 수 있다. 이때 기존 컨슈머의 마지막 메시지를 빠르게 가져올 수 있어야 장애를 빠르게 복구될 수 있는 것이다. 

 

카프카에서는 메시지의 위치를 오프셋이라고 부르는데, 이 오프셋은 숫자 형태로 나타낸다. 이 정보들은 카프카에서 가장 안전한 장소인 "__consumer_offsets" 토픽에 각 컨슈머 그룹별로 오프셋 위치 정보가 기록된다. 

 

 

위의 그림을 보면 컨슈머 그룹에서 __consumer_offsets 토픽에 컨슈머 그룹, 토픽, 파티션 등의 내용을  통합해서 오프셋 정보를 기록한다. 여기서 저장되는 오프셋 값은 컨슈머가 마지막까지 읽은 위치가 아니라 컨슈머가 다음으로 읽어야 할 위치를 말한다. 즉, 토픽에서 C의 메시지까지 읽었고 해당 메시지의 offset은 2라면, __consumer_offsetts 토픽에 저장하는 것은 다음으로 읽어야 할 위치 offset 3이 기록된다. 

 

그룹 코디네이터

컨슈머 그룹에는 여러 컨슈머들이 존재할텐데, 만약 기존에 존재하는 컨슈머가 이 그룹을 탈퇴하는 경우나 또는 새로운 컨슈머가 해당 그룹에 합류하는 경우가 발생할 수 있다. 이때 컨슈머 그룹은 이러한 변화를 인지하고 컨슘하고 있는 토픽의 메시지를 균등하게 분배해야한다. 이렇게 컨슈머 그룹에서 동적으로 변화되는 컨슈머들에게 작업을 균등하게 분해하는 동작을 컨슈머 리밸런싱(consumer rebalancing)이라고 부른다. 

 

바로 이전에 포스팅했던 트랜잭션 코디네이터를 활용하여 '정확히 한 번 전송' 의 메커니즘을 수행하였는데 이와 마찬가지로 안정적인 컨슈머 그룹 관리를 위해 별도의 코디네이터가 존재하는데, 이를 그룹 코디네이터라고 한다. 눈치가 빠르신 분들은 바로 추측을 하셨겠지만, 해당 코디네이터는 구독한 토픽의 파티션들과 그룹의 멤버들을 트래킹하는 역할을 한다. 즉, 파티션 또는 그룹의 변화가 발생하면 컨슈머 리밸런싱 동작이 발생하게끔 말이다. 해당 코디네이터는 각 컨슈머 그룹별로 존재하고 카프카 클러스터 내의 브로커 중 하나에 위치한다. 

 

만약, 컨슈머가 합류하거나 탈퇴할 때는 컨슈머 코디네이터에게 컨슈머가 join 또는 leave 요청을 보내어 자연스럽게 처리하게 되는데 여기서 해당 요청을 보내지 못하고 종료될 때는 어떻게 처리할까? 

 

사실 그룹 코디네이터와 컨슈머들은 서로 하트비트(hearbeat)를 주고 받는데 일종의 살아있음을 알리는 수신호라고 이해하면 된다. 심장이 뛰는지(?) 주기적으로 주고받으면서 살아있니? 살아있니? 확인하는 것이다. 이렇게 주기적으로 확인할 때 특정 컨슈머에 문제가 발생했다고 생각되면, 리밸런싱 동작을 진행한다. 다음의 표는 하트비트를 활용하기 위한 옵션이다. 

 

컨슈머 옵션 설명
hearbeat.interval.ms 3000 기본값은 3000이며, 그룹 코디네이터와 하트비트 인터벌 시간입니다. 해당 시간은 session.timeout.ms보다 낮게 설정해야 하며, 3분의 1 수준이 적절
session.timeout.ms 10000 기본값은 10000이며 어떤 컨슈머가 특정 시간 안에 하트비트를 받지 못하면 문제가 발생했다고 판단해 컨슈머 그룹에서 해당 컨슈머 제거되고 리밸런싱 동작.
max.poll.interval.ms 300000 기본값은 300000이며, 컨슈머는 주기적으로 poll()을 호출해 토픽으로부터 레코드들을 가져오는데, poll() 호출 후 최대 5분간 poll() 호출이 없다면 컨슈머가 문제가 있는 것으로 판단해 리밸런싱 동작. 

 

이제부터 중요한 내용을 언급할 것인데 리밸런싱이라는 동작은 매우 높은 비용이 지출되므로 가급적 리밸런싱이 자주 발생하지 않도록 주의해야한다. 하트비트와 관련된 옵션으로 그 주기를 단축시킬 수도 있고 늘릴 수도 있는데 이때 설정을 바꾸게 되면 다음과 같은 문제가 발생한다. 

 

[짧은 주기 설정]

컨슈머 코디네이터 : "야" 
컨슈머 : "응

** 컨슈머의 타임아웃 or TCP 패킷 손실 **

컨슈머 코디네이터 : "대답이 없네. 죽었구나? 리밸런싱!!"

컨슈머 : "뭐야?? 나 지금 살아있는데 내 몸이 사라지...ㄱ.."

 

[긴 주기 설정]

컨슈머 코디네이터 : "야" 
컨슈머 : "응"
컨슈머 코디네이터 : "살아있군"

** 긴 주기로 하트비트 체크 설정. 그 사이에 컨슈머는 장애로 다운됨
이로 인해 파티션의 메시지를 읽지 못하는 현상 계속 발생.  **

(한참 뒤..)

컨슈머 코디네이터 : "야" 
컨슈머 : (죽었음)  
컨슈머 코디네이터 : "언제 죽었대.."

 

그래서 기본적 설정을 유지하기를 권장하며, 반드시 필요한 경우에만 관련 옵션값을 변경해아한다.

 

스태틱 멤버십

리밸런싱은 무거운 작업이라고 앞서 언급하였다. 그 이유는 해당 작업이 일어나면 컨슈머들은 일시 중지하므로 매우 번거로운 일이다. 따라서 카프카는 불필요한 리밸런싱을 방어하기 위해 2.3 버전부터 스태틱 멤버십(static membership)이라는 개념을 도입하였다. 스태틱 멤버십은 컨슈머 그룹 내에서 컨슈머가 재시작 등으로 그룹에서 나갔다가 다시 합류하더라도 리밸런싱이 일어나지 않게 하는데 예를 들어, 특정 컨슈머가 다시 합류할 때 기존 구성원임을 인식하게 되며 스태틱 멤버십 기능이 적용된 컨슈머는 그룹에서 떠날 때 그룹 코디네이터에게 알리지 않기 때문에 리밸런싱이 발생하지 않는다. 

 

먼저, 스태틱 멤버십을 활용하지 않은 일반 컨슈머 그룹의 리밸런싱 동작 과정을 한 번 알아보자.  

 

일반 컨슈머 그룹의 리밸런싱 동작

 

위의 그림에서 파티션0은 bae03, 파티션1은 bae02, 파티션2는 bae01로 매핑되어 있으며, bae01 컨슈머 프로세스를 강제 종료하게 되면 다음과 같이 변하게 된다. 

 

 

위의 그림에서 현재 bae 토픽의 파티션이 컨슈머의 프로세스가 실행된 브로커와 매핑된 내용을 확인할 수 있다. bae01의 컨슈머가 종료되면서 컨슈머 그룹에서는 리밸런싱이 일어났고 bae03 컨슈머가 두개의 파티션을 할당받게 된다. 여기서 중요한건 bae02 컨슈머가 담당하고 있던 파티션1이 bae03으로 할당되고 bae02는 파티션2가 할당받게 된 것을 확인할 수 있다. 이게 왜 문제가 되냐면, 컨슈머 리밸런싱 동작 과정 중 일시적으로 모든 컨슈머가 일시 중지되는데 위의 그림처럼 컨슈머 하나가 장애가 발생해서 매핑되어있던 관계가 뒤바뀌는 것이다. 이는 대량의 메시지를 컨슘하는 컨슈머 그룹에게 이러한 일시 중지 동작은 매우 부담이 크며 고비용이 드는 작업이다. 

 

 

 

즉, 불필요한 리밸런싱은 최대한 줄여야 한다.  

 

 

 

스태틱 멤버십이 적용된 컨슈머 그룹의 리밸런싱 동작

    'session.timeout.ms': 30000

 

불필요한 커슈머 리밸런싱 동작을 회피하기 위해 스태틱 멤버십을 적용할 수 있는데 "session.timeout.ms" 옵션을 주어 시간을 지정할 수 있다. 위의 예시는 30초 동안 응답이 없으면 그때 리밸런싱 하도록 설정한 것이다. 

 

 

또 다른 예시로, 파티션0은 bae02, 파티션1은 bae01, 파티션 2는 bae03으로 매핑되어 있다. 이때, 스태틱 멤버십이 적용된 상태로 bae01 컨슈머 프로세스를 강제 종료되었을 때 어떻게 변하는지 확인해보자.  

 

 

표준 컨슈머를 사용하는 경우에는 컨슈머의 오류가 발생하면 해당 컨슈머를 제거하고 즉시 리밸런싱이 동작되고, 이후 오류가 해결하고 다시 합류하면 또 다시 리밸런싱이 이루어지면서 2번의 리밸런싱이 이루어진다. 하지만, 스태틱 멤버십을 적용한 컨슈머를 활용하는 경우 위의 그림처럼 session.timeout.ms에서 설정한 3초동안 리밸런싱 동작은 발생하지 않는다. 3초의 시간이 지난 후 그때 리밸런싱 동작하며, 만약 컨슈머가 장애가 발생한 후에 설정한 시간 내에 다시 복구되어 그룹에 합류하게 되면 이전과 동일한 파티션에 할당되어 동작하게 할 수 있다. 

 

마치며

트랜잭션 코디네이터에 이어서 컨슈머 코디네이터의 존재와 이를 통해 컨슈머 그룹에서 탈퇴한 컨슈머가 존재할 경우 리밸런싱이라는 동작을 수행하는 것을 알게되었다. 이러한 리밸런싱을 하게되면 오버헤드가 클 것이라고 순간 생각하였지만, 많은 시간을 거치면서 이를 보완하는 새로운 방법(스태틱 멤버십)을 적용한 카프카는 역시나 개발자들이 많이 열광하는 이유인 거 같기도 하다. 다음 포스팅은 카프카 카테고리의 마지막 세션인 파티션 할당 전략에 대해서 알아보고자 한다. 

 

* 참고서적 : 실전 카프카 개발부터 운영까지 - 고승범

반응형

댓글