Skip to main content 집밥서선생

Kafka

Published: 2024-08-12

카프카는 분산 메세지큐따위가 아니라 독일의 대문호 프란츠 카프카가 근본이지

는 카프카 1도몰라서 논문이라도 읽어봤다

Abstract

낮은 레이턴시로 많은 양의 로그를 수집/전달할 수 있는 카프카라는걸 만들어 봤어요

만드는 과정에서 conventional하지 않지만 효율적이고 scalable한 디자인을 좀 적용했더니 다른 메세징큐보다 성능이 좋대요

이미 프로덕션에서 기가바이트 단위의 데이터를 매일 처리하고 있어요

1. Introduction

사용자가 활동할 때마다, 또는 시스템 메트릭은 주기적으로 로그에 쌓이니까, 로그를 기반으로 사용자 활동 및 운영 지표를 추적할 수 있음.

  • 근데 요새는 이걸 프로덕션 데이터 파이프라인에 태워서 비즈니스 로직에서 활용하는 일이 많아짐 (연관 검색어, 추천, 광고 타게팅, anomaly detection 등)
  • 비즈니스 로직에서의 활용성에 따라 수집해야 하는 로그가 덩달아 증가하기도 함 → 로그 양 늘어남 ㅠ

예전(낭만의 시대)에는 서버 접속해서 쌓인 로그 스크랩했지만, 하지만 young하고 mz한(라기엔 10년도 더 된) 분산 로그 수집기들이 나왔다. → 주로 데이터 웨어하우스나 하둡에 저장해서 batch(offline) job

  • 하지만 이젠 낮은 레이턴시를 갖는 real-time(online) task도 처리해야 한다.
  • 로그 수집기 + 메세징 큐와 비슷한 api + scalability + high throughput = 카프카 탄생
  • 로그 데이터를 offline, online으로 모두 사용할 수 있게끔 인프라를 추상화했답니다

2. Related Work

다른 일반적인 MQ는 왜 로그 처리하기에 별로인가?

  • 로그 프로세싱에서 불필요한 요소에 포커스를 맞춤 (transactional/atomic이라던지, ack라던지)
    • 있어서 나쁠 건 없겠지만 로그 처리에 있어서는 굳이..?의 영역임. 로그 몇개 날아가도 세상 망하는거 아니다(진짜 논문에 이렇게 적혀있어요 ㄷㄷ)
  • throughput 중점적으로 디자인되지 않았음
  • 보통 메세징큐는 쏘자마자 받아서 처리하는 것을 상정하므로, 메세지가 쌓이면 성능이 떨어짐
    • 그래서 주기적인 대규모 로드가 걸리면 취약함

반면 다른 분산 로그 수집기들은 대개 offline task를 처리하기에 적합하고, 불필요한 정보까지 노출함

  • 또한 보통은 메세지를 push하는데, 카프카는 pull로 받아온다.
    • 그래서 메세지가 flooding되어서 머신이 터지는 일을 피할 수 있음

3. Architecture and Design Principle

topic: 특정 타입의 메세지 스트림

broker: producer가 publish한 메세지들이 저장되는 서버들. 각 서버는 한 개 이상의 파티셔닝된 토픽을 저장

consumer는 broker에서 1개 이상의 topic을 subscribe하고, pull해서 메세지를 당겨옴

3.1. Efficiency on a Single Partition

