LLM WikiAccess-protected knowledge portal
← 스터디 홈
5편 · 약 22분

파일 레이아웃 최적화: small files, compaction, clustering, partitioning

파일 레이아웃은 왜 운영 문제가 되는가

Lakehouse table format을 처음 배울 때는 transaction log, snapshot, schema evolution 같은 기능이 더 눈에 띈다. 하지만 운영 환경에서 쿼리가 느려지고 비용이 튀는 순간을 따라가 보면, 꽤 자주 원인은 더 낮은 층에 있다. 파일이 너무 많거나, 너무 작거나, 자주 쓰는 조건과 물리 배치가 맞지 않는 것이다.

데이터 레이크의 기본 파일은 보통 Parquet 같은 immutable columnar file이다. 한 번 쓴 파일을 제자리에서 조금만 수정하는 방식이 아니다. streaming ingestion, CDC, micro-batch, upsert, delete가 반복되면 새 파일과 새 metadata가 계속 생긴다. 테이블 포맷은 snapshot과 transaction을 관리해 일관성은 지켜주지만, 파일 레이아웃이 자동으로 항상 좋아지는 것은 아니다.

운영자가 보는 증상은 대체로 비슷하다.

  • 같은 데이터 양인데 query planning 시간이 길어진다.
  • object storage의 list/open request가 늘고, 작은 Parquet 파일을 여는 고정 비용이 커진다.
  • executor task는 많아졌지만 각 task가 읽는 데이터가 작아 scheduling overhead가 늘어난다.
  • partition은 많지만 실제 필터 조건과 맞지 않아 pruning이 잘 안 된다.
  • compaction을 돌렸는데 쓰기 충돌, metadata 증가, 불필요한 rewrite 비용이 생긴다.

그래서 이 장의 핵심 질문은 “어떤 포맷이 더 빠른가”가 아니다. 더 실무적인 질문은 다음이다.

이 테이블의 read/write 패턴을 기준으로, 파일 크기·파티션·정렬·클러스터링을 어디까지 관리해야 하는가?


1. Small files 문제: 파일 개수도 성능 지표다

small files 문제는 “파일이 작으면 압축률이 조금 나쁘다” 정도의 이야기가 아니다. 분산 쿼리 엔진과 object storage에서는 파일 하나를 읽기 전에도 비용이 든다. 파일 목록을 가져오고, metadata를 읽고, split을 계획하고, task를 만들고, 파일을 열고 닫아야 한다. 1TB를 2,000개 파일로 읽는 것과 200,000개 파일로 읽는 것은 같은 1TB가 아니다.

Frequent writes
streaming / CDC / micro-batch
Many small files
10MB × thousands
Planning overhead
list / metadata / split
Read overhead
open / close / tasks
Higher latency
and higher cost
Compaction target
fewer, larger files
Better scan efficiency
but rewrite cost exists
Small files가 쿼리 비용을 키우는 경로

small files가 생기는 흔한 이유는 네 가지다.

  1. 너무 작은 batch: 예를 들어 1분마다 수 MB씩 쓰는 job이 partition별로 파일을 만든다.
  2. 고카디널리티 partition: user_id, request_id 같은 값을 partition으로 쓰면 partition마다 작은 파일이 생긴다.
  3. update/delete 중심 workload: 기존 파일을 직접 수정하지 못하므로 변경이 새 파일 또는 log file로 누적된다.
  4. 잘못된 writer parallelism: repartition 또는 writer task 수가 데이터 양보다 많아 file count가 폭증한다.

운영에서는 “평균 파일 크기”만 보면 부족하다. 평균은 건강해 보여도 특정 partition이 작은 파일 수천 개를 갖고 있을 수 있다. 최소한 다음 지표를 partition 단위로 봐야 한다.

지표왜 보는가
file count per partitionplanning overhead와 object storage request 증가를 확인
small file ratiotarget size보다 너무 작은 파일 비율 확인
total bytes per partitionpartition이 너무 작거나 너무 큰지 판단
query predicate distribution실제 필터 조건과 partition/clustering이 맞는지 판단
rewrite frequencycompaction이 개선인지 churn인지 판단

핵심은 파일 개수도 SLO처럼 관리해야 한다는 점이다. “쿼리가 느려졌다” 이후에만 compaction을 돌리는 것이 아니라, 파일 수·평균 크기·partition skew가 나빠지는 속도를 보면서 쓰기 경로를 조정해야 한다.


2. Compaction: 정리 작업이 아니라 rewrite 작업이다

Compaction은 작은 파일 여러 개를 더 적고 큰 파일로 다시 쓰는 작업이다. Iceberg의 rewrite_data_files, Delta Lake의 OPTIMIZE, Hudi의 clustering 또는 file sizing 기능이 모두 이 문제와 연결된다. 이름은 조금씩 다르지만 본질은 같다. 읽기 효율을 높이기 위해 데이터를 다시 쓰고 metadata를 갱신한다.

여기서 중요한 점은 compaction이 공짜 청소가 아니라는 것이다.

  • 새 데이터 파일을 만든다.
  • 새 snapshot, transaction log entry, commit 또는 replace action이 생긴다.
  • 기존 파일은 바로 지워지지 않고 retention, time travel, cleaner 정책의 영향을 받는다.
  • active writer와 같은 partition을 건드리면 conflict가 날 수 있다.
  • 이미 건강한 파일을 다시 쓰면 metadata churn과 compute 비용만 늘어난다.

따라서 좋은 compaction은 “매일 전체 테이블을 다시 쓰는 작업”이 아니라 상태 기반의 제한적 rewrite에 가깝다.

예를 들어 다음 기준으로 대상을 좁힐 수 있다.

  • small file 개수가 일정 수 이상인 partition만 처리한다.
  • 최근 writer가 계속 쓰는 hot partition은 피하고, 닫힌 시간 partition부터 처리한다.
  • target file size보다 충분히 작은 파일만 처리한다.
  • compaction 1회가 너무 큰 partition을 한 번에 잡지 않도록 file group 단위로 나눈다.
  • read query에서 실제로 자주 읽는 partition을 우선한다.

Iceberg 공식 API의 RewriteDataFiles도 파일 그룹 단위 rewrite, target file size, partial progress 같은 개념을 제공한다. Spark procedure에서는 rewrite_data_files로 binpack, sort, z-order 전략을 실행할 수 있다. Delta Lake는 OPTIMIZE로 small file compaction을 수행하고, Delta 3.1 이후에는 optimized write와 auto compaction 같은 기능도 제공한다. Hudi는 write 중 auto-sizing과 write 이후 clustering이라는 두 경로를 제공한다.

운영 관점에서는 다음 질문이 더 중요하다.

  1. compaction이 읽기 성능을 얼마나 개선했는가?
  2. 같은 partition을 너무 자주 다시 쓰고 있지는 않은가?
  3. compaction이 ingestion job과 리소스를 경쟁하지 않는가?
  4. compaction 후 metadata cleanup, vacuum, snapshot expiration, cleaner가 따라오는가?
  5. 장애 시 compaction은 idempotent하게 재시도 가능한가?

Compaction은 테이블을 빠르게 만들 수 있지만, 과하면 테이블을 바쁘게만 만든다. “정기 실행”보다 “임계값 기반 실행”이 더 안전한 이유가 여기에 있다.


3. Partitioning: 디렉터리 구조가 아니라 pruning 계약이다

Partitioning은 데이터를 directory-like layout으로 나누어 scan 범위를 줄이는 기법이다. 하지만 lakehouse table format에서 partition은 단순한 폴더 설계가 아니다. query engine에게 “이 조건이면 이 파일 묶음은 읽지 않아도 된다”고 알려주는 pruning 계약에 가깝다.

좋은 partition key는 보통 다음 조건을 만족한다.

  • 쿼리의 WHERE 조건에 자주 등장한다.
  • cardinality가 너무 높지 않다.
  • partition별 데이터 크기가 너무 작거나 너무 크지 않다.
  • 시간 흐름에 따라 관리하기 쉽다.
  • 보존 정책, backfill, compaction scope와 잘 맞는다.

반대로 나쁜 partition key는 레이크를 빠르게 망가뜨린다. user_id처럼 값이 너무 많은 컬럼으로 partition하면 수많은 작은 partition과 작은 파일이 생긴다. country처럼 값은 적지만 특정 값에 데이터가 몰리면 skew가 생긴다. event_time을 너무 잘게, 예를 들어 minute 단위로 나누면 streaming workload에서 partition 폭발이 생긴다.

선택장점위험
date partition보존, backfill, 시간 필터에 단순하루 데이터가 너무 크면 partition 내부 scan이 큼
date/hour partition시간 단위 재처리와 pruning이 좋아짐트래픽 낮은 시간대 small files 증가
business dimension partition특정 tenant, region 필터에 효과skew와 고카디널리티 위험
no partition + clusteringpartition 폭발 회피engine/format의 clustering 지원과 metadata pruning에 의존

Iceberg는 partition evolution을 지원하기 때문에 시간이 지나며 partition spec을 바꿀 수 있다. 예를 들어 처음에는 day partition으로 시작했다가 데이터가 커지면 hour transform을 추가할 수 있다. 그러나 evolution이 가능하다는 말이 아무 비용이 없다는 뜻은 아니다. 과거 파일은 과거 spec으로 남고, 새 파일은 새 spec으로 들어온다. 운영자는 spec 변경 이후 compaction, sort order, query plan이 어떻게 달라지는지 확인해야 한다.

Delta Lake도 전통적인 partitioning을 지원하지만, 최근에는 liquid clustering처럼 partition column을 유연하게 재정의하고 OPTIMIZE로 점진적 clustering을 적용하는 방향을 제공한다. 다만 liquid clustering은 partitioning 및 ZORDER와의 호환 조건, Delta protocol feature, 기존 데이터 reclustering 범위 같은 제약을 반드시 확인해야 한다.

Partitioning을 설계할 때의 실무 원칙은 간단하다.

partition은 “나누고 싶은 기준”이 아니라 “안 읽어도 되는 파일을 안정적으로 제외할 수 있는 기준”이어야 한다.


4. Clustering과 sorting: 같은 partition 안에서 더 잘 건너뛰게 한다

Partitioning은 큰 덩어리를 나누는 기술이다. 하지만 partition 하나가 수십 GB 또는 수 TB가 되면, partition pruning만으로는 부족하다. 이때 clustering, sorting, z-ordering 같은 파일 내부·파일 간 배치 전략이 중요해진다.

Clustering의 목적은 자주 함께 조회되는 레코드를 물리적으로 가까이 두는 것이다. 이렇게 하면 파일별 min/max statistics, column statistics, data skipping index가 더 잘 작동한다. 예를 들어 event_date partition 안에서 customer_id 조건이 자주 쓰인다면, customer_id 기준으로 정렬하거나 clustering하면 특정 고객 범위가 일부 파일에 모여 더 많은 파일을 skip할 수 있다.

대표 전략은 다음처럼 구분할 수 있다.

전략설명잘 맞는 경우
binpack작은 파일을 target size에 맞게 묶음파일 수가 많고 정렬 이슈는 크지 않을 때
sort한두 컬럼 기준으로 정렬해 range scan 개선시간, id 범위 검색이 많은 경우
z-order여러 컬럼을 함께 고려해 multidimensional locality 개선여러 필터 조합이 자주 나오는 경우
liquid clusteringDelta에서 partition/ZORDER 대안으로 clustering column을 점진 적용access pattern이 바뀌거나 고카디널리티 필터가 중요한 경우

Iceberg의 rewrite_data_files는 binpack뿐 아니라 sort, z-order 전략을 지원하는 환경이 있다. Delta Lake의 ZORDER BY는 관련 값이 같은 파일에 모이도록 해 file skipping을 개선하는 데 쓰이고, liquid clustering은 clustering column을 테이블 수준에서 관리하며 OPTIMIZE 실행 시 점진적으로 layout을 개선한다. Hudi clustering은 eligible file group을 골라 target file size에 맞춘 새 file group을 만들고, 필요하면 지정한 컬럼 기준으로 정렬한다.

다만 clustering은 “한 번 해두면 끝”이 아니다. 새 데이터는 계속 들어오고, query pattern은 바뀐다. 오늘은 tenant_id가 중요했지만 다음 분기에는 product_idevent_type 조합이 중요해질 수 있다. 그래서 clustering 운영은 다음 순서로 접근하는 편이 안전하다.

  1. query log에서 실제 predicate와 join key를 본다.
  2. partition pruning으로 충분한지 먼저 확인한다.
  3. partition 내부에서 scan bytes가 큰 경우 clustering 후보 컬럼을 고른다.
  4. 전체 테이블이 아니라 최근 또는 hot partition 일부로 실험한다.
  5. 파일 수, scan bytes, planning time, write amplification을 before/after로 비교한다.

Clustering은 읽기 성능을 개선하지만 쓰기 비용을 늘린다. “빨라질 것 같아서”가 아니라, 특정 쿼리 패턴의 scan bytes를 줄인다는 가설을 갖고 적용해야 한다.


5. 포맷별로 같은 문제를 다르게 푼다

Iceberg, Delta Lake, Hudi는 모두 lakehouse table format이지만 파일 레이아웃 최적화의 운영 감각은 조금씩 다르다.

Apache Iceberg

Iceberg에서는 metadata table과 snapshot 구조를 통해 현재 파일 분포를 관찰하고, rewrite_data_files 같은 action/procedure로 data file을 다시 쓴다. 일반적인 compaction은 작은 파일을 target size에 맞춰 binpack한다. 필요하면 sort order 또는 z-order 전략으로 clustering 효과를 낼 수 있다.

Iceberg 운영에서 특히 중요한 점은 partition spec과 sort order의 관계다. partition evolution이 가능하므로 과거 spec과 현재 spec이 공존할 수 있다. 이 상태에서 무작정 compaction을 돌리기보다, 어떤 spec의 파일이 얼마나 남았는지와 query pruning이 어떻게 작동하는지 확인해야 한다.

Delta Lake

Delta Lake에서는 OPTIMIZE가 small file compaction의 대표 명령이다. Delta Lake 문서는 small file이 read query를 느리게 만들며, 여러 small file을 더 큰 파일로 합치는 방식으로 overhead를 줄인다고 설명한다. open-source Delta 3.1 이후에는 optimized write와 auto compaction도 제공되어, 쓰기 경로에서 small files 생성을 줄이는 선택지가 생겼다.

ZORDER BY는 여러 컬럼의 값을 비슷한 파일에 모아 file skipping을 개선하는 데 쓰인다. Delta의 liquid clustering은 partitioning과 ZORDER의 한계를 줄이기 위한 기능으로, 고카디널리티 컬럼 필터, skew, 빠르게 커지는 테이블, 바뀌는 access pattern에 유리하다고 문서화되어 있다. 다만 table feature와 protocol compatibility를 바꾸므로 도입 전 reader/writer 호환성을 확인해야 한다.

Apache Hudi

Hudi는 small files 대응을 ingestion 경로에 더 강하게 넣은 편이다. 공식 문서는 Hudi가 write 중 auto-sizing으로 적절한 크기의 파일을 만들려 하고, 기본 Parquet base file target size와 small file limit을 설정할 수 있다고 설명한다. 또한 clustering table service로 small files를 target size에 맞게 묶고, sort key 기준으로 locality를 개선할 수 있다.

Hudi에서는 compaction과 clustering을 구분해야 한다. Merge-on-Read의 compaction은 delta log를 base file에 병합해 최신 file slice를 만드는 작업이다. Clustering은 파일 레이아웃을 최적화하는 작업이다. 둘 다 rewrite를 수반하지만 해결하는 문제가 다르다. MoR 테이블에서는 compaction backlog와 clustering backlog가 서로 다른 운영 지표로 관리되어야 한다.


6. 운영 체크리스트: 언제 무엇을 바꿀 것인가

파일 레이아웃 문제를 운영할 때는 “어떤 명령을 실행할까”보다 “어떤 신호에서 어떤 조치를 할까”가 먼저다.

신호가능한 원인우선 조치
planning time 증가파일 수 또는 manifest/log metadata 증가file count, snapshot/metadata growth 확인
scan bytes가 과도함partition/clustering이 predicate와 불일치query log 기반 partition/clustering 재검토
특정 partition만 느림skew, hot partition, small files 집중partition별 file count/bytes 분포 확인
compaction 후 비용 증가불필요한 rewrite, target size 부적절compaction 조건과 scope 제한
streaming write 지연 증가auto compaction 또는 file sizing 비용write path 최적화와 async maintenance 분리
object storage request 증가small files, 많은 partition listing파일 수 감축과 metadata pruning 개선

실무적인 실행 순서는 다음이 안전하다.

  1. 관찰: table metadata에서 partition별 file count, file size distribution, snapshot/log 증가를 본다.
  2. 쿼리 연결: 느린 쿼리가 어떤 partition과 컬럼을 읽는지 확인한다.
  3. 쓰기 경로 수정: writer batch size, parallelism, partition key, target file size를 먼저 조정한다.
  4. 제한적 compaction: 이미 나빠진 partition만 대상으로 binpack 또는 sort compaction을 적용한다.
  5. clustering 실험: query predicate가 반복되는 경우 일부 partition에서 clustering 효과를 측정한다.
  6. 정리 정책 확인: snapshot expiration, vacuum, cleaner, metadata cleanup이 retention 요구와 맞는지 확인한다.

