학습 목표

로그 세그먼트의 물리 구조

공식 구현 문서는 저장 구조를 이렇게 설명합니다. 파티션이 두 개인 my-topic이라는 토픽의 로그는 my-topic-0my-topic-1 두 디렉터리로 구성됩니다. 각 로그 파일은 자신이 담고 있는 첫 메시지의 오프셋으로 이름이 지어지며, 그래서 처음 생성되는 파일은 00000000000000000000.log가 됩니다. 이후 파일은 설정된 최대 로그 파일 크기(S)만큼 떨어진 정수 이름을 갖습니다.

로그 세그먼트 물리 구조 — 봉인된 세그먼트와 활성 세그먼트 파티션 디렉터리 orders-0 안에 세그먼트가 세 개 있는 모습을 그린 그림입니다. 각 세그먼트는 자신이 담은 첫 오프셋을 20자리 0 채움 숫자로 파일명에 쓰고, 같은 이름에 확장자만 다른 파일 세 개로 이루어집니다. .log 는 레코드 배치 본문, .index 는 오프셋에서 파일 안 물리 위치로 가는 희소 인덱스, .timeindex 는 타임스탬프에서 오프셋으로 가는 인덱스입니다. 트랜잭션이 쓰이면 .txnindex, 멱등 프로듀서 상태를 위해 .snapshot 파일도 함께 생깁니다. 앞의 두 세그먼트는 봉인되어 읽기 전용이고, 마지막 하나만 활성 세그먼트로 쓰기를 받습니다. 활성 세그먼트는 segment.bytes 기본값 1기비바이트에 도달하거나 segment.ms 기본값 7일이 지나면 새 세그먼트로 롤되고 봉인됩니다. 활성 세그먼트는 리텐션 삭제 대상도 아니고 컴팩션 대상도 아닙니다. 로그 세그먼트 물리 구조 — 파티션 하나가 디스크에서 어떻게 생겼나 /var/lib/kafka-logs/orders-0/ 00000000000000000000 오프셋 0 ~ 9131 .log 레코드 배치 .index 오프셋 → 위치 .timeindex 시각 → 오프셋 봉인 · 읽기전용 00000000000000009132 오프셋 9132 ~ 18263 .log .index .timeindex .txnindex .snapshot 필요할 때만 봉인 · 읽기전용 00000000000000018264 오프셋 18264 ~ (증가 중) .log ← 여기에만 append .index .timeindex 활성 세그먼트 · 쓰기중 새 세그먼트로 롤되는 조건 (둘 중 먼저) segment.bytes 기본 1073741824 (1 GiB) segment.ms 기본 604800000 (7일) 활성 세그먼트는 예외입니다 리텐션 삭제 대상이 아닙니다 (D-061) 컴팩션 대상도 아닙니다 (D-062) 파일명은 그 세그먼트가 담은 첫 오프셋을 20자리로 채운 값입니다. .index 는 모든 레코드가 아니라 일정 간격마다 기록하는 희소 인덱스입니다. segment.index.bytes 기본 10485760 (10 MiB) · 읽기는 파일명으로 이진 탐색해 세그먼트를 고릅니다.
로그 세그먼트 물리 구조 — 파티션 디렉터리 안의 봉인된 세그먼트들과 하나뿐인 활성 세그먼트, 그리고 각 세그먼트에 딸린 인덱스 파일들의 관계
파티션 디렉터리의 실제 내용 — 세그먼트마다 파일 세트가 있습니다
$ 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를 허용한다"고 서술합니다. 활성 세그먼트가 특별한 이유는 두 가지입니다.

세그먼트 내부를 직접 열어 보기

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 삭제 단위 — 레코드가 아니라 세그먼트 단위로 지워집니다 시간축 위에 네 개의 세그먼트로 이루어진 파티션 로그를 그린 그림입니다. 가로축은 10일 전부터 지금까지이고, retention.ms 기본값 604800000 즉 7일에 해당하는 지금에서 7일 전 지점에 세로 경계선이 그려져 있습니다. 세그먼트 S0 과 S1 은 세그먼트 안의 가장 큰 타임스탬프가 경계보다 오래되어 세그먼트 전체가 삭제됩니다. 세그먼트 S2 는 8일 전부터 3일 전까지의 레코드를 담고 있습니다. 이 세그먼트의 가장 큰 타임스탬프는 3일 전이므로 아직 보관 대상입니다. 그 결과 S2 안에 있는 8일 전, 7일 반 전 레코드까지 경계를 넘겼는데도 함께 남습니다. 세그먼트 S3 은 활성 세그먼트여서 애초에 삭제 대상이 아닙니다. 즉 실제 보관량은 retention.ms 만으로 정해지지 않고 segment.ms 와 segment.bytes 가 함께 정합니다. 삭제는 레코드 단위로 일어나지 않습니다. retention 삭제 단위 — 삭제는 세그먼트 통째로 일어납니다 retention.ms 경계 = 지금 − 7일 파티션 로그 orders-0 S0 S1 S2 — 8일 전 ~ 3일 전 레코드 S3 (활성) 10일 전 8일 전 6일 전 4일 전 2일 전 지금 판정 세그먼트 삭제 보관 — 최대 타임스탬프(3일 전)가 경계 안쪽 활성 — 삭제 대상 아님 핵심 — S2 안의 8일 전 · 7일 반 전 레코드는 경계를 넘겼는데도 남아 있습니다. 시간 판정은 세그먼트 안 가장 큰 타임스탬프 하나로 합니다. 레코드마다 따로 지우지 않습니다. 그래서 실제 보관량은 항상 retention.ms 이상이 됩니다. 보관량을 실제로 좌우하는 설정 segment.ms 기본 604800000 (7일) segment.bytes 기본 1073741824 (1 GiB) 보관 기준 설정 retention.ms 기본 604800000 (7일) retention.bytes 기본 −1 (크기 제한 없음) 7일이 지나면 정확히 사라진다고 기대하려면 segment.ms 를 보관 기간보다 훨씬 짧게 잡아야 합니다.
retention 삭제 단위 — 세그먼트 단위로 삭제되기 때문에 segment.ms/segment.bytes가 실제 디스크 보관량을 좌우합니다. 기본값(1 GiB / 7일)에서 왜 "보관 기간을 줄여도 디스크가 안 줄어드는지" 보여줍니다.

리텐션과 세그먼트 설정

리텐션·세그먼트 토픽 설정 (Apache Kafka 4.3 기본값)
설정 기본값 설명 튜닝 포인트
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) 보관을 제공합니다.

용도는 명확합니다 — 애플리케이션 크래시나 시스템 장애 후 상태 복원, 운영 중 재시작 후 캐시 재적재입니다. 공식 문서는 컴팩션 덕분에 "로그가 최근에 변경된 키뿐 아니라 모든 키의 최종 값에 대한 완전한 스냅샷을 담게 된다"고 설명합니다. 그래서 다운스트림 컨슈머가 전체 변경 이력을 보관하지 않고도 자신의 상태를 복원할 수 있습니다.

로그 컴팩션 before / after — 키별 최신값만 남고 활성 세그먼트는 제외됩니다 오프셋 0부터 11까지 열두 개의 레코드가 세그먼트 세 개에 나뉘어 있는 로그입니다. S0 은 오프셋 0에서 3, S1 은 4에서 7 로 둘 다 봉인된 세그먼트이고, S2 는 오프셋 8에서 11 인 활성 세그먼트입니다. 레코드는 순서대로 A=1, B=1, A=2, C=1, B=2, A=3, B=null, D=1, A=4, C=2, B=3, D=2 입니다. B=null 은 tombstone 즉 삭제 표시입니다. 컴팩션을 실행하면 봉인된 세그먼트 범위에서만 키별 최신값이 남습니다. 오프셋 0, 1, 2, 4 가 사라지고 C=1, A=3, B=null, D=1 이 남습니다. tombstone 인 B=null 은 즉시 사라지지 않고 delete.retention.ms 기본값 86400000 즉 하루가 지난 뒤에 제거됩니다. 활성 세그먼트 S2 는 컴팩션 대상이 아니므로 A=4, C=2, B=3, D=2 는 중복이든 아니든 그대로 남습니다. 사라진 오프셋 번호는 재사용되지 않으며, 읽기는 그 자리를 건너뜁니다. 컨트롤로 컴팩션 실행과 delete.retention.ms 경과를 차례로 적용해 볼 수 있습니다. 로그 컴팩션 — cleanup.policy=compact 일 때 무엇이 남는가 1단계 — 컴팩션 전: 같은 키가 여러 번 나타난 채로 전부 남아 있습니다 S0 · 봉인 — 컴팩션 대상 0 A = 1 1 B = 1 2 A = 2 3 C = 1 S1 · 봉인 — 컴팩션 대상 4 B = 2 5 A = 3 6 B null 7 D = 1 S2 · 활성 — 컴팩션 대상 아님 8 A = 4 9 C = 2 10 B = 3 11 D = 2 봉인된 S0·S1 에서 A 는 0·2·5 에, B 는 1·4·6 에 나타납니다. 컴팩션은 이 범위에서 키별 마지막 것만 남깁니다. 활성 세그먼트 S2 의 A=4 · B=3 은 중복이지만 손대지 않습니다. 봉인된 뒤에야 대상이 됩니다. tombstone (value = null) 그 키의 이전 값들을 지우는 삭제 표시입니다. 자신은 delete.retention.ms 뒤에 사라집니다. 기본값 cleanup.policy = delete delete.retention.ms = 86400000 (1일) 사라진 오프셋 번호는 재사용되지 않습니다. 읽기는 그 자리를 건너뛰므로 오프셋이 띄어져 보이는 것은 정상입니다.
컴팩션 before/after — 키가 중복된 로그가 컴팩션을 거쳐 키별 최신값만 남는 과정. tombstone 처리와 활성 세그먼트가 제외되는 것을 함께 표시합니다. 버튼으로 컴팩션을 실행해 로그 head와 tail의 변화를 관찰합니다.

