개발 일지/Kafka

[Kafka] 프로듀서의 내부 동작 원리 파헤치기

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

개요

이번 포스팅에서는 프로듀서가 어떻게 동작하는지 내부 원리를 알아보려고 한다. 기존에 필자가 알고있었던 프로듀서의 역할은 메시지를 발행하여 카프카에 전송하는 것인데, 그 안에서도 파티셔너라는 친구가 중간에 개입하면서 어떠한 토픽으로 보낼지 나침반 역할을 하게 된다. 또한, 해당 메시지들은 즉시 카프카로 전송하는 것이 아니라 내부적으로 어떠한 조건이 충족되었을 때 수행된다고 한다. 추가적으로 카프카에서 중복 데이터를 처리하는 방식도 존재한다고 한다. 이러한 부분들을 깊게 학습하고 동작 흐름을 자세히 살펴보고자 한다. 

 

파티셔너

카프카의 토픽은 성능 향상을 위한 병렬 처리가 가능하도록 하기 위해 파티션으로 나뉘고, 최소 하나 또는 둘 이상의 파티션으로 구성된다. 그리고 프로듀서는 토픽으로 메시지를 보낼 때 해당 토픽의 어느 파티션으로 보낼지 결정해야하는데, 이때 사용하는 것이 파티셔너이다. 

 

기본적으로 메시지의 키를 해시 처리해서 파티션을 구하는 방식을 사용하는데 메시지의 키 값이 동일하면 해당 메시지들은 모두 같은 파티션을 전송된다. 예를 들면, 파티션이 3개일 경우, 메시지가 들어온다고 가정하자. 만약, 메시지의 해시키가 31이라면 31%3 >> 1 파티션으로 보내고, 메시지의 해시키가 60이라면 60%3 >> 0 파티션으로 보내게된다. 

 

다만, 레코드(메시지)의 키값은 필숫값은 아니므로 관리자는 별도의 레코드 키값을 지정하지 않고도 메시지를 전송할 수 있지만 이때 키 값은 null이 된다. 이처럼 키값을 정하지 않았을 때 어떻게 처리를 하는지 알아볼 것이다. 여기서 중요한 것은, 파티셔너를 거친 후의 레코드들은 바로 카프카로 전송되지 않는다는 것에 유의하자.

 

여기서 또 중요한 개념은 전송된 레코드들은 프로듀서의 버퍼 메모리 영역에서 대기한 후 최소 레코드의 수를 충족(배치처리)한 후 카프카로 전송한다는 것이다.  즉, 단건의 메시지를 계속 카프카로 전송하는 것이 아니라 한 번에 다량의 메시지를 묶어서 배치전송을 하는 것이다. 

 

 

why?

 

 

그냥 메시지를 받은대로 즉시 카프카로 전송하면 되는데 왜 이렇게 하는걸까? 이유는 다음과 같다. 

 

택배를 최대 10개를 담을 수 있는 트럭이 있다가 가정하자. 이 트럭은 목적지까지 어떻게 운반해야 효율적일까? 
당연, 그리디하게 생각해보면 최대 수용 개수 10개를 채우게 되면 그때 목적지로 출발하면 된다. 하지만, 상자 1~2개만 채워졌다고 즉시 출발하면 가능한 수용 용량이 남아있음에도 운반하기 때문에 굉장히 비효율적일 것이다. 

그렇다면, 트럭이 담을 수 있는 최대 수용 개수를 꼭 다 채워야만 출발해야하는건지도 고민해봐야한다. 만약, 최대 수용 개수가 채워지지 않는다면, 트럭은 출발을 못해서 물건을 구매한 구매자 입장에서는 계속해서 물건을 받지 못할 것이다. 그래서 이 운송트럭을 처리량을 늘릴 것인지, 아니면 지연 없는 운송을 해야할 지 선택을 해야한다. 

 

