0. 들어가기 전
바로 전에 Kafka의 기본 이론에 대해서 알아봤었습니다.
https://ksh-coding.tistory.com/160
[Kafka] Apache Kafka 알아보기 (기본 이론)
0. 들어가기 전이전에 MSA 프로젝트를 진행할 때, Kafka를 사용해본 적이 있습니다.하지만 그때는 먼저 구현을 했어야 했기에 제대로 된 Kafka의 이론은 모른채 구현만 쫓아갔던 기억이 있습니다.
ksh-coding.tistory.com
이번에는 공식문서에서 언급하는 좀 더 심화적인 내용들을 살펴보도록 하겠습니다.
Kafka 공식문서의 Design 챕터에서는 Kafka의 내부 구조, 원리를 다루고 어떤 장점이 있는지를 소개하고 있습니다.
하나씩 알아보도록 하겠습니다.
1. Persistence
Kafka의 각 이벤트 메시지들은 어느 곳에, 어떻게 저장될까요?
그리고 Kafka에서의 메시지 Read/Write 성능은 어떨까요?
해당 챕터에서는 이와 관련한 내용을 다룹니다.
📚 Dont'fear the filesystem! : Kafka는 filesystem을 사용하지만, 성능이 좋다!
Kafka relies heavily on the filesystem for storing and caching messages. There is a general perception that "disks are slow" which makes people skeptical that a persistent structure can offer competitive performance.
- Kafka는 message를 저장하고 캐싱할 때 filesystem을 사용한다.
- 보통 Disk는 느리지만, Disk 구조를 적절히 설계한다면 빠를 수 있다.
※ 일반적인 Disk 구조 문제점
- 일반적으로 Disk의 선형 Read/Write(순차 I/O) 속도는 빠르지만, 데이터를 탐색할 때의 Random I/O는 상당히 느리다.
- 순차 I/O의 쓰기 성능은 600MB/sec인 반면, Random I/O의 쓰기 성능은 100KB/sec으로 약 6000배나 차이가 난다.
- 이러한 차이를 보완하기 위해서 현대 OS는 메모리 회수 시 메인 메모리의 free 메모리를 Disk 캐싱에 사용한다. (Random I/O 발생을 줄이기 위해)
- Kafka에서 이러한 메인 메모리를 Disk 캐싱에 사용한다면 문제가 발생할 수 있다.
- Kafka는 JVM 기반으로 구축되는데, JVM 위에서 인메모리 캐시를 사용하면 객체 메모리 오버헤드가 많아지고 GC에 데이터가 커져서 더 느려지게 된다.
※ Kafka Disk 캐싱
- 따라서, Kafka에서는 메모리에 Disk를 캐싱하는 것이 아니라 OS 커널의 페이지 캐시에 Disk를 캐싱한다.
- 이로 인해 메인 메모리를 여유롭게 사용하면서 GC의 패널티 없이 사용할 수 있다.
- 또, 인메모리 캐싱은 서비스가 재시작될 때 다시 빌드해야하므로 성능이 좋지 않은데, 페이지 캐시에 Disk 캐싱을 하게되면 OS의 영역이기 때문에 재시작과 관련 없이 캐시를 유지한다.
- 따라서, Kakfa의 모든 데이터는 filesystem에 'persistent log' 형태로 기록된다. (페이지 캐시에 저장)
📚 Constant Time Suffices : Kafka는 Read/Write 시간복잡도가 O(1)이다! (랜덤 I/O 대신 순차 I/O 사용)
- 일반적인 메시징 시스템에서는 B-Tree를 사용하여 메시지별로 메타 데이터를 유지합니다.
- 하지만, B-Tree는 시간복잡도가 O(log N)으로 일반적으로 빠르지만 Disk 작업에서는 상당히 느릴 수 있습니다.
- Disk는 병렬 작업을 수행하지 못하고 초당 약 10ms로 탐색하므로 여러 건의 작업 시 오버헤드가 매우 높아집니다.
- Kafka는 filesystem 기반 Queue로, 모든 연산을 로그 기반의 순차 I/O를 사용하여 O(1)으로 빠르게 디스크 탐색을 합니다.
- 메시지를 파일에 추가하는 방식으로 데이터 기록
- 시간 복잡도가 O(1)이기 때문에 저장된 파일의 개수에 영향을 받지 않고 많이 적재되어 있더라도 성능을 유지
- 따라서, 다른 메시징 시스템과 달리 사용자가 성능을 고려할 필요 없이 메시지를 원하는 기간동안 적재해놓고 재소비 가능
2. Efficiency
이번 챕터에서는 Kafka에서 어떤 식으로 메시지들을 효율적으로 처리하는지 알아보도록 하겠습니다.
📚 Message 그룹화 + 페이지 캐시 Read로 비효율성 제거
- 모든 연산이 순차 I/O임에 따라 다음과 같은 2가지 문제가 발생합니다.
- 작은 크기의 I/O가 빈번하게 발생 : 순차적으로 O(1)로 Write를 하기 때문에 작은 크기의 메시지가 버퍼에 쌓이지 않고 매번 I/O를 발생시켜 비효율적일 수 있다.
- 과도한 바이트 복사 발생 : Kafka 자체 메모리 내부에 버퍼 및 캐시가 존재하는데, 메시지 처리 시마다 메시지를 복사해서 버퍼에 저장하고 캐싱하므로 과도한 바이트 복사가 일어나서 비효율적일 수 있다.
- EX) 작은 크기의 메시지들이 자주 처리되면 Network 비용이 처리 시마다 발생하고 바이트 복사가 매번 일어나서 비효율적
- 작은 크기의 I/O가 빈번하게 발생하는 비효율성을 제거하기 위해, Kafka는 메시지들을 그룹화해서 저장한다.
- Kafka Producer는 로그에 그룹화된 큰 메시지들을 저장하고 Consumer도 그룹화된 큰 메시지들을 소비하게 된다.
- 과도한 바이트 복사를 피하기 위해 '페이지 캐시'에 한 번만 복사하고 이후에는 저장된 메시지들을 재사용한다.
📚 End-to-end Batch Compression : 여러 메시지들을 Batch 압축
- Kafka는 메시지를 그룹화하는 것뿐만 아니라 Batch를 사용해서 메시지들을 압축한다.
- Batch 메시지들은 로그에 압축된 상태로 저장되고, Consumer가 해당 압축 데이터를 해제하여 사용한다.
3. Producer
이번 챕터에는 메시지를 publish하는 Producer의 설계에 대해서 알아보겠습니다.
📚 Leader Broker & Follower Broker
Producer를 보기 전에, Event를 저장하는 Storage 역할을 하는 Broker에 대해서 조금 더 보충이 필요합니다.
Kafka Cluster의 여러 Broker들은 파티션별로 Leader, Follower의 역할을 가집니다.

