본문 바로가기

streaming

Apache Kafka 정리

카프카란

  • 데이터 스트림을 저장하고 처리하는 데 사용할 수 있는 확장 가능하고 내결합성 높은 분산 스트리밍 플랫폼이다.
  • 카프카 클러스터는 데이터 스트림을 저장하는데, 이 데이터 스트림은 애플리케이션에서 연속적으로 생성되고 다른 애플리케이션에서 순차적이고 점진적으로 소비되는 메시지/이벤트의 시퀸스다.
  • 카프카는 펍/섭 메시지, 스트리밍 ETL, 데이터 스트림 처리와 같은 다양한 사용 사례 범주를 지원한다.

Apache Kafaka

 

동작

토픽은 파티션으로 나뉜다. 메시지가 토픽에 기록될 때, 프로듀서는 메시지의 파티션을 지정해야 한다. 프로듀서는 자신이 작성하는 메시지에 대해 어떤 파티션 전략도 사용할 수 있다. 기본적으로 메시지는 키별로 해시 분할되므로 동일한 키를 가진 모든 메시지가 동일한 파티션에 기록된다. 각 메시지는 파티션 내에서 메시지의 위치, 즉 단조롭게 증가하는 시퀸스 번호가 있는 관련 오프셋이 있다. 메시지의 오프셋은 메시지가 파티션에 추가되는 순서에 따라 암시적으로 결정된다.

따라서 항목 내의 각 메시지는 파티션과 오프셋에 의해 고유하게 식별된다. 카프카는 단일 파티션 내에서 엄격한 메시지 순서를 보장한다. 즉, 파티션을 읽는 모든 소비자가 파티션에 추가된 것과 동일한 순서로 모든 메시지를 수신하도록 보장한다. 다른 파티션 또는 다른 항목의 메시지 사이에는 순서가 보장되지 않는다.

토픽은 동시에 여러 프로듀서가 쓸 수 있다. 여러 프로듀서가 동일한 파티션에 쓰는 경우 해당 메시지는 인터리브된다. 컨슈머가 같은 토픽에서 읽으면 컨슈머 그룹을 구성할 수 있다. 컨슈머 그룹 내에서 각 개별 소비자는 파티션의 하위 집합에서 데이터를 읽는다. 카프카는 각 파티션이 그룹 내에서 정확히 하나의 소비자에게 할당되도록 보장한다.서로 다른 소비자 집단이나 어느 소비자 집단에도 속하지 않는 소비자들은 서로 독립적이다. 따라서 두 소비자 그룹이 동일한 주제를 읽으면 모든 메시지가 두 그룹에게 전달된다. 소비자들이 서로 독립적이기 때문에, 각각의 소비자들은 자신만의 속도로 메시지를 읽을 수 있다. 따라서 분산 시스템에 바람직한 속성인 디커플링이 발생하고 시스템이 스트래글러에 대해 견고해진다.

 

브로커

카프카 브로커는 메시지를 디스크에 안정적으로 저장한다. 전통적인 메시징/펍-섭 시스템과 달리 카프카는 전달 후 메시지를 삭제하지 않기 때문에 메시지를 장기간 저장하는 데 사용할 수 있다. 토픽은 메시지를 저장해야 하는 기간을 지정하는 retention time으로 구성된다. 토픽 보존을 시간 대신 바이트 단위로 지정하여 디스크 공간에 상한을 적용할 수도 있다. retention boundaries에 도달하면 카프카는 로그 끝에 있는 파티션을 샂제한다.

로그 압축

로그 압축은 파티션 단위로 적용되므로 동일한 키에 대한 업데이트를 동일한 파티션에 기록해야 한다. 성능상의 이유로 브로커는 오래된 메시지를 즉시 삭제하지 않지만 압축은 백그라운드 프로세스로 정기적으로 트리거된다. 삭제 표시는 null 값을 가진 메시지이며, 해당 키에 대한 모든 이전 업데이트(삭제 표시 자체 포함)를 삭제할 수 있음을 나타낸다.

스케일링 및 로드밸런싱

