Offset을 처음으로 되돌렸는데, 지난 주문은 왜 보이지 않을까요?앞 글에서는 consumer group의 읽기 위치를 옮겨 같은 record를 다시 읽었어요. 그런데 책갈피를 앞쪽으로 옮겨도, 이미 버린 책장은 돌아오지 않죠. Kafka에서도 읽기 위치와 기록의 수명은 서로 다른 규칙이에요. 이번에는 아무도 읽지 않은 record가 retention으로 사라지는 것을 확인해요. 이어 새 record를 남기고, 실제로 남아 있는 범위에서만 replay할 수 있다는 경계까지 봅니다.
시작 상태를 맞춰요
첫 record 실습의 Kafka 4.3.1 broker가 실행 중이어야 해요. Docker라면aha-kafka, 직접 설치라면 같은 data directory로 실행한 broker에 접속합니다. Client 주소는 선택한 환경 안의 localhost:9092예요.
앞 글의 orders.paid, order-sequence, shipping-service, analytics-service는 그대로 둬요. 이번에는 아직 없는 order-retention topic과 retention-reader group만 사용합니다. 외부 database나 배송 API는 호출하지 않아요.
검증 환경
검증에 사용한 서버는 4 vCPU, RAM 27 GiB이며 시작할 때 disk 여유는 약 32 GiB였어요. 같은 서버에서 container와 직접 설치 broker의 network·data directory를 분리했어요. 이 자원은 실제 검증 환경이며 Kafka의 최소 사양이 아니에요. 독자는 두 탭 중 하나를 고르면 됩니다.
책갈피와 책장 정리를 나눠 생각해요
Consumer group의 committed offset은 책갈피에 가까워요. 반면 retention은 오래된 책장을 정리하는 규칙에 가까워요. 책갈피가 꽂혀 있다는 이유로 정리가 멈추지는 않아요. 이 비유의 한계도 있어요. Kafka가 record 한 개를 책 한 장처럼 따로 골라 지우는 것은 아니에요.cleanup.policy=delete에서는 partition log를 나눈 segment 단위로 오래된 기록을 제거해요.
segment.bytes와segment.ms는 새 segment로 넘어가는 크기·시간 조건이에요.retention.ms는 시간 보존 조건이에요. Segment 안의 가장 큰 timestamp를 기준으로 만료 여부를 판단해요.retention.bytes는 partition별 크기 보존 조건이에요. 시간 조건과 함께 설정하면 어느 한쪽으로도 오래된 segment가 삭제될 수 있어요.- Consumer가 읽었는지, 업무를 끝냈는지, commit했는지는 이 삭제의 승인 조건이 아니에요.
cleanup.policy=delete에 한정해요. Key별 상태를 남기는 compaction이나 tiered storage의 보관 규칙까지 같은 그림으로 일반화하면 안 됩니다.
Retention 1초는 1초 뒤 삭제 완료가 아니에요
이번에는retention.ms=1000을 잠깐 적용해요. 그래도 record별 1초 TTL처럼 동작하지 않아요. Segment의 timestamp, roll 조건, broker의 주기적인 삭제 검사에 영향을 받아요.
Kafka 4.3.1의 log.retention.check.interval.ms 기본값은 300000, 즉 5분이에요. 이 실습도 그 값을 사용해요. segment.ms=1000을 설정한 뒤 새 record를 append해서 roll을 관찰하고, retention 적용 뒤에는 실제 earliest offset의 변화를 기다립니다.
Kafka 4.3.1은 마지막 비어 있지 않은 segment까지 만료되면 새 빈 segment를 만든 뒤 기존 segment들을 지울 수 있어요. “Active segment의 record는 항상 남는다”고 가정하지 마세요. 이번 실습에서는 기존 세 record가 모두 사라진 상태까지 확인해요.
보관 범위가 줄어드는 것을 직접 확인해요
두 탭의 topic·key·offset·group은 같아요. Bash 함수kafka는 CLI 경로만 줄여 쓰는 도우미예요. 데이터와 설정은 해당 설치 경로의 broker에만 적용합니다.
- Docker
- Ubuntu 직접 설치
Docker CLI는 container 안의 처음 두 record는 offset 0·1, 나중 record는 offset 2에 놓여요. 세 record의 key·value와 offset 0·1·2를 확인해요. Earliest 결과는 Dry run의 Earliest는 3, latest는 4예요. Topic을 다시 만들지 않았으므로 새 record는 offset 0이 아닌 3에 들어갑니다.Consumer는 아직 committed offset 0에서 시작하려 하므로 Dry run의
/opt/kafka/bin에 있어요. 아래 함수를 정의한 같은 Bash 터미널에서 이 탭을 끝까지 진행하세요.1
폐기용 topic을 만들고 설정을 확인해요
시간·크기 보존을 먼저 꺼서 관찰 도중 record가 사라지지 않게 해요. 이름이 이미 있으면 중단하고 새 실습용 이름을 모든 명령에 일관되게 사용하세요.
PartitionCount: 1, ReplicationFactor: 1과 지정한 설정을 확인해요. LogAppendTime은 broker가 기록한 시간을 써서 producer 시계 차이를 줄이기 위한 실습 설정이에요.2
Record를 나눠 보내 segment를 바꿔요
segment.ms=1000을 넘긴 뒤 새 append를 보내 roll 조건을 검사하게 합니다.3
Segment 파일을 확인해요
00000000000000000000.log와 00000000000000000002.log를 확인해요. 파일명은 segment가 시작하는 base offset이에요. .index, .timeindex는 탐색을 돕는 파일이며 record 본문 파일과 구분합니다. 공식 image의 기본 log 경로를 사용했어요. 경로를 바꿨다면 실제 log.dirs에서 확인하세요.4
삭제 전 record와 범위를 읽어요
order-retention:0:0, latest는 order-retention:0:3이에요. Latest 3은 다음에 쓸 위치이며, 아직 offset 3의 record는 없어요.5
읽지 않은 group의 위치를 남겨요
NEW-OFFSET=0을 확인한 뒤 실행하세요. CURRENT-OFFSET=0, LOG-END-OFFSET=3, LAG=3이면 아직 읽지 않은 위치를 만들었어요.6
짧은 retention을 적용하고 삭제를 관찰해요
기존 record 만료 확인이 출력되고 latest도 3인지 확인하세요. Group의 committed offset은 여전히 0이에요. 읽을 수 있는 시작 위치만 3으로 이동했어요.420초는 실습을 멈출 기준이지 Kafka의 삭제 보장 시간이 아니에요. 확인 문구가 없으면 다음 단계로 넘어가지 말고 broker 로그와 실제 설정을 확인하세요. 위 명령은 성공·시간 초과 모두 짧은 retention을 해제합니다. 중간에 Ctrl+C로 끊었다면 retention.ms=-1 변경을 직접 실행하세요.7
새 record를 쓰고 범위 밖 위치를 확인해요
OffsetOutOfRangeException으로 끝나요. 이번 단계에서는 의도한 오류예요. auto.offset.reset=none으로 자동 이동을 막았고, auto commit도 꺼서 실패 관찰 중 위치를 바꾸지 않아요.8
남아 있는 offset 3으로 이동해 replay해요
NEW-OFFSET=3을 확인하고 실행하면 order-45:RECHECKED만 읽어요. 종료 뒤 CURRENT-OFFSET=4, LOG-END-OFFSET=4, LAG=0인지 확인하세요. 삭제된 offset 0·1·2가 복원된 것은 아니에요.결과를 세 위치로 읽어요
두 트랙에서 확인한 위치 변화는 다음과 같았어요. 이 표의 latest는 transaction을 쓰지 않는 이번 실습의 log end offset이에요.
삭제 직후에도 group의 lag는
3 - 0 = 3으로 보일 수 있어요. 하지만 그 세 record는 이미 읽을 수 없어요. Lag라는 위치 차이만으로 replay할 수 있는 record 수를 판단하면 안 되는 이유예요.
파일을 다시 보면 .deleted가 붙은 파일이 잠시 남을 수도 있어요. 읽기 범위에서 제거하는 것과 disk에서 파일을 없애는 것은 별도 단계예요. file.delete.delay.ms 기본값은 60000ms지만, 이 값을 “record를 그동안 더 읽을 수 있는 시간”으로 해석하면 안 됩니다.
Auto offset reset은 복구가 아니라 새 시작 위치 선택이에요
Committed offset이 없거나 현재 보관 범위를 벗어나면 consumer의auto.offset.reset 정책이 적용돼요.
Kafka 4.3.1에서 기본값은
latest예요. 이 글은 위 세 정책만 비교하며, 실습에서는 none을 명시했어요. --from-beginning도 “topic을 만든 순간부터 복원”이 아니라 현재 남은 earliest에서 시작하도록 하는 선택이에요. 유효한 committed offset이 있으면 그 위치가 우선합니다.
CLI에 삭제된 offset 0을 reset 목표로 넣는 것도 복구 방법이 아니에요. Kafka 4.3.1의 reset 도구는 보관 범위보다 작은 요청을 earliest로 보정할 수 있으므로, **요청 숫자보다 dry run의 실제 NEW-OFFSET**을 읽어야 해요.
복구 상태를 정리하고 실습 data를 지워요
정상적으로 끝났다면order-retention은 retention.ms=-1, retention.bytes=-1이고 offset 3의 새 record만 남아 있어요. Consumer는 종료됐고 retention-reader의 committed offset은 4예요. 이전 주문 record를 되살린 상태가 아니라, 남은 기록에서 다시 읽을 수 있게 된 상태입니다.
- Docker
- Ubuntu 직접 설치
order-retention이 사라지고, orders.paid와 order-sequence가 남아 있는지 확인하세요.
Docker와 Ubuntu 직접 설치 모두 segment base offset 0·2, 삭제 전 범위 03, 만료 뒤 빈 범위 33을 확인했어요. 읽지 않은 group의 committed offset 0은 그대로 남았고, 범위 밖 오류가 발생했어요. 새 record를 offset 3으로 쓴 뒤 dry run과 reset을 거쳐 그 record만 다시 읽었습니다.
자, 정리해볼까요?
- Group의 책갈피와 topic의 보관 정책은 따로 움직여요. 아직 읽지 않은 record도 삭제될 수 있어요.
- 시간 보존은 record별 정각 삭제가 아니라 segment와 주기적인 검사에 의해 동작해요.
- Earliest와 latest가 같으면 남은 record가 없는 상태예요. 다음 append가 offset 0부터 다시 시작하는 것은 아니에요.
- Replay는 남아 있는 기록을 다시 읽는 동작이에요. Offset reset은 삭제한 데이터를 복원하지 않아요.
- 복구 작업 전에는 committed offset뿐 아니라 partition의 실제 보관 범위와 side effect를 함께 확인해야 해요.