Blog
icebergperformancetuninglakehousesparktrino

Apache Iceberg 성능 튜닝 가이드 — 주의 사항과 핵심 파라미터 총정리

Iceberg 운영에서 성능을 좌우하는 주의 사항, 안티 패턴, 그리고 write/read/commit/compaction/manifest 카테고리별 핵심 테이블 프로퍼티와 엔진(Spark/Trino/Flink) 설정을 한 편으로 정리합니다.

Data Dynamics2026년 5월 23일18 min read

Iceberg는 도입 자체보다 "운영하면서 성능이 어떻게 변하는가" 가 훨씬 중요합니다. 메타데이터가 누적되고, 작은 파일이 쌓이고, Delete 파일이 늘어나고, Snapshot이 만 단위가 되면 동일한 쿼리가 수십 배 느려질 수 있습니다. 이 글은 Iceberg 성능을 좌우하는 주의 사항과 카테고리별 핵심 파라미터를 실무 관점에서 정리합니다.


1. 성능 관점에서 본 Iceberg의 동작 모델

성능 튜닝 전에 두 가지 핵심을 짚고 갑니다.

1.1 쿼리 한 번에 일어나는 일

Catalog Lookup              → metadata.json 위치
Read metadata.json          → 현재 Snapshot 결정
Read Manifest List          → 파티션 통계로 manifest pruning
Read Manifest Files (병렬)  → 컬럼 stats로 data file pruning
Open Data Files (병렬)      → Parquet footer + row group skip
Scan Row Groups             → 필요한 row group만 디코딩

각 단계마다 파일 수, 파일 크기, 통계 품질, 정렬 상태가 성능에 영향을 줍니다. 한쪽을 너무 키우면 다른 쪽이 무너집니다.

1.2 쓰기 한 번에 일어나는 일

Plan write                  → 파티션·정렬 정책 적용
Write data files            → Parquet 인코딩 + 통계 수집
Write delete files (MoR)    → Position/Equality Delete
Write manifest files        → 새 파일들의 entry 추가
Write manifest list         → 기존 + 신규 manifest 결합
Atomic commit               → Catalog CAS로 snapshot pointer 교체

Commit이 잦으면 manifest와 snapshot이 폭증합니다. 쓰기 빈도와 파일 크기가 사실상 가장 큰 성능 결정 요인 입니다.


2. 가장 중요한 주의 사항 10가지

순서대로 영향력이 큰 항목입니다.

2.1 Small File 누적

스트리밍/CDC에서 가장 흔한 문제. 1~10MB 파일이 수만 개가 되면 plan 시간이 분 단위로 늘어납니다.

  • 증상: EXPLAIN 결과 파일 수가 수만 개, scan 시간 대비 plan 시간이 비정상적으로 김
  • 해결: 정기적 rewrite_data_files, write.target-file-size-bytes 조정

2.2 Snapshot Expiration 미실행

Snapshot이 누적되면 metadata.json이 GB 단위로 비대해지고, GC도 안 됩니다.

  • 증상: metadata.json 파일이 수십 MB 이상, 테이블 오픈 자체가 느림
  • 해결: 일 단위 expire_snapshots

2.3 너무 잦은 Commit

스트리밍에서 초 단위 commit을 하면 manifest가 폭증합니다.

  • 권장: Flink commit interval ≥ 1분, 가능하면 5~10분
  • 해결: rewrite_manifests로 주기적 통합

2.4 Delete File 누적 (MoR)

V2 MoR에서 Delete 파일이 누적되면 읽기 시 매번 anti-join을 수행합니다.

  • 증상: 읽기 시간이 시간이 지날수록 선형 증가
  • 해결: rewrite_position_deletes, V3 Deletion Vector 채택

2.5 잘못된 파티셔닝

  • 고-카디널리티 컬럼 파티셔닝 (e.g., user_id): list 비용 폭발, manifest 폭증
  • 너무 세분화된 시간 파티션 (e.g., 1만개 미만 row마다 hourly): 파일 수 폭증
  • 파티션 컬럼 누락: 풀스캔

2.6 통계 누락 또는 부정확

컬럼 stats(min/max, null count)가 없거나 부정확하면 manifest pruning이 무력화됩니다.

  • 원인: 너무 많은 컬럼에 대한 stats 수집 → 일부 컬럼은 미수집 설정
  • 해결: write.metadata.metrics.default + 자주 사용하는 컬럼 위주 metrics.column.<col>