메시지는 토픽에 저장되고 토픽은 파티션으로 분한된다.파티션은 항목 내에서 독립적인 단위이기 때문에 파티션을 사용하면 클러스터 내에서 수평으로 확장하고 로드 밸런싱을 수행할 수 있다. 같은 토픽의 파티션이 서로 다른 브로커에 저장되더라도 브로커 동기화가 필요 없어 카프카 클러스터는 브로커 수에 따라 선형적으로 확장된다. 단일 브로커는 단일 파티션의 용량만 제한하지만 항목을 임의의 파티션 수로 만들 수 있으므로 실제로는 제한이 없다. 전체적으로 항목의 읽기/쓰기 처리량 및 스토리지 요구사항은 단일 서버 크기에 의해 제한되지 않으며, 시스템에 새로운 브로커를 추가하여 클러스터 용량을 늘릴 수 있다. 클러스터 내의 로드 밸런싱을 위해 파티션을 브로커 간에 재할당할 수 있다.

카프카 클러스터에는 마스터 노드가 없으며, 모든 브로커는 클러스터에서 제공하는 모든 서비스를 수행할 수 있다. 마스터 노드가 병목 현상을 일으킬 수 있으므로 이 설계는 선형 확장을 지원한다. 브로커 조정을 위해 카프카는 확장 가능하고 내결함성 있으며 가용성이 높은 분산 조정 서비스인 아파치 주키퍼를 사용한다. 카프카는 주키퍼를 사용하여 주제, 파티션, 파티션-브로커 매핑 등에 대한 모든 클러스터 메타데이터를 안정적이고 가용성이 높은 방식으로 저장한다.

장애 복구 및 고가용성

내결함성과 고가용성을 보장하기 위해 파티션을 여러 브로커에 복제할 수 있다. 유지 관리해야 할 파티션 복사본 수를 나타내는 개별 복제 팩터를 사용하여 각 항목을 구성할 수 있다. 파티션당 엄격한 메시지 순서 보장을 위해 복제는 리더-팔로워 패턴을 사용한다. 각 파티션에는 하나의 지시선과 구성 가능한 수의 팔로워가 있다. 모든 읽기 및 쓰기 요청은 리더가 처리하는 반면 팔로워는 백그라운드에서 리더에게 모든 쓰기를 복제한다. 리더를 호스팅하는 브로커가 실패하면 카프카는 Zookeeper를 통해 리더 선거를 시작하고, 팔로워 중 한 명이 새로운 리더가 된다. 모든 고객에게 새 리더 정보가 업데이트되고 모든 읽기/쓰기 요청이 새 리더에게 전송된다. 실패한 브로커가 복구되면 클러스터에 다시 가입하고 모든 호스트 파티션이 팔로워가 된다.

메시지 전달

카프카에서 데이터를 읽는 것은 전통적은 메시징/퍼블리셔-섭스크립 시스템과 비교해 약간 다르게 동작한다. 브로커는 전달 후 메시지를 삭제하지 않는다. 이 설계에는 여러 이점이 있다.

  • 브로커가 컨슈머의 리딩 진행 상황을 추적할 필요가 없다. 브로커에 대한 오버헤드 트래킹 프로세스가 없으므로 읽기 처리량이 증가한다.
  • 브로커가 진행 상황을 트래킹하지 않고 전달 후 데이터를 삭제하지 않기 때문에 컨슈머가 과거로 돌아가 오래된 데이터를 다시 재처리할 수 있다. 또한 새로 만들어진 컨슈머들은 오래된 데이터를 검색할 수 있다.

이 접근법의 단점은 컨슈머 클라이언트가 자신의 진행 상황을 직접 추적해야 한다는 것이다. 이는 컨슈머가 읽고자 하는 다음 메시지의 오프셋을 저장함으로써 발생한다. 이 오프셋은 브로커에 대한 읽기 요청에 포함되며 브로커는 요청된 오프셋부터 시작하여 컨슈머에게 연속적인 메시지를 배달한다. 수신된 모든 메시지를 처리한 후, 컨슈머는 그에 따라 오프셋을 업데이트하고 다음 읽기 요청을 클러스터로 보낸다.

컨슈머가 나중에 중지되었다가 다시 시작된 경우 일반적으로 중단한 부분부터 계속 읽어야 한다. 이를 위해 컨슈머는 오프셋을 안정적으로 저장하여 재시작 시 검색할 수 있도록 해야 한다. 카프카에서 컨슈머들은 브로커에게 오프셋을 커밋할 수 있다. 오프셋 커밋 브로커는 오프셋 토픽이라는 특별한 항목에 소비자 오프셋을 안정적으로 저장한다. 오프셋 항목도 분할 및 복제되므로 확장 가능하고 내결함성이 있으며 가용성이 높다. 이를 통해 카프카는 다수의 소비자를 동시에 관리할 수 있다. 오프셋 항목은 오프셋이 손실되지 않도록 로그 압축을 사용하도록 구성된다.

