주문이 두 번 도착했고 한 번은 사라졌다 · 무엇을 남기고 무엇을 버리나 · 이론
무엇을 남기고 무엇을 버리나 — retention · compact · min.insync.replicas
한 줄 요약
토픽 설정은 세 가지 질문에 답한다. 얼마나 오래 남길 것인가(retention.ms,retention.bytes), 키마다 마지막 값만 남길 것인가(cleanup.policy=compact),
쓰기를 성공으로 치려면 복제본이 몇 개 확인해야 하는가(min.insync.replicas 와acks=all). 삭제와 압축은 세그먼트 단위로 일어나고 활성 세그먼트는 건드리지
않는다 — "지웠는데 남아 있다" 의 대부분이 이것이다.
왜 이게 필요했나
Kafka 는 소비해도 지우지 않으므로 무언가는 지워야 한다. [토픽 설정 문서](https://kafka.apache.org/43/configuration/topic-configs/)
의 cleanup.policy 는 두 정책을 둔다. delete(기본)는 보존 시간이나 크기
한도에 이른 옛 세그먼트를 버리고, compact 는 키마다 최신 값을 남기는 로그
압축을 켠다. 둘을 함께 적으면(delete,compact) 옛 세그먼트는 보존 규칙으로
버리고 남은 세그먼트는 압축한다. 빈 목록은 무한 보존이다.
압축이 필요한 이유는 [설계 문서](https://kafka.apache.org/43/design/design/)의
'Log Compaction' 절이 예로 설명한다. 사용자 123 의 이메일이 세 번 바뀌었을 때,
시간 보존은 오래된 변경을 통째로 버려 처음부터 읽어도 현재 상태를 복원할 수
없게 만든다. 압축은 키마다 마지막 갱신을 반드시 남겨 로그가 모든 키의 최종
값 스냅샷이 되게 한다 — DB 변경 구독, 이벤트 소싱, 상태 저널링이 이 위에 선다.
어떻게 동작하나
시간 보존은 세그먼트 단위다. retention.ms 기본값은 604800000(7일)이고
-1 이면 무제한이다. 문서는 이것을 "컨슈머가 얼마나 빨리 읽어야 하는가에 대한
SLA" 라고 부른다. retention.bytes 는 파티션당 크기 한도이며 기본 -1(없음)이다.
그런데 지워지는 단위는 레코드가 아니라 세그먼트 파일이다. segment.ms(기본
7일)나 segment.bytes(기본 1GiB)에 이르러 세그먼트가 굴러가야 옛 세그먼트가
지워질 후보가 되고, 활성 세그먼트는 절대 지워지지 않는다. 그리고 브로커는
[브로커 설정](https://kafka.apache.org/43/configuration/broker-configs/)의log.retention.check.interval.ms(기본 300000, 5분)마다만 검사한다. 그래서retention.ms=1분 을 주어도 세그먼트가 굴러가지 않았거나 검사 주기가 안 됐으면
남아 있다. 실습 VM 은 검사 주기를 10초로 줄여 두었다.
events (retention.ms=20000, segment.ms=10000) 세그먼트 0: e1 e2 e3 ← 12초 뒤 e4 가 들어오며 굴러간다 (닫힘) 세그먼트 4: e4 ← 활성. 절대 지워지지 않는다 20초 + 검사 주기 뒤 → 세그먼트 0 삭제 → 가장 앞 오프셋이 3 이 된다압축은 키마다 마지막 값을 남긴다. 설계 문서의 보장 — 압축은 순서를 바꾸지
않고 레코드를 빼기만 하며, 오프셋은 절대 바뀌지 않는다(빠진 오프셋은 그 다음
오프셋과 같은 위치로 취급된다). 처음부터 읽는 컨슈머는 모든 키의 최종 상태를
쓰인 순서로 본다. 키에 null 값을 쓴 레코드는 삭제 표식(tombstone)이며, 그 키의
이전 레코드를 지우게 하고 자신도 delete.retention.ms(기본 1일) 뒤에 사라진다.
언제 압축되는가는 min.cleanable.dirty.ratio(기본 0.5 — 로그의 절반이 중복이어야
움직인다), min.compaction.lag.ms, max.compaction.lag.ms 가 정하고, 여기서도
활성 세그먼트는 대상이 아니다. 실습은 dirty ratio 를 0.01 로 낮춰 곧 압축되게
한다.
너무 큰 레코드. max.message.bytes(기본 1048588)는 브로커가 받는 레코드
배치의 최대 크기다. 넘는 배치는 프로듀서에 RecordTooLargeException 으로
거절되고 로그에는 아무것도 남지 않는다 — 실측으로 확인했다.
min.insync.replicas 와 acks=all. min.insync.replicas 항목 — 프로듀서가acks=all 로 보낼 때 쓰기가 성공하려면 확인해야 하는 최소 ISR 수(리더 포함)다.
ISR 이 이 수보다 적으면 프로듀서는 NotEnoughReplicas 또는
NotEnoughReplicasAfterAppend 예외를 받는다. 문서가 드는 전형적 구성은 복제
계수 3, min.insync.replicas=2, acks=all 이다 — 과반이 쓰기를 확인해야
성공이고, acks 와 무관하게 ISR 전체에 복제되고 이 조건이 충족되기 전에는
컨슈머에게 보이지 않는다. 기본값은 1 이라 아무 보장이 없다. 이 코스의 VM 은
브로커가 하나라 이 상호작용을 재현할 수 없고(실측으로도 단일 노드에서는
오류가 나지 않았다), 퀴즈로만 확인한다. unclean.leader.election.enable(기본
false)은 ISR 밖의 복제본을 최후의 수단으로 리더로 뽑을지이며, 켜면 데이터를
잃을 수 있다.
실행 중 변경. [운영 문서](https://kafka.apache.org/43/operations/basic-kafka-operations/)
의 'Modifying topics' — `kafka-configs.sh --entity-type topics --entity-name X
--alter --add-config k=v 로 설정을 더하고 --delete-config k` 로 뺀다. 토픽을
다시 만들 필요가 없다.
현장에서 만나는 모습
"보존 1일로 줄였는데 디스크가 안 준다" 는 신고는 세그먼트 크기를 보면 풀린다.segment.bytes 가 1GiB 이고 하루에 200MB 씩 쌓이는 파티션은 닷새가 지나야
세그먼트가 굴러가고, 그때까지 아무것도 지워지지 않는다. segment.ms 를
함께 줄이는 것이 답이다.
"압축 토픽인데 옛 값이 보인다" 는 세 가지를 확인한다. 활성 세그먼트에 있는가
(압축되지 않는다), dirty ratio 를 넘었는가(기본 0.5), 그리고 컨슈머가 처음부터
읽으면서 아직 head 에 도달하지 않았는가(압축은 tail 에서 일어난다). 셋 다
정상 동작이다.
다음 실습에서 할 것
20초 보존 토픽에서 세그먼트를 굴려 가장 앞 오프셋이 오르는 것을 보고, 압축
토픽에서 alice 가 마지막 값 하나로 줄어드는 것을 보고, max.message.bytes
로 큰 레코드가 거절되는 것을 확인한 뒤, 실행 중인 토픽의 보존을 바꾼다.