2.7 Hadoop Catalog 운영 사용

파일 락 기반이라 동시 commit 충돌 시 데이터 손실 위험. 개발 외에는 절대 사용 금지.

2.8 Manifest 비대화

수천 개 manifest를 매 쿼리마다 스캔하면 plan 시간이 폭증합니다.

  • 해결: commit.manifest-merge.enabled=true, 주기적 rewrite_manifests

2.9 Sort Order 미설정

자주 필터링하는 컬럼이 정렬되지 않으면 manifest min/max 범위가 너무 넓어 pruning이 안 됩니다.

2.10 Engine별 Vectorized Reader 미사용

Spark/Trino의 vectorized Parquet reader를 끄면 2~5배 느려집니다. 기본값 ON이지만 운영 중 의도치 않게 꺼지는 사례가 있습니다.


3. 핵심 테이블 프로퍼티 — Write

테이블에 TBLPROPERTIES로 설정하는 값들입니다. 가장 흔히 쓰는 값 위주로 정리합니다.

3.1 파일 크기와 포맷

프로퍼티기본값설명
write.format.defaultparquet데이터 파일 포맷 (parquet, orc, avro)
write.target-file-size-bytes536870912 (512MB)타깃 파일 크기. 분석 워크로드 권장 256MB~1GB
write.parquet.row-group-size-bytes134217728 (128MB)Parquet row group 크기. 너무 크면 row-group skip 효과 감소
write.parquet.page-size-bytes1048576 (1MB)Parquet page 크기
write.parquet.dict-size-bytes2097152 (2MB)Dictionary encoding 최대 크기
write.parquet.compression-codeczstdzstd/snappy/gzip/lz4 — 분석은 zstd 권장
write.parquet.compression-level(codec별)zstd는 1~9, 보통 3
write.parquet.bloom-filter-enabled.column.<col>false고-카디널리티 동등 필터에 사용
write.metadata.compression-codecnone메타데이터 압축. 대규모 테이블은 gzip 권장
write.object-storage.enabledfalseS3 키 prefix 랜덤화 — 대규모 테이블에서 throttling 회피
ALTER TABLE db.orders SET TBLPROPERTIES (
  'write.target-file-size-bytes' = '536870912',
  'write.parquet.compression-codec' = 'zstd',
  'write.parquet.compression-level' = '3',
  'write.metadata.compression-codec' = 'gzip',
  'write.object-storage.enabled' = 'true'
);

3.2 분포(Distribution)와 정렬

프로퍼티기본값설명
write.distribution-modehash (Spark v3+)none/hash/range. 파일 수와 skew 트레이드오프
write.delete.distribution-mode(write와 동일)Delete 파일 분포
write.update.distribution-mode(write와 동일)Update 분포
write.spark.fanout.enabledfalse파티션별 fanout 라이터. 파티션이 매우 많을 때 ON

hash는 파티션 단위로 데이터를 묶어 작은 파일을 줄이지만 skew에 약하고, range는 정렬과 함께 균등 분배에 유리합니다.

3.3 Row-level 연산 모드

프로퍼티기본값(V2)설명
write.delete.modecopy-on-writecopy-on-write 또는 merge-on-read
write.update.modecopy-on-write같음
write.merge.modecopy-on-write같음
write.delete.target-file-size-bytes67108864 (64MB)Delete 파일 크기
write.delete.parquet.compression-codeczstdDelete 파일 압축

CDC/잦은 변경은 MoR(merge-on-read), 배치 ETL은 CoW를 권장합니다. 모든 모드를 일치시키지 않으면 의외의 비용이 발생합니다.

3.4 메타데이터 정리

프로퍼티기본값설명
write.metadata.delete-after-commit.enabledfalsecommit 후 오래된 metadata.json 자동 삭제
write.metadata.previous-versions-max100보관할 metadata.json 버전 수

운영 환경에서는 보통 delete-after-commit=true, previous-versions-max=20 정도로 설정합니다.

3.5 통계 수집

프로퍼티기본값설명
write.metadata.metrics.defaulttruncate(16)모든 컬럼에 대한 기본 stats 모드
write.metadata.metrics.column.<col>컬럼별 override (none/counts/truncate(N)/full)
write.metadata.metrics.max-inferred-column-defaults100기본 stats를 수집할 최대 컬럼 수

컬럼이 수백 개인 wide 테이블에서는 stats가 metadata를 비대하게 만듭니다. 자주 필터링하는 5~10개 컬럼은 full, 나머지는 counts 또는 none을 권장합니다.

