백엔드 데이터 인프라 108편 — Kafka Log 파일 구조 (Segment · Index · Offset)

2026-05-17백엔드 데이터 인프라

백엔드 데이터 인프라 108편. Kafka Log 파일의 바이트 단위 구조 — partition 디렉토리·segment 파일·index·timeindex 파일·offset 의 의미·binary search lookup·kafka-dump-log.sh 운영 디버깅까지 풀어쓴 학습 노트.

📚 백엔드 데이터 인프라 · 108편 — Kafka Log 파일 구조 (Segment · Index · Offset)

이 글은 백엔드 데이터 인프라 시리즈 130편 중 108편이에요. Part 5-6 고급 인프라 까지 끝났다면, 이번 108편부터는 Part 5-7 — Kafka Internals (5편)로 들어갑니다. 가장 깊은 내부 영역이고, 첫 글은 Log 파일 구조 — 카프카가 디스크에 실제로 어떻게 데이터를 쌓는가입니다.

Internals가 어렵게 느껴지는 이유

운영 도구나 API보다 한 단계 더 깊은 영역이에요. "Kafka가 디스크에 어떻게 데이터를 쓰는가" 같은 바이트 단위 이야기라서 그렇습니다.

첫째, 일상에서 자주 다루지 않습니다. 디버깅이나 성능 분석에만 쓰이거든요. 그래도 알아두면 깊은 운영 사고가 났을 때 직접 풀어낼 수 있어요.

둘째, 공식 문서가 짧습니다. implementation 영역은 설명이 간결한 편이고, 자세한 건 코드·KIP(Kafka Improvement Proposal, 기능 제안 문서)·source 분석으로 보충해야 합니다.

이 글에서 log 파일에 대한 거의 모든 걸 정리할게요.

Partition Directory 구조

각 partition은 별도 디렉토리로 떨어집니다.

/var/kafka-data/
├── my-topic-0/                # partition 0
│   ├── 00000000000000000000.log         # segment file
│   ├── 00000000000000000000.index       # offset index
│   ├── 00000000000000000000.timeindex   # time index
│   ├── 00000000000000123456.log
│   ├── 00000000000000123456.index
│   ├── 00000000000000123456.timeindex
│   └── leader-epoch-checkpoint
├── my-topic-1/                # partition 1
│   └── ...
└── ...

핵심은 세 가지예요. 디렉토리 이름은 <topic>-<partition> 형태로 붙고, 한 partition 안에 segment file(데이터를 일정 크기로 잘라 저장한 청크 파일)이 여러 개 들어 있어요. 그리고 segment 파일 이름은 그 파일에 들어있는 첫 메시지의 offset을 20자리 0-padded로 표현한 값입니다.

Segment File — .log

구조

log entry 1: [4-byte length N][N bytes message]
log entry 2: [4-byte length M][M bytes message]
log entry 3: [4-byte length K][K bytes message]
...

각 메시지는 앞에 4 byte length prefix(뒤따라올 본문의 길이)가 붙고, 그 뒤로 N bytes의 메시지 본문(key·value·timestamp·headers 등)이 이어집니다.

Offset

각 메시지의 unique ID는 64-bit integer offset이에요. partition 안에서 처음부터 0, 1, 2, … 순차로 증가하고, byte position이 아니라 logical position(몇 번째 메시지인가)입니다.

Segment Roll

log.segment.bytes=1073741824    # 1GB 도달 시 새 segment
log.segment.ms=604800000         # 7일 후 강제 roll

조건을 충족하면 현재 segment를 닫고 새 segment를 만들어요. append를 받는 건 언제나 active segment 하나뿐입니다.

Deletion

retention.ms 또는 retention.bytes를 초과하면 segment를 통째로 삭제합니다. 메시지를 하나씩 지우지 않는 이유는 그쪽이 훨씬 효율적이라서예요.

Index File — .index

역할

offset → 파일 내 byte position 매핑이에요. binary search lookup(정렬된 데이터를 절반씩 잘라 찾는 탐색)을 가속하기 위한 보조 파일입니다.

구조 (sparse index)

offset → physical position (bytes from segment start)

12000  → 0
12100  → 4096
12200  → 8192
12300  → 12288
...

모든 메시지를 인덱싱하지는 않아요. index.interval.bytes=4096(기본 4KB)마다 entry를 하나씩 만드는 sparse index(드문드문 표시한 인덱스) 방식이라 디스크와 메모리를 아낄 수 있어요.

Lookup 흐름

consumer 가 offset 12150 요청
  ↓
binary search in .index
  ↓
가장 가까운 entry 찾음 = (12100, 4096)
  ↓
.log 파일의 4096 byte 부터 sequential scan
  ↓
offset 12150 까지 점프
  ↓
메시지 반환

sparse index로 대략의 위치를 잡고 그 뒤로 sequential scan으로 남은 거리를 좁히는, 메모리와 속도가 균형 잡힌 설계예요.

TimeIndex File — .timeindex

역할

timestamp → offset 매핑이에요. 시간 기반 lookup에 씁니다.

timestamp        → offset
1747475100000   → 12000
1747475200000   → 12500
1747475300000   → 13100
...

활용

consumer.offsetsForTimes(...) 를 호출할 때 한 번 쓰이고, retention.ms 기반 삭제에서도 message.timestamp.type 설정에 따라 참조돼요.

여기서 시험 함정이 하나 있어요 — timeindex도 sparse라는 점입니다. 특정 timestamp를 넣어도 정확히 그 시각의 offset이 나오는 게 아니라 근사값, 정확히는 offset_at_or_after(timestamp) 가 나옵니다.

Leader Epoch Checkpoint

leader-epoch-checkpoint:
  0 0       # epoch 0 = offset 0 부터
  3 12000   # epoch 3 = offset 12000 부터 (leadership 변경)
  5 25000   # epoch 5 = offset 25000 부터

Leader가 바뀔 때마다 epoch(리더십 세대 번호)가 올라가요. unclean leader election(데이터 손실을 감수하고 뒤처진 replica를 leader로 올리는 선출) 직후에 데이터를 truncate하는 시나리오에서 이 checkpoint를 씁니다.

Writes — Sequential Append

84편 persistence에서 다룬 핵심이에요.

Producer → Broker → Active Segment 의 끝에 append (sequential I/O)
                  → 동시에 .index, .timeindex 도 sparse 업데이트

모든 write가 sequential I/O(파일 끝에만 이어 붙이는 디스크 패턴)로 처리돼요. 같은 디스크에서도 random보다 6,000배 빠른 패턴입니다.

Reads — Pull 모델

Consumer.fetch(topic, partition, offset, max_bytes)
  ↓
broker:
  1. offset 으로 segment file 찾음 (filename 으로 binary search)
  2. .index 로 byte position 찾음
  3. .log 의 해당 position 부터 max_bytes 만큼 read
  4. consumer 에게 chunk 반환

한 chunk에 여러 메시지가 같이 담겨 돌아와요. consumer는 그 chunk 안을 iterate하면서 메시지를 하나씩 꺼냅니다.

OutOfRangeException

존재하지 않는 offset(너무 작거나 너무 큰 값)을 요청하면 OutOfRangeException(범위 밖 offset 예외)이 떨어져요. consumer 쪽에서 처리해야 하는데, 보통 auto.offset.reset으로 정책을 정합니다. earliest면 처음부터, latest면 끝부터 다시 잡고, none이면 에러를 그대로 throw합니다.

kafka-dump-log.sh — 디버깅 도구

운영에서 segment 파일 내용을 직접 들여다볼 때 쓰는 도구예요.

기본 사용

$ kafka-dump-log.sh \
    --files /var/kafka-data/my-topic-0/00000000000000000000.log

Dumping /var/kafka-data/my-topic-0/00000000000000000000.log
Starting offset: 0
baseOffset: 0 lastOffset: 49 count: 50 baseSequence: 0 ...
| offset: 0 ... key: user42 payload: ...
| offset: 1 ... key: user99 payload: ...
...

각 메시지의 메타데이터가 전부 찍혀 나와요.

Index 파일

$ kafka-dump-log.sh \
    --files /var/kafka-data/my-topic-0/00000000000000000000.index

offset: 0 position: 0
offset: 100 position: 4096
offset: 200 position: 8192

자주 쓰는 자리

  • 데이터 검증 — 특정 메시지가 실제로 저장됐는지 확인
  • Corruption 디버깅 — 손상된 segment 분석
  • 포렌식 — 어떤 메시지가 언제 어디에 있었나 추적
  • 성능 분석 — index 엔트리 수와 간격 확인

Log 운영 명령

Segment 정보

$ ls -la /var/kafka-data/my-topic-0/
-rw-r--r-- 1 kafka kafka 1073741824 May 17 12:00 00000000000000000000.log
-rw-r--r-- 1 kafka kafka     262136 May 17 12:00 00000000000000000000.index
-rw-r--r-- 1 kafka kafka     393204 May 17 12:00 00000000000000000000.timeindex

각 파일 크기를 확인하는 가장 단순한 방법입니다.

Log Cleaner 모니터링 (compacted topic)

/var/kafka-data/cleaner-offset-checkpoint 가 cleaner thread(중복 key를 정리하는 백그라운드 스레드)의 진행 상황을 저장합니다.

Flush Policy — 다시 짚어보기

84편·104편에서 다룬 내용을 한 번 더 정리할게요.

log.flush.interval.messages=Long.MAX_VALUE      # 무한 (기본)
log.flush.interval.ms=Long.MAX_VALUE             # 무한 (기본)

기본값은 명시적 fsync(디스크에 강제로 동기화하는 시스템 콜)를 호출하지 않습니다. OS pagecache(커널이 관리하는 메모리 캐시)가 알아서 디스크로 내려주고, 데이터 보장은 replication 쪽이 더 강하게 잡아줍니다.

특정 broker만 fsync를 강제할 수도 있지만 성능이 100~1000배 떨어져서 비권장입니다.

File Format 진화 (Magic Byte)

Magic byte 0: Kafka 0.7.x (very old)
Magic byte 1: Kafka 0.8.x ~ 0.9.x
Magic byte 2: Kafka 0.10.x+ (현재)

v2에서 RecordBatch(여러 메시지를 묶어 하나의 배치로 저장하는 단위) 모델이 도입됐어요. batch 단위로 통째로 저장하면서 compression·CRC·timestamp가 한 배치 안에 다 들어갑니다.

새 클라이언트는 v2 형식을 쓰고, 옛 클라이언트와 호환하려면 broker가 변환을 끼워줘야 해서 그만큼 성능 부담이 생겨요.

한계·실무 함정

1. Segment 너무 작으면 파일 폭증

segment.bytes=100KB 처럼 작게 잡으면 segment file이 수만 개로 불어납니다. file descriptor와 메모리가 같이 부담을 받아요. 최소 100MB ~ 1GB 정도가 무난합니다.

2. Index 파일 크기

segment.index.bytes=10485760 (기본 10MB)이 한 segment가 가질 수 있는 index의 한계예요. 초과하면 그 시점에 segment roll이 일어납니다.

3. Log Corruption

디스크 오류나 갑작스러운 종료로 segment가 손상될 수 있어요. Kafka는 startup 시점에 자동으로 복구를 시도하고, 심한 경우는 수동으로 dump-log 떠서 분석해야 합니다.

4. Disk I/O 분리

log 디렉토리와 OS·application 로그를 같은 디스크에 두면 I/O 경합이 납니다. 디스크 분리는 필수예요 (104편).

5. JBOD partition 불균등

JBOD(Just a Bunch Of Disks, 여러 디스크를 단순 묶음으로 쓰는 구성) 환경에서는 partition이 한 디스크로 쏠릴 수 있어요. 모니터링과 manual rebalance를 챙기거나 Cruise Control 같은 자동화 도구로 분산을 맡깁니다.

시험 직전 한 번 더 — Kafka Log 함정 압축 노트

  • 디렉토리 = <topic>-<partition> per partition
  • 각 partition 안 = 여러 segment file
  • Segment 파일 = <first-offset>.log + .index + .timeindex
  • Segment file 구조 = [4-byte length][N bytes message] 연속
  • Offset = 64-bit integer, partition 안 unique
  • 처음부터 0, 1, 2, ... 순차 증가
  • Segment Roll = log.segment.bytes (1GB) 또는 log.segment.ms (7일)
  • Active segment 만 append, 오래된 segment 통째로 삭제
  • .index = offset → byte position sparse index
  • index.interval.bytes=4096 마다 entry
  • Lookup = binary search + sequential scan
  • .timeindex = timestamp → offset sparse, offsetsForTimes
  • Sparse 라 정확한 timestamp 의 정확한 offset 보장 X (근사)
  • leader-epoch-checkpoint = leader 변경 시 epoch 추적
  • Unclean leader election 후 truncate 시 사용
  • Writes = sequential append (6000배 빠른 디스크 패턴)
  • Reads = offset → segment → index → log file
  • 한 chunk 에 여러 메시지 포함 → consumer iterate
  • OutOfRangeException = auto.offset.reset 으로 처리
  • kafka-dump-log.sh = segment 파일 직접 검사 (디버깅)
  • 데이터 검증·corruption 디버깅·포렌식·index 분석
  • Flush Policy 기본 = 무한 (OS pagecache 의존)
  • 명시 fsync = 성능 100~1000배 하강
  • Magic byte v2 (Kafka 0.10+) = RecordBatch 모델
  • 함정 — segment 너무 작음 = file 폭증
  • 함정 — Index 파일 크기 한계
  • 함정 — Log corruption (자동 복구·수동 분석)
  • 함정 — Disk I/O 분리 (OS log 와 같은 디스크 X)
  • 함정 — JBOD partition 불균등 → 모니터링

공식 문서: Kafka Log Implementation 에서 자세한 사양을 확인할 수 있어요.

시리즈 다른 편 (앞뒤 글 모음)

이전 글:

다음 글:

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!