Apache Iceberg 성능 튜닝 가이드 — 주의 사항과 핵심 파라미터 총정리
Iceberg 운영에서 성능을 좌우하는 주의 사항, 안티 패턴, 그리고 write/read/commit/compaction/manifest 카테고리별 핵심 테이블 프로퍼티와 엔진(Spark/Trino/Flink) 설정을 한 편으로 정리합니다.
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.default | parquet | 데이터 파일 포맷 (parquet, orc, avro) |
write.target-file-size-bytes | 536870912 (512MB) | 타깃 파일 크기. 분석 워크로드 권장 256MB~1GB |
write.parquet.row-group-size-bytes | 134217728 (128MB) | Parquet row group 크기. 너무 크면 row-group skip 효과 감소 |
write.parquet.page-size-bytes | 1048576 (1MB) | Parquet page 크기 |
write.parquet.dict-size-bytes | 2097152 (2MB) | Dictionary encoding 최대 크기 |
write.parquet.compression-codec | zstd | zstd/snappy/gzip/lz4 — 분석은 zstd 권장 |
write.parquet.compression-level | (codec별) | zstd는 1~9, 보통 3 |
write.parquet.bloom-filter-enabled.column.<col> | false | 고-카디널리티 동등 필터에 사용 |
write.metadata.compression-codec | none | 메타데이터 압축. 대규모 테이블은 gzip 권장 |
write.object-storage.enabled | false | S3 키 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-mode | hash (Spark v3+) | none/hash/range. 파일 수와 skew 트레이드오프 |
write.delete.distribution-mode | (write와 동일) | Delete 파일 분포 |
write.update.distribution-mode | (write와 동일) | Update 분포 |
write.spark.fanout.enabled | false | 파티션별 fanout 라이터. 파티션이 매우 많을 때 ON |
hash는 파티션 단위로 데이터를 묶어 작은 파일을 줄이지만 skew에 약하고, range는 정렬과 함께 균등 분배에 유리합니다.
3.3 Row-level 연산 모드
| 프로퍼티 | 기본값(V2) | 설명 |
|---|---|---|
write.delete.mode | copy-on-write | copy-on-write 또는 merge-on-read |
write.update.mode | copy-on-write | 같음 |
write.merge.mode | copy-on-write | 같음 |
write.delete.target-file-size-bytes | 67108864 (64MB) | Delete 파일 크기 |
write.delete.parquet.compression-codec | zstd | Delete 파일 압축 |
CDC/잦은 변경은 MoR(merge-on-read), 배치 ETL은 CoW를 권장합니다. 모든 모드를 일치시키지 않으면 의외의 비용이 발생합니다.
3.4 메타데이터 정리
| 프로퍼티 | 기본값 | 설명 |
|---|---|---|
write.metadata.delete-after-commit.enabled | false | commit 후 오래된 metadata.json 자동 삭제 |
write.metadata.previous-versions-max | 100 | 보관할 metadata.json 버전 수 |
운영 환경에서는 보통 delete-after-commit=true, previous-versions-max=20 정도로 설정합니다.
3.5 통계 수집
| 프로퍼티 | 기본값 | 설명 |
|---|---|---|
write.metadata.metrics.default | truncate(16) | 모든 컬럼에 대한 기본 stats 모드 |
write.metadata.metrics.column.<col> | — | 컬럼별 override (none/counts/truncate(N)/full) |
write.metadata.metrics.max-inferred-column-defaults | 100 | 기본 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-size | 134217728 (128MB) | 한 task가 처리할 데이터 크기 |
read.split.metadata-target-size | 33554432 (32MB) | Metadata 테이블 split 크기 |
read.split.planning-lookback | 10 | Split 패킹 lookback |
read.split.open-file-cost | 4MB | 파일 오픈 비용 추정값(작은 파일 결합 임계치) |
read.parquet.vectorization.enabled | true | Parquet vectorized reader |
read.parquet.vectorization.batch-size | 5000 | Vectorized 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-retries | 4 | OCC 충돌 시 재시도 횟수 |
commit.retry.min-wait-ms | 100 | 최소 대기 |
commit.retry.max-wait-ms | 60000 | 최대 대기 |
commit.retry.total-timeout-ms | 1800000 (30분) | 총 재시도 타임아웃 |
commit.status-check.num-retries | 3 | Commit 상태 확인 재시도 |
스트리밍 + 배치가 동시에 같은 테이블에 쓰는 환경이라면 num-retries를 8~10으로 올리는 것이 안전합니다.
5.2 Manifest 통합
| 프로퍼티 | 기본값 | 설명 |
|---|---|---|
commit.manifest.target-size-bytes | 8388608 (8MB) | 타깃 manifest 크기 |
commit.manifest.min-count-to-merge | 100 | 이 이상이면 manifest 머지 |
commit.manifest-merge.enabled | true | Commit 시 자동 manifest 머지 |
스트리밍 commit이 잦은 테이블은 min-count-to-merge를 50~80으로 낮춰 manifest 폭증을 미리 막습니다.
5.3 Snapshot 보존
| 프로퍼티 | 기본값 | 설명 |
|---|---|---|
history.expire.max-snapshot-age-ms | 432000000 (5일) | Snapshot 최대 보존 기간 |
history.expire.min-snapshots-to-keep | 1 | 최소 보존 Snapshot 수 |
history.expire.max-ref-age-ms | (제한 없음) | 브랜치/태그 최대 보존 기간 |
테이블별로 차등 적용을 권장합니다. 마스터 테이블은 30일, 시계열 fact는 7일 등.
6. Compaction 절차(Action) 파라미터
rewrite_data_files 호출 시 함께 전달하는 옵션입니다.
| 옵션 | 기본값 | 설명 |
|---|---|---|
target-file-size-bytes | 테이블 프로퍼티 | 타깃 파일 크기 |
min-input-files | 5 | 이 값 이상의 파일이 있어야 그룹 처리 |
min-file-size-bytes | (target × 0.75) | 이보다 작으면 후보 |
max-file-size-bytes | (target × 1.8) | 이보다 크면 후보 |
max-file-group-size-bytes | 107374182400 (100GB) | 한 그룹의 최대 크기 |
max-concurrent-file-group-rewrites | 5 | 동시 처리 그룹 수 |
partial-progress.enabled | false | 그룹별 commit으로 partial commit 허용 |
partial-progress.max-commits | 10 | partial commit 최대 횟수 |
rewrite-job-order | none | bytes-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 = 8007.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 = trueTrino에서 가장 큰 차이를 만드는 옵션은 iceberg.io.manifest.cache-enabled 입니다. 대형 테이블 plan 시간이 절반 이하로 줄어듭니다.
9. 엔진별 설정 — Flink
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가 돌지 않아서. 새로운 파라미터를 도입하기 전에, 운영 자동화부터 점검하시기 바랍니다.
관련 글: