💻 개발 이야기/Apache Kafka

[Kafka] 넓고 얕게 카프카를 이해해보자

Jinseong Hwang 2024. 2. 24. 05:24

 
안녕하세요!
다소 난해하고 복잡해 보이는 카프카에 대해서 넓고 얕게 알아보겠습니다.
 

00 | 이 글에서 얻을 수 있는 것

  • 카프카 시스템을 구성하는 요소를 중고 거래 시스템에 빗대어 이해할 수 있다.
  • 카프카 토픽과 파티션의 상관관계에 대해 이해할 수 있다.
  • ISR 상태에 대해 이해할 수 있다.
  • 카프카에 대해 아는 척할 수 있다.

 
 

01 | 카프카 구성 요소

https://docs.confluent.io/kafka/introduction.html

아파치 카프카(Apache Kafka)는 실시간 스트리밍 데이터 파이프라인을 구축하는...
와 같은 어려운 정의는 구글 어디에서나 찾아볼 수 있습니다.
 
카프카는 메시지를 누군가에게 전달하는 시스템이라고 이해하시면 됩니다.
비유를 들어서 조금 더 쉽게 풀어보겠습니다.
 

당근마켓 페이스북

만약 제가 당근마켓에서 중고 맥북을 구매하려 한다고 가정합시다. 우선 판매자는 맥북을 박스에 잘 담아서 포장합니다. 그리고 우체국에 가져가서 발신인, 수신인 주소를 작성하며 택배 접수합니다. 그리고 시간이 지나면 집배원이 저에게 택배를 전달해 줍니다. 
 
중고거래 + 우체국 이야기와 함께 카프카를 구성하는 여러 구성 요소에 대해 알아봅시다.
 
1. 카프카 클러스터(Kafka Cluster)는 우체국 전체 시스템과 유사합니다. 우정사업본부가 있고, 그 아래에 각 지역별 우체국 지점과 물류 센터와 배송 기사, 집배원 분들이 계십니다. 
 
2. 브로커(Broker)는 우체국의 물류 창고와 유사합니다. 우체국 시스템이 동작하기 위해서는 지역 별로 우체국 지점이 존재해야 합니다. 클러스터에서 해야 하는 리소스 관리, 메시지 처리 등 실제 작업을 진행하는 것은 각 브로커입니다. 물리적인 서버 인스턴스 단위입니다. 여러 인스턴스가 모여서 하나의 클러스터를 구성합니다.
 
3. 토픽(Topic)은 우체국에서 배송 물건을 분류하는 것과 유사합니다. 큰 택배, 작은 택배, 등기 우편, 익일 특급 등 물건의 크기, 목적, 시간에 따라 여러 개로 물건을 분류합니다. 익일 특급은 빠르게 처리되고 일반 우편은 느리게 처리되듯 목적에 따라 메시지를 분류할 수 있습니다. 서로 다른 목적을 가진 메시지들이 각 목적을 띄고 있는 토픽으로 들어가게 됩니다.
 
4. 파티션(Partition)은 토픽의 세부 단위입니다. 익일 특급 우편물을 여러 수신인에게 전달해야 하는데 효율적으로 전달하기 위해서 한 명의 집배원이 모두 전달하는 것이 아니라 여러 명의 집배원이 전달을 합니다. 진성, 지훈, 홍철 3명의 집배원이 있다고 가정합시다. 진성 바구니에 우편물 10개, 지훈 바구니에 우편물 10개, 홍철 바구니에 우편물 10개가 들어가 있습니다. 우체국에서는 나눠서 보관하고 있다가 나중에 각 집배원이 도착하면 본인의 바구니에 담긴 우편물을 가져갑니다. 즉, 파티션은 병렬 처리의 핵심입니다. (빌드업..?)
 
5. 오프셋(Offset)은 파티션의 세부 단위입니다. 개별 우편물과 유사합니다. 수신인 정보를 보고 내 택배인지 옆집 사람 택배인지 구별할 수 있습니다. 마찬가지로 오프셋은 파티션 내에 저장된 메시지를 고유하게 식별할 수 있는 단위입니다. 오프셋 값은 001부터 시작해서 002, 003, ... (실제로는 0이 훨씬 많습니다.)처럼 하나씩 값이 올라갑니다. 새로운 메시지는 이전 오프셋보다 높은 값을 가집니다.
 