Simple Storage

  • 토픽의 각 파티션은 로그에 해당하며, 로그는 거의 비슷한 크기의 파일 세그먼트로 나뉜다.

    producer가 메세지를 publish하면, 브로커는 메세지를 마지막 파일 세그먼트에 적절히 추가한다.

    더 나은 성능을 특정 수의 메세지가 publish되거나 일정 시간이 지나면 위해 파일 세그먼트를 디스크에 flush한다. flush된 메세지만 cunsumer에게 노출된다. (대충 disk flush를 bulky하게 한다는 말인듯)

  • 다른 메세징 시스템과는 달리 카프카의 메세지는 명시적인 id가 없다

    대신 각 메세지는 로그 안 논리적 오프셋으로 구분하며, 랜덤 액세스가 가능한 구조를 유지해야 하는 오버헤드를 피할 수 있다. (대신 특정 메세지의 오프셋을 알려면 그 전 메세지의 오프셋 + 크기를 알아야 함)

  • consumer는 특정 파티션의 메세지를 순차적으로 읽기 때문에, 마지막 메세지의 오프셋 + 크기를 통해 다음 에 읽을 메세지의 오프셋을 알아낼 수 있음

    이렇게 알아낸 오프셋 + 읽어올 바이트로 구성된 비동기 pull 요청을 보내게 됨

  • 브로커는 정렬된 오프셋 목록을 sorted list로 가지고 있고, 요청이 오면 오프셋 목록을 기반으로 파일 세그먼트에서 메세지를 찾아 consumer에게 보내줌

Efficient transfer

  • producer는 한 번에 한 메세지만 submit하지만, consumer의 각 pull 요청마다 거의 수 백 kb에 달하는 메세지를 이터레이션 돌아야 한다
  • 카프카는 메모리에 메세지를 캐싱하지 않는 대신, 운영체제 레벨의 page 캐시를 믿는다.
    • 메세지가 이중으로 캐싱되는 걸 피하기 위함
    • broker 프로세스가 재시작해도 캐시가 남아있음 (cold start 방지)
    • GC overhead가 줄어들기 때문에 VM 기반 언어에서도 효율적
    • producer, consumer가 모두 순차적으로 메세지에 접근하므로, 운영체제 기반의 캐싱 전략(write-through, read-ahead)이 효과적으로 통함!
  • producer, consumer 모두 성능이 TB 단위의 데이터에 대해서도 linear하게 비례함
  • 동일한 메세지가 여러 consumer에서 여러 번 consume될 수 있음.

    일반적으로 소켓을 통해 로컬 파일을 원격으로 보내면 다음의 단계를 거침

    1. os가 스토리지에서 데이터를 읽어와 페이지 캐시에 올림
    2. 페이지 캐시에서 애플리케이션 버퍼에 복사
    3. 애플리케이션 버퍼에서 커널 버퍼에 복사
    4. 커널 버퍼가 소켓을 통해 데이터를 전송
    • 4개의 복사, 2개의 시스템 콜
    • 근데 UNIX 계열에 있는 sendfile API를 써서 파일 채널에서 소켓 채널로 데이터를 바로 쏠 수 있음. 2개의 복사, 1개의 시스템 콜이 생략됨

Stateless broker

  • 다른 메세징 시스템과는 다르게, consumer가 메세지를 얼마나 consume했는지를 broker에서 관리하지 않는다.
    • 따라서 브로커의 복잡도와 오버헤드를 줄일 수 있다
    • 하지만 메세지가 잘 소비되었는지 알 수 없어서 어떤 메세지를 삭제해야 할 지 결정하는 게 좀 애매함
    • 보통 메세지 보존 정책으로 time-based SLA를 사용함 (일정 기간 이상 브로커에 보관된 메세지 삭제)
    • 대부분의 경우 일정 시간이 지나면 알아서 지워지기 때문
  • 이러한 디자인 덕분에 consumer가 이전 offset의 데이터를 불러오는 것도 가능함
    • 서비스에 잠깐 문제가 생겼는데 고쳐진 후 다시 불러온다던지 등등

