-
카프카를 탄생시킨 핵심 설계 원칙: 2011 NetDB 논문과 초고처리량 아키텍처
ARCHIVE 2026. 10. 10. 09:02반응형참고: 2011년 아테네에서 열린 ACM SIGOPS NetDB 워크숍에서 제이 크렙스(Jay Kreps), 네하 나르케데(Neha Narkhede), 준 라오(Jun Rao)가 발표한 카프카 창립 논문 "Kafka: a Distributed Messaging System for Log Processing"과 [[코딩하는기술사]]의 아키텍처 분석 강의를 바탕으로 정리한 기술 분석입니다.
📌 핵심 요약
2011년 공개된 아파치 카프카(Apache Kafka) 창립 논문은 전통적인 엔터프라이즈 메시지 큐의 무거운 보장(2PC 분산 트랜잭션, 메시지별 영속성, B-Tree 인덱스, 임의 삭제)을 과감히 버리고, 추가 전용 로그(Append-Only Log), 오프셋 기반 주소화, 리눅스 sendfile 제로카피(Zero-Copy) 및 OS 페이지 캐시 위임, 무상태 브로커(Stateless Broker)와 시간 기반 보존, 컨슈머 풀(Pull) 모델이라는 의도적 트레이드오프를 통해 초당 수십만 건의 대규모 이벤트를 처리하는 분산 스트리밍 아키텍처를 제시했습니다.
1. 카프카의 탄생 배경: 2011년 링크드인의 병목
🔍 클릭하여 확대flowchart TD subgraph DataVolume["폭발하는 대용량 로그"] A1["사용자 활동 로그 (클릭, PV, 검색, 반응)"] A2["시스템 운영 메트릭 (CPU, 메모리, 레이턴시, 에러)"] end subgraph Shift["데이터 활용 패러다임의 전환"] B1["과거: 오프라인 배치 분석 (수 시간~하루 단위 지연 허용)"] B2["현재: 실시간 프로덕션 기능 (초 단위 지연 제약)\n- 추천, 광고 타겟팅, 보안/스팸 탐지, 뉴스 피드"] end DataVolume --> Shift Shift --> Bottleneck["기존 엔터프라이즈 MQ 및 로그 수집기 붕괴\n-> 새로운 고처리량 분산 메시징 시스템 필요"]1.1 두 가지 로그의 폭발적 증가
2011년 당시 링크드인(LinkedIn)은 서비스 성장과 함께 두 종류의 데이터 폭증에 직면했습니다.
- 사용자 활동 로그 (User Activity Logs): 클릭, 페이지뷰, 검색 쿼리, 연결 요청 등 사용자 행동 이벤트
- 운영 메트릭 (Operational Metrics): 서버 CPU, 메모리 사용률, 네트워크 I/O, 서비스 지연시간, 에러 스택 등 분산 시스템 가동성 지표
1.2 비즈니스 데이터보다 수십 배 큰 볼륨과 실시간 제약
- 로그 볼륨의 역설: 로그 데이터는 일반 RDB에 적재되는 트랜잭션 데이터보다 수십 배 이상 큽니다. 클릭률(CTR)을 분석하려면 클릭된 이벤트뿐 아니라 화면에 노출되었으나 클릭되지 않은 수백만 건의 미노출 이벤트까지 기록해야 분모와 분자를 계산할 수 있습니다.
- 실시간 프로덕션 기능으로의 전환: 과거 로그는 하둡(Hadoop/HDFS)에 야간 배치로 모아 분석해도 충분했습니다. 반면 추천 알고리즘, 실시간 타겟 광고, 스팸 탐지, 뉴스 피드 랭킹 등 프로덕션 핵심 기능이 실시간 데이터에 의존하면서 "수 초(Few seconds) 이내의 지연시간으로 데이터를 수집·전달하는 실시간 스트리밍 인프라"가 필수가 되었습니다.
2. 기존 메시지 큐와 로그 집계기의 구조적 한계
비교 항목 엔터프라이즈 메시지 큐 (IBM MQ, ActiveMQ, RabbitMQ) 기존 로그 집계기 (Scribe, Flume) 설계 목적 신뢰성 높은 비즈니스 금융·트랜잭션 메시지 교환 오프라인 하둡(HDFS) 벌크 데이터 적재 보장 수준 2PC 분산 트랜잭션, 메시지별 영속성 및 개별 ACK, 복잡한 라우팅 일시적 버퍼링 후 대량 덤프 스토리지/인덱스 B-Tree 기반 메시지 ID 인덱스, 수시 디스크 임의 I/O 파일 기반 단순 버퍼 전달 모델 푸시(Push) 모델: 브로커가 컨슈머에게 직접 전송 푸시 모델 기반 파이프라인 치명적 병목 로그 데이터는 일부 유실을 감수할 수 있는데 과잉 보장으로 초당 처리량(Throughput) 급락, 느린 컨슈머 발생 시 브로커 메모리 고갈 수 시간 지연을 전제한 오프라인 배치 구조로 수 초 이내 실시간 처리 불가
3. 카프카 전체 아키텍처와 구성 요소
🔍 클릭하여 확대flowchart LR P1["Producer 1"] -->|"배치 발행 (Push)"| B1 P2["Producer 2"] -->|"배치 발행 (Push)"| B2 subgraph BrokerCluster["Kafka Broker Cluster (Stateless)"] subgraph B1["Broker 1"] T1P0["Topic A - Partition 0\n(Append-Only Segments)"] T1P1["Topic A - Partition 1\n(Append-Only Segments)"] end subgraph B2["Broker 2"] T1P2["Topic A - Partition 2\n(Append-Only Segments)"] T1P3["Topic A - Partition 3\n(Append-Only Segments)"] end end subgraph ConsumerGroupA["Consumer Group A (실시간 서비스)"] C1["Consumer A1\n(Reads P0, P1)"] C2["Consumer A2\n(Reads P2, P3)"] end subgraph ConsumerGroupB["Consumer Group B (하둡 배치 적재)"] CB1["Consumer B1\n(Reads P0, P1, P2, P3)"] end T1P0 -.->|"자율 폴링 (Pull)"| C1 T1P1 -.->|"자율 폴링 (Pull)"| C1 T1P2 -.->|"자율 폴링 (Pull)"| C2 T1P3 -.->|"자율 폴링 (Pull)"| C2 T1P0 -.->|"독립 폴링 (Pull)"| CB1 T1P1 -.->|"독립 폴링 (Pull)"| CB1 T1P2 -.->|"독립 폴링 (Pull)"| CB1 T1P3 -.->|"독립 폴링 (Pull)"| CB1 ZK["ZooKeeper (분산 조율자)\n- 클러스터 메타데이터\n- Ephemeral 노드로 장애 감지\n- 리밸런싱 트리거"] -.-> BrokerCluster ZK -.-> ConsumerGroupA- 토픽(Topic): 메시지를 논리적으로 분류하는 단위 (
user-clicks,page-views,service-metrics등) - 파티션(Partition): 토픽을 수평 분할한 물리적 분산 및 병렬 처리의 최소 단위
- 브로커(Broker): 메시지를 저장하는 서버 클러스터. 중앙 마스터가 없는 대등(Peer) 노드 구조
- 프로듀서(Producer): 메시지를 발행하는 주체. 파티션 키 해싱 또는 라운드로빈 방식으로 특정 브로커 파티션에 데이터를 배치 전송
- 컨슈머(Consumer) 및 컨슈머 그룹(Consumer Group): 브로커로부터 데이터를 자율적으로 당겨오는(Pull) 주체. 그룹 내 파티션은 1:1 배타 할당되며, 서로 다른 컨슈머 그룹은 동일 토픽을 독립 속도로 완전 소비
- 주키퍼(ZooKeeper): 클러스터 메타데이터 관리, 브로커/컨슈머 가동 상태 감시(Ephemeral Node), 리밸런싱 트리거 조율 (현대 3.x~4.x 버전에서는 KRaft로 대체)
4. 초고처리량을 견인한 5대 핵심 설계 원칙
4.1 추가 전용 로그 (Append-Only Log)와 순차 I/O
각 파티션은 약 1GB 단위의
세그먼트(Segment)파일들로 분할 저장됩니다. 프로듀서가 발행한 메시지는 활성(Active) 세그먼트 파일의 맨 끝에 추가(Append)만 수행됩니다. 수정(Update), 삭제(Delete), 중간 삽입(In-place Insert)은 배제됩니다.HDD와 SSD 모두 순차 쓰기(Sequential Write)는 임의 쓰기(Random Write)보다 수백~수천 배 빠릅니다. 복잡한 트리 리밸런싱 없이 디스크 헤드의 물리적 이동을 최소화하여 디스크 I/O 성능을 하드웨어 한계치까지 활용합니다.
4.2 메시지 ID 폐지와 오프셋(Offset) 기반 순차 주소화
개별 메시지에 UUID를 부여하면 ID와 실제 디스크 위치를 매핑하기 위해 무거운 B-Tree 인덱스가 필요하며 디스크 탐색 비용(Seek Overhead)이 발생합니다.
카프카는 메시지 ID를 제거하고, 파티션 내에서 0부터 단조 증가하는 64비트 정수 오프셋(Offset)만으로 메시지를 식별합니다. 각 세그먼트 파일의 시작 오프셋만을 메모리 배열로 가볍게 유지하며, 컨슈머가 특정 오프셋을 요청하면 배열에서 이진 탐색(Binary Search)으로 세그먼트 파일을 특정하고 그 위치부터 바이트 단위 순차 읽기만 수행합니다. 리눅스 커널의 선행 읽기 캐싱(Read-ahead / Prefetching)이 활성화되어 후속 데이터가 메모리에 미리 로드됩니다.
4.3 제로카피(Zero-Copy)와 OS 페이지 캐시 위임
🔍 클릭하여 확대flowchart TD subgraph Traditional["전통적인 전송 방식 (4번 복사, 2번 컨텍스트 스위치)"] Disk1["디스크"] -->|"1. DMA 복사"| PC1["OS 페이지 캐시"] PC1 -->|"2. CPU 복사 (SysCall Read)"| App1["JVM 애플리케이션 버퍼"] App1 -->|"3. CPU 복사 (SysCall Write)"| KB1["커널 소켓 버퍼"] KB1 -->|"4. DMA 복사"| NIC1["네트워크 인터페이스(NIC)"] end subgraph ZeroCopy["카프카 제로카피 (sendfile 시스템 콜: 2번 복사, 0번 CPU 복사)"] Disk2["디스크"] -->|"1. DMA 복사"| PC2["OS 페이지 캐시"] PC2 -->|"2. DMA 소켓 다이렉트 전송 (CPU 복사 없음)"| NIC2["네트워크 인터페이스(NIC)"] end- JVM 힙 메모리 캐싱 배제: 카프카 브로커는 자체 애플리케이션 레벨에서 메시지를 캐싱하지 않습니다. JVM 힙에 객체를 캐싱하면 메모리 사용량이 늘어나고 대용량 트래픽에서 가비지 컬렉션(GC) 일시정지(STW)가 발생하기 때문입니다.
- OS 페이지 캐시 위임: 브로커 프로세스가 재시작되어도 OS 페이지 캐시는 커널 레벨에서 유지됩니다(Warm Cache 보장).
- 리눅스
sendfileAPI 활용: 디스크에서 읽은 페이지 캐시 데이터를 애플리케이션 유저 영역으로 복사하지 않고, 커널 공간 내에서 곧바로 네트워크 소켓 버퍼로 파이프라인 전송합니다. 복사 횟수가 4회에서 2회로, 시스템 콜이 2회에서 1회로 줄어들어 CPU 점유율이 낮아집니다.
4.4 무상태 브로커(Stateless Broker)와 시간 기반 보존
전통적인 MQ는 브로커가 컨슈머별 읽기 상태(ACK, 소비 완료 여부)를 추적하느라 디스크 I/O와 락 경합이 급증했습니다. 카프카 브로커는 컨슈머가 어디까지 읽었는지 일체 기억하지 않는 완전한 무상태(Stateless) 구조를 갖습니다.
- 소비 위치의 책임 이전: 컨슈머 자신이 다음에 읽을 오프셋을 직접 추적하고 관리합니다.
- 시간 기반 데이터 보존 (Time-based Retention): 소비 여부와 관계없이 설정된 기간(예: 7일)이 지나면 세그먼트 파일 단위로 일괄 삭제합니다.
- 리와인드(Rewind / Replay) 지원: 브로커가 메시지를 임의 삭제하지 않으므로 컨슈머는 과거 오프셋으로 커서를 되돌려 데이터를 재소비할 수 있습니다.
1. 컨슈머 애플리케이션 버그 패치 후 과거 데이터 재처리
2. 데이터 웨어하우스(DW) ETL 파이프라인 장애 복구 시 특정 시점부터 재적재
3. 컨슈머 프로세스 비정상 종료 후 마지막 커밋 오프셋부터 무손실 복구
4.5 푸시가 아닌 풀(Pull) 모델과 슬로우 컨슈머 격리
브로커가 데이터를 일방적으로 밀어넣는 푸시 모델에서는 처리가 느린 컨슈머(Slow Consumer)가 있을 때 버퍼 오버플로우나 메시지 유실이 발생하거나 브로커 전체 지연으로 번집니다.
카프카의 풀 모델에서는 각 컨슈머가 자신의 처리 성능과 배치 크기에 맞춰 브로커에 데이터를 능동 요청(Poll)합니다. 동일한 토픽 데이터를 실시간 밀리초 지연의 추천 엔진과 1시간 단위 대량 배치로 처리하는 분석 파이프라인이 브로커 간섭 없이 독립적인 속도로 병렬 소비할 수 있습니다.
5. 분산 조율과 병렬 처리: 파티션과 컨슈머 그룹
🔍 클릭하여 확대flowchart TD subgraph TopicPartitions["Topic A (4 Partitions)"] P0["Partition 0"] P1["Partition 1"] P2["Partition 2"] P3["Partition 3"] end subgraph Case1["컨슈머 2대 운영: 각 컨슈머 2개 파티션 담당"] CA1["Consumer 1"] --> P0 CA1 --> P1 CA2["Consumer 2"] --> P2 CA2 --> P3 end subgraph Case2["컨슈머 4대로 스케일아웃: 각 컨슈머 1개 파티션 담당 (병렬성 극대화)"] CB1["Consumer 1"] --> P0 CB2["Consumer 2"] --> P1 CB3["Consumer 3"] --> P2 CB4["Consumer 4"] --> P3 end subgraph Case3["컨슈머 5대 투입: 1대는 유휴(Idle) 상태 발생"] CC5["Consumer 5 (유휴 노드: 파티션 할당 불가)"] end5.1 파티션 배타 할당과 분산 락 제거
동일 컨슈머 그룹 내에서 하나의 파티션은 오직 하나의 컨슈머에게만 배타적으로 할당됩니다. 하나의 파티션을 여러 컨슈머가 동시에 읽으면 메시지 순서 보장과 중복 방지를 위해 복잡한 분산 락(Lock)이 필요합니다. 1:1 매핑을 강제함으로써 분산 락을 완전히 없애고 처리량을 선형적으로 확장했습니다.
시스템의 최대 동시 병렬성(Max Parallelism)은 토픽의 파티션 개수에 의해 결정됩니다.
5.2 오버 파티셔닝(Over-Partitioning) 전략
트래픽 증가와 컨슈머 노드 증설을 대비하여, 초기 컨슈머 수보다 넉넉한 수의 파티션을 미리 생성해 두는 오버 파티셔닝(Over-partitioning)이 권장됩니다. 파티션이 넉넉하면 운영 중 무중단으로 컨슈머 서버를 투입해 선형 확장을 달성할 수 있습니다.
6. 전달 보장과 정렬 규칙의 트레이드오프
6.1 At-least-once (최소 한 번 전달)
카프카는 2PC(Two-Phase Commit) 트랜잭션 오버헤드를 피하기 위해 기본적으로 최소 한 번 전달(At-least-once) 방식을 채택했습니다. 네트워크 장애나 컨슈머 재시작 시 일부 메시지가 중복 전달될 수 있으나, 통계 로그나 대규모 이벤트 수집 도메인에서는 멱등 처리나 집계 오차 범위 내에서 수용 가능하다고 판단했습니다.
6.2 단일 파티션 내 순서 보장 vs 파티션 간 순서 비보장
- 단일 파티션: 레코드가 인입된 순서대로 완벽히 정렬됩니다.
- 파티션 간 순서: 서로 다른 파티션 간에는 글로벌 순서를 보장하지 않습니다. 순서가 반드시 지켜져야 하는 이벤트(특정 사용자 ID의 연속 트랜잭션 등)는 동일한 파티션 키(Partition Key)를 부여하여 동일 파티션으로 해싱 라우팅함으로써 순서를 강제합니다.
7. 2011년 벤치마크 실측 수치
논문 섹션 5의 벤치마크 결과는 아키텍처 선택이 만들어낸 성능 격차를 수치로 증명합니다.
🔍 클릭하여 확대xychart-beta title "프로듀서 처리량 비교 (50개 배치 전송, 초당 메시지 수)" x-axis ["ActiveMQ", "RabbitMQ", "Apache Kafka"] y-axis "초당 전송 메시지 수" 0 --> 60000 bar [30000, 37000, 52188]🔍 클릭하여 확대xychart-beta title "컨슈머 소비 처리량 비교 (초당 메시지 수)" x-axis ["ActiveMQ", "RabbitMQ", "Apache Kafka"] y-axis "초당 소비 메시지 수" 0 --> 25000 bar [5200, 5000, 22000]- 프로듀서 처리량: 카프카는 50개 배치 전송 시 초당 52,188건을 기록하며 1Gbps 네트워크 대역폭을 거의 100% 포화시켰습니다. (RabbitMQ 약 37,000건, ActiveMQ 약 30,000건).
- 메시지 오버헤드: 카프카의 프로토콜 오버헤드는 메시지당 9바이트였습니다. 반면 ActiveMQ는 JMS 헤더와 B-Tree 인덱스 메타데이터로 메시지당 144바이트를 소모했습니다.
- 컨슈머 처리량과 Zero Disk Write: 카프카 컨슈머는 초당 22,000건을 소비하며 기존 MQ(약 5,000건) 대비 4배 이상의 처리량을 달성했습니다. 컨슈머가 초당 수만 건을 읽어가는 동안 카프카 브로커 서버에서는 디스크 쓰기 I/O가 단 한 번도 발생하지 않았습니다(Zero Disk Write). 무상태 브로커와 OS 페이지 캐시 위임의 실효성이 확인된 지점입니다.
8. 카프카의 진화: 2011년 논문에서 현대 카프카(4.0)까지
2011년 논문 발표 당시 명시되었던 한계점들은 지난 15년간 단계적으로 개선되었습니다.
영역 2011년 논문 (초기 카프카) 현대 카프카 (3.x ~ 4.x) 데이터 복제 (Replication) 단일 브로커 저장 (서버 장애 시 데이터 유실 위험) 리더-팔로워 복제 및 ISR(In-Sync Replicas) 메커니즘을 통한 고가용성·무손실 보장 메타데이터 조율 외부 ZooKeeper 의존 (10만 개 이상 파티션 확장 병목) KRaft (Kafka Raft 메타데이터 모드) 도입으로 ZooKeeper 완전 제거 (Kafka 4.0 정식) 오프셋 저장소 ZooKeeper 영속 노드 내부 압축 토픽 __consumer_offsets에 이벤트 로그로 기록전달 보장 수준 At-least-once (중복 발생 가능) Idempotent Producer 및 Exactly-Once Semantics (EOS, 분산 트랜잭션) 지원 데이터 파이프라인 생태계 단순 프로듀서/컨슈머 API Kafka Connect (DB, S3 무코드 연동) 및 Kafka Streams (실시간 스트림 연산) 스토리지 계층화 로컬 디스크 의존 Tiered Storage: 오래된 세그먼트를 AWS S3/GCS 등 원격 오브젝트 스토리지로 계층화
9. 소프트웨어 아키텍트가 얻을 수 있는 설계 교훈
- 문제를 명확하고 엄밀하게 정의하라
카프카의 성능은 "로그 처리를 위한 초고처리량 분산 메시징"이라는 명확한 문제 정의에서 출발했습니다. 모든 범용 요구사항을 동시에 만족하려 했다면 이 아키텍처는 탄생하기 어려웠습니다. - 트레이드오프를 투명하게 공개하고 선택하라
카프카 팀은 2PC 트랜잭션, B-Tree 인덱스, 즉시 삭제, 개별 ACK를 버렸다고 논문에 명시했습니다. 포기한 항목을 명확히 정의한 대가로 단순성과 처리량을 확보했습니다. - 운영체제(OS)와 기본기를 신뢰하라
JVM 힙에 자체 캐시를 만들어 GC와 경쟁하는 대신, 수십 년간 검증된 리눅스 OS 페이지 캐시와sendfile제로카피 시스템 콜에 위임했습니다. - 단순함이 궁극의 확장성을 만든다
추가 전용(Append-only), 순차 오프셋(Offset), 무상태(Stateless) 구조는 브로커가 상태를 기억하지 않게 만듭니다. 복잡성을 덜어낼수록 분산 시스템의 신뢰성과 수평 확장성은 강화됩니다.
반응형'ARCHIVE' 카테고리의 다른 글
카프카(Kafka)를 메시지 큐로만 쓰면 안 되는 이유: 'The Log'와 분산 커밋 로그 아키텍처 (0) 2026.09.21