6. 프로듀서(Producer)는 메시지를 발행하는 주체입니다. 맥북 판매자와 유사합니다. 판매자가 맥북을 팔아야겠다 마음을 먹고 박스에 담아 포장을 해서 우체국으로 접수합니다. 마찬가지로 메시지에 데이터를 담아서 적절한 카프카 토픽으로 메시지를 전달합니다. 프로듀서가 선택한 토픽 내에서 어떤 파티션에 담을지는 라운드 로빈, 해시 혹은 다양한 방식으로 결정할 수 있습니다.
 
7. 컨슈머(Consumer)는 메시지를 소비하는 주체입니다. 집배원과 구매자를 결합한 것과 유사합니다. 아까 파티션을 설명할 때 이름이 적힌 바구니가 있다고 설명했습니다. 집배원은 이름에 맞는 바구니에서 우편물들을 꺼내 수신인에게 전달합니다. 컨슈머에는 컨슈머 리스너(@KafkaListener at spring-kafka)가 존재합니다. 컨슈머 리스너가 집배원의 역할이고 DB에 저장하는 등의 비즈니스 로직이 구매자의 역할입니다.
 
한눈에 살펴보기!

https://www.scaler.com/topics/kafka-tutorial/kafka-issues/

 
 

02 | 토픽과 파티션의 상관관계

결국 핵심적으로 메시지는 토픽 단위로 그룹화됩니다. 파티션 개수에 따라 전체적인 성능에 큰 영향을 미치기 때문에, 토픽을 만들 때 파티션을 몇 개로 만들지 신중하게 고민해야 합니다.
 
토픽을 처음 만들 때, 다음 3가지를 고려해서 파티션의 개수를 정해야 합니다.
 
첫째, 데이터 처리량을 고려해야 합니다.
파티션 1개는 (컨슈머 그룹 내의) 컨슈머 1개와 매핑됩니다. 1대 1 매핑이 되기 때문에 파티션 개수를 늘리면 컨슈머 개수도 늘어나서 데이터 처리량이 늘어납니다. 따라서 파티션 개수를 정할 때는 해당 토픽의 데이터를 처리하는데 필요한 처리량이 어느 정도인지 파악해야 합니다.
 
데이터 처리량을 늘리기 위해서는 2가지 방법이 있습니다. 첫 번째는 컨슈머 자체의 처리량을 늘리는 것입니다. 프로세서, 메모리 등 하드웨어 스펙을 늘리거나 JVM 튜닝을 해서 성능을 개선할 수 있습니다. 그러나 컨슈머는 일반적으로 외부 데이터소스(DocumentDB, S3, Hadoop, Oracle, ...)와 연동되는 경우가 많아서 성능 개선이 처리량을 늘리는 데 큰 도움이 되지 않을 수 있습니다. 두 번째는 메시지를 처리하는 컨슈머의 개수를 늘리는 것입니다. 병렬 처리량이 늘어나기 때문에 데이터 처리량을 확실하게 개선할 수 있습니다.
 
프로듀서가 초당 100개의 메시지를 발행하고, 컨슈머 1대가 초당 40개의 메시지를 처리할 수 있다면 컨슈머는 최소 3대는 있어야 안정적으로 카프카를 사용할 수 있습니다. 일반적으로 컨슈머 전체 데이터 처리량이 프로듀서 데이터 처리량보다 많아야 합니다. 다만 반드시 지켜야 하는 것은 아니며 운영 환경과 유사한 환경에서 철저하게 테스트 후 문제없음이 확인됐다면 상황에 맞게 컨슈머 개수를 설정하셔도 됩니다.
 
둘째, 메시지 키 사용 여부를 고려해야 합니다.
프로듀서에서 메시지를 발행한 순서를 컨슈머에서도 동일하게 지켜야 하는 상황이라면 메시지 키 사용 여부를 고려해야 합니다. 메시지 키를 사용하고 있다면 프로듀서는 메시지 키를 해싱해서 어떤 파티션으로 메시지를 넣을 것인지 결정합니다. 만약 파티션 개수가 달라지면(늘어나면) 메시지가 기존에 들어가던 파티션이 아닌 다른 파티션으로 들어갈 가능성이 생기게 됩니다. 메시지 순서는 파티션 내에서만 유효하기 때문입니다.
 
메시지 처리 순서가 보장되어야 한다면 순서를 유지할 수 있게 커스텀 파티셔너를 개발하고 적용하면 됩니다. 하지만 역시 가장 깔끔한 것은 순서에 의존하지 않게 메시지를 설계하는 것입니다. 순서를 고려하지 않아도 된다면 컨슈머를 늘리고 싶을 때 파티션도 늘리면 간단하게 해결되기 때문입니다.
 
셋째, 브로커와 컨슈머의 영향도를 고려해야 합니다.
파티션은 각 브로커(물리 인스턴스)의 파일 시스템을 기반으로 동작합니다. 따라서 파티션이 늘어나는 만큼 브로커가 감당해야 하는 데이터 양이 많아집니다. 운영체제는 프로세스 당 열 수 있는 파일 최대 개수를 제한하고 있습니다. 따라서 안정적으로 카프카를 사용하기 위해서는 각 브로커 당 파티션 개수를 모니터링해야 합니다. 만약 브로커 하나가 관리하는 파티션이 너무 많은데 더 늘려야 하는 상황이라면 브로커를 늘리는 방향으로도 생각해 보면 좋습니다.
 

참고) 제가 지금 사용하고 있는 컴퓨터인 Macbook Pro, M2 Pro, 32GB, Ventura에서는 Soft-limit이 256개, Hard-limit이unlimited로 설정되어 있네요!
 
 

03 | 데이터 정리 정책 (Clean-Up Policy)

카프카를 계속 사용하다 보면 데이터는 계속해서 쌓입니다. AWS MSK나 EBS를 기반으로 카프카를 사용하고 있다면 데이터가 쌓임에 따라 운영 비용도 증가합니다. 따라서 불필요해진 데이터는 삭제해 주는 것이 여러모로 좋습니다.
 
Clean-Up Policy에 대해 알아보기 전에 세그먼트 단위에 대해서도 알고 있어야 합니다. 세그먼트란 토픽의 데이터를 저장하는 명시적인 파일 시스템의 단위입니다. 세그먼트는 파티션마다 별개로 생성되며 세그먼트의 파일 이름은 오프셋 중 가장 작은 값이 됩니다.

segment.bytes={}

위 설정 값으로 세그먼트의 크기를 결정할 수 있습니다. 설정한 크기가 넘어가면 세그먼트 파일을 닫고 새로운 세그먼트 파일을 만들어서 저장하기 시작합니다. 데이터를 저장하기 위해 현재 사용 중인 세그먼트를 액티브 세그먼트(Active Segment)라고 합니다. 파티션에 단 하나 존재합니다.
 
더 이상 사용하지 않는 데이터를 삭제할 때는 `cleanup.policy` 옵션을 사용해서 삭제할 수 있습니다. 카프카에서는 Delete와 Compact, 2가지 옵션을 제공해 주고 있습니다.
 
Delete 방식

cleanup.policy=delete

Delete 방식은 시간 또는 크기에 따라 세그먼트 단위로 삭제하는 정책입니다.
 

retension.ms(minutes, hours)={}
retension.bytes={}
log.retension.check.interval.ms={}
  • `retension.ms(minutes, hours)` => 세그먼트를 보유할 최대 기간. 기본값은 7일 (기업에서는 3일 많이 사용함)
  • `retension.bytes` => 파티션당 로그 적재 바이트값. 기본값은 -1(미지정)
  • `log.retension.check.interval.ms` => 세그먼트가 삭제 영역에 들어왔는지 확인하는 간격. 기본값은 5분

 

https://medium.com/draftkings-engineering/kafka-workshop-how-to-efficiently-use-kafka-in-a-high-volume-production-environment-f4d6dfc67092

 
Delete 방식의 장점은 다음과 같습니다.

  • 메시지가 보존 기간이나 파티션 크기에 따라 삭제되므로 저장 공간이 일정 수준으로 유지됩니다.
  • 시간 또는 크기 기반으로 메시지를 제거하므로, 처리 속도가 빠릅니다.

단점은 다음과 같습니다.

  • 메시지가 보존 기간이 만료되면 삭제되므로, 오래된 데이터에 대한 액세스가 제한됩니다.

주로 다음과 같은 경우에 사용합니다.

  • 실시간 분석, 스트림 처리, 대량의 데이터를 처리하는 경우에 적합합니다.
  • 오래된 데이터가 필요하지 않은 시나리오에 적합합니다.

 
Compact 방식

cleanup.policy=compact

Compact 방식은 시간 또는 크기에 따라 세드먼트 내 동일 Key 오래된 레코드를 삭제하는 정책입니다. (단, 액티브 세그먼트에는 해당 안됨) 이 방식은 일반적인 압축과는 개념이 조금 다릅니다.
 
Compact 방식에 대해 이야기하기 전에 tail 영역과 head 영역에 대해 가볍게 알고 넘어가는 것이 좋습니다.

  • tail 영역은 압축(compact) 정책에 의해 압축이 완료된 레코드들입니다. clean log라고도 부릅니다. tail 영역 내 모든 Key의 레코드는 중복되지 않는다는 특징이 있습니다.
  • head 영역은 압축 정책이 수행되기 전 레코드들입니다. dirty log라고도 부릅니다. head 영역 내 레코드 Key가 중복될 수 있다는 특징이 있습니다.

 

min.cleanable.dirty.ratio={}
  • `min.cleanable.dirty.ratio` => 액티브 세그먼트를 제외한 나머지 세그먼트들에 남아 있는 tail 영역의 레코드 개수와 head 영역의 레코드 개수의 비율을 뜻합니다. 0.5로 설정했다면, tail 영역의 레코드 개수와 head 영역의 레코드 개수가 동일할 경우 압축이 실행됩니다.
    • 높은 값(ex 0.9)으로 설정했다면, 한번 압축을 수행할 때 많은 데이터가 줄어드므로 압축 효과가 좋지만 다 찰 때까지 데이터가 계속 쌓이므로 저장 효율이 좋지 않습니다.
    • 낮은 값(ex 0.1)으로 설정했다면, 압축이 너무 자주 일어나서 최신 데이터만 유지할 수 있지만 압축이 자주 발생해서 브로커에 부담을 줄 수 있습니다.

 

https://medium.com/draftkings-engineering/kafka-workshop-how-to-efficiently-use-kafka-in-a-high-volume-production-environment-f4d6dfc67092

 
Compact 방식의 장점은 다음과 같습니다.

  • 중복 키를 가진 이전 메시지를 삭제하므로, 키별로 가장 최신 메시지만 유지됩니다.
  • 저장 공간이 절약되며, 중복 데이터를 처리할 필요가 없으므로 처리 속도가 빠릅니다.
  • 메시지 보존 기간과 관계없이 중복 키의 이전 메시지를 제거합니다.

단점은 다음과 같습니다.

  • 로그 클린업 프로세스가 더 복잡하고, 성능에 영향을 줄 수 있습니다.
  • 로그 압축 프로세스는 주기적으로 실행되므로, 공간 절약 효과는 즉시 나타나지 않을 수 있습니다.

주로 다음과 같은 경우에 사용합니다.

  • 변경 데이터 캡처(CDC), 이벤트 소싱, 키-값 스토어와 같이 키별로 최신 메시지만 필요한 경우에 적합합니다.
  • 오래된 데이터에 대한 액세스가 필요한 경우에 적합합니다.
  • Kafka Consumer Group의 Partition 단위로 offset 값을 기록 및 관리하기 위해 사용하는 `__consumer_offsets` 토픽에서도 사용합니다.

 