- Kafka Cluster의 Broker는 기본적으로 모든 Partition의 정보를 알고 있다.
- 하지만, 각 Partition에서 메시지를 Read/Write하는 Broker는 단 1대이고, 이를 Leader Broker라고 한다.
- Partition별로 Leader Broker가 아닌 Broker들은 해당 Partition의 Follower Broker가 된다.
- Follower Broker가 존재하는 이유는 Partition의 Leader Broker에 장애가 발생했을 때 빠르게 Leader로 승격되는 구조를 만들어서 HA를 보장할 수 있기 때문이다.
- 따라서, 모든 Follower Broker들은 각 파티션의 Leader Broker로부터 지속적으로 새로운 메시지를 확인하여 복제한다.
📚 Load Balancing
1. 메시지 Publish 시 Broker Load Balancing : 메시지 Publish 시 어떤 Broker가 메시지를 Read/Write할지 어떻게 결정할까?
- Kafka의 Producer들은 Kafka Cluster의 모든 Kafka Node에게 어떤 Broker가 살아있는지, Partition의 Leader Broker들은 어떤 Broker인지 메타데이터를 요청하여 제공받는다.
- Producer가 메시지를 Publish 할 때, 모든 Broker에게 메시지를 publish 하지 않고 Publish할 Partition의 Leader Broker에게만 메시지를 보낸다.
2. 메시지 Publish 시 Partition Load Balancing : 메시지 Publish 시 어떤 Partition에 메시지가 적재될 지 어떻게 결정할까?
- Kafka는 기본적으로 라운드 로빈 방식을 사용하여 적재할 파티션을 결정하지만, 조건을 통해 적재할 파티션을 지정할 수 있다.
- 적재할 파티션 지정 : Event Key를 설정하여 지정
- Event Key가 설정된 Event는 해당 Key를 해싱하여 적재할 Partition을 정한다.
- 따라서, 동일한 Event Key를 가진 Event는 모두 같은 Partition에 저장된다.
📚 Asynchronous Send
- Kafka는 Batch를 통해 Producer에서 메시지를 비동기로 Publish 할 수 있다.
- 비동기 Send의 의미는 여러 건의 메시지를 1건씩 동기적으로 처리하지 않고 비동기로 처리한 후 Batch 처리한다는 의미이다.
- Producer에서 메시지를 바로 Publish하지 않고 메모리에 쌓아놨다가 하나의 Request에 모두 담아서 보낼 수 있다.
- 메모리에 적재된 메시지의 양 (batch.size) or 최대 대기 시간 (linger.ms)을 설정하여 그 이상이 되면 Request를 보내도록 설정할 수 있다.
- 이를 통해 약간의 지연 시간(버퍼 시간)을 감수하고 처리량을 늘릴 수 있다.
4. Consumer
이번 챕터에는 메시지를 consume하는 Consumer의 설계에 대해서 알아보겠습니다.
📚 Kafka Consumer 동작 (feat. Offset)
- Kafka Consumer는 메시지를 소비할 Partition의 Leader Broker에게 'fetch' 요청을 하는 방식으로 동작한다.
- Consumer는 각 'fetch' 요청마다 로그에 offset을 지정하고 해당 위치부터 끝 위치까지의 로그 Chunk를 다시 받는다.
- Offset이란, 파티션 내에서 데이터가 기록된 순서를 나타내는 고유 번호이다.
- EX) Offset 1~10이 있을 때 fetch 요청 Offset이 4라면 Offset 4~10의 데이터를 받는다.
- 즉, Consumer는 해당 offset에 해당하는 로그를 재소비할 수 있다.
📚 Broker -> Consumer (Push) vs. Broker <- Consumer (Pull)
An initial question we considered is whether consumers should pull data from brokers or brokers should push data to the consumer.
Kafka는 메시지 Consume 시 Broker에서 Conusmer에 Push할지, Consumer가 Broker에서 Pull할지를 고민했다.
- Broker -> Consumer (Push)
- Broker가 Consumer에 Push하는 방식은 Broker가 데이터 전송 속도를 제어하기 때문에 처리량이 다른 다양한 Consumer를 사용하기에 어려움이 있다.
- EX) Push 방식을 사용하는 목적이 메시지가 Publish 될 때 바로 Consumer에서 처리하도록 하기 위함이지만, Consumer가 많을 때 처리량이 다르다면 문제가 된다.
- 또, 처리량이 같더라도 Publish 양이 엄청나게 많아지면 Consumer가 과부하가 걸릴 수 있다.
- Broker <- Consumer (Pull)
- Push 방식의 단점을 Pull 방식을 사용하면 극복할 수 있다.
- Publish 양이 많더라도 Consumer가 처리할 수 있을 때만 Pull 받아서 처리하기 때문에 과부하가 적다.
- 또 다른 장점은 Push 방식보다 효율적으로 Batch로 메시지를 소비할 수 있다.
- Push 방식을 사용하면 Broker에서 메시지를 Batch로 보낼 수는 있지만, Consumer에서 소비할 수 있는지는 알지 못한채 보내게 되기 때문에 의미가 없게 된다.
- Pull 방식에서는 항상 Consumer의 처리 속도에 맞게 메시지를 가져올 수 있으므로 효율적으로 Batch를 사용할 수 있다.
- Pull 방식의 단점은, 반대로 Broker에 메시지가 없는 경우에도 메시지가 생성될 때까지 Polling 한다는 점이다.
- Kafka에서는 이러한 단점을 방지하기 위해 Long Polling을 사용하여 응답이 올 때까지 연결을 끊지 않고 대기하여 효율을 조금 올린다.
- Push 방식의 단점을 Pull 방식을 사용하면 극복할 수 있다.
📚 Consumer Position
Keeping track of what has been consumed is, surprisingly, one of the key performance points of a messaging system.
메시징 시스템에서 핵심 포인트 중 하나는 '소비된 메시지를 추적하는 것'입니다.
※ AS-IS 메시징 시스템 동작
- 대부분의 메시징 시스템은 Broker에서 어떤 메시지가 소비되었는지에 대한 메타데이터를 보관합니다.
- 이때, 보통 메시징 시스템들의 스토리지 구조는 확장성이 좋지 않기 때문에 메시지가 소비되면 보관한 메타데이터를 삭제합니다.
- 이를 통해 데이터 크기를 작게 유지할 수 있기 때문에 확장을 줄일 수 있습니다.
- Broker에서 메시지가 소비되었다는 판단은 2단계에 거쳐서 발생합니다.
- Consumer가 메시지를 전달받았을 때 -> 'sent not consumed'의 의미로 마킹
- Consumer가 메시지 소비 후 Broker에게 메시지 소비 알림 -> 소비한 것으로 기록
이러한 동작은 다음과 같은 문제가 발생할 수 있습니다.
- Consumer가 Broker에 소비 알림을 보내지 못한 경우
- 2가지 선택 존재
- Broker에서는 소비되지 않음으로 간주하고 메시지를 다른 Consumer에게 전달하여 재소비
- Consumer가 이미 처리한 메시지라면 메시지 중복 재소비가 발생
- Broker에서 메시지를 삭제하지 않고 유지
- 미처리 상태의 메시지가 Broker에 지속적으로 쌓여 크기가 커질 수 있음
따라서, 위와 같이 AS-IS의 메시징 시스템으로는 해결할 수 없는 문제가 있습니다.
이를 Kafka에서는 다음과 같은 방식으로 해결합니다.
※ Kafka의 Offset 기반 Consume
- Kafka는 Broker가 아닌 Consumer가 메시지를 소비한 Position(Offset)을 관리합니다.
- Topic 내의 여러 파티션은 정확히 각 1개의 Consumer에 의해 메시지가 소비됩니다.
- 따라서, Partition의 Offset은 할당된 Consumer가 마지막으로 메시지를 소비한 위치입니다.
- 이러한 Partition의 Offset을 메시지 소비 시마다 주기적으로 저장하여 메시지 소비를 추적할 수 있습니다.
- 이는 하나의 정수에 불과하기 때문에 데이터 크기가 작고, 따라서 주기적으로 저장해도 성능에 무리가 없습니다.
- 이를 통해 이전에 소비했던 메시지도 필요한 경우 이전 Offset으로 돌아가서 메시지를 재소비 할 수 있습니다.
5. Message Delivery Semantics
위에서 Producer와 Consumer 동작에 대해서 살펴봤으니, 이제는 메시지 전달 방식에 대해서 살펴보도록 하겠습니다.
* At most once—Messages may be lost but are never redelivered.
* At least once—Messages are never lost but may be redelivered.
* Exactly once—this is what people actually want, each message is delivered once and only once.
- At most once : 메시지가 유실될 수 있지만 절대 다시 전달되지 않는다.
- At least once : 메시지가 절대 유실되지 않지만 다시 전달될 수 있다.
- Exactly once : 메시지가 정확히 1번만 전달된다.
먼저, Kafka의 메시지 Commit에 대해서 알아봅시다.
When publishing a message we have a notion of the message being 'committed' to the log.
- Kafka에서 Message는 파티션 로그에 저장되었을 때 'committed' 되었다고 한다.
- Message가 커밋된 이후에는 활성 상태의 복제본이 최소 1대의 Broker에 존재하는 한 데이터가 유실되지 않는다.
- Kafka의 0.11.0.0 버전 이전/이후로 메시지 전달 보장 방식(semantic)이 추가되었다.
- 이전 : Producer가 메시지 커밋 응답을 받지 못했을 때 메시지를 resend 할 수 밖에 없었다.
- 해당 동작은 실제로 메시지가 전달되었음에도 재전송이 발생할 경우 다시 로그에 기록되므로 'At least once'의 의미를 가진다.
- 이후 : 'idempotent delivery option(멱등 전달)' & 메시지 트랜잭션 기능
- idempotent delivery (멱등 전달)
- 로그에 중복 커밋이 생기지 않도록 각 Producer에게 ID 할당하고, 메시지에 Sequence Number 할당
- 해당 동작은 브로커에서 Producer ID + Sequence Number로 메시지를 식별하여, 같은 프로듀서에서 메시지를 중복으로 커밋되지 않게 방지한다.
- 이는 'Exactly once'의 의미를 가진다.
- 메시지 트랜잭션
- Producer가 여러 토픽 파티션에 메시지를 send할 때 해당 메시지들을 트랜잭션처럼 묶어서 원자성 보장
- 여러 토픽 파티션에 메시지를 전달할 때도 원자성을 보장하여 'Exactly once'의 의미를 가진다.
- idempotent delivery (멱등 전달)
- 이전 : Producer가 메시지 커밋 응답을 받지 못했을 때 메시지를 resend 할 수 밖에 없었다.
결론적으로, Kafka는 기본적으로 'At least once'로 메시지를 받지 못하면 재전송하는 방식을 사용하지만,
Idempotent delivery & 트랜잭션 기능을 통해 메시지를 1번만 보내는 'At exactly once' 방식으로 동작할 수 있다.
6. Replication
Kafka replicates the log for each topic's partitions across a configurable number of servers.
This allows automatic failover to these replicas when a server in the cluster fails so messages remain available in the presence of failures.
- Kafka는 각 토픽 파티션들의 로그를 여러 개의 서버에 복제(replicate)한다.
- 이러한 기능을 통해 클러스터 내의 서버에 장애가 발생하더라도 복제본으로 복구가 가능하다.
- Replication의 단위는 토픽 파티션
- 각 파티션별로 1개의 Leader Broker와 나머지 Follower Broker 존재
- Produce/Consume(Write/Read) 작업은 모두 Leader Broker에서 처리
- 이때 해당 파티션에 쌓이는 메시지의 로그는 Leader Broker, Follower Broker에 모두 적재된다.
- ISR (In-Sync Replica) : 리더와 동일한 데이터 상태를 유지하며, 리더와 동기화되어 있는 복제본 Follower Broker들의 집합
- Leader Broker가 나머지 Follower Broker들의 로그 동기화 상태를 모니터링하여 ISR에 포함
※ Leader Broker 장애 발생 시 Failover 동작
- 정상적인 시나리오에서 메시지 적재 시 메시지 로그를 Follower Broker에 적재, 동기화된 Follower Broker를 ISR에 포함
- Leader Broker 장애 발생 시 클러스터 내의 'Controller Broker'가 ISR에서 1개의 Follower Broker를 Leader로 승격
7. Log Compaction
해당 부분은 저장 공간과 복원의 관점에서 어떻게 Kafka에서 Log를 저장하고 복원하는지에 관한 설명이 나와있습니다.
일반적인 로그 Retention은 다음과 같이 동작합니다.
- Infinite Retension
- 영구적으로 모든 업데이트 로그를 저장하고 삭제하지 않는다.
- 이 경우에 사용하지 않는 데이터들도 모두 저장되므로 저장 공간 문제가 생기고 실용적이지 않다.
- Simple Retension (일정 기간동안만 저장)
- 기간을 설정하여 로그를 저장하고 기간이 지나면 해당 로그를 삭제
- 일정 시간 후에는 로그가 삭제되므로 해당 시간 후 과거 로그를 재현하기 어려워진다.
- ex) 업데이트가 잦지 않은 유저 정보의 업데이트 내역이 Retension 이후에 사라져서, 해당 유저 정보가 사라진다면 재현이 어렵다.
※ Kafka의 Log Compaction
Kafka에서는 위의 2가지 방식의 문제점을 절충한 Log Compaction 방식을 사용한다.
Log compaction is a mechanism to give finer-grained per-record retention, rather than the coarser-grained time-based retention.
The idea is to selectively remove records where we have a more recent update with the same primary key. This way the log is guaranteed to have at least the last state for each key.
- Log Compaction은 시간 기반 Retension이 아닌 Record별 보존이다.
- Log가 적재될 때 동일한 Primary Key로 최근 업데이트된 Log를 제거한다.
- 따라서, Log에는 해당 Primary Key의 가장 최근 상태가 영구적으로 보존된다.
- 이러한 방식을 통해 저장 공간도 효율적으로 사용하고, 업데이트가 잦지 않은 로그도 최근 상태를 보존하게 된다.

