레이크하우스 표 형식 — Apache Iceberg 를 metadata 로 이해한다
작은 파일 30번의 커밋을 정리한다 — 태그, 압축, 만료, 고아 순서로
목표
작은 묶음 30개를 커밋 30번으로 넣어 스트리밍 적재가 만드는 작은 파일과 스냅샷 더미를 재현한 뒤, 되돌아갈 시점에 태그를 걸고, rewrite_data_files 로 압축하고, expire_snapshots 로 옛 스냅샷과 그 파일을 지우고, remove_orphan_files 로 아무도 가리키지 않는 파일을 치운다. 단계마다 파일 수와 스냅샷 수가 어떻게 바뀌는지 기록한다.
왜 중요한가
Iceberg 는 아무것도 스스로 지우지 않는다. 커밋마다 새 파일과 새 metadata 가 쌓이고, 압축해도 옛 파일은 옛 스냅샷이 가리키고 있어 그대로 남는다. 그래서 정리를 안 한 표는 읽기가 느려지고(작은 파일·매니페스트가 많아서) 저장소는 계속 커진다. 정리는 세 가지 다른 일이다. 압축은 작은 파일을 큰 파일로 다시 써서 지금 스냅샷을 빠르게 한다. 만료는 오래된 스냅샷과, 그것들만 가리키던 파일을 지운다 — 타임트래블로 갈 수 있는 과거가 그만큼 줄어든다. 고아 정리는 어떤 스냅샷도 가리킨 적 없는 파일(실패한 잡의 흔적)을 지운다 — 지금 쓰이는 중인 파일까지 지우지 않게 충분한 시간 여유를 둔다. 셋 다 되돌릴 수 없다. 그래서 순서가 중요하다. 되돌아가야 할 시점이 있다면 만료 전에 태그를 걸어야 하고, 고아 정리의 시간 여유는 줄이지 않는다.
단계
- /root/ice/mnt/trickle.py(앱
ice-mnt-trickle)로lake.mnt.orders를PARTITIONED BY (days(order_ts))·format-version 2 로 만들고/data/ice/batches/batch-001.csv부터batch-030.csv까지를 묶음마다 한 커밋씩 넣으세요. - /root/ice/mnt/before.py 로 지금 스냅샷 수·데이터 파일 수·평균 파일 크기를 /root/ice/mnt/out/before.json 에 쓰세요.
- /root/ice/mnt/tag.py(앱
ice-mnt-tag)로 열 번째 커밋 스냅샷에 태그batch10을RETAIN 30 DAYS로 붙이세요. - /root/ice/mnt/compact.py(앱
ice-mnt-compact)로rewrite_data_files를 부르고 결과를 /root/ice/mnt/out/compact.json 에 쓰세요. - /root/ice/mnt/expire.py(앱
ice-mnt-expire)로 지금보다 오래된 스냅샷을retain_last => 1로 만료하세요. /root/ice/warehouse/mnt/orders/data에 오래된 고아(stray-old.parquet, 수정 시각 나흘 전)와 새 파일(stray-new.parquet)을 두고, /root/ice/mnt/orphans.py(앱ice-mnt-orphans)로remove_orphan_files를 먼저dry_run으로 돌려 /root/ice/mnt/out/orphans_dry.txt 에 적은 뒤 실제로 돌리세요.- /root/ice/mnt/after.py 로 정리 뒤 스냅샷 수·데이터 파일 수·디스크의 Parquet 파일 수를 /root/ice/mnt/out/after.json 에 쓰세요.
- /root/ice/mnt/report.md 에
## 압축## 만료와 태그## 고아 파일세 절을 쓰세요.
참고
- 프로시저는
spark.sql("CALL lake.system.<이름>(table => 'lake.mnt.orders', …)")로 부릅니다. 인자에는 식이 아니라 값(문자열·TIMESTAMP 리터럴)을 줍니다. remove_orphan_files는older_than을 지금부터 24시간보다 가깝게 주면 거절합니다. 쓰는 중인 파일(아직 커밋 전이라 고아처럼 보이는 파일)을 지우는 사고를 막기 위해서입니다. 주지 않으면 사흘 전이 기본값입니다.- 흔한 실수: 태그를 걸기 전에 만료를 돌려 열 번째 스냅샷이 사라지는 것 — 되돌릴 방법이 없습니다. 그렇게 됐다면
DROP TABLE lake.mnt.orders PURGE뒤 1단계부터 하세요. - 공식 문서: Maintenance · Spark Procedures — rewrite_data_files · expire_snapshots · remove_orphan_files · Branching and Tagging — retention
작은 묶음 30개, 커밋 30번
/root/ice/mnt/trickle.py 를 앱 이름 ice-mnt-trickle 로 만들어 lake.mnt.orders(열 여섯 개, PARTITIONED BY (days(order_ts)), 'format-version' = '2')를 만들고 /data/ice/batches/batch-001.csv 부터 batch-030.csv 까지 차례로 하나씩 append() 하세요.
묶음 하나가 커밋 하나, 스냅샷 하나이고, 날짜 파티션마다 작은 파일이 하나씩 생깁니다. 채점기는 30번째 커밋 직후의 metadata 에서 스냅샷 30개가 모두 그 묶음의 행 수를 더한 append 인지 봅니다.
정리 전의 숫자
/root/ice/mnt/before.py(pyiceberg)로 스냅샷 수·데이터 파일 수(total-data-files)·평균 파일 크기(total-files-size ÷ 파일 수, 정수 나눗셈)를 /root/ice/mnt/out/before.json 에 {"snapshots", "data_files", "avg_file_bytes"} 로 쓰세요.
지금 스냅샷의 요약에는 표 전체의 누적 값(total-*)이 들어 있어 매니페스트를 다 읽지 않고도 파일 수와 크기를 알 수 있습니다. Parquet 파일 하나가 수 KB 라면 읽을 때 파일을 여는 비용이 데이터를 읽는 비용보다 커집니다.
지우기 전에 이름부터
/root/ice/mnt/tag.py 를 앱 이름 ice-mnt-tag 로 만들어 lake.mnt.orders.snapshots 를 committed_at 순으로 읽고 열 번째 스냅샷에 CREATE TAG batch10 AS OF VERSION <ID> RETAIN 30 DAYS 를 거세요.
만료는 older_than 보다 오래된 스냅샷을 지우지만, 태그나 브랜치가 가리키는 스냅샷은 그 이름표의 보존 기간이 남아 있는 동안 지우지 않습니다. 그래서 태그는 만료보다 먼저 걸어야 합니다. 채점기는 태그가 30번째 커밋 직후 metadata 의 열 번째 스냅샷을 가리키는지 봅니다.
압축 — 작은 파일을 다시 쓴다
/root/ice/mnt/compact.py 를 앱 이름 ice-mnt-compact 로 만들어 CALL lake.system.rewrite_data_files(table => 'lake.mnt.orders', options => map('min-input-files', '2')) 를 부르고, 결과 행의 rewritten_data_files_count·added_data_files_count 를 /root/ice/mnt/out/compact.json 에 {"rewritten", "added"} 로 쓰세요.
압축은 같은 파티션의 작은 파일을 읽어 큰 파일로 다시 쓰고 'replace' 스냅샷 하나로 커밋합니다. 행은 하나도 바뀌지 않습니다. 옛 작은 파일은 목록에서만 빠지고 디스크에는 남습니다 — 옛 스냅샷(과 태그)이 아직 가리키니까요. 채점기는 replace 커밋의 요약과 여러분의 두 값을 견줍니다.
만료 — 옛 스냅샷과 그 파일이 지워진다
/root/ice/mnt/expire.py 를 앱 이름 ice-mnt-expire 로 만들어 지금 시각을 읽고 CALL lake.system.expire_snapshots(table => 'lake.mnt.orders', older_than => TIMESTAMP '<지금>', retain_last => 1) 을 부르세요.
만료는 스냅샷을 metadata 에서 지우고, 남은 스냅샷 어느 것도 가리키지 않는 파일을 디스크에서 지웁니다. 지금 main 과 태그 batch10 의 스냅샷만 남고, 태그가 가리키는 열 번째 스냅샷의 작은 파일들은 지워지지 않습니다. 채점기는 남은 스냅샷과 디스크의 파일을 봅니다.
고아 정리 — 시간 여유를 두고
/root/ice/warehouse/mnt/orders/data 에 기존 데이터 파일 하나를 복사해 stray-old.parquet(수정 시각 touch -d '4 days ago')와 stray-new.parquet(지금)을 만드세요. 그다음 /root/ice/mnt/orphans.py 를 앱 이름 ice-mnt-orphans 로 만들어 remove_orphan_files(table => 'lake.mnt.orders', dry_run => true) 결과의 orphan_file_location 을 한 줄에 하나씩 /root/ice/mnt/out/orphans_dry.txt 에 쓰고, 이어서 dry_run 없이 한 번 더 부르세요.
두 파일 모두 어느 스냅샷도 가리키지 않는 고아입니다. 그런데 방금 생긴 파일은 지금 누군가 쓰는 중인 커밋의 파일일 수도 있습니다. 그래서 older_than 보다 새 파일은 건드리지 않습니다(기본 사흘). 채점기는 dry_run 목록에 오래된 것만 있었는지, 실제로 오래된 것만 지워졌는지 봅니다.
정리 뒤의 숫자
/root/ice/mnt/after.py(pyiceberg)로 남은 스냅샷 수·지금 스냅샷의 데이터 파일 수·/root/ice/warehouse/mnt/orders/data 아래 Parquet 파일 수를 /root/ice/mnt/out/after.json 에 {"snapshots", "data_files", "files_on_disk"} 로 쓰세요.
디스크의 파일 수는 지금 스냅샷의 파일 수보다 많습니다. 태그 batch10 이 지키는 작은 파일들과, 고아 정리가 일부러 남긴 새 파일이 있기 때문입니다. 그 차이를 설명할 수 있으면 정리 작업을 이해한 것입니다.
정리 작업을 운영 절차로
/root/ice/mnt/report.md 에 ## 압축 ## 만료와 태그 ## 고아 파일 세 절을 쓰세요. 첫 절에는 압축 전 데이터 파일 수(2단계)와 압축 뒤 지금 스냅샷의 데이터 파일 수(7단계)를 숫자로 넣으세요.
이 셋을 매일 도는 작업으로 만든다면 어떤 순서·주기·기준(older_than, retain_last, 고아 시간 여유)으로 두겠습니까. 스트리밍으로 1분마다 커밋하는 표라면 무엇이 달라지는지도 적어 보세요.