(부끄러운 과거 썰)
저는 카프카의 Clean-Up Policy에 대해 모르고 있었습니다. 회사에서 컨슈머 쪽에 작은 이슈를 발견해서 디버깅을 위해 문제가 발생한 토픽의 메시지를 열어봤습니다. 하지만 어떠한 메시지도 남아있지 않았습니다. 알고 보니 3일이 지나면 삭제되는 정책으로 설정이 되어 있었습니다. 따라서 이 이슈가 발생한 지 3일이 넘었음을 알게 되었고 프로듀서에서 메시지 발행을 제대로 못하고 있다는 것을 알 수 있었습니다.
 
 

04 | 파티션 복제

카프카는 대규모 데이터를 빠르고 안전하게 처리하고 저장하는 데 집중해서 설계되었습니다. 토픽은 하나 이상의 파티션으로 이루어지고, 각 파티션은 여러 서버(브로커)에 걸쳐 복제될 수 있습니다. 즉, 복제는 필수가 아닙니다. 복제될 때 리더 파티션과 팔로워 파티션으로 구분됩니다.
 

https://jack-vanlightly.com/blog/2018/9/2/rabbitmq-vs-kafka-part-6-fault-tolerance-and-high-availability-with-kafka

 
리더 파티션 (Leader Partition)

  • 리더 파티션은 특정 파티션의 데이터 쓰기 및 읽기 요청을 담당하는 주 파티션입니다.
  • 각 파티션에는 하나의 리더만 있으며, 모든 프로듀서와 컨슈머는 리더와 직접 통신합니다.
  • 리더는 자신의 데이터를 팔로워 파티션과 동기화하여 데이터의 복제본을 유지합니다.
  • 카프카 클러스터에서는 서버 장애가 발생했을 때, 다른 서버의 팔로워 파티션이 리더로 승격될 수 있도록 카프카 컨트롤러가 자동 리더 선출을 진행합니다.

팔로워 파티션 (Follower Partition)

  • 팔로워 파티션은 리더 파티션의 복제본을 유지하는 역할을 합니다. 읽기 요청을 직접 처리하지 않으며, 이는 주로 데이터 복제와 시스템의 내결함성(fault-tolerance)을 위해 존재합니다.
  • 팔로워들은 리더로부터 데이터를 주기적으로 동기화받으며, 이 과정은 백그라운드에서 자동으로 수행됩니다.

 
 

05 | ISR (In-Sync-Replicas)

ISR은 말 그대로 모든 Replica(복제본)들이 Sync(동기화) 상태에 있다는 것을 의미합니다. 즉, 모든 리더 파티션과 팔로워 파티션이 동일한 값을 가지고 있는 것을 ISR 상태에 있다고 표현합니다.
 
ISR이라는 표현이 나온 이유는, 팔로워 파티션이 리더 파티션으로부터 데이터를 받아서 동기화하기까지 약간의 시간차가 발생하기 때문입니다. 이런 차이를 모니터링하기 위해 리더 파티션은 `replica.lag.time.max.ms` 시간만큼의 주기로 팔로워 파티션의 동기화 상태를 판단합니다. 설정된 시간이 지났음에도 동기화가 되지 않는다면 해당 팔로워 파티션에 문제가 발생했다고 생각하고 ISR 그룹에서 제거합니다.
 
리더 파티션이 있는 서버에 장애가 발생했을 때, ISR 그룹에 있는 팔로워 파티션만 리더로 선출될 자격을 얻습니다. ISR 그룹 밖에 있는 파티션을 리더로 뽑을 경우 데이터의 유실이 발생할 수 있기 때문입니다. 하지만 `unclean.leader.election.enable=true`로 설정한다면 ISR 그룹 밖의 파티션도 리더로 선출될 수 있습니다. 리더 파티션 장애 발생 시점에 ISR 그룹에 어떠한 파티션도 없을 경우 토픽은 중단됩니다. 하지만 해당 설정을 활성화하면 데이터 유실이 발생하더라도 토픽은 계속 사용할 수 있고 서비스의 장애로 전파를 막을 수 있습니다. 이 부분도 데이터의 특성에 따라 판단하는 것이 옳습니다.
 
 

06 | References

 

쌰라웃 투 https://github.com/dkswnkk