LabHub
시작하기
배우기 러닝패스 코스

레이크하우스 표 형식 — Apache Iceberg 를 metadata 로 이해한다

표 하나를 만들고 metadata.json 에서 데이터 파일까지 따라 내려간다

LabHub 에서 이어서 보기

목표

Spark 로 Iceberg 표를 만들고 하루치 주문을 두 번 커밋한 뒤, 카탈로그 → metadata.json → 매니페스트 리스트 → 매니페스트 → 데이터 파일로 이어지는 나무를 도구 없이 손으로 따라 내려간다. 매 층에서 찾은 값을 파일로 적고, 채점기가 그 값을 실제 메타데이터와 견준다.

왜 중요한가

Iceberg 표는 '디렉터리' 가 아니라 '파일 목록' 이다. 읽는 쪽은 디렉터리를 훑지 않고 metadata.json 이 가리키는 목록만 믿는다. 그래서 커밋은 파일을 옮기는 일이 아니라 새 목록을 써 두고 카탈로그의 포인터 한 칸을 바꾸는 일이고, 그 한 칸이 바뀌는 순간 읽는 사람 모두가 새 상태를 본다. 이 구조를 손으로 한 번 따라가 보면 뒤의 모든 기능이 같은 원리의 변주라는 것이 보인다. 타임트래블은 옛 스냅샷의 목록을 읽는 것이고, 롤백은 포인터를 옛 스냅샷으로 돌리는 것이며, 압축과 만료는 목록을 다시 쓰고 더는 아무도 가리키지 않는 파일을 지우는 것이다. 장애가 났을 때 가장 먼저 보는 것도 이 나무다. "왜 이 행이 안 보이나" 는 거의 언제나 "그 파일이 지금 스냅샷의 목록에 있나" 로 바뀐다.

단계

  1. /root/ice/meta/create.py(앱 ice-meta-create)로 네임스페이스 lake.meta 와 표 lake.meta.orders 를 만드세요. 열은 order_id STRING, customer_id STRING, region STRING, amount INT, status STRING, order_ts TIMESTAMP, 속성은 'format-version' = '2' 입니다.
  2. /root/ice/meta/load.py 가 날짜 하나를 인자로 받아 /data/ice/orders/<날짜>.csv 를 표에 한 번 커밋하게 만들고, 2026-03-01 을 넣으세요.
  3. 같은 스크립트로 2026-03-02 를 넣어 스냅샷을 둘로 만드세요.
  4. 카탈로그 /root/ice/catalog.dbiceberg_tables 에서 이 표의 metadata_locationprevious_metadata_location 을 읽어 /root/ice/meta/out/pointer.txt 에 두 줄로 쓰세요.
  5. 현재 metadata.json 에서 스냅샷 목록을 뽑아 /root/ice/meta/out/snapshots.json 에 쓰세요.
  6. 현재 스냅샷의 매니페스트 리스트(Avro)를 풀어 매니페스트 목록을 /root/ice/meta/out/manifests.json 에 쓰세요.
  7. 그 매니페스트들을 풀어 지금 살아 있는 데이터 파일 목록을 /root/ice/meta/out/datafiles.json 에 쓰세요.
  8. /root/ice/meta/report.md## 포인터 ## 스냅샷 ## 파일 세 절을 쓰세요. 둘째 절에는 스냅샷 수를, 셋째 절에는 매니페스트 수와 데이터 파일 수를 넣으세요.

참고

표 만들기 — 스냅샷 없는 첫 metadata

/root/ice/meta/create.py 를 앱 이름 ice-meta-create 로 만들어 lake.meta 네임스페이스와 lake.meta.orders 표(열 여섯 개, 'format-version' = '2')를 만들고 spark-submit 으로 돌리세요.

표를 만들면 metadata 파일(00000-….metadata.json)이 하나 생기고, 카탈로그에 그 경로가 한 줄 들어갑니다. 아직 커밋한 자료가 없으니 스냅샷은 없습니다. 채점기는 첫 metadata 파일에 스냅샷이 없는지, 형식 판과 열 이름·형이 맞는지 봅니다.

첫 커밋 — 스냅샷 하나

/root/ice/meta/load.py 를 앱 이름 ice-meta-load 로 만들어, 인자로 받은 날짜의 /data/ice/orders/<날짜>.csv 를 스키마를 주어 읽고 writeTo("lake.meta.orders").append() 로 넣게 하세요. spark-submit load.py 2026-03-01 로 돌리세요.

append 한 번이 커밋 한 번이고 커밋 한 번이 스냅샷 하나입니다. 채점기는 첫 스냅샷이 부모 없이 append 로 생겼는지, 요약(summary)의 added-records 가 그날 파일의 행 수와 같은지 봅니다.

둘째 커밋 — 부모를 가리키는 스냅샷

같은 스크립트로 spark-submit load.py 2026-03-02 를 돌려 스냅샷을 정확히 둘로 만드세요.

새 스냅샷은 앞 스냅샷을 부모(parent-snapshot-id)로 가리키고, 시퀀스 번호가 하나 늘어납니다. 첫 스냅샷의 파일은 다시 쓰이지 않고 새 파일 하나만 더해집니다. 같은 날짜를 두 번 넣었다면 표를 PURGE 로 지우고 1단계부터 다시 하세요.

카탈로그의 포인터 두 칸

/root/ice/catalog.dbiceberg_tables 에서 meta.orders 행의 metadata_locationprevious_metadata_location 을 읽어 /root/ice/meta/out/pointer.txt 첫 줄과 둘째 줄에 쓰세요.

JDBC 카탈로그는 표마다 한 줄뿐입니다. 커밋은 '지금 값이 내가 읽은 값과 같을 때만 새 값으로 바꾼다' 는 조건부 UPDATE 이고, 바뀌기 전 값이 previous 칸에 남습니다. sqlite3 -separator 로 두 칸을 두 줄로 찍을 수 있습니다.

metadata.json 의 스냅샷 목록

현재 metadata.json(ice-loc meta.orders)에서 /root/ice/meta/out/snapshots.json{"current_snapshot_id": 정수, "snapshots": [{"snapshot_id", "parent_snapshot_id", "sequence_number", "manifest_list"}, …]} 모양으로 만드세요.

metadata.json 의 키는 하이픈을 씁니다(current-snapshot-id, parent-snapshot-id). jq 에서는 .["snapshot-id"] 처럼 대괄호로 읽습니다. 스냅샷 ID 는 19자리 정수라 손으로 옮기면 틀리기 쉽습니다 — jq 로 그대로 옮기세요.

매니페스트 리스트 — 매니페스트들의 목록

현재 스냅샷의 manifest_list 파일을 ice-avro 로 풀어 /root/ice/meta/out/manifests.json[{"manifest_path", "added_snapshot_id", "added_files_count", "existing_files_count"}, …] 배열로 쓰세요.

둘째 스냅샷의 매니페스트 리스트에는 첫 커밋이 만든 매니페스트가 그대로 다시 들어 있습니다. 새 커밋은 옛 매니페스트를 고쳐 쓰지 않고 가리키기만 합니다 — 그래서 커밋이 싸고, 옛 스냅샷이 그대로 남습니다. added_snapshot_id 로 어느 커밋이 만든 매니페스트인지 봅니다.

매니페스트 — 데이터 파일과 행 수

manifests.json 의 매니페스트들을 풀어 status 가 2(DELETED)가 아닌 항목의 데이터 파일을 /root/ice/meta/out/datafiles.json[{"file_path", "record_count"}, …] 로 쓰세요.

매니페스트 한 줄(엔트리)은 status(0 EXISTING·1 ADDED·2 DELETED)와 data_file 구조체입니다. 읽는 엔진은 이 목록과 열 통계(하한·상한)만 보고 어느 파일을 열지 정합니다 — 디렉터리를 훑지 않습니다. 채점기는 목록이 지금 스냅샷의 살아 있는 파일과 정확히 같은지, 행 수 합이 표의 행 수와 같은지 봅니다.

나무를 한 장으로

/root/ice/meta/report.md## 포인터 ## 스냅샷 ## 파일 세 절을 쓰세요. 둘째 절에는 스냅샷 수를, 셋째 절에는 현재 스냅샷의 매니페스트 수와 데이터 파일 수를 숫자로 넣으세요.

누군가 '어제 넣은 자료가 안 보인다' 고 할 때 어느 층부터 확인할지 순서로 적어 보세요. 숫자는 여러분의 out/ 파일에서 옮깁니다.