head 와 tail

컴팩션을 이해하는 열쇠는 로그를 headtail로 나누어 보는 것입니다.

공식 문서가 강조하는 세부사항 두 개입니다.

컴팩션이 보장하는 4가지

공식 문서가 명시하는 컴팩션 보장
보장내용
① head를 따라잡은 컨슈머는 모든 메시지를 봅니다 head 구간 안에 머무는 컨슈머는 쓰인 모든 메시지를 순차 오프셋으로 봅니다. min.compaction.lag.mshead에 최소 얼마나 남을지를 보장할 수 있고, max.compaction.lag.ms컴팩션 대상이 되기까지의 최대 지연을 보장할 수 있습니다.
② 순서는 항상 유지됩니다 컴팩션은 메시지를 재정렬하지 않습니다. 일부를 제거할 뿐입니다.
③ 오프셋은 절대 바뀌지 않습니다 오프셋은 로그 안의 위치에 대한 영구 식별자입니다.
④ 처음부터 읽는 컨슈머는 모든 키의 최종 상태를 봅니다 추가로, delete.retention.ms보다 짧은 시간 안에 head에 도달하면 삭제된 레코드의 tombstone도 모두 봅니다. 그보다 뒤처지면 tombstone을 놓칠 수 있습니다 — tombstone 제거가 읽기와 동시에 진행되기 때문입니다.

tombstone — null value 로 삭제를 표현합니다

공식 문서 정의입니다. 키가 있고 페이로드가 null인 메시지는 로그에서의 삭제로 취급되며, 이런 레코드를 tombstone이라고 부릅니다. 이 삭제 마커는 그 키의 이전 메시지를 모두 제거합니다(이후에 오는 같은 키의 메시지도 마찬가지).

그런데 tombstone 자체도 영원히 남지 않습니다. 일정 시간이 지나면 공간을 회수하기 위해 tombstone도 로그에서 정리됩니다. 그 시점을 정하는 것이 delete.retention.ms(기본 86400000, 1일)입니다.

tombstone 전송 — value 를 null 로
// 키는 반드시 있어야 합니다. 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에 다시 나타나는 레코드를 제거하는 백그라운드 스레드 풀"입니다. 각 클리너 스레드는 다음과 같이 동작합니다.

  1. log head 대 log tail 비율이 가장 높은 로그를 고릅니다.
  2. head에 있는 각 키의 마지막 오프셋에 대한 간결한 요약을 만듭니다.
  3. 로그를 처음부터 끝까지 재복사하면서, 뒤에 다시 나타나는 키를 제거합니다. 새로 만든 깨끗한 세그먼트는 즉시 로그에 교체(swap)되므로, 추가로 필요한 디스크 공간은 세그먼트 하나 분량입니다(로그 전체 복사가 아닙니다).

공식 문서는 요약 자료구조가 엔트리당 정확히 24바이트를 쓰는 해시 테이블이라고 밝힙니다. 그래서 8 GB 클리너 버퍼로 한 번의 반복에 약 366 GB의 log head를 정리할 수 있습니다(1 kB 메시지 기준).

컴팩션 관련 설정 (Apache Kafka 4.3 기본값)
설정 레벨 기본값 의미
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 설정이 함께 지정되면, 로그는 다음 중 하나라도 충족하는 즉시 컴팩션 대상이 됩니다.

  1. dirty 비율 임계값을 충족했고, 로그에 dirty 레코드가 최소 min.compaction.lag.ms 동안 있었을 때
  2. 로그에 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목록이므로 두 정책을 함께 지정할 수 있습니다. 공식 문서의 설명은 정확합니다 — "이 경우 오래된 세그먼트는 리텐션 시간·크기 설정에 따라 폐기되고, 남아 있는 세그먼트는 컴팩션된다."

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).