카프카는 여러 전달 semantic, 즉 최대 한 번, 최소 한 번, 정확히 한 번을 지원한다. 사용자가 얻는 semantic은 클라이언트 구성 및 사용자 코드뿐만 아니라 클러스터/토픽 구성과 같은 여러 요소에 따라 달라진다.

프로튜서가 토픽에 데이터를 쓸 때, 브로커들은 프로듀서에게 성공적인 쓰기를 인정한다. 만약 프로듀서가 승인을 받지 못한다면, 메시지를 토픽에 기록하지 않아 손실될 수 있기 때문에, 이를 무시한다. 대신, 프로듀서는 쓰기를 재시도하여 적어도 한 번 이상의 semantic을 생성할 수 있다. 첫 번째 글쓰기는 성공적일 수 있었지만, 인정이 끊길 수도 있다. 카프카는 또한 idempotence를 이용하여 정확히 한 번 쓰기를 지원한다. 이를 위해 각 메시지에는 내부적으로 고유한 식별자가 할당된다. 프로듀서는 작성하는 각 메시지에 이 식별자를 첨부하고 브로커는 항목에 있는 각 메시지의 메타데이터로 식별자를 저장한다. 프로듀서 쓰기 재시도의 경우 브로커는 메시지 식별자를 비교하여 중복 쓰기를 탐지할 수 있다. 이 중복제거 메커니즘은 프로듀서 및 브로커의 내부 방식이며 애플리케이션 레벨 중복으로부터 보호되지 않는다.

트랜잭션

카프카는 트랜잭션이라고 불리는 원자 다중 파티션 쓰기를 지원한다. 카프카 트랜잭션은 데이터베이스 트랜잭션과 다르며 ACID 보증에 대한 개념이 없다. 카프카의 트랜잭션은 서로 다른 주제 및 파티션에 걸쳐 있는 여러 메시지의 원자적 쓰기와 유사하다. 트랜잭션 프로듀서는 해당 API 호출을 수행하여 트랜잭션을 위해 먼저 초기화된다. 브로커는 생산자에 대한 트랜잭션 쓰기를 수락할 준비가 된다. 프로듀서가 보낸 모든 메시지는 현재 트랜잭션에 속하며, 트랜잭션이 완료되지 않는 한 어떤 컨슈머에게도 전달되지 않는다. 트랜잭션 내의 메시지는 클러스터 내의 모든 토픽/파티션으로 전달될 수 있다. 트랜잭션의 모든 메시지가 전송되면 프로듀셔가 트랜잭션을 커밋한다. 브로커는 트랜잭션을 커밋하기 위해 2단계 커밋 프로토콜을 구현하며 트랜잭션에 속한 모든 메시지를 성공적으로 “쓰기”하거나 아예 쓰지 않는다. 이 경우 로그에서 어떤 메시지도 삭제되지 않지만 모든 메시지가 중단됨으로 표시된다.

오프셋

메시지 전달 섹션에서 논의된 바와 같이, 컨슈머는 자신의 읽기 진행 상황을 직접 트래킹할 필요가 있다. 내결함성을 이유로 소비자들은 정기적으로 카프카에 오프셋을 커밋한다. 컨슈머는 이에 대해 두 가지 전략을 적용할 수 있는데, 메시지를 받을 후 먼저 오프셋을 하고 그 후에 메시지를 처리하거나, 역순으로 메시지를 먼저 처리하고 마지막에 오프셋을 커밋할 수 있다. 커밋 우선 전략은 메시지가 수신되고 메시지가 처리되기 전에 오프셋이 커밋되면 이 메시지는 실패 시 다시 전달되지 않는다. 즉, 실패 후 소비자는 오프셋을 마지막으로 커밋된 오프셋으로 복구하여 실패한 메시지 이후에 읽기를 재개한다.

대조적으로 프로세스 우선 전략은 최소 한 번 이상의 semantic를 제공한다. 수신 메시지를 성공적으로 처리한 후 소비자가 오프셋을 업데이트하지 않기 때문에 오류를 복구한 후에는 항상 이전 오프셋으로 돌아간다. 따라서 처리된 메시지를 다시 읽어 출력에 중복이 발생할 수 있다.

