7 분 소요

image

“Kafka: a Distributed Messaging System for Log Processing” 논문을 개인 공부 및 리뷰를 위해 쓴 글입니다.

논문 출처 pdf : Kafka paper
kafka 공식 문서 : kafka.apache.org/documentation/




1. Introduction

  • 회사에서 생성되는 대량의 로그 데이터가 있다.
  • 로그 데이터에는 일반적으로 다음과 같이 포함된다.

    1. 로그인, 페이지 뷰, 클릭, 좋아요, 공유, 댓글 및 검색 쿼리에 해당하는 사용자 활동 이벤트
    2. 서비스 콜 스택, 통화 지연 시간, 오류같은 운영(operational) 메티릭과 각 머신의 CPU, 메모리, 네트워크 또는 디스크 이용률과 같은 시스템(system) 메트릭

      • 메트릭(metrics) : 키 값 쌍으로 캡처된 단순한 숫자 측정 1


  • 인터넷 애플리케이션에서 activity 데이터를 현장에서 직접 사용되는 생산 데이터 파이프라인의 일부가 되고 있다.
  • 이러한 용도는 다음과 같은 기능이 포함된다.
    1. 검색 관련성
    2. 항목 인기 또는 활동 스트림의 동시 발생에 의한 추천
    3. 광고 타겟팅 및 보고(report)
    4. 스팸 또는 비승인 데이터 스크래핑같은 남용 행위로부터 보호하는 보안 애플리케이션
    5. 사용자 상태 업데이트나 친구나 연결들을 읽어오기 위한 액션을 집계하는 뉴스피드(newsfeed) 기능


  • 이러한 로그 데이터의 생산과 실시간 사용은 데이터 시스템에 새로운 과제(challenge)를 안겨준다.
  • 예를 들어 검색, 추천, 광고에는 세분화된 클릭률을 계산하는 경우가 많은데, 사용자가 클릭할 때마다 로그 레코드가 생성될 뿐만 아니라 클릭하지 않은 각 페이지의 수십 개 항목에 대해서도 로그 레코드가 생성된다.


  • 링크드인에서는 실시간 애플리케이션을 몇 초 이내의 지연으로 지원해야 한다는 사실을 알아냈다.
  • 따라서 전통적인 로그 애그리게이터와 메시징 시스템의 이점을 결합한 카프카(Kafka)라는 로그 처리를 위한 새로운 메시징 시스템(messaging system)을 구축했다.
  • 카프카는 분산(distributed)과 확장 가능하고(scalable) 높은 처리량(throughput)을 제공한다.
  • 카프카는 메시징 시스템과 유사한 API를 제공하며 애플리케이션이 로그 이벤트를 실시간으로 소비할 수 있도록 한다.
  • 모든 유형의 로그 데이터를 온라인 및 오프라인에서 모두 사용할 수 있도록 싱글 소프트웨어를 활용할 수 있기 때문에 인프라를 크게 단순화한다.



  • 기존의 메시징 시스템은 종종 비동기(asynchronous) 데이터 흐름을 처리하기 위한 이벤트 버스(event bus)로서 중요한 역할을 한다.
  • 하지만, 로그 처리에 적합하지 않은 이유들이 있다.
  1. 엔터프라이즈 시스템은 풍부한 전달 보증(delivery guarantee) 세트를 제공하는 데 초점을 맞추는 데 이러한 기능은 불일치가 있다.

    • 전달 보장(delivery guarantee)는 로그 데이터를 수집하기 위해 과잉이다.
    • 불필요한 기능들은 API와 이러한 시스템의 기본 구현 모두의 복잡성을 증가시킨다.
  2. 많은 시스템은 기본 설계 제약만큼 처리량에 중점을 두지 않는다.
  3. 기존 메시징 시스템은 여러 대의 컴퓨터에 메시지를 분할하고 저장하는, 즉 분산 지원에 취약하다.
  4. 지속적인 소비보다는 주기적으로 큰 로드를 수행하는 데이터 웨어하우징 애플리케이션과 같은 오프라인 컨슈머(consumer)의 경우처럼 메시지 축적이 허용되면 성능이 크게 저하된다.


  • 링크드인에서는 각 컨슈머가 유지할 수 있는 최대 속도로 메시지를 검색하고 처리할 수 있는 것보다 빠르게 푸시되는 메시지에 의해 플러딩(flood)되는 것을 피할 수 있기 때문에 pull 모델이 애플리케이션에 더 적합하다는 것을 발견했다.



3. Kafka Architecture and Design Principles

  • 기존 시스템의 한계 때문에, 저자들은 새로운 메시징 기반 로그 애그리게이터인 카프카를 개발했다.
  • 다음은 카프카의 기본 개념이다.
    • 특정 유형의 메시지 스트림은 토픽(topic)에 의해 정의된다.
      • 토픽(topic) : 메시지를 구분하는 단위 2
    • 프로듀서(producer)는 메시지를 토픽에 발행한다(publish).
    • 발행된 메시지들은 브로커(broker)라는 서버 집합에 저장된다.
    • 컨슈머(consumer)는 브로커로부터 하나 이상의 토픽들을 구독(subscribe)할 수 있고, 브로커로부터 구독한 메시지를 데이터를 pull하여 소비(consume)할 수 있다.
  • 메시징은 개념적으로 간단하며, 저자들은 위 내용을 반영하기 위해 카프카 API를 똑같이 단순하게 만들려고 했다.


  • 다음은 프로듀서의 샘플 코드이다.
/* Sample producer code */
producer = new Producer(...);
message = new Message("test message str".getBytes());
set = new MessageSet(message);
producer.send("topic1", set);


  • 다음은 컨슈머의 샘플 코드이다.
    • 토픽을 구독하기 위해 컨슈머는 토픽을 위한 하나 이상의 메시지 스트림을 먼저 생성한다.
    • 메시지에 의해 발행된 메시지는 서브 스트림으로 균일하게 분산된다.
    • 각 메시지 스트림은 생성되는 메시지의 연속적인 스트림에 대해 이터레이터(iterator) 인터페이스를 제공한다.
    • 그런 다음, 컨슈머는 스트림의 모든 메시지에 대해 반복하고 메시지의 페이로드(payload)를 처리한다.
      • 페이로드(payload) : 사용에 있어서 전송되는 데이터 3
    • 현재 사용할 메시지가 더 이상 없는 경우 이터레이터는 새 메시지가 토픽에 발행될 때까지 블록한다.
/* Sample consumer code */
streams[] Consumer.createMessageStreams("topic1", 1);
for (message : streams[0]) {
  bytes = message.payload();
  // do something with the bytes
}


  • 카프카는 다수의 컨슈머가 토픽의 모든 메시지의 싱글 복사복을 공동으로 소비하는 포인트 투 포인트 전달 모델(point-to-point delivery model)과 다수의 컨슈머가 각각 토픽의 자체 복사본을 retrieve하는 발행/구독 모델(publish/subscribe model)을 모두 지원한다.
    • retrieve : 데이터를 검색하고(searching), 찾고(locating), 반환하는(returning) 과정을 설명하는 데 사용되는 용어이다. 4


  • 카프카의 전체 아키텍처는 다음과 같다.
    • 카프카는 본질적으로 분산되어 있기 때문에, 카프카 클러스터는 일반적으로 여러 브로커로 구성된다.
    • 로드 밸런싱(load balancing)을 위해 토픽을 여러 파티션(partition)으로 나누고 각 브로커는 하나 이상의 파티션을 저장한다.
      • 파티션(partition) : 메시지를 저장하는 물리적인 파일 2
    • 여러 프로듀서와 컨슈머가 동시에 메시지를 발행하고 retrieve할 수 있다.

image


3.1 Efficiency on a Single Partition

  • 시스템을 효율적으로 만들기 위해 카프카에서 몇 가지 정책들이 있다.

Simple storage

  • 토픽의 각 파티션은 논리적 로그에 해당한다.
  • 물리적으로 로그는 거의 동일한 크기의 세그먼트(segment) 파일 집합으로 구성된다.
  • 프로듀서가 메시지를 파티션에 발행할 때마다 브로커는 메시지를 마지막 세그먼트 파일에 추가한다.
  • 성능 향상을 위해 구성가능한(configurable) 개수의 메시지가 발행되거나 일정 시간이 경과된 후에만 세그먼트 파일을 디스크에 플러시(flush)한다.
    • 메시지가 플러시된 후에만 컨슈머에게 노출된다.
    • 플러시(flush) : 영속성 컨텍스트의 변경 내용을 DB에 반영하는 것을 말한다. 5


  • 일반적인 메시징 시스템과 달리, 카프카에 저장된 메시지에는 명시적인(explicit) 메시지 ID가 없다. 대신 각 메시지는 로그의 논리적 오프셋(offset)으로 지정된다.
    • 이렇게 하면 메시지 ID를 실제 메시지 위치에 매핑하는 seek-intensive random-access index 구조를 유지하는 데 드는 오버헤드를 피할 수 있다.
    • 오프셋(offset) : 파티션 내 각 메시지의 저장된 상대적 위치 2
  • 다음 메시지의 ID를 계산하려면 현재 메시지의 길이를 ID에 추가해야 한다.
  • 그럼, 메시지 ID와 오프셋을 서로 교환하여 사용할 것이다.


  • 컨슈머는 항상 특정 파티션의 메시지를 순차적으로(sequentially) 소비한다.
  • 컨슈머가 특정 메시지 오프셋을 승인하는 경우, 이는 컨슈머가 파티션에서 해당 오프셋 이전에 모든 메시지를 수신했음을 의미한다.
  • 소비자는 애플리케이션이 소비할 수 있도록 데이터의 버퍼를 가지기 위해 브로커에게 비동기 풀(pull) 요청을 발행(issue)한다.
    • 각 풀 요청에는 소비가 시작되는 메시지의 오프셋과 가져올 수 있는 바이트 수가 포함된다.
  • 각 브로커는 모든 세그먼트 파일에서 정렬된 오프셋 목록을 메모리에 보관한다.
  • 브로커는 오프셋 목록을 검색하여 요청된 메시지가 있는 세그먼트 파일을 찾아 데이터를 컨슈머에게 다시 보낸다.
  • 컨슈머가 메시지를 수신한 후, 소비할 다음 메시지의 오프셋을 계산하여 다음 풀 요청에 사용한다.


  • 다음 그림은 카프카는 로그의 레이아웃과 메모리 내 인덱스를 설명한다.

image


Efficient transfer

  • 최종(end) 컨슈머 API는 한 번에 하나의 메시지를 반복하지만, 소비자로부터의 각 풀(pull) 요청은 특정 크기까지 여러 개의 메시지를 retrieve한다.
    • 최종(end) : 실제 사용하는 사람
  • 컨슈머로부터의 각 풀(pull) 요청은 특정 크기까지 여러 개의 메시지를 retrieve한다.
  • 또한 카프카는 카프카 계층에서 메모리에 메시지를 명시적으로 캐싱하는 것을 피하고, 대신 기본 파일 시스템 페이지 캐시에 의존한다.
  • 이것은 이중 버퍼링(double buffering)을 피할 수 있는 이점이 있다.
    • 메시지는 오직 페이지 캐시에만 캐시된다.
  • 따라서 브로커 프로세스가 재시작되더라도 웜 캐시(warm cache)를 유지할 수 있는 추가적인 이점이 있다.

버퍼(buffer) : 어떤 장치에서 다른 장치로 데이터를 송신할 때 일어나는 시간의 차이나 데이터 흐름의 속도 차이를 조정하기 위해 일시적으로 데이터를 기억시키는 장치.
싱글버퍼(single buffer)의 경우 채널이 데이터를 버퍼에 저장하면 프로세서가 처리하는 방식으로 진행된다. 이경우 채널이 데이터를 저장하는 동안에는 데이터에 대한 처리가 이루어질 수 없으며, 프로세서가 데이터를 처리하는 동안에는 다른 데이터가 저장될 수 없게 된다.
이중 버퍼(double buffer)의 경우에는 데이터에 대한 저장과 처리가 동시에 일어날 수 있다. 6


  • 카프카는 처리 중인 메시지를 전혀 캐싱하지 않으므로 메모리를 수집하는 데 드는 가비지(garbage) 오버헤드가 거의 없으므로 VM 기반 언어로 효율적으로 구현할 수 있다.


  • 또한 카프카는 컨슈머를 위해 네트워크 액세스를 최적화한다.
  • 카프카는 멀티 구독자 시스템이며 하나의 메시지는 다른 컨슈머 애플리케이션에 의해 여러 번 소비될 수 있다.
  • 리눅스에는 파일 채널에서 소켓 채널로 직접 바이트를 전송할 수 있는 sendfile API가 존재하는데, 카프카는 이를 활용하여 로그 세그먼트 파일의 바이트를 브로커에서 컨슈머에게 효율적으로 전달한다.


