Spark 与 pyiceberg 共用一个目录,重命名表,并找回已删除的表
한국어 원문으로 표시합니다.
목표
JDBC 카탈로그(SQLite 파일 하나)를 Spark 와 pyiceberg 가 함께 쓰게 하고, 커밋이 카탈로그의 포인터 한 칸을 바꾸는 일이라는 것을 커밋 전후 값으로 확인한다. 표 이름 바꾸기와 지우기가 카탈로그에서만 일어나고 파일은 그대로라는 것을 보고, metadata 파일 하나로 지운 표를 되살린다.
왜 중요한가
Iceberg 표의 '진짜 상태' 는 metadata 파일에 있고, 카탈로그는 이름 하나를 그 파일 하나에 잇는 작은 표다. 그런데 이 작은 표가 동시 쓰기의 심판이다. 두 작가가 동시에 커밋하면 카탈로그는 '내가 읽은 포인터가 아직 그대로일 때만 바꾼다' 는 조건으로 한쪽만 받아 준다. 카탈로그가 이 원자적 교체를 못 하면 표가 깨진다. 같은 카탈로그를 여러 엔진이 보면 Spark 가 만든 표를 파이썬 잡이 이어 쓰고, 그 결과를 다시 Spark 가 읽는다. 한편 이름·위치·파일이 서로 다른 층이라는 것을 모르면 사고가 난다. 이름을 바꾸면 파일도 옮겨졌을 거라 믿고 옛 경로를 지우거나, DROP TABLE 이 공간을 비웠을 거라 믿고 기다리거나, 반대로 실수로 지운 표를 영영 잃었다고 포기한다.
단계
- /root/ice/cat/make.py(앱
ice-cat-make)로lake.cat.orders(format-version 2)를 만들고 2026-03-01·03-02·03-03 세 파일을 한 번에 커밋하세요. - /root/ice/cat/list.py(pyiceberg,
load_catalog("lake"))로 네임스페이스와 표 목록을 /root/ice/cat/out/tables.json 에 쓰세요. - /root/ice/cat/customers.py(pyiceberg)로
/data/ice/customers.csv를 읽어lake.cat.customers를 만들고 넣으세요. - /root/ice/cat/join.py(앱
ice-cat-join)로 두 표를customer_id로 조인해 등급별 주문 수 표lake.cat.tier_counts(tier, orders)를 만드세요. lake.cat.orders에 2026-03-04 를 커밋하면서 커밋 전 경로·커밋 뒤 경로·커밋 뒤 이전 칸을 /root/ice/cat/out/pointer.json 에 쓰세요.lake.cat.tier_counts를lake.cat.tier_summary로 이름을 바꾸고, 바꾸기 전후의 metadata 경로를 /root/ice/cat/out/rename.json 에 쓰세요.lake.cat.tier_summary를 PURGE 없이 지운 뒤 남은 데이터 파일 수를 세고, 지우기 직전의 metadata 파일로lake.cat.tier_restored를 등록해 /root/ice/cat/out/restore.json 에 쓰세요.- /root/ice/cat/report.md 에
## 포인터## 이름과 위치## 지우기와 되살리기세 절을 쓰세요.
참고
- 카탈로그는
sqlite3 /root/ice/catalog.db "select * from iceberg_tables"로 그대로 볼 수 있습니다.ice-loc <ns>.<표>는 현재 metadata 경로를 찍습니다. - pyiceberg 설정은
~/.pyiceberg.yaml에 있습니다(카탈로그 이름lake,type: sql). Spark 쪽 설정은/opt/spark/conf/spark-defaults.conf입니다. - pyiceberg 가 처음 카탈로그를 열 때 'v0 schema' 경고를 찍습니다. Java 의 JdbcCatalog 가 만든 옛 모양(뷰 칸 없음)이라는 뜻이고 표를 쓰는 데는 문제가 없습니다.
RENAME TO뒤의 새 이름에는 카탈로그를 붙이지 않습니다(cat.tier_summary).lake.cat.tier_summary로 쓰면 Spark 가lake.cat이라는 네임스페이스를 찾다가 NoSuchNamespaceException 으로 멈춥니다(실측).- 흔한 실수: 7단계에서
DROP TABLE … PURGE를 쓰는 것 — 파일까지 지워져 되살릴 수 없습니다. 그렇게 됐다면 4단계부터 다시 하세요. - 공식 문서: JDBC Catalog · Spark DDL · Spark Procedures — register_table · pyiceberg — SQL Catalog
Spark 가 표를 만든다 — 커밋 한 번
/root/ice/cat/make.py 를 앱 이름 ice-cat-make 로 만들어 lake.cat 네임스페이스와 lake.cat.orders(열 여섯 개, 'format-version' = '2')를 만들고, 2026-03-01·03-02·03-03 세 파일을 한 번의 append() 로 넣으세요.
spark.read.csv([경로1, 경로2, 경로3], …) 처럼 목록을 주면 한 DataFrame 이 됩니다. 커밋이 한 번이면 스냅샷도 하나입니다. 채점기는 첫 스냅샷의 행 수가 세 파일의 합인지, 요약에 Spark 가 남긴 engine-name 이 있는지 봅니다.
pyiceberg 로 같은 카탈로그 보기
/root/ice/cat/list.py 를 pyiceberg 의 load_catalog("lake") 로 만들어 네임스페이스와 표 목록을 /root/ice/cat/out/tables.json 에 {"namespaces": ["cat", …], "tables": ["cat.orders", …]} 로 쓰고 python3 list.py 로 돌리세요.
pyiceberg 는 ~/.pyiceberg.yaml 에서 lake 카탈로그 설정(type: sql, uri: sqlite:////root/ice/catalog.db)을 읽습니다. 서버 없이 같은 파일을 여는 것이라 Spark 가 만든 표가 그대로 보입니다. 이름은 튜플로 나오니 점으로 이어 붙이세요.
pyiceberg 가 표를 만든다
/root/ice/cat/customers.py 로 /data/ice/customers.csv 를 pyarrow 로 읽어(signup_date 는 date32) lake.cat.customers 를 만들고 넣으세요. 다시 돌려도 행이 두 배가 되지 않게 overwrite() 를 쓰세요.
create_table_if_not_exists("cat.customers", schema=arrow_table.schema) 는 pyarrow 스키마를 Iceberg 스키마로 옮기며 열마다 필드 ID 를 매깁니다. pyiceberg 가 쓴 스냅샷의 요약에는 Spark 가 남기는 engine-name 이 없습니다 — 채점기는 그걸로 누가 썼는지 봅니다.
Spark 가 두 엔진의 표를 조인한다
/root/ice/cat/join.py 를 앱 이름 ice-cat-join 으로 만들어 lake.cat.orders 와 lake.cat.customers 를 customer_id 로 조인하고, 등급(tier)별 주문 수를 lake.cat.tier_counts(열 tier, orders)로 만드세요(CREATE TABLE … AS SELECT).
누가 썼든 Iceberg 표는 같은 스펙의 metadata 와 Parquet 이라 엔진은 상관하지 않습니다. 채점기는 원본 CSV 로 등급별 주문 수를 직접 계산해 견줍니다(orders 는 이때 3월 1–3일 치).
커밋 한 번에 포인터가 한 칸 움직인다
lake.cat.orders 의 현재 metadata 경로를 적어 둔 뒤 /root/ice/cat/append.py(날짜 인자)로 2026-03-04 를 커밋하고, 커밋 뒤 경로와 카탈로그의 previous_metadata_location 을 읽어 /root/ice/cat/out/pointer.json 에 {"before", "after", "previous_after"} 로 쓰세요.
커밋은 새 metadata 파일을 먼저 다 써 둔 다음, 카탈로그에서 '지금 값이 before 일 때만 after 로 바꾼다' 는 조건부 갱신 한 번으로 끝납니다. 그래서 커밋 뒤 previous 칸은 커밋 전 경로와 같아야 합니다. jq -n --arg 로 셸 변수를 JSON 으로 묶을 수 있습니다.
이름은 카탈로그에만 있다
ALTER TABLE lake.cat.tier_counts RENAME TO cat.tier_summary 를 돌리고(새 이름에는 카탈로그 lake 를 붙이지 않습니다), 바꾸기 전(tier_counts)과 뒤(tier_summary)의 metadata 경로를 /root/ice/cat/out/rename.json 에 {"before", "after"} 로 쓰세요.
JDBC 카탈로그의 이름 바꾸기는 iceberg_tables 한 줄의 table_name 을 고치는 UPDATE 입니다. 표 위치(location)와 파일은 그대로라, 새 이름의 표가 여전히 …/cat/tier_counts/ 아래 파일을 가리킵니다. 채점기는 두 경로가 같은지, 옛 이름이 카탈로그에서 사라졌는지 봅니다.
지운 표를 metadata 파일 하나로 되살린다
lake.cat.tier_summary 의 metadata 경로를 적어 둔 뒤 PURGE 없이 DROP TABLE 하고, /root/ice/warehouse/cat/tier_counts/data 에 남은 Parquet 파일 수를 세고, CALL lake.system.register_table(table => 'lake.cat.tier_restored', metadata_file => '<그 경로>') 로 되살리세요. /root/ice/cat/out/restore.json 에 {"metadata_file", "files_left"} 를 쓰세요.
PURGE 없는 DROP 은 카탈로그의 한 줄만 지웁니다. metadata 파일이 스키마·스냅샷·파일 목록을 전부 들고 있으니, 그 경로만 알면 어느 카탈로그에든 다시 등록할 수 있습니다(카탈로그를 옮기거나 백업에서 되살릴 때 쓰는 방법입니다). PURGE 를 붙였다면 파일이 지워져 되살릴 수 없습니다.
카탈로그가 하는 일과 하지 않는 일
/root/ice/cat/report.md 에 ## 포인터 ## 이름과 위치 ## 지우기와 되살리기 세 절을 쓰세요. 셋째 절에는 7단계에서 센 남은 파일 수를 숫자로 넣으세요.
운영에서 카탈로그를 바꾸거나(예: JDBC → REST) 백업에서 표를 되살릴 때 무엇을 옮겨야 하고 무엇은 그대로 두면 되는지를 적어 보세요.