Kafka 입문2
카프카의 기본 구성
카프카는 데이터를 받아서 전달하는 데이터 버스의 역할을 한다.
- 프로듀서(Producer) : 데이터(메시지)를 만들어서 주는 쪽
- 컨슈머(Consumer) : 카프카에서 데이터를 빼내서 소비하는 쪽
- 주키퍼(zookeeper) : 카프카의 정상 동작을 보장하기 위해 메타데이터를 관리하고, 브로커의 정상상태 점검(health check) 및 리더 선출(leader election)을 담당한다.
- 브로커(broker) : 카프카 애플리케이션이 설치된 서버 또는 노드로, 프로듀서로부터 메시지를 받아 저장하고 컨슈머에게 전달하는 역할을 한다.
- 토픽(topic) : 카프카는 메시지 피드들을 토픽으로 구분하고, 각 토픽의 이름은 카프카 내에서 고유하다.
- 파티션(partition) : 병렬 처리 및 고성능을 얻기 위해 하나의 토픽을 여러 개로 나눈 것을 말함
- 세그먼트(segment) : 프로듀서가 전송한 실제 메시지가 브로커의 로컬 디스크에 저장되는 파일
- 메시지(message) / 레코드(record) : 프로듀서가 브로커로 전송하거나 컨슈머가 읽어가는 데이터 조각
리플리케이션
각 메시지들을 여러 개로 복제해서 카프카 클러스터 내 브로커들에 분산시키는 동작을 의미한다.
리플리케이션 덕분에 하나의 브로커가 종료되더라도 카프카는 안정성을 유지할 수 있다.
--partition 1, --replication-factor 3
- 토픽의 파티션이 리플리케이션 되는 것
왜 최소 3개를 해야 할까?
카프카의 리플리케이션은 리더(Leader) 1개 + 팔로워(Follower) N개 구조로 동작한다.
| 리플리케이션 수 | 브로커 1개 장애 시 | 결과 |
|---|---|---|
| 1개 | 복구 불가 | 데이터 유실, 서비스 중단 |
| 2개 | 팔로워만 남음 | 과반수 미달로 리더 선출 불가 |
| 3개 | 2개 남음 | 과반수 충족, 정상 서비스 지속 |
카프카는 과반수(Quorum) 원칙에 따라 브로커가 살아 있어야 리더를 선출하고 서비스를 지속할 수 있다.
- 리플리케이션이 1개라면 해당 브로커가 죽는 순간 복구할 방법이 없다.
- 리플리케이션이 2개라면 1개 장애 발생 시 살아남은 팔로워 1개만으로는 과반수(2개 중 2개 필요)를 채우지 못해 리더 재선출이 불가능하다.
- 리플리케이션이 3개라면 1개 장애 발생 시 나머지 2개가 과반수(3개 중 2개)를 형성하여 리더를 재선출하고 서비스를 이어갈 수 있다.
따라서 브로커 1대의 장애를 허용하면서 안정적으로 운영하려면 최소 리플리케이션 팩터를 3으로 설정하는 것이 권장된다.
파티션
하나의 토픽이 한 번에 처리할 수 있는 한계를 높이기 위해 토픽 하나를 여러 개로 나눠 병렬 처리가 가능하게 만든 것
분산 처리가 가능해진다.
- 주의점
- 파티션 수는 초기 생성 후 언제든지 늘릴 수 있지만, 한 번 늘린 파티션 수는 절대로 줄일 수 없다.
- 따라서, 초기에 토픽을 생성할 때 파티션 수를 작게(2개-4개) 생성 후 메시지 처리량이나 컨슈머의 LAG 등을 모니터링하면서 조금씩 늘려가는 방법이 가장 좋다.
- LAG : ‘프로듀서가 보낸 메시지 수(카프카에 남아 있는 메시지 수) - 컨슈머가 가져간 메시지 수’
- LAG를 통해서 컨슈머에 지연이 없는지 확인할 수 있다.
- LAG : ‘프로듀서가 보낸 메시지 수(카프카에 남아 있는 메시지 수) - 컨슈머가 가져간 메시지 수’
세그먼트
로그 파일의 형태로 브로커의 로컬 디스크에 저장됨
프로듀서를 이용해 보낸 메시지는 토픽의 파티션에 저장되고, 각 메시지들은 세그먼트라는 로그 파일의 형태로 브로커의 로컬 디스크에 저장된다.
카프카의 핵심 개념
분산 시스템
- 카프카는 여러 브로커로 구성된 클러스터로 동작하며, 단일 장애점(SPOF)이 없다.
- 부하가 높은 경우 브로커를 추가하는 것만으로 수평 확장이 가능하다.
- 기존 서비스를 중단하지 않고도 클러스터에 브로커를 추가할 수 있음
- 새로 추가된 브로커는 자동으로 파티션을 재분배받아 부하를 나눔
- 클러스터 내 모든 브로커가 클라이언트 요청을 처리할 수 있어, 특정 브로커에 부하가 집중되지 않음
페이지 캐시
- 카프카는 OS의 페이지 캐시를 적극 활용하는 방식으로 설계되어 있음
- 페이지 캐시 : 직접 디스크에 읽고 쓰는 대신, 물리 메모리 중 애플리케이션이 사용하지 않는 잔여 메모리를 활용
- 디스크 I/O 접근이 줄어들어 성능을 크게 높일 수 있다.
- 카프카 프로세스를 재시작해도 OS가 관리하는 페이지 캐시는 유지되므로, 재시작 후에도 빠르게 데이터를 서빙할 수 있다.
1
2
프로듀서 → 카프카(페이지 캐시) → 디스크 (비동기 플러시)
컨슈머 ← 카프카(페이지 캐시) ← 디스크
- 제로 카피(Zero Copy) : 카프카는 컨슈머에게 데이터를 전달할 때 커널 영역의 페이지 캐시에서 직접 네트워크 소켓으로 데이터를 전송한다.
- 일반적인 방식은
디스크 → 커널 버퍼 → 애플리케이션 버퍼 → 소켓 버퍼순으로 복사가 발생하지만, - 제로 카피는
커널 버퍼 → 소켓 버퍼로 직접 전달하여 CPU 사용량과 지연 시간을 줄인다.
- 일반적인 방식은
배치 전송 처리
- 카프카는 메시지를 건별로 전송하지 않고, 일정량을 모아 한 번에 배치로 전송한다.
- 수많은 통신을 묶어 처리하면 단건 통신 대비 네트워크 오버헤드를 크게 줄일 수 있다.
- 프로듀서 옵션
batch.size와linger.ms로 배치 크기와 대기 시간을 조절할 수 있다.batch.size: 배치의 최대 크기(바이트). 이 크기에 도달하면 즉시 전송linger.ms: 배치가 꽉 차지 않더라도 이 시간이 지나면 전송. 처리량과 지연 간의 트레이드오프를 조절하는 핵심 설정
압축 전송
- 카프카에서 지원하는 압축 타입 :
gzip,snappy,lz4,zstd
| 압축 타입 | 압축률 | 속도 | 권장 상황 |
|---|---|---|---|
| gzip | 높음 | 느림 | 대역폭이 좁고 CPU 여유가 있을 때 |
| zstd | 높음 | 보통 | 높은 압축률과 적절한 속도가 필요할 때 |
| lz4 | 보통 | 매우 빠름 | 지연 시간이 중요한 실시간 처리 |
| snappy | 보통 | 빠름 | 균형 잡힌 범용 사용 |
- 프로듀서에서 압축 후 브로커에 저장되고, 컨슈머가 받을 때 압축 해제된다.
- 배치 전송과 결합하면 더 많은 데이터를 압축하므로 효율이 높아진다.
토픽, 파티션, 오프셋
- 토픽 : 카프카가 메시지를 저장하는 논리적 단위 (메일 시스템의 이메일 주소 개념)
- 토픽 이름은 클러스터 내에서 고유해야 함
- 파티션 : 토픽을 물리적으로 분할한 단위로, 병렬 처리의 핵심
- 각 파티션은 독립적인 로그 파일로 관리되며, 서로 다른 브로커에 분산 저장될 수 있음
- 파티션 수만큼 컨슈머를 병렬로 연결할 수 있어, 처리량을 선형적으로 늘릴 수 있음
- 오프셋 : 파티션 내 각 메시지의 위치를 나타내는 순차 증가 정수(64비트)
- 오프셋은 파티션마다 독립적으로 관리됨 (서로 다른 파티션의 오프셋 0은 별개의 메시지)
- 컨슈머는 오프셋을 기준으로 어디까지 읽었는지 추적하며, 장애 후 재시작해도 이어서 읽을 수 있음
- 토픽 전체 기준의 메시지 순서는 보장되지 않지만, 파티션 내부의 순서는 항상 보장된다.
1
2
3
4
Topic: orders
Partition 0: [offset 0] [offset 1] [offset 2] ...
Partition 1: [offset 0] [offset 1] [offset 2] ...
Partition 2: [offset 0] [offset 1] [offset 2] ...
고가용성 보장
- 분산 시스템이기 때문에 하나의 서버나 노드가 다운되어도 다른 브로커가 역할을 대신하여 서비스를 이어갈 수 있다.
- ISR(In-Sync Replicas) : 리더 파티션과 동기화 상태를 유지하고 있는 팔로워들의 집합
- ISR에 포함된 팔로워만 리더 후보가 될 수 있음
- 팔로워가 리더를 따라가지 못하면 ISR에서 제외되고, 다시 따라잡으면 재합류함
- 리더 브로커 장애 시, ISR 내의 팔로워 중 하나가 새로운 리더로 선출되어 서비스가 자동으로 복구된다.
min.insync.replicas설정으로 최소 동기화 복제본 수를 지정하여, 데이터 유실 없는 안정적인 운영이 가능하다.
주키퍼의 의존성
- 주키퍼는 카프카 클러스터 운영에 필요한 메타데이터를 지노드(znode) 형태로 저장하고 관리한다.
- 브로커 등록 및 상태 관리
- 토픽 설정 및 파티션 정보 관리
- 컨트롤러 브로커 선출 (파티션 리더 선정 등 클러스터 전반의 결정을 담당)
- ACL(접근 제어 목록) 관리
KRaft 모드 (Kafka 3.x 이상) : 카프카는 주키퍼 의존성을 없애기 위해 KRaft(Kafka Raft Metadata) 모드를 도입했다. 카프카 자체 내부에 Raft 합의 알고리즘을 구현하여 메타데이터를 직접 관리하며, 카프카 4.0부터는 주키퍼 없이 운영하는 것이 기본(default)이 되었다.
프로듀서의 기본 동작
- ProducerRecord: 카프카로 전송하기 위한 실제 데이터
- 토픽, 파티션, 키, 밸류로 구성됨
Producer 기본 흐름
- 프로듀서는 카프카로 레코드를 전송할 때, 카프카의 특정 토픽으로 메시지를 전송함
- 따라서 레코드에는 토픽과 밸류(메시지 내용)이 필수값임
- 선택사항(옵션) : 특정 파티션을 지정하기 위한 레코드의 파티션, 특정 파티션에 레코드들을 정렬하기 위한 레코드의 키
- 각 레코드들은 프로듀서의 send() 메소드를 통해 Serializer, Partitioner를 거치게 됨
- 프로듀서 레코드에서 파티션을 지정했따면, 파티셔너는 아무 동작도 하지 않고 지정된 파티션으로 레코드를 전달함
- 파티션을 지정하지 않았을 경우에는 키를 가지고 파티션을 선택함 (기본적으로 라운드 로핀 방식으로 동작)
- 프로듀서 내부에서는 send() 메소드 동작 이후 레코드들을 파티션별로 잠시 모아두게 됨
- 프로듀서가 카프카로 전송하기 전, 배치 전송을 하기 위함
- 전송이 실패하면 재시도 동작이 이뤄짐
- 지정된 횟수만큼의 재시도가 실패하면 최종 실패를 전달
- 전송이 성공하면 메타데이터를 리턴하게 됨
프로듀서 옵션
- bootstrap.servers
- 카프카 클러스터는 클러스터 마스터라는 개념이 없음
- 클러스터 내 모든 서버가 클라이언트의 요청을 받을 수 있음
- 클라이언트가 카프카 클러스터에 처음 연결하기 위한 호스트와 포트 정보를 나타냄
- client.dns.lookup
- 하나의 호스트에 여러 IP를 매핑해 사용하는 일부 환경에서 클라이언트가 하나의 IP와 연결하지 못할 경우에 다른 IP로 시도하는 설정
- 기본값(use_all_dns_ips)는 DNS에 할당된 호스트의 모든 IP를 쿼리하고 저장함
- 첫 번쨰 IP로 접근이 실패하면, 종료하지 않고 다음 IP로 접근을 시도합니다.
- acks
- 프로듀서가 카프카 토픽의 리더 측에 메시지를 전송한 후 요청을 완료하기를 결정하는 옵션
- 0 : 빠른 전송을 의미하지만, 일부 메시지 손실 가능성이 있음
- 1 : 리더가 메시지를 받았는지 확인하지만, 모든 팔로워를 전부 확인하지는 않음
- all(-1) : 팔로워가 메시지를 받았는지 여부를 확인함, 다소 느릴 수 있지만, 하나의 팔로워가 있는 한 메시지는 손실되지 않음
- 프로듀서가 카프카 토픽의 리더 측에 메시지를 전송한 후 요청을 완료하기를 결정하는 옵션
- buffer.memory
- 프로듀서가 카프카 서버로 데이터를 보내기 위해 잠시 대기(배치 전송이나 딜레이 등)할 수 있는 전체 메모리 바이트이다.
- compression.type
- 프로듀서가 메시지 전송 시 선택할 수 있는 압축 타입
- none, gzip, snappy, lz4, zstd 중 원하는 타입을 선택할 수 있음
- enable.idempotence
- 설정을 true로 하는 경우 중복 없는 전송이 가능
- 이와 동시에 max.in.flight.requests.per.connection은 5 이하, retries는 0 이상, acks는 all로 설정해야 함
- max.in.flight.requests.per.connection
- 하나의 커넥션에서 프로듀서가 최대한 ACK 없이 전송할 수 있는 요청 수
- 메시지 순서가 중요하면 1로 설정할 것을 권장하지만, 성능은 다소 떨어짐
- retries
- 일시적인 오류로 인해 전송에 실패한 데이터를 다시 보내는 횟수
- batch.size
- 프로듀서는 동일한 파티션으로 보내는 여러 데이터를 함께 배치로 보내려고 시도함
- 적절한 배치 크기 설정이 성능에 도움을 줌
- linger.ms
- 배치 형태의 메시지를 보내기 전에 추가적인 메시지를 위해 기다리는 시간을 조정하고, 배치 크기에 도달하지 못한 상황에서 linger.ms 제한 시간에 도달했을 때 메시지를 전송함
- transactional.id
- ‘정확히 한 번 전송’을 위해 사용하는 옵션이며, 동일한 TransactionalId에 한해 정확히 한 번을 보장함
- 옵션을 사용하기 전 enable.idempotence를 true로 설정해야 함
중복 없는 전송 vs 정확히 한 번 전송
| 구분 | 중복 없는 전송 (Idempotent) | 정확히 한 번 전송 (Exactly Once) |
|---|---|---|
| 설정 옵션 | enable.idempotence=true | enable.idempotence=true + transactional.id 설정 |
| 보장 범위 | 프로듀서 → 브로커 (단일 파티션) | 프로듀서 → 브로커 (여러 파티션 포함) |
| 트랜잭션 | 미지원 | 지원 (원자적 쓰기) |
| 컨슈머 설정 | 없음 | isolation.level=read_committed 필요 |
| 성능 영향 | 낮음 | 비교적 높음 |
중복 없는 전송 (Idempotent Producer)
- 프로듀서가 재전송으로 인해 동일한 메시지를 여러 번 보내더라도, 브로커가 중복을 감지해 한 번만 저장한다.
- 브로커는 각 프로듀서에게 PID(Producer ID) 를 부여하고, 메시지마다 시퀀스 번호(Sequence Number) 를 함께 기록한다.
- 동일한 PID + 시퀀스 번호의 메시지가 다시 오면 브로커가 무시(중복 제거)
- 보장 범위: 단일 프로듀서 세션 내, 단일 파티션에 대해서만 중복 없음을 보장한다.
- 프로듀서가 재시작되면 PID가 새로 발급되므로, 이전 세션의 중복은 감지하지 못한다.
1
2
3
4
5
프로듀서 재전송 시나리오 (idempotent)
프로듀서 → [PID=1, seq=5, msg="A"] → 브로커 저장
네트워크 오류로 ACK 미수신 → 프로듀서 재전송
프로듀서 → [PID=1, seq=5, msg="A"] → 브로커가 seq=5 이미 처리됨을 감지 → 무시
정확히 한 번 전송 (Exactly Once / Transactional)
- 중복 없는 전송에 트랜잭션을 더한 것으로, 여러 파티션에 걸친 쓰기를 원자적으로 처리한다.
transactional.id를 설정하면 프로듀서가 재시작되더라도 동일한 ID로 이전 트랜잭션을 커밋 or 중단할 수 있어, 프로세스 재시작에도 정확히 한 번을 보장한다.- 컨슈머 측에서
isolation.level=read_committed로 설정해야 커밋된 메시지만 읽을 수 있다.- 설정하지 않으면(기본값
read_uncommitted) 아직 트랜잭션이 완료되지 않은 메시지도 읽을 수 있음
- 설정하지 않으면(기본값
1
2
3
4
5
6
7
트랜잭션 처리 흐름
beginTransaction()
→ 파티션 A에 메시지 전송
→ 파티션 B에 메시지 전송
commitTransaction() ← 두 파티션 모두 성공해야 커밋
중간에 실패하면 abortTransaction()으로 전부 롤백
정리하면, 중복 없는 전송은 단일 파티션 내 재전송 중복만 제거하고, 정확히 한 번 전송은 여러 파티션에 걸쳐 원자적으로 쓰면서 프로세스 재시작 상황까지 포함해 완전한 EOS(Exactly-Once Semantics)를 보장한다.
컨슈머의 기본 동작
- 컨슈머를 이용해 토픽에 저장된 메시지를 가져올 수 있다.
- 컨슈머 그룹 : 하나 이상의 컨슈머들이 모여 있는 그룹을 의미, 컨슈머는 반드시 컨슈머 그룹에 속하게 됨
- 각 파티션의 리더에게 카프카 토픽에 저장된 메시지를 가져오기 위한 요청을 보냄
- 파티션 수와 컨슈머 수는 일대일로 매핑되는 것이 이상적임
컨슈머 수가 파티션 수 보다 많다면?
- 컨슈머 수가 더 많다고 해서 빠르게 토픽의 메시지를 가져오거나 처리량이 높아지는 것은 아님
- 컨슈머가 그냥 대기 상태로만 존재하게 됨
컨슈머 그룹 내에서 리밸런싱 동작을 통해 장애가 발생한 컨슈머의 역할을 동일한 그룹에 있는 다른 컨슈머가 대신 수행하므로 굳이 장애 대비를 위한 추가 컨슈머 리소스를 할당하지 않아도 된다.
컨슈머의 주요 옵션
- bootstrap.servers : 프로듀서와 동일하게 브로커의 정보를 입력함
- fetch.min.bytes : 한 번에 가져올 수 있는 최소 데이터 크기
- 만약 지정한 크기보다 작은 경우, 요청에 응답하지 않고 데이터가 누적될 때까지 기다림
- group.id : 컨슈머가 속한 컨슈머 그룹을 식별하는 식별자. 동일한 그룹 내의 컨슈머 정보는 모두 공유됨
- heartbeat.interval.ms : 하트비트가 있다는 것은 컨슈머의 상태가 active 임을 의미
- session.timeout.ms와 밀접한 관계가 있으며, 일반적으로 session.timeout.msdml 1/3로 설정함
- max.partition.fetch.bytes : 파티션당 가져올 수 있는 최대 크기를 의미
- session.timeout.ms : 이 시간을 이용해, 컨슈머가 종료된 것인지를 판단함
- 컨슈머는 주기적으로 하트비트를 보내야 하고, 만약 이 시간 전까지 하트비트를 보내지 않았다면 해당 컨슈머는 종료된 것으로 간주하고 컨슈머 그룹에서 제외하고 리밸런싱을 시작함
- enable.auto.commit : 백그라운드로 주기적으로 오프셋을 커밋함
- auto.offset.reset : 카프카에서 초기 오프셋이 없거나 현재 오프셋이 더 이상 존재하지 않는 경우에 다음 옵션으로 reset함
- earliest: 가장 초기의 오프셋값으로 설정함
- latest: 가장 마지막의 오프셋값으로 설정함
- none: 이전 오프셋값을 찾지 못하면 에러를 나타냄
- fetch.max.bytes : 한 번의 가져오기 요청으로 가져올 수 있는 최대 크기임
- group.instance.id : 컨슈머의 고유한 식별자. 설정한다면 static 멤버로 간주되어, 불필요한 리밸런싱을 하지 않음
- isolation.level : 트랜잭션 컨슈머에서 사용되는 옵션, read_uncommited는 기본값으로 모든 메시지를 읽고, read_committed는 트랜잭션이 완료된 메시지만 읽음
- max.poll.records : 한 번의 poll() 요청으로 가져오는 최대 메시지 수
- partition.assignment.strategy : 파티션 할당 전략이며 기본값은 range임
- fetch.max.wait.ms : fetch.min.bytes에 의해 설정된 데이터보다 적은 경우 요청에 대한 응답을 기다리는 최대 시간