이렇게 Kafka 공식문서의 Design 챕터를 살펴봤습니다!
이론 부분이고 공식문서를 번역하여 나열한 수준이지만 Kafka의 기본 Design을 살펴볼 수 있었습니다.
'Kafka' 카테고리의 다른 글
Transactional Outbox Pattern을 통해 Event Message 발행 보장하기 (0) | 2025.03.03 |
---|---|
[Kafka] Apache Kafka 공식 문서 살펴보기 (기본 이론) (1) | 2024.11.30 |
0. 들어가기 전
바로 전에 Kafka의 기본 이론에 대해서 알아봤었습니다.
https://ksh-coding.tistory.com/160
[Kafka] Apache Kafka 알아보기 (기본 이론)
0. 들어가기 전이전에 MSA 프로젝트를 진행할 때, Kafka를 사용해본 적이 있습니다.하지만 그때는 먼저 구현을 했어야 했기에 제대로 된 Kafka의 이론은 모른채 구현만 쫓아갔던 기억이 있습니다.
ksh-coding.tistory.com
이번에는 공식문서에서 언급하는 좀 더 심화적인 내용들을 살펴보도록 하겠습니다.
Kafka 공식문서의 Design 챕터에서는 Kafka의 내부 구조, 원리를 다루고 어떤 장점이 있는지를 소개하고 있습니다.
하나씩 알아보도록 하겠습니다.
1. Persistence
Kafka의 각 이벤트 메시지들은 어느 곳에, 어떻게 저장될까요?
그리고 Kafka에서의 메시지 Read/Write 성능은 어떨까요?
해당 챕터에서는 이와 관련한 내용을 다룹니다.
📚 Dont'fear the filesystem! : Kafka는 filesystem을 사용하지만, 성능이 좋다!
Kafka relies heavily on the filesystem for storing and caching messages. There is a general perception that "disks are slow" which makes people skeptical that a persistent structure can offer competitive performance.
- Kafka는 message를 저장하고 캐싱할 때 filesystem을 사용한다.
- 보통 Disk는 느리지만, Disk 구조를 적절히 설계한다면 빠를 수 있다.
※ 일반적인 Disk 구조 문제점
- 일반적으로 Disk의 선형 Read/Write(순차 I/O) 속도는 빠르지만, 데이터를 탐색할 때의 Random I/O는 상당히 느리다.
- 순차 I/O의 쓰기 성능은 600MB/sec인 반면, Random I/O의 쓰기 성능은 100KB/sec으로 약 6000배나 차이가 난다.
- 이러한 차이를 보완하기 위해서 현대 OS는 메모리 회수 시 메인 메모리의 free 메모리를 Disk 캐싱에 사용한다. (Random I/O 발생을 줄이기 위해)
- Kafka에서 이러한 메인 메모리를 Disk 캐싱에 사용한다면 문제가 발생할 수 있다.
- Kafka는 JVM 기반으로 구축되는데, JVM 위에서 인메모리 캐시를 사용하면 객체 메모리 오버헤드가 많아지고 GC에 데이터가 커져서 더 느려지게 된다.
※ Kafka Disk 캐싱
- 따라서, Kafka에서는 메모리에 Disk를 캐싱하는 것이 아니라 OS 커널의 페이지 캐시에 Disk를 캐싱한다.
- 이로 인해 메인 메모리를 여유롭게 사용하면서 GC의 패널티 없이 사용할 수 있다.
- 또, 인메모리 캐싱은 서비스가 재시작될 때 다시 빌드해야하므로 성능이 좋지 않은데, 페이지 캐시에 Disk 캐싱을 하게되면 OS의 영역이기 때문에 재시작과 관련 없이 캐시를 유지한다.
- 따라서, Kakfa의 모든 데이터는 filesystem에 'persistent log' 형태로 기록된다. (페이지 캐시에 저장)
📚 Constant Time Suffices : Kafka는 Read/Write 시간복잡도가 O(1)이다! (랜덤 I/O 대신 순차 I/O 사용)
- 일반적인 메시징 시스템에서는 B-Tree를 사용하여 메시지별로 메타 데이터를 유지합니다.
- 하지만, B-Tree는 시간복잡도가 O(log N)으로 일반적으로 빠르지만 Disk 작업에서는 상당히 느릴 수 있습니다.
- Disk는 병렬 작업을 수행하지 못하고 초당 약 10ms로 탐색하므로 여러 건의 작업 시 오버헤드가 매우 높아집니다.
- Kafka는 filesystem 기반 Queue로, 모든 연산을 로그 기반의 순차 I/O를 사용하여 O(1)으로 빠르게 디스크 탐색을 합니다.
- 메시지를 파일에 추가하는 방식으로 데이터 기록
- 시간 복잡도가 O(1)이기 때문에 저장된 파일의 개수에 영향을 받지 않고 많이 적재되어 있더라도 성능을 유지
- 따라서, 다른 메시징 시스템과 달리 사용자가 성능을 고려할 필요 없이 메시지를 원하는 기간동안 적재해놓고 재소비 가능
2. Efficiency
이번 챕터에서는 Kafka에서 어떤 식으로 메시지들을 효율적으로 처리하는지 알아보도록 하겠습니다.
📚 Message 그룹화 + 페이지 캐시 Read로 비효율성 제거
- 모든 연산이 순차 I/O임에 따라 다음과 같은 2가지 문제가 발생합니다.
- 작은 크기의 I/O가 빈번하게 발생 : 순차적으로 O(1)로 Write를 하기 때문에 작은 크기의 메시지가 버퍼에 쌓이지 않고 매번 I/O를 발생시켜 비효율적일 수 있다.
- 과도한 바이트 복사 발생 : Kafka 자체 메모리 내부에 버퍼 및 캐시가 존재하는데, 메시지 처리 시마다 메시지를 복사해서 버퍼에 저장하고 캐싱하므로 과도한 바이트 복사가 일어나서 비효율적일 수 있다.
- EX) 작은 크기의 메시지들이 자주 처리되면 Network 비용이 처리 시마다 발생하고 바이트 복사가 매번 일어나서 비효율적
- 작은 크기의 I/O가 빈번하게 발생하는 비효율성을 제거하기 위해, Kafka는 메시지들을 그룹화해서 저장한다.
- Kafka Producer는 로그에 그룹화된 큰 메시지들을 저장하고 Consumer도 그룹화된 큰 메시지들을 소비하게 된다.
- 과도한 바이트 복사를 피하기 위해 '페이지 캐시'에 한 번만 복사하고 이후에는 저장된 메시지들을 재사용한다.
📚 End-to-end Batch Compression : 여러 메시지들을 Batch 압축
- Kafka는 메시지를 그룹화하는 것뿐만 아니라 Batch를 사용해서 메시지들을 압축한다.
- Batch 메시지들은 로그에 압축된 상태로 저장되고, Consumer가 해당 압축 데이터를 해제하여 사용한다.
3. Producer
이번 챕터에는 메시지를 publish하는 Producer의 설계에 대해서 알아보겠습니다.
📚 Leader Broker & Follower Broker
Producer를 보기 전에, Event를 저장하는 Storage 역할을 하는 Broker에 대해서 조금 더 보충이 필요합니다.
Kafka Cluster의 여러 Broker들은 파티션별로 Leader, Follower의 역할을 가집니다.