ALTER TABLE db.orders SET TBLPROPERTIES (
  'write.metadata.metrics.default' = 'counts',
  'write.metadata.metrics.column.order_id' = 'full',
  'write.metadata.metrics.column.ts' = 'full',
  'write.metadata.metrics.column.customer_id' = 'full'
);

4. 핵심 테이블 프로퍼티 — Read

프로퍼티기본값설명
read.split.target-size134217728 (128MB)한 task가 처리할 데이터 크기
read.split.metadata-target-size33554432 (32MB)Metadata 테이블 split 크기
read.split.planning-lookback10Split 패킹 lookback
read.split.open-file-cost4MB파일 오픈 비용 추정값(작은 파일 결합 임계치)
read.parquet.vectorization.enabledtrueParquet vectorized reader
read.parquet.vectorization.batch-size5000Vectorized batch row 수
ALTER TABLE db.orders SET TBLPROPERTIES (
  'read.split.target-size' = '268435456',
  'read.split.open-file-cost' = '8388608'
);

open-file-cost는 매우 중요합니다. 작은 파일이 많을 때 이 값을 올리면 task당 파일 결합이 늘어 plan 효율이 좋아집니다.


5. Commit / Manifest 관련

5.1 Commit 재시도

프로퍼티기본값설명
commit.retry.num-retries4OCC 충돌 시 재시도 횟수
commit.retry.min-wait-ms100최소 대기
commit.retry.max-wait-ms60000최대 대기
commit.retry.total-timeout-ms1800000 (30분)총 재시도 타임아웃
commit.status-check.num-retries3Commit 상태 확인 재시도

스트리밍 + 배치가 동시에 같은 테이블에 쓰는 환경이라면 num-retries를 8~10으로 올리는 것이 안전합니다.

5.2 Manifest 통합

프로퍼티기본값설명
commit.manifest.target-size-bytes8388608 (8MB)타깃 manifest 크기
commit.manifest.min-count-to-merge100이 이상이면 manifest 머지
commit.manifest-merge.enabledtrueCommit 시 자동 manifest 머지

스트리밍 commit이 잦은 테이블은 min-count-to-merge를 50~80으로 낮춰 manifest 폭증을 미리 막습니다.

5.3 Snapshot 보존

프로퍼티기본값설명
history.expire.max-snapshot-age-ms432000000 (5일)Snapshot 최대 보존 기간
history.expire.min-snapshots-to-keep1최소 보존 Snapshot 수
history.expire.max-ref-age-ms(제한 없음)브랜치/태그 최대 보존 기간

테이블별로 차등 적용을 권장합니다. 마스터 테이블은 30일, 시계열 fact는 7일 등.


6. Compaction 절차(Action) 파라미터

rewrite_data_files 호출 시 함께 전달하는 옵션입니다.

옵션기본값설명
target-file-size-bytes테이블 프로퍼티타깃 파일 크기
min-input-files5이 값 이상의 파일이 있어야 그룹 처리
min-file-size-bytes(target × 0.75)이보다 작으면 후보
max-file-size-bytes(target × 1.8)이보다 크면 후보
max-file-group-size-bytes107374182400 (100GB)한 그룹의 최대 크기
max-concurrent-file-group-rewrites5동시 처리 그룹 수
partial-progress.enabledfalse그룹별 commit으로 partial commit 허용
partial-progress.max-commits10partial commit 최대 횟수
rewrite-job-ordernonebytes-asc/bytes-desc/files-asc/files-desc
CALL demo.system.rewrite_data_files(
  table => 'db.orders',
  strategy => 'sort',
  sort_order => 'ts ASC NULLS LAST, customer_id ASC',
  options => map(
    'target-file-size-bytes', '536870912',
    'max-concurrent-file-group-rewrites', '10',
    'partial-progress.enabled', 'true',
    'partial-progress.max-commits', '50',
    'rewrite-job-order', 'bytes-asc'
  )
);

대형 테이블 컴팩션은 반드시 partial-progress.enabled=true. 그렇지 않으면 한 번의 실패로 수 시간의 작업이 날아갑니다.

6.1 Z-Order 컴팩션

CALL demo.system.rewrite_data_files(
  table => 'db.orders',
  strategy => 'sort',
  sort_order => 'zorder(customer_id, ts)',
  options => map('target-file-size-bytes', '536870912')
);