소비자는 모든(중단된 메시지 포함) 또는 커밋된 메시지만 읽도록 구성할 수 있다. 이는 다른 트랜잭션 시스템과 유사한 읽기 비커밋 및 읽기 커밋 모드에 해당한다. 중단된 메시지는 항목에서 삭제되지 않으며 모든 소비자에게 배달된다. 따라서 읽기 커밋 모드의 소비자는 중단된 메시지를 필터링/드랍하여 응용 프로그램으로 배달하지 않는다.

 

카프카 커넥트 프레임워크

카프카 커넥트는 분산 파일 시스템, 데이터베이스 키 밸류 스토어 등과 같은 외부 시스템과 카프카를 통합하기 위한 프레임워크다. 내부적으로 카프카 커넥트는 “카프카 브로커” 섹션에 설명된 대로 프로듀서/컨슈머 클라이언트를 사용하지만 프레임워크는 각 시스템에 대해 구현되어야 할 많은 기능과 모범 사례를 구현한다. 또한 완벽하게 관리되고 내결함성이 있으며 가용성이 높은 방식으로 실행할 수 있다.

ConnectAPI는 데이터를 카프카로 수집하고 데이터 스트림을 분산 파일 시스템, 데이터베이스 등과 같은 외부 시스템으로 내보내는데 사용한다.

Connect API는 커넥터를 사용하여 각 유형의 외부 시스템과 통신한다. 소스 커넥터는 지속적으로 외부 소스 시스템의 데이터를 읽고 카프카에 기록을 쓰는 반면 싱크 커넥터는 지속적으로 카프카의 데이터를 소비하고 기록을 외부 시스템으로 보낸다. HDFS, S3, 관계형 데이터베이스, 문서 데이터베이스 시스템, 기타 메시징 시스템, 파일 시스템, 미터법 시스템, 분석 시스템 등 다양한 시스템에 많은 커넥터를 사용할 수 있다. 개발자는 다른 시스템용 커넥터를 만들 수 있다.

카프카 커넥트는 독립적으로 사용하거나 시스템 클러스터에 배포할 수 있다. 외부 시스템에 연결하려면 사용자가 외부 시스템의 세부 사항과 원하는 동작을 정의하는 커넥터에 대한 구성을 만들고 이 구성을 Connect 작업자 중 하나에 업로드한다. JVM에서 실행되는 작업자는 커넥터를 배포하고 커넥터의 작업을 클러스터에 분산시킨다. 소스 커넥터 작업은 외부 시스템으로부터 데이터를 로드하고 레코드를 생성하며, Connect는 이 데이터를 해당하는 Kafka 항목에 기록한다. 싱크 커넥터 태스크의 경우 Connect는 지정된 항목을 소비하고 이러한 레코드를 태스크로 전달하며, 이 작업은 레코드를 외부 시스템으로 보내는 역할을 한다.

SMT (Single-Message Transforms)

Connect API를 사용하면 카프카로 가져오거나 내보내는 개별 메시지에 대해 단순 변환(Single-message Transforms, SMT)을 지정할 수 있다. 이러한 변환은 커넥터와 무관하며 상태 비저장 작업을 허용한다. 표준 변환 기능은 이미 카프카가 제공하고 있지만 초기 데이터 정리를 수행하기 위한 맞춤형 변환 구현도 가능하다. 좀 더 복잡한 변환이 필요하기 때문에 SMT로는 충분하지 않은 경우 Streams API를 대신 사용할 수 있다.

 

결론

아파티 카프카는 확장 가능하고 내결함성이 있으며 가용성이 높은 분산 스트리밍 플랫폼이다. 팩트와 체인지로그 스트림을 저장하고 처리할 수 있으며 스트림 처리에서 스트임 테이블 이중화를 이용한다. 카프카의 트랜잭션은 정확히 한 번 스트림 처리 semantic을 허용하고 정확히 한 번으로 엔드 투 엔드 데이터 파이프라인을 단순화한다. 게다가, 카프카는 커넥트 API를 통해 다른 시스템에 연결될 수 있기 때문에 조직의 중앙 데이터 허브로 사용될 수 있다. 데이터 스트림 처리를 위해 Streams API는 개발자들이 카프카 클러스터로부터 입력 스트림을 읽고 결과를 카프카에 다시 쓰는 정교한 스트림 처리 파이프라인을 지정할 수 있게 한다.

 

참고자료

[1]Kreps, Jay, Neha Narkhede, and Jun Rao. "Kafka: A distributed messaging system for log processing." Proceedings of the NetDB. Vol. 11. 2011.

[2]Sax, Matthias J., S. Sakr, and A. Zomaya. "Apache Kafka." (2019).