기본개념 · 7장
스토리지·리텐션·컴팩션
Kafka의 저장 계층은 단순합니다 — 파티션 하나가 디렉터리 하나이고, 그 안에 세그먼트 파일들이 있습니다. 그런데 이 단순함에서 실무 사고가 나옵니다. 리텐션 삭제는 레코드 단위가 아니라 세그먼트 단위로 일어나고, 컴팩션은 활성 세그먼트를 건드리지 않으며, 즉시 실행되지도 않습니다. 이 세 가지를 정확히 알면 "보관 기간을 1시간으로 줄였는데 디스크가 안 줄어든다", "컴팩션 토픽인데 중복 키가 보인다" 같은 문의가 사라집니다.
학습 목표
- 로그 세그먼트의 파일 구성과 활성 세그먼트의 특별한 지위를 설명할 수 있습니다.
- 삭제가 세그먼트 단위로 일어난다는 점에서
segment.ms/segment.bytes가 실제 보관량을 좌우하는 이유를 설명할 수 있습니다. - 컴팩션의 대상·시점·제외 조건을 정확히 말하고, tombstone의 수명을 설명할 수 있습니다.
compact,delete조합이 무엇을 하는지 알고, 언제 쓰는지 판단할 수 있습니다.- Tiered Storage의 설정 축과 제약(컴팩션 토픽 미지원)을 알 수 있습니다.
- 디스크 용량을 산정하고, 페이지 캐시와 zero-copy가 성능에 기여하는 원리를 설명할 수 있습니다.
로그 세그먼트의 물리 구조
공식 구현 문서는 저장 구조를 이렇게 설명합니다.
파티션이 두 개인 my-topic이라는 토픽의 로그는
my-topic-0과 my-topic-1 두 디렉터리로 구성됩니다.
각 로그 파일은 자신이 담고 있는 첫 메시지의 오프셋으로 이름이 지어지며,
그래서 처음 생성되는 파일은 00000000000000000000.log가 됩니다.
이후 파일은 설정된 최대 로그 파일 크기(S)만큼 떨어진 정수 이름을 갖습니다.
$ ls -1 /var/lib/kafka/data/orders-0/
00000000000000000000.index # 오프셋 → 파일 위치 인덱스
00000000000000000000.log # 레코드 배치 본문
00000000000000000000.timeindex # 타임스탬프 → 오프셋 인덱스
00000000000000000000.snapshot # 프로듀서 상태 스냅샷 (멱등성/트랜잭션용)
00000000000000451209.index
00000000000000451209.log
00000000000000451209.timeindex
00000000000000451209.snapshot
00000000000000902688.index # ← 활성 세그먼트 (지금 append 되는 곳)
00000000000000902688.log
00000000000000902688.timeindex
leader-epoch-checkpoint # 리더 에포크 기록
partition.metadata
| 파일 | 역할 | 관련 설정 |
|---|---|---|
.log |
레코드 배치 본문. 프로듀서가 보낸 형식 그대로 저장되어 변환 없이 네트워크로 전송할 수 있습니다 | segment.bytes · segment.ms |
.index |
오프셋 → 파일 내 물리 위치 매핑. 희소(sparse) 인덱스입니다 | segment.index.bytes · index.interval.bytes |
.timeindex |
타임스탬프 → 오프셋 매핑. --to-datetime 오프셋 리셋과 시간 기반 리텐션에 쓰입니다 |
index.interval.bytes (타임스탬프가 직전 인덱스보다 클 때만 기록) |
.snapshot |
프로듀서 상태(PID·시퀀스 번호) 스냅샷. 멱등성·트랜잭션 복구에 사용됩니다 | — |
leader-epoch-checkpoint |
리더 에포크 기록. 리더 교체 시 로그 절단(truncation) 지점을 정확히 판단하는 데 씁니다 | — |
활성 세그먼트 — 하나뿐이고 특별합니다
파티션마다 지금 append되고 있는 세그먼트가 정확히 하나 있습니다. 이것이 활성 세그먼트입니다. 공식 문서는 쓰기에 대해 "로그는 항상 마지막 파일로 가는 순차 append를 허용한다"고 서술합니다. 활성 세그먼트가 특별한 이유는 두 가지입니다.
- 리텐션 삭제 대상이 아닙니다. 삭제는 봉인된(rolled) 세그먼트에 대해서만 일어납니다.
- 컴팩션 대상이 아닙니다. 공식 문서 표현 그대로 "마지막 세그먼트, 즉 현재 쓰여지고 있는 세그먼트를 제외한 모든 로그 세그먼트가 컴팩션 대상"이며, "활성 세그먼트는 그 안의 모든 메시지가 최소 컴팩션 시간 lag보다 오래되었더라도 컴팩션되지 않습니다."
세그먼트 내부를 직접 열어 보기
kafka-dump-log.sh — 세그먼트와 인덱스 디코딩# 레코드 배치 헤더만 (본문 없이 구조 확인)
bin/kafka-dump-log.sh --files /var/lib/kafka/data/orders-0/00000000000000000000.log
# 레코드 본문까지 출력
bin/kafka-dump-log.sh --print-data-log \
--files /var/lib/kafka/data/orders-0/00000000000000000000.log
# 인덱스 파일 검증
bin/kafka-dump-log.sh --index-sanity-check \
--files /var/lib/kafka/data/orders-0/00000000000000000000.index
# 클러스터 메타데이터 로그 디코딩 (KRaft)
bin/kafka-dump-log.sh --cluster-metadata-decoder \
--files metadata_log_dir/__cluster_metadata-0/00000000000000000000.log
리텐션 — 삭제는 세그먼트 단위입니다
두 가지 삭제 기준
공식 문서는 로그 매니저가 삭제 대상 세그먼트를 고르는 두 지표를 설명합니다.
- 시간 기준 — 레코드 타임스탬프를 봅니다. 중요한 세부사항: 세그먼트 파일 안의 가장 큰 타임스탬프가 그 세그먼트 전체의 리텐션 시간을 정합니다 (레코드 순서는 무관). 즉 세그먼트에 하나라도 최신 레코드가 있으면 그 세그먼트 전체가 남습니다.
- 크기 기준 — 기본적으로 비활성화되어 있습니다(
retention.bytes=-1). 활성화하면 파티션 전체 크기가 한도 안으로 들어올 때까지 가장 오래된 세그먼트를 계속 지웁니다.
두 정책이 동시에 켜져 있으면 어느 한쪽으로든 삭제 대상이 된 세그먼트는 삭제됩니다.
segment.ms/segment.bytes가
실제 디스크 보관량을 좌우합니다. 기본값(1 GiB / 7일)에서 왜 "보관 기간을 줄여도 디스크가 안 줄어드는지" 보여줍니다.
리텐션과 세그먼트 설정
| 설정 | 기본값 | 설명 | 튜닝 포인트 |
|---|---|---|---|
retention.ms |
604800000 (7일) |
delete 정책에서 로그를 보관하는 최대 시간. -1이면 무제한 |
공식 문서 표현으로 "컨슈머가 데이터를 얼마나 빨리 읽어야 하는지에 대한 SLA"입니다 |
retention.bytes |
-1 (비활성) |
파티션 하나가 커질 수 있는 최대 크기 | 파티션 레벨입니다. 토픽 전체 한도를 계산하려면 파티션 수를 곱하세요 |
segment.bytes |
1073741824 (1 GiB) |
세그먼트 파일 하나의 최대 크기 | 최소값이 1048576(1 MiB)입니다. 공식 문서: "세그먼트가 크면 파일이 적지만 리텐션 제어가 덜 세밀해집니다" |
segment.ms |
604800000 (7일) |
세그먼트가 꽉 차지 않아도 강제로 롤하는 주기 | 리텐션이 짧은 토픽에서 반드시 함께 줄여야 하는 값 |
segment.jitter.ms |
0 | 예정된 롤 시각에서 무작위로 뺄 최대 지터 | 세그먼트 롤이 한꺼번에 몰리는 thundering herd를 완화합니다 |
segment.index.bytes |
10485760 (10 MiB) |
오프셋 인덱스 최대 크기. 미리 할당되고 롤 시에만 축소됩니다 | 공식 문서: "바꿀 필요가 거의 없습니다" |
file.delete.delay.ms |
60000 (1분) |
파일시스템에서 실제로 지우기 전 대기 시간 | 세그먼트가 삭제 표시된 뒤 실제 용량이 즉시 줄지 않는 이유입니다 |
index.interval.bytes |
4096 | 인덱스 엔트리를 추가하는 간격 | — |
preallocate |
false | 새 세그먼트 생성 시 파일을 미리 할당 | — |
# ✕ 이것만으로는 부족합니다
bin/kafka-configs.sh --bootstrap-server localhost:9092 \
--alter --entity-type topics --entity-name short-lived \
--add-config retention.ms=3600000
# ✓ 세그먼트 롤 주기와 크기를 함께 줄입니다
bin/kafka-configs.sh --bootstrap-server localhost:9092 \
--alter --entity-type topics --entity-name short-lived \
--add-config 'retention.ms=3600000,segment.ms=600000,segment.bytes=134217728'
# 보관 1시간 세그먼트 10분 롤 세그먼트 128MiB
# 현재 적용된 설정 확인 (동적 설정만 보입니다)
bin/kafka-configs.sh --bootstrap-server localhost:9092 \
--describe --entity-type topics --entity-name short-lived
로그 컴팩션
컴팩션의 보장은 한 문장입니다. 공식 문서 표현으로 "로그 컴팩션은 단일 토픽 파티션의 로그 안에서 각 메시지 키에 대해 최소한 마지막으로 알려진 값을 항상 보존한다"는 것입니다. 시간 기반 리텐션이 거친(coarse-grained) 보관이라면, 컴팩션은 레코드별로 더 세밀한(finer-grained) 보관을 제공합니다.
용도는 명확합니다 — 애플리케이션 크래시나 시스템 장애 후 상태 복원, 운영 중 재시작 후 캐시 재적재입니다. 공식 문서는 컴팩션 덕분에 "로그가 최근에 변경된 키뿐 아니라 모든 키의 최종 값에 대한 완전한 스냅샷을 담게 된다"고 설명합니다. 그래서 다운스트림 컨슈머가 전체 변경 이력을 보관하지 않고도 자신의 상태를 복원할 수 있습니다.
head 와 tail
컴팩션을 이해하는 열쇠는 로그를 head와 tail로 나누어 보는 것입니다.
- head — 기존 Kafka 로그와 동일합니다. 오프셋이 촘촘하고 순차적이며 모든 메시지를 보존합니다. 이 구간이 "아직 정리되지 않은(dirty)" 부분입니다.
- tail — 컴팩션이 적용된 구간입니다. 키별 최신 값만 남습니다.
공식 문서가 강조하는 세부사항 두 개입니다.
- tail의 메시지는 처음 쓰였을 때 부여된 오프셋을 그대로 유지합니다. 오프셋은 절대 바뀌지 않습니다.
- 모든 오프셋은 여전히 유효한 위치입니다. 컴팩션으로 사라진 오프셋에서 읽기를 시작하면 그 다음으로 존재하는 오프셋의 메시지부터 반환됩니다. 공식 문서 예시: 오프셋 36, 37, 38이 모두 동등한 위치이고, 그중 어디서 시작해도 38부터 시작하는 메시지 세트를 받습니다.
컴팩션이 보장하는 4가지
| 보장 | 내용 |
|---|---|
| ① head를 따라잡은 컨슈머는 모든 메시지를 봅니다 | head 구간 안에 머무는 컨슈머는 쓰인 모든 메시지를 순차 오프셋으로 봅니다.
min.compaction.lag.ms로 head에 최소 얼마나 남을지를 보장할 수 있고,
max.compaction.lag.ms로 컴팩션 대상이 되기까지의 최대 지연을 보장할 수 있습니다. |
| ② 순서는 항상 유지됩니다 | 컴팩션은 메시지를 재정렬하지 않습니다. 일부를 제거할 뿐입니다. |
| ③ 오프셋은 절대 바뀌지 않습니다 | 오프셋은 로그 안의 위치에 대한 영구 식별자입니다. |
| ④ 처음부터 읽는 컨슈머는 모든 키의 최종 상태를 봅니다 | 추가로, delete.retention.ms보다 짧은 시간 안에 head에 도달하면
삭제된 레코드의 tombstone도 모두 봅니다.
그보다 뒤처지면 tombstone을 놓칠 수 있습니다 — tombstone 제거가 읽기와 동시에 진행되기 때문입니다. |
tombstone — null value 로 삭제를 표현합니다
공식 문서 정의입니다. 키가 있고 페이로드가 null인 메시지는 로그에서의 삭제로 취급되며, 이런 레코드를 tombstone이라고 부릅니다. 이 삭제 마커는 그 키의 이전 메시지를 모두 제거합니다(이후에 오는 같은 키의 메시지도 마찬가지).
그런데 tombstone 자체도 영원히 남지 않습니다.
일정 시간이 지나면 공간을 회수하기 위해 tombstone도 로그에서 정리됩니다.
그 시점을 정하는 것이 delete.retention.ms(기본 86400000, 1일)입니다.
// 키는 반드시 있어야 합니다. value 만 null 입니다.
producer.send(new ProducerRecord<String, String>("user-profiles", userId, null));
// ✕ 키가 null 이면 tombstone 이 아닙니다 — 컴팩션이 대상으로 삼을 키가 없습니다.
producer.send(new ProducerRecord<String, String>("user-profiles", null, null));
언제 컴팩션이 일어나는가 — 즉시가 아닙니다
컴팩션은 로그 클리너(log cleaner)가 처리합니다. 공식 문서 정의로는 "로그 세그먼트 파일을 재복사하면서, 키가 로그 head에 다시 나타나는 레코드를 제거하는 백그라운드 스레드 풀"입니다. 각 클리너 스레드는 다음과 같이 동작합니다.
- log head 대 log tail 비율이 가장 높은 로그를 고릅니다.
- head에 있는 각 키의 마지막 오프셋에 대한 간결한 요약을 만듭니다.
- 로그를 처음부터 끝까지 재복사하면서, 뒤에 다시 나타나는 키를 제거합니다. 새로 만든 깨끗한 세그먼트는 즉시 로그에 교체(swap)되므로, 추가로 필요한 디스크 공간은 세그먼트 하나 분량입니다(로그 전체 복사가 아닙니다).
공식 문서는 요약 자료구조가 엔트리당 정확히 24바이트를 쓰는 해시 테이블이라고 밝힙니다. 그래서 8 GB 클리너 버퍼로 한 번의 반복에 약 366 GB의 log head를 정리할 수 있습니다(1 kB 메시지 기준).
| 설정 | 레벨 | 기본값 | 의미 |
|---|---|---|---|
cleanup.policy |
topic | delete |
delete / compact / delete,compact. 빈 목록이면 무한 보관(어떤 정리도 하지 않음) |
min.cleanable.dirty.ratio |
topic | 0.5 | 정리 대상이 되기 위한 dirty 비율 하한. 공식 문서 표현: "로그의 50% 넘게 컴팩션된 로그는 정리하지 않습니다" — 최대 낭비 공간을 50%로 묶습니다 |
min.compaction.lag.ms |
topic | 0 | 메시지가 최소 이만큼은 uncompacted 상태로 head에 남습니다. head를 따라잡은 컨슈머가 모든 변경을 볼 수 있게 보장합니다 |
max.compaction.lag.ms |
topic | 9223372036854775807 (사실상 무제한) |
메시지가 컴팩션 대상이 되지 않은 채 남을 수 있는 최대 시간. 생산량이 적은 토픽이 무한정 정리되지 않는 것을 막습니다 |
delete.retention.ms |
topic | 86400000 (1일) |
tombstone 마커 보관 시간. 초기 적재 소요 시간의 상한이기도 합니다 |
log.cleaner.threads |
broker | 1 | 클리너 백그라운드 스레드 수. 컴팩션 토픽이 많으면 늘려야 합니다 |
log.cleaner.io.max.bytes.per.second |
broker | 사실상 무제한 | 클리너의 읽기+쓰기 I/O 합계 상한. 프로듀서·컨슈머에 영향을 주지 않도록 스로틀할 수 있습니다 |
log.cleaner.backoff.ms |
broker | 15000 | 정리할 로그가 없을 때의 대기 시간 |
공식 문서는 min.cleanable.dirty.ratio와 두 lag 설정의 결합 규칙을 정확히 서술합니다.
lag 설정이 함께 지정되면, 로그는 다음 중 하나라도 충족하는 즉시 컴팩션 대상이 됩니다.
- dirty 비율 임계값을 충족했고, 로그에 dirty 레코드가 최소
min.compaction.lag.ms동안 있었을 때 - 로그에 dirty 레코드가 최대
max.compaction.lag.ms기간 동안 있었을 때
max.compaction.lag.ms가 설정되지 않으면
min.cleanable.dirty.ratio를 넘지 않는 로그는 컴팩션되지 않습니다.
그리고 이 마감은 강한 보장이 아닙니다 — 클리너 스레드 가용성과 실제 컴팩션 시간에 좌우됩니다.
공식 문서는 uncleanable-partitions-count,
max-clean-time-secs, max-compaction-delay-secs 메트릭을 모니터링하라고 권고합니다.
compact,delete 조합
cleanup.policy는 목록이므로 두 정책을 함께 지정할 수 있습니다.
공식 문서의 설명은 정확합니다 —
"이 경우 오래된 세그먼트는 리텐션 시간·크기 설정에 따라 폐기되고,
남아 있는 세그먼트는 컴팩션된다."
| 값 | 동작 | 대표 용도 |
|---|---|---|
delete 기본값 |
리텐션 시간·크기를 넘긴 세그먼트를 폐기 | 이벤트 스트림, 로그, 메트릭 |
compact |
키별 최신 값을 보존. 시간이 지나도 데이터가 사라지지 않습니다 | 상태 저장 토픽, Streams changelog, Connect 내부 토픽, __consumer_offsets |
compact,delete |
컴팩션 + 오래된 세그먼트 폐기. 키별 최신 값도 리텐션이 지나면 사라집니다 | 키 공간이 무한히 늘어나는 상태 토픽(예: 세션 ID, 주문 ID 키). 컴팩션만으로는 영원히 자랍니다 |
| 빈 목록 | 어떤 정리도 하지 않습니다. 무한 보관 | 감사 로그 등 절대 지우면 안 되는 경우 (디스크 관리를 직접 해야 합니다) |
# 생성 시 지정
bin/kafka-topics.sh --create --topic user-profiles \
--bootstrap-server localhost:9092 \
--partitions 12 --replication-factor 3 \
--config cleanup.policy=compact \
--config min.cleanable.dirty.ratio=0.1 \
--config delete.retention.ms=604800000 \
--config segment.ms=3600000
# 키 공간이 계속 늘어나는 상태 토픽 → compact,delete 로
bin/kafka-configs.sh --bootstrap-server localhost:9092 \
--alter --entity-type topics --entity-name session-state \
--add-config 'cleanup.policy=compact,delete,retention.ms=2592000000'
# 컴팩션 + 30일 지난 세그먼트는 폐기
Tiered Storage
공식 문서가 설명하는 동기는 명확합니다. Kafka 데이터는 대부분 tail read로 소비되며, tail read는 디스크 대신 OS 페이지 캐시가 처리합니다. 오래된 데이터는 백필이나 장애 복구 목적으로 드물게 디스크에서 읽힙니다. 그렇다면 그 오래된 데이터를 굳이 브로커 로컬 디스크에 둘 이유가 없습니다.
Tiered Storage는 클러스터를 로컬 계층과 원격 계층으로 나눕니다. 로컬 계층은 지금까지와 같은 브로커 로컬 디스크이고, 원격 계층은 HDFS나 S3 같은 외부 스토리지에 완료된(봉인된) 로그 세그먼트를 저장합니다(KIP-405).
local.retention.*과 retention.*이 각각 어느 경계를 담당하는지 보여줍니다.
설정 축 — 로컬과 전체가 따로입니다
| 설정 | 레벨 | 기본값 | 의미 |
|---|---|---|---|
remote.log.storage.system.enable |
broker | false | 브로커에서 tiered storage 기능 전체를 켭니다. 기본은 꺼져 있습니다 |
remote.log.storage.manager.class.name |
broker | — | RemoteStorageManager 구현 클래스. Apache Kafka는 구현체를 제공하지 않습니다 — 반드시 지정해야 합니다 |
remote.log.metadata.manager.class.name |
broker | 내부 토픽 기반 구현 | 원격 세그먼트 메타데이터의 수명 관리. 기본 구현은 내부 토픽을 씁니다 |
remote.log.metadata.manager.listener.name |
broker | — | 기본 내부 토픽 구현을 쓸 때 필수입니다 |
remote.storage.enable |
topic | false | 토픽 단위 스위치. 끌 때는 remote.log.delete.on.disable=true도 함께 설정해야 합니다 |
local.retention.ms |
topic | -2 | 로컬 세그먼트 보관 시간. -2는 retention.ms 값을 쓴다는 뜻입니다. 유효값은 항상 retention.ms 이하여야 합니다 |
local.retention.bytes |
topic | -2 | 로컬 세그먼트 보관 크기. -2는 retention.bytes를 씁니다 |
remote.fetch.max.wait.ms |
broker | 500 | 원격 fetch 파티션이 포함된 요청의 응답 대기 상한. 모든 원격 파티션이 완료·실패하거나 이 시간이 지나야 응답합니다 |
제약 — 시험과 실무 모두에서 중요합니다
# 브로커 쪽이 이미 remote.log.storage.system.enable=true 인 상태에서
bin/kafka-topics.sh --create --topic tiered-events \
--bootstrap-server localhost:9092 \
--config remote.storage.enable=true \
--config local.retention.ms=3600000 \
--config retention.ms=2592000000
# 로컬 1시간 전체(원격 포함) 30일
# 원격 로그를 읽기 전용으로 만들고 더 이상 업로드하지 않기
bin/kafka-configs.sh --bootstrap-server localhost:9092 \
--alter --entity-type topics --entity-name tiered-events \
--add-config 'remote.storage.enable=true,remote.log.copy.disable=true,local.retention.ms=-2,local.retention.bytes=-2'
# ↑ local.retention 을 -2 로 되돌리는 이유: 복사가 꺼지면 로컬 리텐션 정책이
# 더 이상 적용되지 않아 예상치 못한 디스크 풀이 생길 수 있습니다 (공식 문서 경고)
# tiered storage 를 완전히 끄고 원격 로그도 삭제
bin/kafka-configs.sh --bootstrap-server localhost:9092 \
--alter --entity-type topics --entity-name tiered-events \
--add-config 'remote.storage.enable=false,remote.log.delete.on.disable=true'
디스크 용량 산정
용량 산정의 기본 식은 단순합니다. 다만 실제로 필요한 값보다 항상 크게 나오는 항목들이 있어 그 부분을 빠뜨리면 배포 후에 디스크가 부족해집니다.
파티션당 저장량 = 초당 유입 바이트 / 파티션 수 × 보관 기간(초) × (1 - 압축률)
브로커당 필요 용량 = 파티션당 저장량 × 복제 계수 × (브로커당 파티션 수) + 여유
전체 클러스터 필요 용량
= 초당 유입 바이트 × 보관 기간(초) × 복제 계수 × (1 - 압축률) × (1 + 여유율)
예) 유입 50 MB/s, 보관 7일, RF=3, 압축률 70%(=0.7), 여유 30%
50 MB/s × 604800 s = 30,240,000 MB ≈ 30.24 TB (원본, 압축 전)
30.24 TB × (1 - 0.7) = 9.07 TB (압축 후)
9.07 TB × 3 = 27.2 TB (복제 포함)
27.2 TB × 1.3 = 35.4 TB (여유 30%)
→ 브로커 6대면 대당 약 5.9 TB
산정에서 빠뜨리기 쉬운 항목
| 항목 | 왜 필요한가 | 권장 |
|---|---|---|
| 세그먼트 롤 오차 | 삭제가 세그먼트 단위이므로 항상 리텐션보다 조금 더 보관됩니다. 최악의 경우 세그먼트 하나 분량이 파티션마다 여분으로 남습니다 | segment.bytes × 파티션 수를 더하세요 |
| 인덱스 파일 | segment.index.bytes(기본 10 MiB)가 미리 할당되고 롤 시점에만 축소됩니다. 활성 세그먼트마다 오프셋 인덱스와 타임 인덱스가 있습니다 |
파티션 수 × 20 MiB 정도를 여유로 |
| 컴팩션 작업 공간 | 클리너가 세그먼트를 재복사하며 세그먼트 하나 분량의 추가 공간을 씁니다 | 컴팩션 토픽이 있으면 segment.bytes × 클리너 스레드 수 |
| 복구 여유 | 브로커 한 대가 빠지면 남은 브로커가 그 파티션의 리더/팔로워를 인수합니다 | 디스크 사용률을 60~70% 이하로 유지 |
| 파티션 재할당 중 이중 보관 | kafka-reassign-partitions 실행 중에는 기존 위치와 새 위치에 동시에 데이터가 존재합니다 |
이동시킬 파티션 크기만큼 여유 확보 |
file.delete.delay.ms |
삭제 표시 후 기본 1분 동안 파일이 그대로 있습니다 | 급격한 유입 스파이크에서는 이 지연도 계산에 넣으세요 |
kafka-log-dirsbin/kafka-log-dirs.sh --bootstrap-server localhost:9092 \
--describe --topic-list orders,payments
# 파티션별 size 가 JSON 으로 나옵니다. 브로커별 편중을 여기서 확인합니다.
페이지 캐시와 zero-copy
Kafka가 JVM 위에서 동작하면서도 높은 처리량을 내는 이유는 자체 캐시를 만들지 않고 OS에 맡긴 설계에 있습니다. 공식 설계 문서의 서술을 따라가면 근거가 분명합니다.
페이지 캐시 — 캐시를 직접 만들지 않는 선택
현대 OS는 남는 메모리 전부를 디스크 캐시로 돌리는 데 거의 주저하지 않습니다. 모든 디스크 읽기와 쓰기는 이 통합 캐시를 통과하고, 직접 I/O를 쓰지 않으면 이 기능을 끌 수 없습니다. 따라서 프로세스가 자체 캐시를 유지하면 같은 데이터가 OS 페이지 캐시에 중복 저장됩니다.
공식 문서가 정리하는 결론은 세 가지입니다.
- 남는 메모리 전체를 자동으로 쓸 수 있으므로 가용 캐시가 최소 2배가 됩니다.
- 개별 객체가 아니라 간결한 바이트 구조를 저장하므로 다시 2배 가까이 이득입니다. 공식 문서 예: 32 GB 머신에서 GC 부담 없이 28~30 GB 캐시를 얻습니다.
- 서비스가 재시작해도 캐시가 warm하게 유지됩니다. in-process 캐시는 재구축해야 하며(10 GB 캐시면 10분이 걸릴 수 있음) 아니면 완전히 cold하게 시작합니다.
그래서 Kafka의 설계는 뒤집혀 있습니다 — 메모리에 최대한 담아 두고 공간이 부족해질 때 허둥지둥 flush하는 대신, 모든 데이터를 즉시 파일시스템의 영속 로그에 씁니다(반드시 디스크로 flush하지는 않고). 실질적으로는 커널의 페이지 캐시로 전달되는 것입니다.
zero-copy — sendfile 로 복사 4번을 1번으로
브로커가 유지하는 메시지 로그는 프로듀서와 컨슈머가 쓰는 것과 같은 형식의
메시지 세트가 순차적으로 쓰인 파일 디렉터리일 뿐입니다.
이 공통 형식이 가장 중요한 최적화를 가능하게 합니다 —
페이지 캐시에서 소켓으로의 네트워크 전송입니다.
Linux에서는 sendfile 시스템 콜이 이를 담당합니다.
공식 문서가 대조하는 두 경로입니다.
| 방식 | 복사 횟수 | 경로 |
|---|---|---|
| 일반 경로 | 4회 복사 + 시스템 콜 2회 | 디스크 → 커널 페이지 캐시 → 유저 공간 버퍼 → 커널 소켓 버퍼 → NIC 버퍼 |
sendfile (zero-copy) |
NIC 버퍼로의 마지막 복사만 | 커널 페이지 캐시 → NIC 버퍼 (OS가 직접 전송) |
효과가 특히 큰 경우는 한 토픽에 컨슈머가 여럿일 때입니다. 데이터는 페이지 캐시로 정확히 한 번만 복사되고 소비마다 재사용되므로, 유저 공간으로 매번 복사할 필요가 없습니다. 공식 문서 표현으로 "메시지 소비 속도가 네트워크 연결의 한계에 근접"합니다. 그리고 컨슈머가 대체로 따라잡은 클러스터에서는 디스크 읽기 활동이 전혀 관찰되지 않습니다 — 전부 캐시에서 서빙되기 때문입니다.
흔한 오해
시험 포인트 요약
확인 문제
세그먼트 단위 삭제, 활성 세그먼트의 예외성, tombstone과 delete.retention.ms,
retention.bytes가 파티션 단위라는 점이 집중적으로 나옵니다.
이어서 볼 곳
- 8장 · 스키마와 직렬화 Schema Registry, wire format, 호환성 매트릭스와 배포 순서.
- 2장 · 아키텍처와 핵심 개념 토픽·파티션·세그먼트 계층과 오프셋 4종, 복제와 ISR.
- 케이스 8 · cleanup.policy 를 잘못 바꿨다 컴팩션 토픽을 delete 로 바꿨을 때 상태 복원이 불가능해진 사고.
- 케이스 10 · 메시지 크기 설정 5종의 불일치 브로커·토픽·프로듀서·컨슈머 설정이 어긋나 전송이 실패하는 과정.
- 설정 치트시트 토픽·브로커 설정 전체 표와 처리량/지연/내구성 프리셋.
- CCDAK · Fundamentals 23% 도메인의 압축 정리와 외워야 할 기본값 표.
공식 문서 출처
이 장의 설정 기본값·동작·인용 문장은 모두 아래에서 확인했습니다 (Apache Kafka 4.3 문서 기준).
- Implementation — Log — 파티션 디렉터리 구조, 세그먼트 파일 이름 규칙, "데이터는 로그 세그먼트 단위로 삭제된다", 시간 기준 판정에 세그먼트의 최대 타임스탬프를 쓴다는 점, 크기 기준 리텐션이 기본 비활성, CRC 기반 복구
- Design — Log Compaction — 컴팩션의 정의와 용도, head/tail 구조, 오프셋 불변성, tombstone과 delete retention point, 4가지 보장, 로그 클리너 동작 3단계와 24바이트 요약 테이블, 활성 세그먼트가 컴팩션 대상이 아니라는 서술, lag 설정의 결합 규칙, 권장 모니터링 메트릭
- Design — Persistence — 페이지 캐시 중심 설계, 32 GB 머신에서 28~30 GB 캐시, 재시작 후 warm 캐시
- Design — Efficiency —
sendfile과 복사 4회 → 1회, 다중 컨슈머에서의 재사용, SSL 활성 시sendfile미사용 - Topic Configs —
retention.ms(604800000),retention.bytes(-1, 파티션 단위),segment.bytes(1073741824),segment.ms(604800000),segment.index.bytes(10485760),index.interval.bytes(4096),cleanup.policy(delete),min.cleanable.dirty.ratio(0.5),delete.retention.ms(86400000),min/max.compaction.lag.ms,file.delete.delay.ms(60000),max.message.bytes(1048588),local.retention.ms/bytes(-2),remote.storage.enable(false),compression.type(producer),flush.messages/flush.ms - Broker Configs —
log.segment.bytes,log.retention.hours(168),log.cleaner.threads(1),log.cleaner.enable(true, deprecated),log.cleaner.io.max.bytes.per.second,log.cleaner.backoff.ms(15000),num.recovery.threads.per.data.dir(2),remote.log.storage.system.enable(false),remote.fetch.max.wait.ms(500) - Operations — Tiered Storage — tail read 동기, 로컬/원격 2계층, KIP-405,
RemoteStorageManager를 Apache Kafka가 제공하지 않는다는 점, 원격 업로드 후에야 로컬 삭제 가능, 끄기 절차, 제약 4가지(컴팩션 토픽 미지원 포함) - Operations — Basic Kafka Operations —
kafka-configs.sh·kafka-log-dirs.sh사용법 - Operations — KRaft (Debugging) —
kafka-dump-log.sh의--cluster-metadata-decoder사용법