Z-Order는 2개 이상의 컬럼이 조합 필터링되는 경우에 효과적입니다.

6.2 Delete 파일 컴팩션

CALL demo.system.rewrite_position_deletes(
  table => 'db.orders',
  options => map(
    'rewrite-job-order', 'bytes-asc',
    'partial-progress.enabled', 'true'
  )
);

MoR 운영 테이블은 데이터 컴팩션보다 Delete 컴팩션이 더 중요할 수 있습니다.

6.3 Manifest 재작성

CALL demo.system.rewrite_manifests(
  table => 'db.orders',
  use_caching => true
);

Plan 시간이 평소보다 길어졌다면 가장 먼저 확인할 작업입니다.


7. 엔진별 설정 — Spark

7.1 SparkSession 레벨

# Iceberg vectorized reader
spark.sql.iceberg.vectorization.enabled = true
 
# Timestamp 처리 (timezone-aware)
spark.sql.iceberg.handle-timestamp-without-timezone = false
 
# Catalog 캐시
spark.sql.catalog.demo.cache-enabled       = true
spark.sql.catalog.demo.cache.expiration-interval-ms = 30000
 
# Adaptive Query Execution
spark.sql.adaptive.enabled                       = true
spark.sql.adaptive.coalescePartitions.enabled    = true
spark.sql.adaptive.skewJoin.enabled              = true
 
# Shuffle partitions (write distribution=hash와 함께)
spark.sql.shuffle.partitions = 800

7.2 쓰기 옵션 (DataFrame writer)

(df.writeTo("demo.db.orders")
    .option("write-format", "parquet")
    .option("target-file-size-bytes", "536870912")
    .option("check-ordering", "false")     # 정렬 검증 스킵
    .option("fanout-enabled", "true")      # 파티션이 많을 때
    .append())

7.3 읽기 옵션

(spark.read.format("iceberg")
    .option("split-size", "268435456")
    .option("lookback", "20")
    .option("file-open-cost", "8388608")
    .option("vectorization-enabled", "true")
    .load("demo.db.orders"))

8. 엔진별 설정 — Trino

iceberg.properties 또는 카탈로그 단위로 설정합니다.

connector.name = iceberg
iceberg.catalog.type = rest
iceberg.rest-catalog.uri = http://iceberg-rest:8181
 
# 성능 관련
iceberg.split-size                          = 128MB
iceberg.max-splits-per-second               = 100
iceberg.use-file-size-from-metadata         = true
 
# Manifest 캐시 (중요)
iceberg.metadata-cache.enabled              = true
iceberg.metadata-cache.max-size             = 1000
iceberg.metadata-cache.ttl                  = 1h
iceberg.io.manifest.cache-enabled           = true
iceberg.io.manifest.cache.max-total-bytes   = 104857600
iceberg.io.manifest.cache.expiration-interval-ms = 60000
 
# Parquet
parquet.use-column-index                    = true
parquet.use-bloom-filter                    = true
parquet.max-read-block-size                 = 16MB
 
# Statistics 기반 CBO
iceberg.table-statistics-enabled            = true
iceberg.extended-statistics.enabled         = true

Trino에서 가장 큰 차이를 만드는 옵션은 iceberg.io.manifest.cache-enabled 입니다. 대형 테이블 plan 시간이 절반 이하로 줄어듭니다.


CREATE TABLE orders_iceberg (
  id BIGINT,
  ts TIMESTAMP(3),
  amount DECIMAL(10,2)
) WITH (
  'connector' = 'iceberg',
  'catalog-type' = 'rest',
  'uri' = 'http://iceberg-rest:8181',
  'warehouse' = 's3://my-warehouse',
 
  -- Commit 빈도 (가장 중요)
  'write-parallelism' = '8',
  'upsert-enabled' = 'true',
  'equality-field-columns' = 'id',
 
  -- 파일 크기
  'write.target-file-size-bytes' = '134217728',   -- 128MB (스트리밍은 작게)
 
  -- Checkpoint 기반 commit
  'write.distribution-mode' = 'hash'
);

Flink는 checkpoint마다 commit 하므로 checkpoint interval이 사실상 commit interval입니다.

# flink-conf.yaml
execution.checkpointing.interval = 5min
execution.checkpointing.min-pause = 1min

체크포인트가 너무 빈번하면 manifest와 snapshot이 폭증하고, 너무 드물면 데이터 가시성이 떨어집니다. 5~10분이 일반적입니다.


