CRC는 attributes부터 끝까지만 덮고, 앞의 baseOffset·partitionLeaderEpoch는 제외한다. 이유는:
baseOffset — offset은 broker가 부여한다. producer는 보낼 때 모르는 값.
partitionLeaderEpoch — leader 교체 시 broker가 찍는 값.
이 둘을 CRC 범위에서 빼놓았기에, broker가 offset/epoch를 수정해도 CRC를 다시 계산할 필요가 없다. producer가 만든 CRC가 그대로 살아남는다 → 이게 zero-copy를 가능하게 한다.
③ 언제, 누가 검증하나 — 3+1 지점
[Producer] [Broker] [Consumer]
batch 만들며 ──→ 수신 시 검증 ──→ disk ──→ fetch 후 검증
CRC 계산·기록 (transit 오류) (그대로 저장) (전달 직전)
│
recovery / log cleaner scan 시에도 검증
Producer: batch 조립하며 CRC32C 계산해 헤더에 기록
Broker 수신: 전송 중 깨짐 검출 → 불일치면 CorruptRecordException → producer 재전송
Broker 저장/복구: CRC 포함 그대로 디스크 append. recovery·log cleaner scan 때도 검증
Consumer 수신: fetch한 batch를 application에 넘기기 직전 재계산·비교
④ 왜 CRC32C인가
C = Castagnoli 다항식. 일반 CRC32와 다른 다항식.
최신 CPU에 하드웨어 가속 명령어(SSE4.2 crc32)가 있어 매우 빠름.
0.11(포맷 v2)부터 표준. 그 이전 v0/v1은 일반 CRC32를 record마다 사용 — 흥미롭게도 이 옛 방식이 MyKafka와 더 닮음.
3. CRC 충돌과 silent corruption
"일치"와 "충돌"은 다른 말
CRC 검증이 "일치 ✅"로 나올 때, 그 뒤에는 두 가지 진실이 숨어 있다. CRC는 이 둘을 구분 못 한다.
겉보기
실제
빈도
일치 ✅
데이터가 진짜 멀쩡함 (정상)
거의 전부 (99.999…%)
일치 ✅
깨졌는데 우연히 CRC가 똑같음 ← 충돌
극히 드묾 (~1/2³²)
충돌이 나면? → 깨진 데이터가 멀쩡한 척 통과
깨진 데이터 → CRC 다시 계산 → 하필 저장된 CRC와 같음
→ Kafka: "일치! 멀쩡하네 ✅"
→ consumer에게 그대로 전달 (에러 안 남)
silent data corruption — 아무 에러도 안 뜨고, 깨진 record를 정상 데이터로 착각해 전달한다. 시스템이 "나 멀쩡해"라고 거짓말하는 가장 위험한 상태.
그래도 겁먹을 필요 없는 이유 — CRC의 수학적 보장
CRC32는 단순 해시보다 흔한 오류 패턴을 100% 보장해서 잡는다.
single-bit 오류 (1비트 뒤집힘) → 무조건 검출 ✅
연속 32비트 이하 burst 오류 → 무조건 검출 ✅
홀수 개 비트 오류 → 검출
충돌이 나려면 이 보장 범위를 벗어나는 패턴이어야 하고, 그 잔여 확률은 약 1/2³² ≈ 43억분의 1.
충돌까지 막으려면 — 다른 계층으로 보완
Kafka는 "CRC32C면 속도 대비 충분"이라는 트레이드오프를 택했다. 더 강한 보장이 필요하면 다른 층을 겹친다.
계층
막는 것
ECC RAM
메모리 bit flip을 하드웨어가 정정
파일시스템 체크섬 (ZFS, Btrfs)
디스크 레벨 강한 검증
앱 레벨 해시/서명 (SHA-256)
강한 무결성 + 악의적 변조 방어
오염의 정체 — CRC mismatch는 "저장 시점의 그 데이터가 아니다"만 알려준다. 원인(디스크 bit rot · bad sector · RAM 오류 · 버스 오류)은 알려주지 않는다. 주 무대는 디스크에 멀쩡히 저장됐다 믿은 데이터가 시간이 지나 썩는 bit rot이다. append-only라 "이미 쓴 위치를 코드가 덮어쓰는" 일은 정상 동작상 일어나지 않는다.
4. CRC vs Offset — 역할 구분
"이 record를 처음 읽는 건가 / 이미 읽었나"는 CRC가 아니라 다른 메커니즘의 일이다.
같은 record를 100번 다시 읽어도 CRC는 매번 "안 깨졌네 ✅"만 답한다. 조회 횟수·순서·중복과는 완전히 별개다.
5. 실제 Kafka vs MyKafka
차이는 세 양동이로 나뉜다.
① 본질은 같음 — 뼈대는 진짜 Kafka와 동일
개념
MyKafka도 실 Kafka처럼
append-only 로그
partition = 디스크에 줄줄이 쌓는 로그
length-prefix framing
4바이트 길이 접두
segment 파일명 = baseOffset
%020d.log → 디렉토리가 곧 인덱스
sparse + mmap 인덱스
4KB 간격 sparse, mmap
broker가 offset 부여
클라이언트가 못 정함
partition = 병렬성 단위
partition당 락, 늘리면 throughput 비례
꼬리 자르기 복구
부분 쓰기된 trailing record truncate
batch로 syscall 분할상환
1×1000 batch가 61배 빠름
"모든 게 로그"
offset도 그냥 또 다른 로그(__consumer_offsets)
② 구현은 했지만 단순화 — 본질 동일, 디테일 다름
항목
MyKafka
실제 Kafka
CRC
record별 CRC32
batch별 CRC32C (offset/epoch 제외 → zero-copy 가능)
partitioner 해시
contentHashCode (JVM 의존)
murmur2 (언어 독립적)
__consumer_offsets
1 partition, compaction 미구현
50 partition + log-compacted
FETCH
없으면 즉시 빈 응답 (busy-poll)
long-poll (fetch.max.wait.ms)
segment 크기
1MB (학습용)
1GB 기본
ApiKey
5개
수십 개 (JoinGroup, Heartbeat…)
③ 아예 안 넣음 — 구조적 부재 (전부 분산·신뢰성)
영역
MyKafka
실 Kafka
부재의 한계
Replication
단일 노드
Leader/Follower + ISR + acks=all
노드 죽으면 데이터 손실·중단
Controller
단순 메타
KRaft (Zookeeper 제거)
클러스터 메타 조율 불가
Group coordination
group = 문자열
rebalance, heartbeat, sticky assignment
consumer 자동 재분배 없음
Exactly-once
at-least-once
idempotence + transactions
중복/유실 가능
Zero-copy
byte copy 재직렬화
sendfile() / FileRegion
fetch마다 user-space 복사
Log compaction
sparse만
같은 key 옛 record 삭제
offsets 토픽 무한 증가
Durability tuning
OS flush 의존
channel.force()(fsync) 정책
전원 손실 시 OS 버퍼 유실
Client SDK
서버만 있음
Producer/Consumer 라이브러리
표준 클라이언트 없음
가장 큰 차이 3개 — ① 분산 통째 부재(Replication + Controller): "distributed commit log"에서 distributed가 빠짐. ② Group coordination 부재: consumer 자동 partition 재분배(rebalance) 없음. ③ Zero-copy 미구현: 성능 천장. ②의 CRC가 offset 제외하는 설계가 곧 이 zero-copy를 가능케 하므로 CRC 차이 ↔ zero-copy 부재가 한 묶음.
6. 동작 과정 흐름도
실 Kafka 파란 단계MyKafka 초록 단계
실 Kafka PRODUCE (발행) 과정
1
Producer가 batch 조립
같은 (topic, partition)끼리 record를 모음. partitioner: key 있으면 murmur2(key) % N, 없으면 round-robin. linger.ms / batch.size 동안 모아 압축 후 CRC32C 계산.
2
네트워크 전송 → Leader broker
해당 partition의 leader replica에게만 보냄.
3
Broker 검증 + offset 부여
CRC 검증(transit 오류) → baseOffset 할당 → append-only 로그에 그대로 기록(순차 디스크 쓰기).
key = encode(group, topic, partition), value = offset(8B).
3
내부 토픽에 append
__consumer_offsets(1 partition)에 그냥 또 다른 append. 새 저장소 없이 Log 메커니즘 재활용.
4
cache 갱신
ConcurrentHashMap에 최신값. FETCH_OFFSET은 cache lookup(디스크 접근 0).
5
재시작 시 복구
내부 토픽을 처음부터 끝까지 scan → cache 재구축. 같은 key 반복 commit → 옛 record는 garbage(compaction 도입 시 정리될 의미).
두 흐름의 공통 뼈대 — ① broker가 offset 부여, ② append-only 순차 쓰기, ③ batch로 syscall 분할상환, ④ sparse index로 위치 탐색, ⑤ 클라이언트가 offset을 들고 다님(broker는 consumer 상태를 거의 안 가짐). 가장 큰 갈림길은 FETCH 마지막 단계: 실 Kafka는 zero-copy(sendfile), MyKafka는 재직렬화(byte copy) — 그리고 그걸 가능케 하는 게 §2의 "CRC가 offset을 안 덮는" 설계다.