대량의 메시지를 처리할 때 처리량을 높여야 하는 경우라면 사용자는 효율적인 배치 전송을 위해 프로듀서의 설정을 변경해야한다. 혹은 지연 없는 전송이 목표라면 배치 전송 관련 설정을 제거해야한다. 즉, 전송 목표를 정하고 목적에 따라 프로듀서의 옵션값을 조금씩 조정해가면서 최적의 값을 찾아가는 것이 중요하다. 

 

아무튼 지금부터 학습할 내용은 배치 전송과 관련된 전략 2가지를 소개하려고 한다. 

 

 

라운드 로빈 전략 

 

 

라운드 로빈 전략은 cpu 스케줄링 방식에도 있는 전략이므로 자세한 내용은 생략하지만, 간단하게 설명해보면 다음과 같다. 위의 그림을 보면 레코드 1은 파티션 0으로, 레코드 2는 파티션 1로, 레코드 3은 파티션 3으로, 레코드 4는 파티션 4로 .... 이런식으로 들어온 레코드들을 공평하게 순차적으로 나눠주는 것이다. 

 

여기서 문제점이 뭐냐면, 최소 레코드의 수를 충족할 경우 카프카로 전송한다고 앞서 언급한바 있다. 물론, 관리자가 프로듀서의 옵션을 조정해서 특정 시간을 초과하면 즉시 카프카로 레코드들을 전송할 수 있지만 이러한 설정을 할 경우, 토픽 bae-파티션2와 같이 배치와 압축의 효과를 얻지 못한 채 레코드 하나만 카프카로 전송될 수 있으므로 비효율적이다. 

 

* 위의 예시에서 운송 트럭이 택배 하나만 받아도 출발하는 상황

 

그렇다면 이러한 비효율적인 문제를 해결하는 방법이 있을까? 바로 스티키 파티셔닝(Sticky partitioning) 전략이다. 

 

스티키 파티셔닝 전략 

아파치 카프카 2.4 버전부터는 스티키 파티셔닝 전략을 사용하게 되었는데, 라운드 로빈 방식와 어떻게 달라졌는지 알아보기로 하자. 

 

필자가 예전에 학습했던 내용들 중 API 게이트웨이에서도 Sticky Session 방식으로 웹 어플리케이션 서버로 라우팅 처리하는 기능이 있다고 들었다. 다만, 그 방식은 트래픽 처리, 동적으로 증설되는 서버 등의 문제로 고정된 서버로 라우팅을 처리하게 되면 오버헤드가 크다는 단점이 있다고 알고 있다. 그래서 카프카의 스티키 파티셔닝 전략을 처음 들었을 때 이게 맞는지 의문이 들었다. 하지만, 내용을 학습하고나니 이 방법이 훨씬 효율적이겠다는 생각이 들었다.

 

실제로 이러한 전략을 적용함으로써 기본 설정에 비해 약 30% 이상 지연시간이 감소하고 프로듀서의 CPU 사용률도 줄어드는 효과를 얻을 수 있었다고 한다.  

 

 

전략은 정말 간단하다. 고정된 파티셔닝으로 계속 보내서 최소 레코드의 수를 충족하면 카프카로 전송하는 것이다. 

위의 그림을 보고 이해해보면, 프로듀서가 키값이 null인 레코드1을 보내고 파티셔너는 키값이 null인 레코드를 확인하고 배치를 위해 임의로 토픽bae-파티션0에 레코드1을 담는다. 이후 null인 레코드2를 받으면 똑같은 파티션으로 레코드2를 담는다. 이러한 과정을 반복 후 해당 파티션에 최소 레코드의 수를 충족하면 즉시 카프카로 배치 전송이 수행되는 것이다. 

 

이전에 언급했던 라운드 로빈 전략에서는 레코드 5개를 처리햇음에도 레코드의 수를 충족하지 못해서 비효율적이였다. 하지만, 스티키 전략을 통해서 성공적으로 배치 전송을 할 수 있게 되었다. 

 

 

중복 없는 전송 

이 내용은 개인적으로 굉장히 중요한 부분이라고 생각한다. 특정 서비스에서 메시지가 중복처리된다면 치명적인 상황이 발생할 수 있을 것이다. 예를 들면, 온라인 쇼핑몰에서 구매 내역이 중복 처리되어 관련 상품이 두번이나 배송될 수있는 상황이 생길 수 있다.

 