- Kafka Cluster의 Broker는 기본적으로 모든 Partition의 정보를 알고 있다.
- 하지만, 각 Partition에서 메시지를 Read/Write하는 Broker는 단 1대이고, 이를 Leader Broker라고 한다.
- Partition별로 Leader Broker가 아닌 Broker들은 해당 Partition의 Follower Broker가 된다.
- Follower Broker가 존재하는 이유는 Partition의 Leader Broker에 장애가 발생했을 때 빠르게 Leader로 승격되는 구조를 만들어서 HA를 보장할 수 있기 때문이다.
- 따라서, 모든 Follower Broker들은 각 파티션의 Leader Broker로부터 지속적으로 새로운 메시지를 확인하여 복제한다.
📚 Load Balancing
1. 메시지 Publish 시 Broker Load Balancing : 메시지 Publish 시 어떤 Broker가 메시지를 Read/Write할지 어떻게 결정할까?
- Kafka의 Producer들은 Kafka Cluster의 모든 Kafka Node에게 어떤 Broker가 살아있는지, Partition의 Leader Broker들은 어떤 Broker인지 메타데이터를 요청하여 제공받는다.
- Producer가 메시지를 Publish 할 때, 모든 Broker에게 메시지를 publish 하지 않고 Publish할 Partition의 Leader Broker에게만 메시지를 보낸다.
2. 메시지 Publish 시 Partition Load Balancing : 메시지 Publish 시 어떤 Partition에 메시지가 적재될 지 어떻게 결정할까?
- Kafka는 기본적으로 라운드 로빈 방식을 사용하여 적재할 파티션을 결정하지만, 조건을 통해 적재할 파티션을 지정할 수 있다.
- 적재할 파티션 지정 : Event Key를 설정하여 지정
- Event Key가 설정된 Event는 해당 Key를 해싱하여 적재할 Partition을 정한다.
- 따라서, 동일한 Event Key를 가진 Event는 모두 같은 Partition에 저장된다.
📚 Asynchronous Send
- Kafka는 Batch를 통해 Producer에서 메시지를 비동기로 Publish 할 수 있다.
- 비동기 Send의 의미는 여러 건의 메시지를 1건씩 동기적으로 처리하지 않고 비동기로 처리한 후 Batch 처리한다는 의미이다.
- Producer에서 메시지를 바로 Publish하지 않고 메모리에 쌓아놨다가 하나의 Request에 모두 담아서 보낼 수 있다.
- 메모리에 적재된 메시지의 양 (batch.size) or 최대 대기 시간 (linger.ms)을 설정하여 그 이상이 되면 Request를 보내도록 설정할 수 있다.
- 이를 통해 약간의 지연 시간(버퍼 시간)을 감수하고 처리량을 늘릴 수 있다.
4. Consumer
이번 챕터에는 메시지를 consume하는 Consumer의 설계에 대해서 알아보겠습니다.
📚 Kafka Consumer 동작 (feat. Offset)
- Kafka Consumer는 메시지를 소비할 Partition의 Leader Broker에게 'fetch' 요청을 하는 방식으로 동작한다.
- Consumer는 각 'fetch' 요청마다 로그에 offset을 지정하고 해당 위치부터 끝 위치까지의 로그 Chunk를 다시 받는다.
- Offset이란, 파티션 내에서 데이터가 기록된 순서를 나타내는 고유 번호이다.
- EX) Offset 1~10이 있을 때 fetch 요청 Offset이 4라면 Offset 4~10의 데이터를 받는다.
- 즉, Consumer는 해당 offset에 해당하는 로그를 재소비할 수 있다.
📚 Broker -> Consumer (Push) vs. Broker <- Consumer (Pull)
An initial question we considered is whether consumers should pull data from brokers or brokers should push data to the consumer.
Kafka는 메시지 Consume 시 Broker에서 Conusmer에 Push할지, Consumer가 Broker에서 Pull할지를 고민했다.
- Broker -> Consumer (Push)
- Broker가 Consumer에 Push하는 방식은 Broker가 데이터 전송 속도를 제어하기 때문에 처리량이 다른 다양한 Consumer를 사용하기에 어려움이 있다.
- EX) Push 방식을 사용하는 목적이 메시지가 Publish 될 때 바로 Consumer에서 처리하도록 하기 위함이지만, Consumer가 많을 때 처리량이 다르다면 문제가 된다.
- 또, 처리량이 같더라도 Publish 양이 엄청나게 많아지면 Consumer가 과부하가 걸릴 수 있다.
- Broker <- Consumer (Pull)
- Push 방식의 단점을 Pull 방식을 사용하면 극복할 수 있다.
- Publish 양이 많더라도 Consumer가 처리할 수 있을 때만 Pull 받아서 처리하기 때문에 과부하가 적다.
- 또 다른 장점은 Push 방식보다 효율적으로 Batch로 메시지를 소비할 수 있다.
- Push 방식을 사용하면 Broker에서 메시지를 Batch로 보낼 수는 있지만, Consumer에서 소비할 수 있는지는 알지 못한채 보내게 되기 때문에 의미가 없게 된다.
- Pull 방식에서는 항상 Consumer의 처리 속도에 맞게 메시지를 가져올 수 있으므로 효율적으로 Batch를 사용할 수 있다.
- Pull 방식의 단점은, 반대로 Broker에 메시지가 없는 경우에도 메시지가 생성될 때까지 Polling 한다는 점이다.
- Kafka에서는 이러한 단점을 방지하기 위해 Long Polling을 사용하여 응답이 올 때까지 연결을 끊지 않고 대기하여 효율을 조금 올린다.
- Push 방식의 단점을 Pull 방식을 사용하면 극복할 수 있다.
📚 Consumer Position
Keeping track of what has been consumed is, surprisingly, one of the key performance points of a messaging system.
메시징 시스템에서 핵심 포인트 중 하나는 '소비된 메시지를 추적하는 것'입니다.
※ AS-IS 메시징 시스템 동작
- 대부분의 메시징 시스템은 Broker에서 어떤 메시지가 소비되었는지에 대한 메타데이터를 보관합니다.
- 이때, 보통 메시징 시스템들의 스토리지 구조는 확장성이 좋지 않기 때문에 메시지가 소비되면 보관한 메타데이터를 삭제합니다.
- 이를 통해 데이터 크기를 작게 유지할 수 있기 때문에 확장을 줄일 수 있습니다.
- Broker에서 메시지가 소비되었다는 판단은 2단계에 거쳐서 발생합니다.
- Consumer가 메시지를 전달받았을 때 -> 'sent not consumed'의 의미로 마킹
- Consumer가 메시지 소비 후 Broker에게 메시지 소비 알림 -> 소비한 것으로 기록
이러한 동작은 다음과 같은 문제가 발생할 수 있습니다.
- Consumer가 Broker에 소비 알림을 보내지 못한 경우
- 2가지 선택 존재
- Broker에서는 소비되지 않음으로 간주하고 메시지를 다른 Consumer에게 전달하여 재소비
- Consumer가 이미 처리한 메시지라면 메시지 중복 재소비가 발생
- Broker에서 메시지를 삭제하지 않고 유지
- 미처리 상태의 메시지가 Broker에 지속적으로 쌓여 크기가 커질 수 있음
따라서, 위와 같이 AS-IS의 메시징 시스템으로는 해결할 수 없는 문제가 있습니다.
이를 Kafka에서는 다음과 같은 방식으로 해결합니다.
※ Kafka의 Offset 기반 Consume
- Kafka는 Broker가 아닌 Consumer가 메시지를 소비한 Position(Offset)을 관리합니다.
- Topic 내의 여러 파티션은 정확히 각 1개의 Consumer에 의해 메시지가 소비됩니다.
- 따라서, Partition의 Offset은 할당된 Consumer가 마지막으로 메시지를 소비한 위치입니다.
- 이러한 Partition의 Offset을 메시지 소비 시마다 주기적으로 저장하여 메시지 소비를 추적할 수 있습니다.
- 이는 하나의 정수에 불과하기 때문에 데이터 크기가 작고, 따라서 주기적으로 저장해도 성능에 무리가 없습니다.
- 이를 통해 이전에 소비했던 메시지도 필요한 경우 이전 Offset으로 돌아가서 메시지를 재소비 할 수 있습니다.
5. Message Delivery Semantics
위에서 Producer와 Consumer 동작에 대해서 살펴봤으니, 이제는 메시지 전달 방식에 대해서 살펴보도록 하겠습니다.
* At most once—Messages may be lost but are never redelivered.
* At least once—Messages are never lost but may be redelivered.
* Exactly once—this is what people actually want, each message is delivered once and only once.
- At most once : 메시지가 유실될 수 있지만 절대 다시 전달되지 않는다.
- At least once : 메시지가 절대 유실되지 않지만 다시 전달될 수 있다.
- Exactly once : 메시지가 정확히 1번만 전달된다.
먼저, Kafka의 메시지 Commit에 대해서 알아봅시다.
When publishing a message we have a notion of the message being 'committed' to the log.
- Kafka에서 Message는 파티션 로그에 저장되었을 때 'committed' 되었다고 한다.
- Message가 커밋된 이후에는 활성 상태의 복제본이 최소 1대의 Broker에 존재하는 한 데이터가 유실되지 않는다.
- Kafka의 0.11.0.0 버전 이전/이후로 메시지 전달 보장 방식(semantic)이 추가되었다.
- 이전 : Producer가 메시지 커밋 응답을 받지 못했을 때 메시지를 resend 할 수 밖에 없었다.
- 해당 동작은 실제로 메시지가 전달되었음에도 재전송이 발생할 경우 다시 로그에 기록되므로 'At least once'의 의미를 가진다.
- 이후 : 'idempotent delivery option(멱등 전달)' & 메시지 트랜잭션 기능
- idempotent delivery (멱등 전달)
- 로그에 중복 커밋이 생기지 않도록 각 Producer에게 ID 할당하고, 메시지에 Sequence Number 할당
- 해당 동작은 브로커에서 Producer ID + Sequence Number로 메시지를 식별하여, 같은 프로듀서에서 메시지를 중복으로 커밋되지 않게 방지한다.
- 이는 'Exactly once'의 의미를 가진다.
- 메시지 트랜잭션
- Producer가 여러 토픽 파티션에 메시지를 send할 때 해당 메시지들을 트랜잭션처럼 묶어서 원자성 보장
- 여러 토픽 파티션에 메시지를 전달할 때도 원자성을 보장하여 'Exactly once'의 의미를 가진다.
- idempotent delivery (멱등 전달)
- 이전 : Producer가 메시지 커밋 응답을 받지 못했을 때 메시지를 resend 할 수 밖에 없었다.
결론적으로, Kafka는 기본적으로 'At least once'로 메시지를 받지 못하면 재전송하는 방식을 사용하지만,
Idempotent delivery & 트랜잭션 기능을 통해 메시지를 1번만 보내는 'At exactly once' 방식으로 동작할 수 있다.
6. Replication
Kafka replicates the log for each topic's partitions across a configurable number of servers.
This allows automatic failover to these replicas when a server in the cluster fails so messages remain available in the presence of failures.
- Kafka는 각 토픽 파티션들의 로그를 여러 개의 서버에 복제(replicate)한다.
- 이러한 기능을 통해 클러스터 내의 서버에 장애가 발생하더라도 복제본으로 복구가 가능하다.
- Replication의 단위는 토픽 파티션
- 각 파티션별로 1개의 Leader Broker와 나머지 Follower Broker 존재
- Produce/Consume(Write/Read) 작업은 모두 Leader Broker에서 처리
- 이때 해당 파티션에 쌓이는 메시지의 로그는 Leader Broker, Follower Broker에 모두 적재된다.
- ISR (In-Sync Replica) : 리더와 동일한 데이터 상태를 유지하며, 리더와 동기화되어 있는 복제본 Follower Broker들의 집합
- Leader Broker가 나머지 Follower Broker들의 로그 동기화 상태를 모니터링하여 ISR에 포함
※ Leader Broker 장애 발생 시 Failover 동작
- 정상적인 시나리오에서 메시지 적재 시 메시지 로그를 Follower Broker에 적재, 동기화된 Follower Broker를 ISR에 포함
- Leader Broker 장애 발생 시 클러스터 내의 'Controller Broker'가 ISR에서 1개의 Follower Broker를 Leader로 승격
7. Log Compaction
해당 부분은 저장 공간과 복원의 관점에서 어떻게 Kafka에서 Log를 저장하고 복원하는지에 관한 설명이 나와있습니다.
일반적인 로그 Retention은 다음과 같이 동작합니다.
- Infinite Retension
- 영구적으로 모든 업데이트 로그를 저장하고 삭제하지 않는다.
- 이 경우에 사용하지 않는 데이터들도 모두 저장되므로 저장 공간 문제가 생기고 실용적이지 않다.
- Simple Retension (일정 기간동안만 저장)
- 기간을 설정하여 로그를 저장하고 기간이 지나면 해당 로그를 삭제
- 일정 시간 후에는 로그가 삭제되므로 해당 시간 후 과거 로그를 재현하기 어려워진다.
- ex) 업데이트가 잦지 않은 유저 정보의 업데이트 내역이 Retension 이후에 사라져서, 해당 유저 정보가 사라진다면 재현이 어렵다.
※ Kafka의 Log Compaction
Kafka에서는 위의 2가지 방식의 문제점을 절충한 Log Compaction 방식을 사용한다.
Log compaction is a mechanism to give finer-grained per-record retention, rather than the coarser-grained time-based retention.
The idea is to selectively remove records where we have a more recent update with the same primary key. This way the log is guaranteed to have at least the last state for each key.
- Log Compaction은 시간 기반 Retension이 아닌 Record별 보존이다.
- Log가 적재될 때 동일한 Primary Key로 최근 업데이트된 Log를 제거한다.
- 따라서, Log에는 해당 Primary Key의 가장 최근 상태가 영구적으로 보존된다.
- 이러한 방식을 통해 저장 공간도 효율적으로 사용하고, 업데이트가 잦지 않은 로그도 최근 상태를 보존하게 된다.

이렇게 Kafka 공식문서의 Design 챕터를 살펴봤습니다!
이론 부분이고 공식문서를 번역하여 나열한 수준이지만 Kafka의 기본 Design을 살펴볼 수 있었습니다.
'Kafka' 카테고리의 다른 글
Transactional Outbox Pattern을 통해 Event Message 발행 보장하기 (0) | 2025.03.03 |
---|---|
[Kafka] Apache Kafka 공식 문서 살펴보기 (기본 이론) (1) | 2024.11.30 |