10. 워크로드별 권장 프로파일

10.1 분석 위주(Read-heavy, 배치 적재)

ALTER TABLE db.orders SET TBLPROPERTIES (
  'write.target-file-size-bytes' = '1073741824',   -- 1GB
  'write.parquet.compression-codec' = 'zstd',
  'write.parquet.compression-level' = '6',
  'write.parquet.row-group-size-bytes' = '268435456',
  'write.delete.mode' = 'copy-on-write',
  'write.distribution-mode' = 'range',
  'read.split.target-size' = '536870912'
);

10.2 스트리밍 적재 + 배치 분석

ALTER TABLE db.events SET TBLPROPERTIES (
  'write.target-file-size-bytes' = '134217728',    -- 128MB
  'write.parquet.compression-codec' = 'zstd',
  'write.parquet.compression-level' = '3',
  'write.delete.mode' = 'merge-on-read',
  'write.distribution-mode' = 'hash',
  'commit.manifest.min-count-to-merge' = '50',
  'write.metadata.delete-after-commit.enabled' = 'true',
  'write.metadata.previous-versions-max' = '20'
);

10.3 CDC (Upsert 빈번)

ALTER TABLE db.cdc_users SET TBLPROPERTIES (
  'write.target-file-size-bytes' = '268435456',
  'write.delete.mode' = 'merge-on-read',
  'write.update.mode' = 'merge-on-read',
  'write.merge.mode' = 'merge-on-read',
  'write.delete.target-file-size-bytes' = '67108864',
  'format-version' = '2',
  'commit.manifest-merge.enabled' = 'true'
);

V3 사용이 가능하면 Deletion Vector 활성화로 읽기 성능을 더 끌어올릴 수 있습니다.


11. 모니터링 — 무엇을 봐야 하나

다음 지표를 일 단위로 추적하면 문제를 조기 발견할 수 있습니다.

지표임계치(예시)조치
테이블당 파일 수100k 초과컴팩션
평균 파일 크기64MB 미만컴팩션 (target-file-size-bytes)
Manifest 수1000 초과rewrite_manifests
Snapshot 수1000 초과expire_snapshots
metadata.json 크기50MB 초과metadata 압축 + snapshot expire
Delete 파일 비율데이터의 10% 초과rewrite_position_deletes
Plan 시간평소의 3배Manifest/캐시 점검
Commit retry 횟수평균 1 초과동시 쓰기 검토

Iceberg 메타데이터 테이블에서 직접 조회할 수 있습니다.

-- 파일 수, 평균 크기
SELECT count(*), avg(file_size_in_bytes)
FROM iceberg.db."orders$files";
 
-- Snapshot 수
SELECT count(*) FROM iceberg.db."orders$snapshots";
 
-- 파티션별 파일 통계
SELECT partition, file_count, total_size, position_delete_record_count
FROM iceberg.db."orders$partitions"
ORDER BY file_count DESC LIMIT 20;

12. 체크리스트

신규 테이블을 만들 때 다음을 한 번씩 확인하세요.

  • write.target-file-size-bytes가 워크로드에 맞게 설정됐는가
  • write.distribution-mode가 의도한 값인가 (none/hash/range)
  • write.delete.mode / write.update.mode / write.merge.mode가 일치하는가
  • write.metadata.delete-after-commit.enabled = true + previous-versions-max 설정
  • 자주 필터링하는 컬럼에 stats(full) 설정
  • write.object-storage.enabled로 S3 throttling 회피 (대규모 테이블)
  • Sort Order 또는 Z-Order 정의
  • 컴팩션, snapshot expire, orphan 정리 스케줄 등록
  • 모니터링 지표 대시보드 등록
  • (Trino) iceberg.io.manifest.cache-enabled = true
  • (Flink) checkpoint interval ≥ 5분

13. 마무리

Iceberg 성능 튜닝의 본질은 "파일 수, 메타데이터 크기, 통계 품질" 세 가지를 균형있게 유지하는 것입니다. 단일 파라미터로 해결되는 문제는 거의 없으며, 워크로드 특성에 맞춰 write/read/commit/compaction을 한 세트로 설계해야 합니다.

가장 자주 만나는 문제는 항상 같습니다 — 스트리밍이 너무 자주 commit하고, 컴팩션과 expire가 돌지 않아서. 새로운 파라미터를 도입하기 전에, 운영 자동화부터 점검하시기 바랍니다.

관련 글: