카프카(Kafka)는 대규모 데이터를 실시간으로 처리하기 위해 최적화된 분산 스트리밍 플랫폼입니다. 고성능 메시지 브로커를 구축하기 위해서는 통신 레이어(RPC)와 데이터 저장 방식(Persistence)에 대한 깊은 이해가 필요합니다.
1. 효율적인 RPC 통신 아키텍처 설계
카프카의 통신 구조에서 흔히 하는 오해 중 하나는 '생성자(Producer) -> 브로커(Broker) -> 소비자(Consumer)'가 하나의 연속된 RPC 호출로 이뤄진다는 점입니다. 하지만 실제 카프카는 생산 단계와 소비 단계가 완전히 분리된 비동기 구조를 가집니다.
- 생산 단계 (Produce): Producer가 클라이언트가 되어 Broker(서버)에게 메시지를 전송합니다. Broker가 수신을 확인(Ack)하면 생산자의 작업은 완료됩니다.
- 소비 단계 (Consume): Consumer가 클라이언트가 되어 Broker에게 메시지를 요청(Poll)합니다. 이는 생산과 완전히 독립적인 타이밍에 발생합니다.
맞춤형 바이너리 프로토콜 설계
카프카는 범용적인 gRPC나 Thrift 대신, 성능을 극대화하기 위해 직접 정의한 바이너리 프로토콜을 사용합니다. 요청(Request) 패킷의 구조는 다음과 같이 설계할 수 있습니다.
[ Packet Length (4 bytes) ]
[ API Identifier (2 bytes) ]
[ Protocol Version (2 bytes) ]
[ Correlation ID (4 bytes) ]
[ Client Metadata (String) ]
[ Actual Payload (Variable) ]
여기서
Correlation ID는 비동기 RPC 구현의 핵심입니다. 클라이언트는 응답을 기다리지 않고 여러 요청을 보낼 수 있으며, 나중에 도착한 응답 패킷의 ID를 보고 어떤 요청에 대한 결과인지 매칭합니다.
브로커의 고성능 네트워크 모델: Reactor 패턴
수만 개의 커넥션을 처리하기 위해 카프카 브로커는 Java NIO 기반의 Reactor 패턴을 활용합니다.
- Acceptor 스레드: 새로운 연결 요청을 수락하고 소켓 채널을 생성합니다.
- Processor 스레드 풀 (I/O 스레드): 각 스레드는 Selector를 가지며, 할당된 커넥션의 읽기/쓰기 이벤트를 감지합니다. 읽어들인 요청은 공유 큐에 삽입합니다.
- Handler 스레드 풀 (Worker 스레드): 공유 큐에서 요청을 꺼내 비즈니스 로직(로그 저장, 메시지 읽기 등)을 실행하고 결과를 응답 큐에 넣습니다.
이러한 분리 구조를 통해 네트워크 I/O 작업이 디스크 작업이나 복잡한 연산에 의해 블로킹되는 것을 방지합니다.
2. 브로커의 데이터 영속성 구현 전략
메시지 브로커의 성능은 데이터를 어떻게 저장하느냐에 따라 결정됩니다. 카프카는 범용 데이터베이스 대신 파일 시스템을 직접 관리하는 방식을 선택했습니다.
로그 세그먼트(Log Segment) 구조
각 파티션은 물리적으로 하나의 디렉토리에 매핑되며, 데이터는 무한정 커지는 하나의 파일이 아닌 여러 개의 '세그먼트'로 나뉘어 저장됩니다.
- *.log: 실제 메시지 데이터가 순차적으로 추가(Append-only)되는 파일입니다.
- *.index: 특정 오프셋(Offset)이 로그 파일의 몇 번째 바이트에 있는지 기록한 희소 인덱스(Sparse Index) 파일입니다.
순차 I/O와 페이지 캐시(Page Cache)
하드 디스크의 랜덤 읽기/쓰기는 매우 느리지만,
순차 I/O(Sequential I/O)는 메모리 접근에 준하는 속도를 낼 수 있습니다. 카프카는 모든 메시지를 로그 끝에 단순히 추가만 함으로써 이 성능을 활용합니다.
또한, 운영체제의 페이지 캐시를 적극적으로 활용합니다. 브로커가 데이터를 디스크에 직접 쓰는 대신 운영체제 메모리(Page Cache)에 쓰면, OS가 백그라운드에서 효율적으로 디스크에 플러시(Flush)합니다. 이는 읽기 성능에도 도움을 주는데, 방금 쓴 메시지를 소비자가 읽으려 할 때 디스크가 아닌 메모리에서 즉시 반환될 확률이 매우 높기 때문입니다.
제로 카피(Zero-Copy) 기술
데이터를 네트워크로 전송할 때, 일반적인 방식은 '디스크 -> 커널 공간 -> 유저 공간(애플리케이션) -> 커널 공간(소켓) -> NIC'의 복사 과정을 거칩니다. 카프카는
sendfile 시스템 호출을 사용하여 유저 공간으로의 불필요한 데이터 복사를 제거합니다. 커널 공간 내에서 데이터가 직접 네트워크 버퍼로 전달되므로 CPU 사용률이 극적으로 낮아집니다.
3. 데이터베이스 기반 저장 방식과의 비교
만약 관계형 데이터베이스(RDBMS)를 사용하여 브로커를 구현한다면 아래와 같은 스키마를 구성할 수 있을 것입니다.
CREATE TABLE message_store (
pk_id BIGINT AUTO_INCREMENT PRIMARY KEY,
stream_topic VARCHAR(128) NOT NULL,
partition_num INT NOT NULL,
seq_offset BIGINT NOT NULL,
msg_key VARBINARY(512),
msg_body LONGBLOB NOT NULL,
timestamp_ms BIGINT,
UNIQUE KEY uq_topic_part_offset (stream_topic, partition_num, seq_offset)
);
하지만 이러한 데이터베이스 방식은 카프카의 방식과 비교했을 때 뚜렷한 한계가 있습니다.
| 구분 |
파일 시스템 (Kafka) |
관계형 데이터베이스 (RDBMS) |
| 쓰기 성능 |
매우 높음 (순차 추가) |
낮음 (B-Tree 인덱스 갱신 및 잠금 경쟁) |
| 읽기 성능 |
매우 높음 (제로 카피 및 페이지 캐시) |
중간 (인덱스 검색 및 복사 오버헤드) |
| 저장 효율 |
최적화된 바이너리 포맷 |
인덱스 및 메타데이터로 인한 오버헤드 큼 |
카프카는 복잡한 트랜잭션이나 임의 조리 질의를 포기하는 대신,
'로그 구조 저장'이라는 단순하고 강력한 설계를 통해 초당 수백만 건의 이벤트를 처리할 수 있는 처리량을 확보했습니다.