Tiered Storage — 로컬 계층과 원격 계층 파티션 로그의 세그먼트 일곱 개를 오래된 것부터 최신 순으로 늘어놓고 앞의 네 개는 원격 계층, 뒤의 세 개는 로컬 계층에 있는 모습을 그린 그림입니다. 경계는 local.retention.ms 또는 local.retention.bytes 입니다. 이 경계를 넘긴 세그먼트는 원격 스토리지로 업로드된 뒤 로컬 디스크에서 삭제되고, 전체 보관 기간은 retention.ms 가 정합니다. 원격 계층은 S3 나 HDFS 같은 외부 스토리지이며 RemoteStorageManager 구현을 사용자가 지정해야 합니다. 원격 세그먼트의 메타데이터는 기본 구현에서 내부 토픽 __remote_log_metadata 에 저장됩니다. 최신 데이터를 따라 읽는 컨슈머는 로컬 디스크와 OS 페이지 캐시에서 읽습니다. 과거 데이터를 재처리하는 컨슈머는 원격 계층에서 읽습니다. 브로커 설정 remote.log.storage.system.enable 과 토픽 설정 remote.storage.enable 은 둘 다 기본값이 false 입니다. 컴팩션 토픽은 Tiered Storage 를 지원하지 않습니다. Tiered Storage — 오래된 세그먼트를 원격 계층으로 옮깁니다 파티션 events-0 오래된 세그먼트 최신 · 활성 S0 S1 S2 S3 S4 S5 S6 활성 local.retention 경계 → 원격 계층 — S3 · HDFS 등 외부 스토리지 RemoteStorageManager 구현을 지정해야 합니다 메타데이터 기본 저장소: __remote_log_metadata 로컬 계층 — 브로커 디스크 활성 세그먼트 + 최근 세그먼트 OS 페이지 캐시로 빠른 tail read 과거 재처리 컨슈머 (backfill) 원격에서 가져오므로 지연이 큽니다 실시간 컨슈머 (tail read) 거의 항상 로컬·캐시에서 처리 기본값 — 켜지 않으면 동작하지 않습니다 remote.log.storage.system.enable = false 브로커 전체 스위치 remote.storage.enable = false 토픽별 스위치 local.retention.ms = -2 −2 는 retention.ms 값을 그대로 씁니다 제약: 컴팩션 토픽은 지원하지 않습니다. 브로커 레벨을 끄기 전에 모든 토픽에서 먼저 해제해야 합니다.
Tiered Storage — 로컬 디스크의 최근 세그먼트와 원격 스토리지의 오래된 세그먼트가 이루는 두 계층. local.retention.*retention.*이 각각 어느 경계를 담당하는지 보여줍니다.

설정 축 — 로컬과 전체가 따로입니다

Tiered Storage 설정 (Apache Kafka 4.3)
설정 레벨 기본값 의미
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 로컬 세그먼트 보관 시간. -2retention.ms 값을 쓴다는 뜻입니다. 유효값은 항상 retention.ms 이하여야 합니다
local.retention.bytes topic -2 로컬 세그먼트 보관 크기. -2retention.bytes를 씁니다
remote.fetch.max.wait.ms broker 500 원격 fetch 파티션이 포함된 요청의 응답 대기 상한. 모든 원격 파티션이 완료·실패하거나 이 시간이 지나야 응답합니다

제약 — 시험과 실무 모두에서 중요합니다

