포스트

로그 컴팩션과 보존 - 상태 토픽이 보장하는 것과 하지 않는 것

Kafka 토픽에는 두 가지 정리 정책이 있다. 기본은 시간이나 크기로 지우는 delete이고, 다른 하나가 키별 최신 값만 남기는 compact다. 후자는 토픽을 “변경 로그”가 아니라 “현재 상태의 스냅샷”으로 쓸 수 있게 하는데, 그 보장의 범위가 생각보다 좁다.

두 정책의 차이

 cleanup.policy=deletecleanup.policy=compact
지우는 기준retention.ms, retention.bytes같은 키의 옛 값
남는 것최근 구간 전체키마다 최소 하나
용도이벤트 스트림상태 스냅샷, 설정, 조회용 테이블

컴팩션은 키가 있는 메시지를 전제한다. 키가 null이면 컴팩션 대상이 아니다.

Kafka 자체가 이 기능을 쓴다. __consumer_offsets가 컴팩션 토픽이고, 키는 (그룹, 토픽, 파티션), 값은 오프셋이다. 오프셋 이력 전체가 필요한 것이 아니라 마지막 값만 필요하므로 이 정책이 맞다.

컴팩션이 실제로 하는 일

로그 클리너가 백그라운드에서 돈다. 활성 세그먼트(지금 쓰고 있는 파일)는 건드리지 않고, 닫힌 세그먼트들을 읽어 키별 최신 값만 남긴 새 세그먼트로 바꾼다.

여기서 중요한 성질 셋.

즉시 일어나지 않는다. min.cleanable.dirty.ratio(기본 0.5)를 넘어야 정리를 시작한다. 즉 정리되지 않은 부분이 절반쯤 쌓여야 돈다. 컴팩션 토픽을 읽어도 같은 키의 옛 값이 여러 번 나올 수 있다. 소비자는 그것을 감당해야 하고, 보통 “나중 것으로 덮어쓰기”면 자연히 해결된다.

오프셋은 유지된다. 메시지가 사라져도 오프셋 번호는 그대로다. 중간에 구멍이 생기므로, 오프셋을 연속 번호로 가정하는 코드는 깨진다.

순서는 유지된다. 파티션 안에서 남은 메시지의 상대 순서는 바뀌지 않는다.

삭제: 툼스톤

컴팩션 토픽에서 키를 지우려면 값이 null인 메시지(툼스톤)를 보낸다. 컴팩션이 돌면 그 키의 옛 값들이 사라지고, 툼스톤 자체도 delete.retention.ms(기본 24시간) 뒤에 사라진다.

이 유예 기간이 필요한 이유는 소비자 때문이다. 툼스톤이 즉시 사라지면, 잠시 멈췄다 돌아온 소비자가 “삭제됐다”는 사실을 못 보고 옛 값을 계속 들고 있게 된다. 유예 기간은 소비자가 삭제를 알아챌 시간이다.

여기서 운영 제약이 나온다. 소비자가 delete.retention.ms보다 오래 멈춰 있었다면, 그 사이의 삭제를 놓쳤을 수 있다. 그런 소비자는 처음부터 다시 읽어야 안전하다.

상태 토픽이 보장하는 것

compact 토픽을 처음부터 끝까지 읽으면, 각 키의 마지막 값을 모두 얻는다. 이것이 Kafka Streams의 KTable, Connect의 상태 저장, 캐시 워밍업이 서 있는 기반이다.

보장하지 않는 것도 분명하다.

  • 정확히 한 번만 나온다는 보장이 없다. 아직 정리되지 않은 중복이 나온다.
  • 특정 시점의 일관된 스냅샷이 아니다. 읽는 동안에도 새 메시지가 들어온다.
  • 삭제된 키를 언제까지 알 수 있는지에 상한이 있다.
  • 무한히 자라지 않는다는 보장도 없다. 키의 종류가 무한히 늘면(예: 키에 UUID를 쓰면) 컴팩션은 아무것도 줄이지 못한다. 키의 카디널리티가 유한해야 이 정책이 의미가 있다.

마지막 항목이 실무에서 가장 자주 어긋난다. “컴팩션을 켜면 알아서 줄어들겠지”라고 두면, 키가 계속 새로 생기는 토픽은 그대로 자란다.

둘을 함께 쓰기

cleanup.policy=compact,delete로 두면 컴팩션도 하고 보존 기간도 적용한다. “최근 N일치 안에서 키별 최신 값”이 된다. 키 카디널리티가 커질 수 있는 상태 토픽에서 안전장치로 쓴다.

이 설명이 깨지는 곳

  • 컴팩션 토픽을 이벤트 이력으로 쓰면 안 된다. 옛 값이 사라지므로 “무슨 일이 있었는가”에 답하지 못한다. 이력이 필요하면 delete 정책의 별도 토픽이다.
  • 로그 클리너가 밀릴 수 있다. log.cleaner.threads가 모자라거나 디스크가 느리면 정리가 쌓인다. 브로커 지표로 확인해야 한다.
  • 컴팩션은 저장 공간을 위한 기능이 아니다. 부수적으로 줄어들긴 하지만, 목적은 “키별 최신 값 유지”다.
  • min.compaction.lag.ms가 있으면 그만큼 지난 메시지만 정리된다. 소비자가 최신 값을 놓치지 않도록 하는 장치다.

무엇을 재면 확인되는가

  1. 컴팩션 토픽을 처음부터 읽어 같은 키가 몇 번 나오는지 센다. dirty.ratio 설정별로 다르다.
  2. 툼스톤을 보내고 delete.retention.ms 전후로 소비자를 붙여 삭제를 인지하는지 확인한다.
  3. 키 카디널리티를 계속 늘려 가며 토픽 크기가 줄어드는지 본다. 줄지 않는 것을 직접 보는 것이 이해에 빠르다.

실무와의 접점

ParityPay 5편의 Outbox 테이블은 DB에 만든 로그이고, 그 글의 한계에 “테이블 정리를 하지 않았다”를 적었다. Kafka의 두 정책이 그 문제의 두 답에 해당한다. 시간 기준으로 지울 것인가(delete), 키별 최신만 남길 것인가(compact). Outbox는 발행 의도의 이력이므로 전자가 맞고, 상태 스냅샷 테이블이라면 후자의 사고방식이 맞다. 무엇을 저장하고 있는지가 정리 정책을 정한다.

정리

  • delete는 시간·크기로, compact는 키별 최신 값으로 정리한다. 컴팩션은 키가 있는 메시지가 전제다.
  • 컴팩션은 즉시 일어나지 않는다. 같은 키의 옛 값이 여러 번 나올 수 있고 소비자가 감당해야 한다.
  • 오프셋은 유지되므로 중간에 구멍이 생긴다. 연속 번호를 가정하면 깨진다.
  • 삭제는 값이 null인 툼스톤으로 하고, 유예 기간은 소비자가 삭제를 알아챌 시간이다.
  • 키 카디널리티가 유한해야 컴팩션이 의미가 있다. 무한히 늘면 아무것도 줄지 않는다.
  • 컴팩션 토픽은 이력이 아니라 스냅샷이다. 이력이 필요하면 별도 토픽이다.

참고

Kafka와 메시징
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

댓글

아직 댓글이 없습니다