Stateless broker

  • 대부분의 다른 메시징 시스템과 달리, 카프카에서는 각 컨슈머가 얼마나 소비했는지에 대한 정보는 브로커가 아니라 컨슈머 자신이 관리한다.
  • 이러한 설계는 브로커의 복잡성과 오버헤드를 상당히 줄여준다.
  • 그러나 브로커는 모든 구독자가 메시지를 소비했는지 여부를 알 수 없기 때문에 메시지를 삭제하기 어렵다.
  • 카프카는 보존(retention) 정책에 대해 단순한 시간 기반 SLA를 사용하여 이 문제를 해결한다.
    • SLA : Service Level Agreement, 서비스 수준 협약서. 여기서 포함될 수 있는 서비스 측정치들은 CPU 가용시간, CPU 응답시간, 서비스 완료시간 등이다. 7
  • 메시지는 특정 기간(일반적으로 7일) 이상 브로커에 보관된 경우 자동으로 삭제된다.
    • 오프라인 컨슈머를 포함한 대부분의 컨슈머는 매일 또는 실시간으로 소비를 마친다.
    • 따라서, 데이터 크기가 커도 카프카의 성능이 저하되지 않는다.


  • 컨슈머는 의도적으로 이전 오프셋으로 되돌리고 데이터를 다시 소비할 수 있다. (이점)