토픽 단위 tiered storage 켜기·끄기
# 브로커 쪽이 이미 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
디스크 산정 계산기 — 처리량·보관기간·복제·압축에서 필요 용량 구하기 처리량, 평균 메시지 크기, 보관 기간, replication.factor, 압축 절감률, 여유율, 브로커 수를 입력해 필요한 디스크 용량을 계산하는 그림입니다. 계산식은 일일 유입량이 초당 메시지 수 곱하기 평균 메시지 크기 곱하기 86400 이고, 필요 용량은 일일 유입량 곱하기 보관 일수 곱하기 1에서 압축 절감률을 뺀 값 곱하기 replication.factor 를 1에서 여유율을 뺀 값으로 나눈 것입니다. 기본 입력은 초당 5000건, 1024바이트, 7일 보관, replication.factor 3, 압축 절감 40퍼센트, 여유 30퍼센트, 브로커 3대입니다. 이때 원본은 약 3.1테라바이트, 압축 후 약 1.86테라바이트, 복제를 곱하면 약 5.57테라바이트, 여유를 더한 클러스터 총 필요량은 약 7.96테라바이트, 브로커 한 대당 약 2.65테라바이트입니다. 막대 길이는 총 필요량을 기준으로 비례합니다. 인덱스 파일, 트랜잭션 상태, 내부 토픽은 계산에 포함되지 않았습니다. 디스크 산정 — 압축과 복제를 빼먹으면 항상 부족해집니다 일일 유입 = 초당 메시지 수 × 평균 메시지 크기 × 86400 필요 용량 = 일일 유입 × 보관일수 × (1 − 압축절감률) × replication.factor ÷ (1 − 여유율) 입력: 초당 5,000건 × 1.0 KiB · 보관 7일 · replication.factor 3 압축 절감 40% · 여유 30% · 브로커 3대 · 초당 5.1 MB/s · 일일 442 GB 원본 (압축·복제 전) 3.10 TB 압축 후 1.86 TB × replication.factor 5.57 TB + 여유 = 클러스터 총량 7.96 TB 브로커 1대당 필요 디스크 2.65 TB TB = 1000 GB 기준입니다. 압축 절감률은 데이터 성격에 따라 크게 달라지므로 실측값을 넣으세요. 인덱스 파일·트랜잭션 상태·내부 토픽·리밸런스 중 임시 증가분은 빠져 있습니다 — 여유율로 흡수합니다. 파티션이 브로커에 고르게 분포한다고 가정했습니다. 편중되면 특정 브로커가 먼저 찹니다.
디스크 산정 계산기 — 처리량 · 보관 기간 · 복제 계수 · 압축률을 조절해 필요한 클러스터 용량과 브로커당 용량이 어떻게 변하는지 확인합니다.

산정에서 빠뜨리기 쉬운 항목

기본 식에 더해야 하는 항목
항목왜 필요한가권장
세그먼트 롤 오차 삭제가 세그먼트 단위이므로 항상 리텐션보다 조금 더 보관됩니다. 최악의 경우 세그먼트 하나 분량이 파티션마다 여분으로 남습니다 segment.bytes × 파티션 수를 더하세요
인덱스 파일 segment.index.bytes(기본 10 MiB)가 미리 할당되고 롤 시점에만 축소됩니다. 활성 세그먼트마다 오프셋 인덱스와 타임 인덱스가 있습니다 파티션 수 × 20 MiB 정도를 여유로
컴팩션 작업 공간 클리너가 세그먼트를 재복사하며 세그먼트 하나 분량의 추가 공간을 씁니다 컴팩션 토픽이 있으면 segment.bytes × 클리너 스레드 수
복구 여유 브로커 한 대가 빠지면 남은 브로커가 그 파티션의 리더/팔로워를 인수합니다 디스크 사용률을 60~70% 이하로 유지
파티션 재할당 중 이중 보관 kafka-reassign-partitions 실행 중에는 기존 위치와 새 위치에 동시에 데이터가 존재합니다 이동시킬 파티션 크기만큼 여유 확보
file.delete.delay.ms 삭제 표시 후 기본 1분 동안 파일이 그대로 있습니다 급격한 유입 스파이크에서는 이 지연도 계산에 넣으세요
실제 사용량 확인 — kafka-log-dirs
bin/kafka-log-dirs.sh --bootstrap-server localhost:9092 \
  --describe --topic-list orders,payments

# 파티션별 size 가 JSON 으로 나옵니다. 브로커별 편중을 여기서 확인합니다.

페이지 캐시와 zero-copy

Kafka가 JVM 위에서 동작하면서도 높은 처리량을 내는 이유는 자체 캐시를 만들지 않고 OS에 맡긴 설계에 있습니다. 공식 설계 문서의 서술을 따라가면 근거가 분명합니다.

페이지 캐시 — 캐시를 직접 만들지 않는 선택

현대 OS는 남는 메모리 전부를 디스크 캐시로 돌리는 데 거의 주저하지 않습니다. 모든 디스크 읽기와 쓰기는 이 통합 캐시를 통과하고, 직접 I/O를 쓰지 않으면 이 기능을 끌 수 없습니다. 따라서 프로세스가 자체 캐시를 유지하면 같은 데이터가 OS 페이지 캐시에 중복 저장됩니다.

공식 문서가 정리하는 결론은 세 가지입니다.

그래서 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가 파티션 단위라는 점이 집중적으로 나옵니다.

공식 문서 출처

이 장의 설정 기본값·동작·인용 문장은 모두 아래에서 확인했습니다 (Apache Kafka 4.3 문서 기준).