Kafka의 기본 구성 요소 (Pub/Sub, 메시지, 토픽·파티션, 브로커)
Kafka: The Definitive Guide (O'Reilly, 2017, 1st Edition) — Neha Narkhede, Gwen Shapira, Todd Palino Chapter 1: Meet Kafka — Publish/Subscribe Messaging ~ Multiple Clusters (pp.1-9)
Kafka를 이해하기 전에 왜 publish/subscribe 메시징이 필요해지는지부터 짚고, Kafka가 데이터를 다루는 단위(메시지, 키, 배치)와 조직하는 단위(토픽, 파티션), 그 위에서 동작하는 클라이언트(프로듀서, 컨슈머, 컨슈머 그룹), 그리고 이들을 호스팅하는 브로커와 클러스터를 순서대로 살펴본다. 리텐션과 로그 컴팩션, 여러 클러스터를 잇는 MirrorMaker까지 다룬다.
Publish/Subscribe 메시징은 어떻게 필요해지는가
Publish/subscribe 메시징은 데이터(메시지)를 보내는 쪽(publisher)이 그것을 특정 수신자에게 직접 보내지 않는 패턴이다. 대신 publisher는 메시지를 어떤 방식으로든 분류하고, 수신자(subscriber)는 자기가 관심 있는 분류를 구독한다. 이런 시스템에는 보통 메시지가 게시되는 중앙 지점인 broker가 있다.
대부분의 pub/sub 사용 사례는 아주 단순하게 시작한다. 예를 들어 애플리케이션이 모니터링 정보를 어딘가로 보내야 해서, 대시보드 앱으로 직접 연결을 하나 만들고 메트릭을 밀어 넣는다. 그러다 장기 분석을 하고 싶어져 메트릭을 저장·분석하는 새 서비스를 띄우고, 애플리케이션이 두 시스템에 모두 쓰도록 고친다. 메트릭을 만드는 애플리케이션이 몇 개 더 늘어나면 모두 같은 연결을 반복해서 만든다. 동료가 알림을 위해 능동적으로 폴링하자고 해서 각 애플리케이션에 메트릭 제공 서버를 하나씩 붙인다. 이쯤 되면 연결을 추적하는 것조차 어려워진다.
쌓인 기술 부채를 갚기 위해 모든 애플리케이션의 메트릭을 받는 단일 애플리케이션을 두고, 필요한 시스템이 조회할 수 있는 서버를 제공한다. 이 시점에서 이미 publish/subscribe 메시징 시스템을 만든 것이다. 문제는 동료들도 로그 메시지, 사용자 행동 추적에서 같은 길을 걸어 서로 다른 pub/sub 시스템 세 개를 유지보수하게 된다는 점이다. 각각 고유한 버그와 한계를 갖고 있다. 필요한 것은 범용 데이터를 게시할 수 있고 비즈니스와 함께 성장하는 '하나의' 중앙화된 시스템이다. Apache Kafka는 바로 이 문제를 풀기 위해 설계된 publish/subscribe 메시징 시스템이다.
핵심 포인트
- pub/sub의 핵심은 publisher가 수신자를 직접 지정하지 않고 메시지를 분류만 한다는 것
- point-to-point 직접 연결은 애플리케이션이 늘어날수록 추적 불가능한 연결 그래프가 된다
- 개별 큐 시스템을 여러 개 운영하면 중복된 유지보수 비용과 각기 다른 버그·한계를 떠안는다
- Kafka는 'distributed commit log' 또는 'distributed streaming platform'으로 묘사되며, 데이터를 순서대로 durable하게 저장해 결정적으로 재생할 수 있다
메시지와 배치, 그리고 스키마
Kafka 안에서 데이터의 단위는 메시지(message)다. 데이터베이스 배경에서 온 사람이라면 하나의 행(row)이나 레코드로 생각하면 된다. Kafka에게 메시지는 단순한 바이트 배열이라 그 안의 데이터는 어떤 형식이나 의미도 갖지 않는다. 메시지에는 키(key)라는 선택적 메타데이터를 붙일 수 있는데, 키 역시 바이트 배열이며 Kafka에게 특별한 의미가 없다. 키는 메시지를 파티션에 더 통제된 방식으로 쓰고 싶을 때 사용한다. 가장 단순한 방식은 키의 일관된 해시를 구하고, 그 결과를 토픽의 전체 파티션 수로 나눈 나머지로 파티션 번호를 정하는 것이다. 이렇게 하면 같은 키를 가진 메시지는 항상 같은 파티션에 기록된다.
효율을 위해 메시지는 배치(batch) 단위로 Kafka에 기록된다. 배치는 같은 토픽·같은 파티션으로 향하는 메시지들의 묶음이다. 메시지 하나마다 네트워크를 왕복하면 오버헤드가 과도해지므로 묶어서 보낸다. 물론 이것은 지연과 처리량 사이의 트레이드오프다 — 배치가 클수록 단위 시간당 처리 가능한 메시지 수는 늘지만, 개별 메시지가 전파되는 데 걸리는 시간은 길어진다. 배치는 보통 압축까지 적용해 전송과 저장 효율을 높이는데, 그 대가로 처리 능력(CPU)을 소모한다.
Kafka 자신에게 메시지는 불투명한 바이트 배열이지만, 내용을 쉽게 이해할 수 있도록 추가적인 구조(스키마)를 부여하는 것이 권장된다. JSON이나 XML 같은 단순한 방식은 쓰기 쉽고 사람이 읽을 수 있지만 견고한 타입 처리와 스키마 버전 간 호환성이 부족하다. 많은 Kafka 개발자는 원래 Hadoop을 위해 개발된 직렬화 프레임워크인 Apache Avro를 선호한다. Avro는 컴팩트한 직렬화 포맷, 메시지 페이로드와 분리되어 있어 변경 시 코드 생성이 필요 없는 스키마, 그리고 하위·상위 호환성을 모두 갖춘 강한 타입과 스키마 진화를 제공한다.
일관된 데이터 포맷이 중요한 이유는 쓰기와 읽기를 분리(decouple)할 수 있기 때문이다. 이 둘이 강하게 결합되면, 메시지를 구독하는 애플리케이션들이 먼저 신·구 포맷을 동시에 처리하도록 업데이트되어야만 게시하는 쪽이 새 포맷으로 넘어갈 수 있다. 잘 정의된 스키마를 공용 저장소에 두면 이런 조율 없이도 Kafka의 메시지를 이해할 수 있다.
핵심 포인트
- 메시지와 키 모두 Kafka에게는 의미 없는 바이트 배열
- 키 해시 modulo 파티션 수 → 같은 키는 항상 같은 파티션
- 배치는 네트워크 왕복 오버헤드를 줄이지만 처리량과 지연의 트레이드오프이며, 보통 압축을 동반한다
- 스키마의 목적은 프로듀서와 컨슈머의 디커플링. Avro가 선호되는 이유는 컴팩트함 + 페이로드와 분리된 스키마 + 스키마 진화
토픽과 파티션
Kafka의 메시지는 토픽(topic)으로 분류된다. 토픽에 가장 가까운 비유는 데이터베이스 테이블이나 파일시스템의 폴더다. 토픽은 다시 여러 개의 파티션(partition)으로 나뉜다. 'commit log'라는 설명으로 돌아가면, 파티션 하나가 곧 하나의 로그다. 메시지는 append-only 방식으로 파티션에 기록되고 처음부터 끝까지 순서대로 읽힌다.
여기서 중요한 함의가 나온다. 토픽은 보통 여러 파티션을 가지므로, 토픽 전체에 대한 메시지 시간 순서는 보장되지 않는다. 순서가 보장되는 범위는 '단일 파티션 안'이다.
파티션은 Kafka가 중복성(redundancy)과 확장성(scalability)을 제공하는 수단이기도 하다. 각 파티션은 서로 다른 서버에 호스팅될 수 있으므로, 하나의 토픽이 여러 서버에 걸쳐 수평 확장되어 단일 서버의 능력을 훨씬 뛰어넘는 성능을 낼 수 있다.
Kafka 같은 시스템에서 데이터를 이야기할 때 스트림(stream)이라는 용어가 자주 쓰인다. 대개 스트림은 파티션 수와 무관하게 하나의 토픽에 해당하는 데이터를 가리키며, 프로듀서에서 컨슈머로 이동하는 하나의 데이터 흐름을 뜻한다. 이 표현은 스트림 처리를 논할 때 가장 흔히 쓰이는데, Kafka Streams, Apache Samza, Storm 같은 프레임워크가 메시지를 실시간으로 다루는 방식이다. 이는 대량 데이터를 나중에 일괄 처리하도록 설계된 Hadoop 같은 오프라인 프레임워크와 대비된다.
핵심 포인트
- 토픽 ≈ 테이블/폴더, 파티션 = 하나의 append-only 로그
- 순서 보장 범위는 토픽 전체가 아니라 단일 파티션 안
- 파티션은 중복성과 수평 확장의 단위 — 각 파티션이 다른 서버에 놓일 수 있다
- stream = 파티션 수와 무관하게 하나의 토픽에 해당하는 데이터 흐름
프로듀서와 컨슈머, 컨슈머 그룹
Kafka 클라이언트는 크게 프로듀서와 컨슈머 두 가지다. 여기에 데이터 통합을 위한 Kafka Connect API와 스트림 처리를 위한 Kafka Streams라는 고급 클라이언트 API가 있는데, 이들은 프로듀서와 컨슈머를 빌딩 블록으로 삼아 그 위에 더 높은 수준의 기능을 제공한다.
프로듀서는 새 메시지를 만든다. 다른 pub/sub 시스템에서는 publisher나 writer라고 부른다. 메시지는 특정 토픽으로 생성되며, 기본적으로 프로듀서는 메시지가 어느 파티션에 쓰이는지 신경 쓰지 않고 토픽의 모든 파티션에 고르게 분산시킨다. 경우에 따라서는 특정 파티션으로 보내기도 하는데, 보통 메시지 키와 파티셔너를 써서 키의 해시를 특정 파티션에 매핑한다. 이렇게 하면 같은 키로 생성된 메시지는 모두 같은 파티션에 기록된다. 비즈니스 규칙을 따르는 커스텀 파티셔너를 쓸 수도 있다.
컨슈머는 메시지를 읽는다. 다른 시스템에서는 subscriber나 reader라고 부른다. 컨슈머는 하나 이상의 토픽을 구독하고 메시지를 생성된 순서대로 읽는다. 어디까지 읽었는지는 오프셋(offset)으로 추적한다. 오프셋은 Kafka가 메시지를 생성할 때 붙이는 계속 증가하는 정수 메타데이터이며, 주어진 파티션 안에서 각 메시지는 고유한 오프셋을 갖는다. 파티션별로 마지막으로 소비한 메시지의 오프셋을 Zookeeper나 Kafka 자체에 저장해 두면, 컨슈머가 멈췄다 다시 시작해도 자기 위치를 잃지 않는다.
컨슈머는 컨슈머 그룹(consumer group)의 일원으로 동작한다. 컨슈머 그룹은 하나의 토픽을 함께 소비하는 하나 이상의 컨슈머다. 그룹은 각 파티션이 오직 한 멤버에게만 소비되도록 보장한다. 예를 들어 컨슈머 셋이 한 그룹으로 네 파티션 토픽을 소비하면, 둘은 파티션 하나씩, 나머지 하나는 파티션 둘을 담당하는 식이다. 컨슈머와 파티션의 매핑을 흔히 그 컨슈머의 파티션 '소유권(ownership)'이라 부른다. 이런 방식으로 컨슈머는 수평 확장해 메시지가 많은 토픽을 소비할 수 있고, 컨슈머 하나가 실패하면 남은 멤버들이 리밸런싱을 통해 빠진 멤버의 파티션을 넘겨받는다.
핵심 포인트
- 고급 API(Connect, Streams)도 내부적으로는 프로듀서·컨슈머 위에 얹혀 있다
- 프로듀서는 기본적으로 파티션에 고르게 분산하고, 키+파티셔너로 특정 파티션에 고정할 수 있다
- 오프셋은 파티션 내에서 고유하며 계속 증가하는 정수. 저장해 두면 재시작 후에도 위치를 잃지 않는다
- 컨슈머 그룹은 각 파티션이 그룹 내 한 멤버에게만 소비되도록 보장하고, 멤버 실패 시 리밸런싱한다
브로커, 클러스터, 그리고 리텐션
단일 Kafka 서버를 브로커(broker)라고 부른다. 브로커는 프로듀서로부터 메시지를 받아 오프셋을 할당하고 디스크 저장소에 커밋한다. 또 컨슈머에게 서비스를 제공해 파티션에 대한 fetch 요청에 응답하고, 디스크에 커밋된 메시지를 돌려준다. 하드웨어와 그 성능 특성에 따라 다르지만, 브로커 하나가 수천 개의 파티션과 초당 수백만 건의 메시지를 어렵지 않게 다룰 수 있다.
브로커는 클러스터의 일부로 동작하도록 설계되었다. 클러스터 안에서 브로커 하나가 클러스터 컨트롤러(cluster controller) 역할도 맡는데, 살아 있는 멤버들 중에서 자동으로 선출된다. 컨트롤러는 파티션을 브로커에 할당하고 브로커 장애를 감시하는 관리 작업을 담당한다. 파티션은 클러스터 내 단일 브로커가 소유하며 그 브로커를 해당 파티션의 리더(leader)라고 부른다. 파티션은 여러 브로커에 할당될 수 있고 그 결과 복제(replication)가 이뤄져 메시지에 중복성이 생기므로, 브로커 장애 시 다른 브로커가 리더십을 넘겨받을 수 있다. 다만 해당 파티션을 다루는 모든 컨슈머와 프로듀서는 리더에 연결해야 한다.
Kafka의 핵심 기능 중 하나는 리텐션(retention), 즉 메시지를 일정 기간 durable하게 저장하는 것이다. 브로커에는 토픽의 기본 리텐션이 설정되어 있는데, 일정 기간(예: 7일) 동안 보관하거나 토픽이 일정 바이트 크기(예: 1GB)에 도달할 때까지 보관하는 식이다. 이 한계에 도달하면 메시지는 만료되어 삭제되므로, 리텐션 설정은 언제든 사용 가능한 데이터의 '최소량'을 의미한다. 개별 토픽마다 자체 리텐션을 설정할 수도 있다 — 예를 들어 트래킹 토픽은 며칠, 애플리케이션 메트릭은 몇 시간만 보관하는 식이다. 토픽을 로그 컴팩션(log compacted)으로 설정할 수도 있는데, 이 경우 Kafka는 특정 키로 생성된 '마지막' 메시지만 남긴다. 마지막 업데이트만 의미가 있는 changelog 형태의 데이터에 유용하다.
핵심 포인트
- 브로커는 오프셋을 할당하고 디스크에 커밋하며 fetch 요청에 응답한다
- 클러스터 컨트롤러는 살아 있는 멤버 중 자동 선출되어 파티션 할당과 브로커 장애 감시를 담당
- 파티션 리더에 프로듀서와 컨슈머가 연결하며, 복제본은 리더 장애 시 승계를 위한 것
- 리텐션은 기간 또는 크기 기준이며 토픽별 재정의 가능. 로그 컴팩션은 키별 '마지막' 메시지만 보관
여러 클러스터와 MirrorMaker
Kafka 배포가 커지면 클러스터를 여러 개 두는 것이 유리해진다. 이유는 크게 세 가지다 — 데이터 유형의 분리(segregation), 보안 요구사항에 따른 격리, 그리고 다중 데이터센터(재해 복구).
특히 여러 데이터센터를 다룰 때는 메시지를 데이터센터 간에 복사해야 하는 경우가 많다. 예를 들어 사용자가 프로필의 공개 정보를 변경하면 어느 데이터센터에서 검색 결과를 보든 그 변경이 보여야 한다. 또는 여러 사이트의 모니터링 데이터를 분석·알림 시스템이 있는 중앙 한 곳으로 모아야 할 수도 있다.
여기서 중요한 제약이 있다. Kafka 클러스터 내부의 복제 메커니즘은 '단일 클러스터 안에서만' 동작하도록 설계되어 있으며, 클러스터 사이에서는 동작하지 않는다. 이를 위해 Kafka 프로젝트는 MirrorMaker라는 도구를 포함한다. 본질적으로 MirrorMaker는 Kafka 컨슈머와 프로듀서를 큐로 연결한 것에 불과하다. 한 Kafka 클러스터에서 메시지를 소비해 다른 클러스터로 생성한다. 예를 들어 두 로컬 클러스터의 메시지를 하나의 집계(aggregate) 클러스터로 모으고, 그 클러스터를 다시 다른 데이터센터로 복사하는 아키텍처를 구성할 수 있다. 애플리케이션 자체는 단순하지만 정교한 데이터 파이프라인을 만드는 데 강력하다.
핵심 포인트
- 다중 클러스터의 이유: 데이터 유형 분리, 보안 격리, 다중 데이터센터(DR)
- Kafka의 복제는 단일 클러스터 내부 전용 — 클러스터 간에는 동작하지 않는다
- MirrorMaker는 컨슈머와 프로듀서를 큐로 이은 도구로 클러스터 간 복사를 수행
이 모듈과 연관된 문항 4개가 문제 은행에 있습니다.
이 내용으로 문제 풀어보기