이 글은 카프카 핵심 가이드 책을 읽고 정리한 글입니다.
이 글에서 다루는 모든 코드는 깃허브에서 확인하실 수 있습니다.
참고
: 본 글에서 소개되는 코드 예시는 현재 시점의 구현을 바탕으로 작성되었으며, 프로젝트가 발전함에 따라 내용이 변경되거나 개선될 수 있음을 미리 알려드립니다.
Prologue
“카프카 핵심 가이드” 2장 내용을 바탕으로 카프카의 기본 구성 요소와 설치, 그리고 운영에 필수적인 설정 값들 정리해보았다.
특히 쿠폰 시스템 개발 환경에서 docker-compose.yml
을 사용하는 상황을 고려하여 실제 설정과 연결해 이해를 돕고자 한다.
자세한 내용은 책의 2장을 참고하자.
카프카 브로커와 주키퍼 (개념 및 설치)
카프카를 운영하려면 브로커와 주키퍼, 두 가지 핵심 개념을 이해해야 한다.
주키퍼 (Zookeeper)
주키퍼
는 카프카 클러스터의 메타데이터를 관리하는 중앙화된 서비스다.
-
역할: 클러스터의 설정 정보, 컨트롤러 정보, 컨슈머 클라이언트 정보 등을 저장하고 동기화한다.
-
주키퍼는 고가용성을 보장하기 위해
앙상블
이라 불리는 클러스터 단위로 작동하도록 설계되었다. (앙상블
이란 여러 대의 주키퍼 서버들이 클러스터 형태로 구성되어 고가용성을 확보한 시스템을 말한다.)-
앙상블은 과반수 이상의 노드가 정상 작동하면 서비스를 유지할 수 있어 홀수 개(3개, 5개 등)의 서버로 구성한다.
-
운영 환경에서는 2대의 노드 장애를 허용하는 5개 노드 구성을 고려하는 것을 권장한다.
-
카프카 브로커 (Kafka broker)
카프카 브로커
는 카프카 클러스터를 구성하는 개별 서버이다.
- 역할: 프로듀서로부터 메시지를 받아 토픽 내 파티션에 저장하고, 컨슈머의 요청에 따라 메시지를 전달한다.
예제: Docker Compose로 카프카 실행하기
이 글의 예제에서 사용하는 docker-compose.yml 파일 중 카프카와 관련된 부분은 아래와 같다.
이 설정은 하나의 주키퍼와 하나의 카프카 브로커를 실행하는 가장 기본적인 구성이다.
# docker-compose.yml
services:
# ... (다른 서비스 생략)
zookeeper:
image: wurstmeister/zookeeper
container_name: zookeeper
ports:
- "2181:2181"
kafka:
image: wurstmeister/kafka:2.12-2.5.0
container_name: kafka
ports:
- "9092:9092"
environment:
KAFKA_BROKER_ID: 1
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_DELETE_TOPIC_ENABLE: "true"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
depends_on:
- zookeeper
이제 위 environment
에 설정된 값들이 어떤 의미인지, 다음 섹션에서 하나씩 자세히 알아보자.
핵심 브로커 매개변수 설정
카프카 브로커를 설정할 때, 클러스터 환경에서 반드시 검토하고 수정해야 하는 핵심 매개변수들이다.
broker.id
- 클러스터 내에서 각 브로커를 식별하는 고유한 정수 ID다.
- 모든 브로커는 서로 다른 ID를 가져야 한다.
listeners
- 프로듀서와 컨슈머가 브로커에 접속하기 위한 네트워크 주소와 포트를 정의한다.
{프로토콜}://{호스트이름}:{포트}
형식으로 설정한다. (e.g.PLAINTEXT://localhost:9092
)
zookeeper.connect
- 브로커가 메타데이터를 저장하고 동기화할 주키퍼 앙상블의 주소를 지정한다.
- 필자의
docker-compose.yml
에서는KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
로 설정되어,zookeeper
라는 서비스 이름으로 주키퍼를 찾는다.
log.dirs
- 메시지 로그 세그먼트가 저장될 디스크 디렉토리 경로다.
- 쉼표로 여러 경로를 지정하면, 카프카는 파티션을 분산 저장하여 I/O 부하를 분산시킨다.
auto.create.topics.enable
- 존재하지 않는 토픽에 접근 시 자동으로 토픽을 생성할지 여부를 결정한다.
- 운영 환경에서는
false
로 설정 하여 의도치 않은 토픽 생성을 방지하고 명시적으로 관리하는 것이 좋다.
delete.topic.enable
- 토픽 삭제 기능을 활성화한다.
- 필자의
docker-compose.yml
에서는KAFKA_DELETE_TOPIC_ENABLE: "true"
로 설정되어 있어 토픽 삭제가 가능하다. - 데이터 보존 정책에 따라 신중하게 설정해야 한다.
토픽의 파티션 수 결정 방법
파티션은 카프카의 병렬 처리와 확장성의 핵심이다. 파티션 수를 정하는 것은 전체 시스템 성능에 큰 영향을 미치므로 신중하게 접근해야 한다.
-
num.partitions
매개변수-
먼저 브로커 설정에 있는
num.partitions
매개변수를 이해해야 한다. -
이 매개변수는
auto.create.topics.enable
이true
일 때, 토픽이 자동으로 생성될 경우의 기본 파티션 수를 결정한다. 기본값은1
이다. -
(중요) 카프카 토픽의 파티션 수는 한번 정해지면 늘릴 수는 있지만, 절대로 줄일 수는 없다. 따라서 초기에 적절한 파티션 수를 산정하는 것이 매우 중요하다.
-
명확한 기준이 없을 때, 많은 사용자는 부하를 고르게 분산시키기 위해 토픽의 파티션 수를 클러스터의 브로커 수와 맞추거나 그 배수로 설정한다. 예를 들어 브로커가 10대라면 파티션을 10개로 생성하여 각 브로커가 파티션 리더를 하나씩 맡게 함으로써 처리량을 최적화할 수 있다.
-
실제로 필자의 docker-compose.yml 파일에는 파티션 수를 설정하는 부분이 없으므로, 이 환경에서 토픽이 자동으로 생성된다면 책의 설명대로 기본값인 파티션
1개
로 생성된다.
-
(중요) 파티션 수는 어떻게 결정해야 하는가?
책에서는 “파티션은 많아야 하지만, 너무 많아서는 안 된다” 는 핵심 원칙을 제시한다.
이는 확장성과 시스템 자원 사용량 사이의 균형을 찾아야 함을 의미한다.
왜 컨슈머 처리량이 핵심인가?
파티션 수를 결정하는 계산법을 이해하기 전에, 카프카의 아키텍처 특성을 알아야 한다.
일반적으로 메시지를 쓰는(Produce) 속도는 읽어서 처리하는(Consume) 속도보다 훨씬 빠르다. 데이터를 쓰는 작업은 비교적 단순하지만, 컨슈머는 메시지를 읽어 데이터베이스에 저장하거나 외부 API를 호출하는 등 복잡한 비즈니스 로직을 수행하기 때문이다.
이 때문에 시스템 전체 처리량의 병목은 프로듀서가 아닌 컨슈머에서 발생하는 경우가 대부분이다.
하나의 컨슈머 그룹 내에서, 하나의 파티션은 오직 하나의 컨슈머에 의해서만 처리될 수 있다. 즉, 특정 파티션에 대한 처리 속도는 해당 파티션에 할당된 단일 컨슈머의 성능에 의해 제한된다.
이것이 바로 컨슈머의 처리량 이 전체 시스템의 성능을 결정하는 핵심 요소가 되는 이유다.
따라서 토픽의 전체 목표 처리량을 달성하려면, 여러 컨슈머가 동시에 메시지를 처리해야 한다. 이를 위해서는 컨슈머들이 각자 다른 파티션에 연결되어 병렬로 작업할 수 있는 환경, 즉 충분한 수의 파티션이 필요하게 된다.
처리량 기반 계산법
위 배경을 바탕으로 책에서는 다음과 같은 실용적인 계산법을 제시한다.
계산법: 필요 파티션 수 = 토픽의 목표 처리량 (MB/s) / 컨슈머 하나의 예상 처리량 (MB/s)
-
예시: 토픽에 대해 초당 1GB(1000MB)의 처리량을 목표로 하고, 컨슈머 애플리케이션 하나가 초당 50MB의 데이터를 처리할 수 있다고 가정하자.
-
계산: 1000MB/s / 50MB/s = 20
-
목표 처리량을 달성하기 위해서는 최소 20개의 파티션이 필요하다. 이렇게 하면 20개의 컨슈머가 각 파티션에 하나씩 붙어 병렬로 작업함으로써 초당 1GB의 데이터를 소비할 수 있다.
계산법 외에도 다음과 같은 요소들을 종합적으로 고려해야 한다.
-
미래 사용량 예측: 현재가 아닌 미래의 예측 사용량을 기준으로 처리량을 계산해야 한다. 특히 메시지 키를 사용해 파티션을 결정하는 경우, 나중에 파티션을 추가하면 키-파티션 매핑이 변경되어 복잡해질 수 있다.
-
브로커 자원: 각 브로커에 할당될 파티션 수와 그에 따른 디스크 공간, 네트워크 대역폭을 고려해야 한다.
-
오버헤드: 파티션은 브로커의 메모리와 CPU 자원을 사용한다. 또한 파티션 수가 너무 많으면 장애 발생 시 리더 선출에 걸리는 시간이 길어지는 등 관리 오버헤드가 증가한다.
-
만약 상세한 처리량 추정이 어렵다면, 경험적으로 파티션의 일일 데이터 증가량이 6GB 미만으로 유지하는 것이 좋다. 일단 작은 크기로 시작해서 나중에 필요할 때 확장하는 것이 처음부터 크게 시작하는 것보다 쉽다.
카프카 클러스터 설정 및 브로커 개수 결정
단일 브로커는 개발용으로 적합하지만, 실제 서비스에서는 여러 브로커를 묶어 클러스터로 구성해야 부하 분산과 데이터 안정성을 확보할 수 있다.
아래 그림2-2와 같이 여러 대의 브로커를 하나의 클러스터로 구성하면 부하를 다수의 서버로 확장하는 이점이 있다.
또한, 복제를 사용함으로써 단일 시스템 장애에서 발생할 수 있는 데이터 유실을 방지할 수 있다.
(책에 따르면, 여기서는 기본적인 카프카 클러스터를 설정하는 단계에 초점을 맞추고, 데이터의 복제와 지속성은 7장에서 다룬다고 나와있다.)
브로커 개수를 결정하는 기준
카프카 클러스터의 적절한 크기는 다음 요소들을 종합적으로 고려해 결정한다.
-
디스크 용량
-
클러스터에 저장해야 할 총 데이터량과
복제 팩터
(Replication Factor)를 고려해야 한다. (복제 팩터
란 하나의 파티션 데이터를 몇 개의 다른 브로커에 복제하여 저장할지를 나타내는 값으로, 데이터의 안정성과 가용성을 높인다) -
최소 브로커 수 =
(총 데이터 저장량 * 복제 팩터)
/브로커당 사용 가능 디스크 용량
-
예시: 10TB의 데이터를 저장해야 하고 복제 팩터가 3이라면 총 30TB의 공간이 필요하다. 브로커 하나가 5TB를 저장할 수 있다면, 최소
6대
의 브로커가 필요하다.
-
-
CPU 및 네트워크 용량
-
피크 타임의 트래픽을 감당할 수 있는지 확인해야 한다.
-
단일 브로커의 네트워크 사용량이 80%에 육박한다면, 컨슈머 증가나 데이터 복제 트래픽을 감당하기 위해 브로커를 추가해야 한다.
-
-
파티션 및 레플리카 수
-
브로커가 관리하는 파티션(레플리카 포함) 수가 너무 많아지면 성능이 저하된다.
-
권장 사항: 브로커당 파티션 레플리카 개수를 14,000개 이하, 클러스터당 100만 개 이하로 유지하는 것을 권장한다.
-
브로커 설정
다수의 카프카 브로커가 하나의 클러스터를 이루려면 두 가지를 설정해야 한다.
-
모든 브로커가 동일한
zookeeper.connect
설정에 동일한 주키퍼 앙상블 주소를 지정한다. -
모든 브로커의
broker.id
가 서로 다른 고유한 값을 갖도록 설정한다.