3.2 Distributed Coordination

  • 이제 프로듀서와 컨슈머가 분산 환경에서 어떻게 행동하는지 살펴본다.
  • 각 프로듀서는 무작위로 선택된 파티션 또는 파티셔닝 키와 함수에 의해 의미론적으로 결정된 파티션에 메시지를 발행할 수 있다.
  • 카프카는 컨슈머 그룹의 개념을 가지고 있다.
  • 각 컨슈머 그룹은 구독된 토픽 세트를 공동으로 소비하는 하나 이상의 컨슈머로 구성된다.
    • 즉, 각 메시지는 그룹 내 컨슈머 중 한 명에게만 전달된다.
  • 서로 다른 컨슈머 그룹은 각각 독립적으로 전체 구독 메시지 세트를 소비하며 컨슈머 그룹 간에 조정(coordination)이 필요하지 않다.
  • 카프카의 목표는 조정 오버헤드를 너무 많이 들이지 않고 브로커에 저장된 메시지를 컨슈머에게 균등하게 나누는 것이다.


  • 카프카의 첫 번째 결정은 토픽 내에서 파티션을 병렬화(parallelism)의 가장 작은 단위로 만드는 것이다.
    • 즉, 주어진 시간에 한 파티션의 모든 메시지는 각 컨슈머 그룹 내의 싱글 컨슈머만 소비한다.
    • 또한, 토픽을 오버 파티셔닝하여 로드 밸런싱을 달성했다.
  • 카프카의 두 번째 결정은 중앙의 마스터 노드가 아니라 컨슈머들이 분산되어 있는 방식으로 그들 사이에서 조정하도록 하는 것이다.
  • 조정을 용이하게 하기 위해, 높은 가용성의 합의(consensus) 서비스인 주키퍼(zookeeper)를 사용한다.
    • 주키퍼는 API와 같은 매우 간단한 파일 시스템을 가지고 있다.
    • 경로를 만들고, 경로 값을 설정하고, 경로 값을 읽고, 경로를 삭제하고, 경로의 하위 항목을 나열할 수 있다.
    • 주피커는 경로에 워처(watcher)를 등록하고 경로 값이 변경되었을 때 알림을 받을 수 있다.
    • 주키퍼는 경로를 임시로 만들 수 있는데, 즉 생성 클라이언트가 사라지면 해당 경로가 주키퍼 서버에 의해 자동으로 제거된다.
    • 주키퍼는 데이터를 여러 서버에 복제하여 데이터의 신뢰성과 가용성을 높인다.


  • 카프카는 다음과 같은 작업에 주키퍼를 사용한다.

    1. 브로커와 컨슈머의 추가 및 제거 탐지
    2. 이벤트가 발생할 때 각 컨슈머에게 재조정 프로세스를 트리거(trigger)
    3. 소비 관계를 유지하고 각 파티션의 소비 오프셋을 추적
  • 특히, 각 브로커 또는 컨슈머가 시작되면 해당 정보를 주키퍼의 브로커 또는 컨슈머 레지스트리에 저장한다.
    • 브로커 레지스트리에는 브로커의 호스트 이름 및 포트, 여기에 저장된 토픽 및 파티션 집합이 포함된다.
    • 컨슈머 레지스트리에는 컨슈머가 속한 컨슈머 그룹과 구독한 토픽 세트를 포함한다.
  • 각 컨슈머 그룹은 주키퍼의 소유권 레지스트리 및 오프셋 레지스트리와 연결된다.
    • 소유권 레지스트리에는 구독된 모든 파티션에 대해 하나의 경로가 있으며 경로 값은 현재 이 파티션에서 사용하는 컨슈머의 ID이다.
    • 오프셋 레지스트리에는 구독한 각 파티션에 대해 파티션에서 마지막으로 사용된 메시지의 오프셋을 저장한다.


  • 주키퍼에서 생성된 경로는 브로커 레지스트리, 컨슈머 레지스트리 및 소유권 레지스트리에 대해 사용 후 삭제되며, 오프셋 레지스트리는 영구적으로 남긴다.
  • 브로커가 실패하면 브로커의 모든 파티션이 브로커 레지스트리에서 자동으로 제거된다.
  • 컨슈머가 실패하면 컨슈머 레지스트리의 항목과 소유권 레지스트리에서 소유한 모든 파티션이 손실된다.
  • 각 컨슈머는 브로커 레지스트리와 컨슈머 레지스트리에 모두 주키퍼 워처를 등록하여, 브로커 세트 또는 컨슈머 그룹이 변경될 때마다 알림이 간다.


  • 컨슈머를 처음 시작하는 동안 또는 컨슈머가 워처를 통해 브로커/컨슈머 변경에 대해 알림을 받으면 컨슈머는 소비해야 할 파티션의 새 하위 집합을 결정하기 위해 재조정 프로세스를 시작한다.

image


3.3 Delivery Guarantees

  • 일반적으로 카프카는 최소 한번만 전달(delivery)을 보장한다.
  • 정확히 한 번 전달은 일반적으로 2단계 커밋이 필요하며 우리 애플리케이션에 필요하지 않다.
  • 대부분의 경우 메시지는 각 컨슈머 그룹에 정확히 한 번 전달된다.
  • 애플리케이션이 중복(duplicate)을 고려하는 경우 컨슈머에게 반환하는 오프셋이나 메시지 내의 일부 고유 키를 사용하여 자체 중복 제거 논리를 추가해야 한다.
    • 이는 2단계 커밋보다 효율적인 접근법이다.


  • 카프카는 싱글 파티션의 메시지가 컨슈머에게 순서대로 전달되도록 보장한다.
  • 그러나 서로 다른 파티션에서 오는 메시지의 순서에 대한 보장은 없다.
  • 로그 손상을 방지하기 위해 카프카는 로그의 각 메시지에 대해 CRC를 저장한다.
    • 순환 중복 검사(cyclic redundancy check, CRC) : 네트워크 등을 통하여 데이터를 전송할 때 전송된 데이터에 오류가 있는지를 확인하기 위한 체크값을 결정하는 방식 8
  • 브로커에 I/O 오류가 있는 경우 카프카는 복구 프로세스를 실행하여 CRC가 일치하지 않는 메시지를 제거한다.





References

댓글남기기