3.2. Distributed Coordination

  • 분산 환경에서, producer는 무작위로, 또는 partitioning key 혹은 partitioning function에 따라 선택된 파티션에 메세지를 publish할 수 있다.
  • 한 개 이상의 consumer가 모여 consumer group을 구성하고, consumer group 안의 consumer는 구독된 topic을 동시에 consume한다
    • 한 consumer group 안에서 각 메세지는 임의의 한 consumer에게만 날아간다.
    • 서로 다른 consumer group은 독립적으로 구독한 message를 소비하며, 이들 간에 coordination은 필요하지 않다.

      ⇒ 오버헤드를 줄이기 위한 디자인

    • 한 토픽에서 특정 파티션의 메세지들은 각 consumer group당 한 consumer에게만 들어감.
      • 만약 여러 consumer가 받을 수 있는 스펙이였다면 그걸 추적하는 데 또 오버헤드가 생겼을 것
      • 파티션을 적당히 많이 잡으면 consumer간 로드 밸런싱도 가능하다고 함..?
  • master node가 따로 없고, consumer들이 알아서 node 선택
    • master failure 걱정할 필요 없다
    • 조정을 용이하게 하기 위해 Zookeper 사용
      • Zookeeper의 기능
        • file system api
        • 특정 path에 observer를 붙여서, path의 children이나 값이 변경되면 노티를 줄 수 있다
        • 특정 path를 생성한 클라이언트가 사라지면 path도 사라지게 할 수 있음 (ephemeral)
        • 데이터의 여러 서버에 replicate해서 고가용성
      • 이런 일에 사용
        • broker 및 consumer 추가 및 제거, rebalance
          • 각 브로커 또는 consumer가 시작되면 zookeeper에 있는 레지스트리에 등록함
            • broker registry: host name, port, 토픽들, 파티션들
            • consumer registry: consumer group 정보(어떤 consumer가 있는지, 얘네가 구독하는 topic)
            • ownership registry: 구독된 모든 파티션에 대한 path가 있고, path의 값은 해당 파티션을 구독하는 consumer의 id
            • offset registry: 각 파티션에서 마지막으로 consume된 메세지의 offset
            • broker, consumer, ownership registry에 생성되는 path는 ephemeral하고, offset registry에 생성되는 path는 persistent
        • 각 파티션-consumer의 relationship과 consume된 offset 트래킹
        • 브로커가 죽으면 broker registry에 등록된 모든 파티션이 자동으로 지워짐
        • consumer가 죽으면 consumer, ownership registry의 모든 엔트리를 잃음
        • 노티 기능을 통해 broker registry, consumer registry에 뭔가 변경이 일어날 때마다 consumer가 노티받음
          • consumer는 이런 노티를 받으면 rebalance 알고리즘에 따라 다른 파티션을 선택하여 계속 데이터를 읽어올 수 있음
          • 근데 여러 consumer가 있는 consumer group에서는 이 노티가 약간씩 다른 시점에 올 수 있고, 이미 한 consumer가 소유하고 있는 파티션을 다른 consumer가 고를 수 있음
          • 이러면 이미 갖고 있는 consumer가 그 파티션을 release하고 약간 기다렸다가 rebalance를 다시 시작 (웬만하면 잘 된다고 합니다)

3.3. Delivery Guarantees

  • 카프카는 at-least-once delevery를 보장한다고 함
    • exactly-once delivery를 달성하기 위해 2pc 등 불필요한 것들이 너무 많음
    • 대부분의 경우 한 번씩 도착하긴 함
    • 간혹 consumer 프로세스가 graceful shutdown을 못하는 경우, 파티션을 물려받은 consumer가 이미 consume된 메세지를 다시 처리해야 하는 경우가 있을 텐데, 그 때 유용하다고 함
  • 한 파티션 안에서 오는 메세지는 순서가 보장되지만, 서로 다른 파티션에서 오는 메세지들의 순서는 보장되지 않음
  • 각 메세지에 대한 CRC를 저장해서 로그 오염을 방지함
  • 브로커가 죽으면 스토리지에 저장된 메세지는 못 쓰게 된다고 하는데, 논문 쓰일 당시에는 브로커마다 리플리케이션 저장하는 기능은 없었나봄

© 2026 JHSeo