이 순서를 거꾸로 하면 문제가 생긴다. 예를 들어 partition key가 잘못되어 매시간 small files가 생기는데 compaction만 매일 돌리면, 매일 같은 문제를 비싼 rewrite로 덮는 구조가 된다. 먼저 small files를 덜 만들고, 그 다음 남은 조각을 compaction으로 고치는 편이 낫다.


7. 예시: 주문 이벤트 테이블의 레이아웃 튜닝

orders_events 테이블이 있다고 하자. Kafka에서 5분 micro-batch로 들어오고, BI 쿼리는 최근 30일 주문을 event_date, tenant_id, status 조건으로 자주 조회한다. 처음 설계는 event_date/hour partition이었다.

초기에는 문제가 없어 보였지만, 소규모 tenant가 많아지면서 시간 partition마다 작은 파일이 쌓였다. 쿼리는 최근 30일을 읽으므로 partition 수가 많고, tenant_id 조건은 partition pruning에 직접 쓰이지 못했다. 운영자는 다음 순서로 접근할 수 있다.

  1. partition별 file count를 확인해 작은 hour partition이 과도한지 본다.
  2. writer batch를 키우거나, 낮은 트래픽 테이블은 event_date partition으로 완화한다.
  3. 닫힌 날짜 partition만 compaction하여 target file size에 맞춘다.
  4. tenant_id, status 조건이 반복된다면 partition 내부 clustering 또는 z-order를 실험한다.
  5. before/after로 planning time, scan files, scan bytes, BI latency를 비교한다.
  6. retention 기간이 지난 snapshot/log/file version cleanup을 별도 정책으로 운영한다.

이 예시에서 정답은 “항상 hour partition을 없애라”가 아니다. 트래픽이 큰 테이블에서는 hour partition이 재처리와 pruning에 도움이 될 수 있다. 문제는 모든 테이블에 같은 partition grain을 적용한 것이다. 파일 레이아웃 최적화는 테이블별 workload에 맞춰야 한다.


정리

파일 레이아웃 최적화는 lakehouse 운영에서 가장 반복적으로 마주치는 일이다. 이 장의 내용을 네 문장으로 정리하면 다음과 같다.

  1. Small files는 저장 공간 문제가 아니라 planning, object storage request, task scheduling, scan efficiency를 모두 건드리는 운영 문제다.
  2. Compaction은 청소가 아니라 rewrite이므로, 전체 테이블 정기 실행보다 상태 기반·범위 제한 실행이 안전하다.
  3. Partitioning은 디렉터리 구조가 아니라 query pruning 계약이며, cardinality와 skew를 함께 봐야 한다.
  4. Clustering, sorting, z-ordering은 partition 내부의 data skipping을 개선하지만, 실제 query log와 before/after 측정 없이 적용하면 rewrite 비용만 늘 수 있다.

Lakehouse table format을 잘 운영한다는 것은 transaction log를 이해하는 것에서 끝나지 않는다. 파일이 어떤 크기로, 어떤 partition에, 어떤 정렬과 clustering 상태로 쌓이는지 계속 관찰하고 조정해야 한다. 결국 빠른 테이블은 좋은 포맷 하나로 만들어지는 것이 아니라, 쓰기 경로에서 작은 파일을 덜 만들고, 읽기 경로에서 건너뛸 수 있는 파일을 늘리는 운영 습관으로 만들어진다.

References

  • Apache Iceberg Docs, “Spark Procedures: rewrite_data_files”, https://iceberg.apache.org/docs/latest/spark-procedures/#rewrite_data_files
  • Apache Iceberg Docs, “Maintenance: Compact data files”, https://iceberg.apache.org/docs/latest/maintenance/#compact-data-files
  • Delta Lake Blog, “Delta Lake Optimize”, https://delta.io/blog/delta-lake-optimize
  • Delta Lake Docs, “Use liquid clustering for Delta tables”, https://docs.delta.io/delta-clustering
  • Delta Lake Documentation, “Delta Lake Z Order”, https://delta-io.github.io/delta-rs/usage/optimize/delta-lake-z-order
  • Apache Hudi Docs, “File Sizing”, https://hudi.apache.org/docs/file_sizing/
  • Apache Hudi Docs, “Clustering”, https://hudi.apache.org/docs/clustering/
  • Apache Hudi Technical Specification, “Table Services: Compaction and Clustering”, https://hudi.apache.org/learn/tech-specs/