이에 따라, 사용자들의 개발 편의를 높이기 위해 중복 없이 전송할 수 있는 기능을 제공하는데 어떻게 중복 없는 전송(멱등성 전송)을 하는지 알아보고자 한다. 

 

* 멱등성 : 동일한 작업을 여러 번 수행하더라도 결과가 달라지지 않는 것. 

 

 

 

위의 그림을 보면서 차근차근 이해해보자. 

 

  1. 프로듀서가 브로커의 bae 토픽으로 메시지A를 전송하는데 이때 PID(Producer ID)와 메시지 번호0을 함께 전송한다.
  2. 브로커는 메시지A를 저장하고, PID와 메시지 번호 0을 메모리에 기록한다. 그리고 ACK를 프로듀서에게 응답한다. 여기서 ACK는 메시지를 잘 받았다는 의미이다. 
  3. 프로듀서는 다음 메시지B를 브로커에게 전송한다. PID는 동일하지만 메시지 번호는 1  증가한다. 
  4. 2번과 동일하게 동작하지만, 네트워크 오류로 프로듀서는 메시지B에 대한 ACK를 받지 못했다. 
  5. 브로커로부터 ACK를 받지 못한 프로듀서는 브로커가 메시지B를 받지 못했다고 판단해 메시지B를 재전송한다. 

 

전체적인 흐름은 쉽게 이해할 것이다. 하지만, 메시지B에 대한 ACK 메시지를 수신하지 못해서 메시지B를 재전송하면 중복 데이터가 발생할 것이라는 의문점이 들 수 있다. 사실, 카프카는 중복 데이터를 처리하는 방법이 존재한다.

 

브로커의 동작의 차이가 존재하는데, 프로듀서가 재전송한 메시지B의 헤더에서 PID(0)와 메시지 번호(1)를 비교해서 메시지B가 이미 브로커에 저장되어 있는 것을 확인한 브로커는 메시지를 중복 저장하지 않고 ACK만 보내는 것이다. 

 

즉, 프로듀서가 보낸 메시지의 시퀀스 번호가 브로커가 갖고 있는 시퀀스 번호보다 정확하게 하나 큰 경우가 아니라면, 브로커는 프로듀서의 메시지를 저장하지 않는다. 바로 이 동작 때문에 메시지 중복을 피할 수 있다.

 

PID와 시퀀스 번호 정보는 브로커의 메모리에 유지되고, 리플리케이션 로그에도 저장된다. 그래서 특정 브로커의 장애 등으로 리더가 변경되더라도 새로운 리더는 PID와 시퀀스 번호를 정확히 알 수 있으므로 중복 없는 메시지 전송이 가능하다. 

 

마치며

프로듀서는 단순히 메시지를 발행해서 카프카로 바로 전송하는 것으로 알고 있었는데, 파티셔너가 중간에 개입하여 처리량을 위한 배치처리 설정을 할 수 있다는 것에 놀라웠다. 또한, 중복 없는 전송을 통해 브로커에서는 중복된 메시지를 처리하기 위한 로직이 수행된다는 것 또한 흥미로운 사실이였다. 다만, 여기서 말하는 것은 브로커에서 중복된 메시지를 처리하기 위한 로직이지, 전송을 한 번만 하는 것이 아니다. 위에서 ACK 메시지를 받지 못해서 메시지B에 대해서 한 번 더 전송하는 것을 볼 수 있었다. 그렇다면, 은행 시스템처럼 정확히 한 번만 메시지 전송을 진행하여 트랜잭션의 원자성을 지키는 처리 과정이 존재할 지 의문이 들 수 있다. 다음 포스팅에서는 "정확히 한 번만 메시지를 전송하는 로직"에 대해서 알아보고자 한다. 

 

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

* 참고영상 : 신뢰성 있는 카프카 애플리케이션을 만드는 3가지 방법 - 최원영 (youtube/kakaotech) 

반응형

댓글