Blog
trinoicebergnifilakehousestreamingdata-platform

Trino Iceberg 테이블에 NiFi로 실시간 적재

Trino에서 만든 Iceberg 테이블에 NiFi로 실시간 적재하는 두 가지 패턴(PutIceberg 직접 쓰기, Trino JDBC 경유)을 비교하고, Iceberg 스냅샷 모델 덕분에 Impala식 REFRESH가 왜 필요 없는지, 그리고 작은 파일·스냅샷 폭증·커밋 충돌 같은 실전 함정을 어떻게 다루는지 정리합니다.

Data Dynamics2026年5月23日30 min read
This post is not yet translated. The original Korean version is shown below.

Impala + Hive 환경에서 외부 도구로 파일을 적재해 본 적이 있다면 익숙한 흐름이 있습니다. 파일을 떨어뜨리고, REFRESH table_name 또는 INVALIDATE METADATA를 던지고, 그제서야 새 데이터가 쿼리에 보입니다. 잊으면 사용자가 어제 데이터를 보고, 잘못 던지면 클러스터 메타스토어 전체가 흔들립니다.

Iceberg로 옮겨오면 이 단계가 통째로 사라집니다. 스냅샷이 곧 메타데이터이고, 라이터의 원자적 커밋이 끝나는 순간 다음 트랜잭션의 모든 리더가 새 데이터를 봅니다. 별도의 REFRESH 호출이 없습니다.

이 글은 Trino에서 만든 Iceberg 테이블에 NiFi로 실시간 적재하는 실전 설계를 정리합니다. 두 가지 적재 경로(PutIceberg 직접 쓰기, Trino JDBC 경유)를 비교하고, "REFRESH 없이 유연하게"라는 표면 아래에서 운영자가 실제로 만나는 함정 — 작은 파일, 스냅샷 폭증, 동시 커밋 충돌, 컴팩션·만료 — 까지 다룹니다.


1. 왜 Iceberg에는 REFRESH가 없나

1.1 Impala/Hive 모델 — 카탈로그가 파일 목록을 안다

전통적인 Hive 테이블에서 "테이블"은 두 부분으로 나뉘어 있습니다.

  • Hive Metastore — 스키마, 파티션 위치, 파일 통계를 메타데이터로 들고 있습니다.
  • HDFS / S3 위의 파일들 — 실제 데이터 파일과 디렉터리.

Impala는 성능을 위해 메타스토어 정보를 catalogd에 캐싱합니다. 외부 도구(NiFi, Spark, Sqoop 등)가 새 파일을 떨어뜨리면, 디스크에는 파일이 있지만 catalogd가 알지 못하기 때문에 쿼리에서 그 파일이 보이지 않습니다.

해결책이 REFRESHINVALIDATE METADATA였습니다.

-- 파티션 단위 메타데이터/파일 목록 재로드
REFRESH events PARTITION (dt='2026-05-23');
 
-- 테이블 전체 메타데이터 무효화 (비싸다)
INVALIDATE METADATA events;

REFRESH는 메타스토어와 파일 시스템을 다시 스캔하라는 운영자 측면의 신호입니다. 이 신호를 빠뜨리거나 잘못 던지면 다음과 같은 문제가 생깁니다.

  • 데이터는 들어왔는데 BI에서 보이지 않습니다.
  • 너무 자주 INVALIDATE METADATA를 부르면 catalogd가 부하를 받고, 클러스터 전체 쿼리 지연이 올라갑니다.
  • 적재 파이프라인과 BI 쿼리 사이의 race가 생깁니다.

1.2 Iceberg 모델 — 메타데이터가 진실의 단일 원본

Iceberg는 같은 문제를 다르게 풉니다. 테이블은 다음 구조를 가집니다.

catalog ──▶ metadata.json (current pointer)
                  │
                  └─▶ snapshot                ← 시점 t에서 본 테이블 상태
                        ├─▶ manifest-list      ← 이 스냅샷이 참조하는 manifest들
                        │     ├─▶ manifest     ← data file들의 목록 + 통계
                        │     │     └─▶ data files (Parquet/ORC/Avro)
                        │     └─▶ ...
                        └─▶ ...

이 모델의 핵심은 두 가지입니다.

  • 모든 리더는 항상 metadata.json을 통과한다. 디렉터리 리스팅으로 파일을 발견하지 않습니다. metadata.json이 가리키는 스냅샷이 곧 "지금 보이는 테이블"입니다.
  • 라이터는 새 metadata.json을 만들어 카탈로그에 원자적으로 swap한다. 이 swap이 끝나면, 다음 쿼리부터 모든 리더는 새 스냅샷을 봅니다.
[Hive/Impala 모델]                   [Iceberg 모델]

외부 라이터 ──▶ 파일 드롭            외부 라이터 ──▶ 파일 드롭
                                                     │
운영자 ──▶ REFRESH                                   ▼
                │                              새 metadata.json 작성
                ▼                                    │
        catalogd 캐시 갱신                            ▼
                │                              카탈로그에 원자적 swap
                ▼                                    │
        리더가 새 파일을 본다                          ▼
                                              리더가 새 스냅샷을 본다
                                              (REFRESH 없음)

라이터의 커밋이 곧 메타데이터 갱신입니다. 운영자가 별도로 던질 신호가 없습니다. 이것이 "Impala의 REFRESH 없이 유연하게"라는 표현이 가능한 이유입니다.

1.3 그래도 남는 한 가지 — 엔진별 카탈로그 캐시

엄밀히 말하면 Iceberg 엔진들도 카탈로그/메타데이터를 단기적으로 캐시합니다. Trino의 Iceberg 커넥터는 기본적으로 iceberg.metadata-cache.enabled=true이며, 짧은 TTL(기본 5분, 설정 가능)을 가집니다. 이는 다음 두 가지로 무효화됩니다.

  • TTL 만료 — 자동으로 다음 조회 시 새 metadata.json을 읽어옵니다.
  • 명시적 무효화CALL iceberg.system.flush_metadata_cache(schema_name => 'sales', table_name => 'events')

대부분의 적재 워크로드에서 TTL 만으로 충분하고, 즉시성이 필요하면 캐시를 꺼두거나(iceberg.metadata-cache.enabled=false) TTL을 짧게 잡으면 됩니다. 운영자가 매 적재마다 REFRESH를 던지는 흐름과는 본질적으로 다릅니다. 캐시는 성능 최적화이지, 데이터 가시성의 조건이 아닙니다.


2. 전제 — Trino에서 Iceberg 테이블 만들기

NiFi 이야기로 들어가기 전에, Trino 쪽에서 어떻게 Iceberg 테이블을 만들었는지 짧게 고정해 두는 것이 좋습니다. 적재 경로 설계가 이 결정에 영향을 받기 때문입니다.

2.1 카탈로그 설정 (예: REST Catalog)

etc/catalog/iceberg.properties:

connector.name=iceberg
iceberg.catalog.type=rest
iceberg.rest-catalog.uri=https://catalog.internal:8181
iceberg.rest-catalog.warehouse=s3://lake/warehouse
iceberg.rest-catalog.security=OAUTH2
iceberg.rest-catalog.oauth2.credential=${ENV:CATALOG_CLIENT_ID}:${ENV:CATALOG_CLIENT_SECRET}
 
fs.native-s3.enabled=true
s3.region=ap-northeast-2
s3.endpoint=https://s3.ap-northeast-2.amazonaws.com
 
iceberg.file-format=PARQUET
iceberg.compression-codec=ZSTD

Hive Metastore나 Glue도 동일한 형태로 설정합니다. REST Catalog는 멀티 엔진 환경에서 권장되는 선택입니다(상세는 Apache Iceberg REST Catalog Server 글을 참고).

2.2 테이블 생성

CREATE TABLE iceberg.sales.events (
    event_id      VARCHAR,
    user_id       BIGINT,
    event_type    VARCHAR,
    payload       VARCHAR,
    event_time    TIMESTAMP(6) WITH TIME ZONE,
    ingest_time   TIMESTAMP(6) WITH TIME ZONE
)
WITH (
    format = 'PARQUET',
    partitioning = ARRAY['day(event_time)'],
    format_version = 2,
    sorted_by = ARRAY['event_time']
);

핵심 결정 몇 가지:

  • format_version = 2 — V2 이상이어야 NiFi/Spark/Flink 등이 row-level delete를 포함한 modern 연산을 사용할 수 있습니다. 단순 append-only라도 V2가 사실상 표준입니다.
  • 파티셔닝은 day(event_time)로 시작 — 너무 잘게 쪼개면 작은 파일이 폭증합니다. 시간 단위는 워크로드 양에 따라 day → hour → bucket(user_id) 순으로 조정합니다.
  • sorted_by로 시간순 정렬 — Trino 측 컴팩션과 dynamic filtering이 모두 시간 컬럼 술어에서 더 잘 동작합니다.

이 시점부터 NiFi가 같은 카탈로그를 바라보며 적재만 하면 됩니다.


3. NiFi에서 Iceberg로 — 두 가지 경로

NiFi에서 Iceberg 테이블에 적재하는 현실적인 옵션은 두 가지입니다.

[옵션 A] NiFi ──▶ PutIceberg ──▶ Iceberg 카탈로그/스토리지 직접 쓰기
[옵션 B] NiFi ──▶ PutDatabaseRecord ──▶ Trino JDBC ──▶ Iceberg

3.1 옵션 A — PutIceberg (직접 쓰기)

NiFi 1.19부터 공식 번들에 포함된 PutIceberg 프로세서는 NiFi가 Iceberg 라이브러리를 직접 호출해 데이터 파일을 쓰고 카탈로그에 커밋합니다. 중간에 다른 엔진이 끼지 않습니다.

장점

  • 처리량이 높다. 데이터 파일을 NiFi 워커가 직접 쓰고, 카탈로그에는 메타데이터 커밋만 합니다. Trino 같은 중간 SQL 엔진을 거치지 않으므로 행 변환·왕복 비용이 없습니다.
  • 백프레셔가 직관적이다. NiFi의 큐와 스레드 풀이 그대로 적재 속도를 결정합니다.
  • 장애 시 재처리가 자연스럽다. FlowFile 단위로 재시도되며, Iceberg의 스냅샷 모델 덕분에 부분 커밋이 남지 않습니다.

단점

  • 카탈로그 호환성에 민감하다. NiFi PutIceberg는 Hive Catalog와 Hadoop Catalog를 1급으로 지원합니다. REST Catalog는 NiFi 2.x 계열에서 지원이 확대되고 있고, Glue/Nessie는 추가 의존성/설정이 필요합니다. 사용 중인 NiFi 버전의 지원 행렬을 먼저 확인해야 합니다.
  • NiFi 노드에 Iceberg/Hadoop 의존성과 스토리지 자격 증명을 둬야 한다. REST Catalog의 vended credentials를 쓰면 자격 증명 표면을 줄일 수 있지만, 이를 위해서는 NiFi의 IcebergCatalogService가 vended credentials를 처리할 수 있어야 합니다.
  • 스키마 진화의 책임이 NiFi 쪽으로 옮겨온다. Trino 측에서 ALTER TABLE로 컬럼을 추가하면, NiFi가 들고 있는 RecordSchema와 어긋나는 순간을 다뤄야 합니다.

3.2 옵션 B — Trino JDBC 경유

PutDatabaseRecord 프로세서를 Trino JDBC DriverConnectionPool에 연결해 INSERT를 실행합니다. 실제로 Iceberg에 쓰는 일은 Trino 워커가 합니다.

장점

  • 카탈로그 종류와 무관하다. Trino가 카탈로그를 들고 있으므로, NiFi는 JDBC만 알면 됩니다. REST/Glue/Nessie/HMS 어느 쪽이든 동일하게 동작합니다.
  • 권한 모델이 단일화된다. 사용자 인증·인가, 행/컬럼 마스킹, 감사 로그가 Trino 한 곳에서 결정됩니다. 운영 관점에서 가장 큰 장점입니다.
  • 스키마 변경에 강하다. Trino가 가장 최근 metadata.json을 읽어 INSERT 계획을 세우므로, 컬럼 추가/타입 변경이 NiFi 측 재배포 없이도 흡수되는 경우가 많습니다.

단점

  • 처리량 상한이 낮다. 모든 행이 Trino 코디네이터를 거쳐 워커로 분산되므로, 직접 쓰기보다 오버헤드가 큽니다. 초당 수만 row 이상의 워크로드에서는 PutIceberg가 더 나은 경우가 많습니다.
  • 커밋이 INSERT 단위로 일어난다. 너무 작은 배치로 INSERT를 던지면 Iceberg에 스냅샷이 빠르게 쌓입니다(아래 6장).
  • Trino 클러스터가 SPOF가 된다. Trino가 죽으면 적재 파이프라인도 멈춥니다.

3.3 무엇을 골라야 하나

대략적인 가이드:

조건권장
처리량이 핵심(초당 만 row 이상)옵션 A (PutIceberg)
카탈로그가 REST/Glue/Nessie 등 다양함옵션 B (Trino JDBC)
권한·감사·마스킹을 Trino 한 곳에서 통제옵션 B (Trino JDBC)
NiFi 노드에 스토리지/카탈로그 자격 증명을 두기 어렵다옵션 B (Trino JDBC)
적재 SLA가 단순(append-only, 스키마 안정)옵션 A (PutIceberg)
운영팀이 Trino를 잘 알고, NiFi는 단순한 게이트웨이로 쓰고 싶다옵션 B (Trino JDBC)

실무에서는 종종 하이브리드로 갑니다. 대용량 fact 테이블은 옵션 A, 권한·감사가 까다로운 PII 테이블은 옵션 B.


4. 옵션 A 구현 — PutIceberg로 직접 쓰기

4.1 NiFi 컨트롤러 서비스 구성

세 가지 서비스를 먼저 만듭니다.

  1. HiveCatalogService (또는 RESTCatalogService — NiFi 버전에 따라) — Iceberg 카탈로그 접속 정보.
  2. AWSCredentialsProviderService — S3 자격 증명. REST Catalog vended credentials를 쓴다면 정적 자격 증명 대신 동적 토큰 캐시 모드로 둡니다.
  3. JsonTreeReader — 인입 FlowFile의 Record 스키마 정의.

HiveCatalogService 예시

Catalog URI:           thrift://hms.internal:9083
Warehouse Location:    s3://lake/warehouse
Hadoop Configuration:  /opt/nifi/conf/core-site.xml,/opt/nifi/conf/hdfs-site.xml
Kerberos Credentials:  (필요 시 KerberosControllerService 참조)

Hadoop Configuration 파일에 S3A 자격 증명 프로바이더, S3 endpoint, retry 정책을 함께 박아 둡니다. 자격 증명을 NiFi 프로퍼티에 평문으로 넣는 것은 피하고, NiFi의 Parameter Provider 또는 환경 변수로 분리합니다.

4.2 인입 흐름 설계

가장 흔한 형태는 Kafka → NiFi → Iceberg입니다.

ConsumeKafkaRecord_2_6 ──▶ UpdateRecord ──▶ PutIceberg
        │                       │                │
        │                       │                ├─ Success: 종료
        │                       │                └─ Failure: 재시도 큐 + DLQ
        │                       │
        └─ schema = JsonTreeReader (Avro schema text)
                             writer = JsonRecordSetWriter

핵심은:

  • Record-oriented 프로세서를 일관되게 쓴다. ConsumeKafkaRecord, UpdateRecord, PutIceberg 모두 Record 추상화를 공유하므로, 스키마가 한 번만 정의되면 됩니다.
  • UpdateRecord에서 ingest_time 추가:
    • Field: /ingest_time
    • Replacement: ${now():format("yyyy-MM-dd'T'HH:mm:ss.SSSXXX")}
  • PutIceberg 설정:
    • Catalog Service: 위에서 만든 HiveCatalogService
    • Catalog Namespace: sales
    • Table Name: events
    • File Format: PARQUET
    • Maximum File Size: 128 MB — 작은 파일 방지 (아래 6장)
    • Unmatched Column Behavior: IGNORE_UNMATCHED_COLUMNS (테이블 측 컬럼 추가에 관대)
    • Number of Commit Retries: 4

4.3 배치 크기 = 스냅샷 빈도

이 한 줄이 옵션 A 운영의 핵심입니다.

NiFi에서 PutIceberg가 호출되는 단위가 그대로 Iceberg 스냅샷 1개가 됩니다. ConsumeKafkaRecord의 Max Poll Records나 MergeRecord 프로세서의 배치 크기로 묶음을 만든 뒤 PutIceberg에 던지는 것이 정석입니다.

ConsumeKafkaRecord (Max Poll Records: 5000)
       │
       ▼
MergeRecord (Min Records: 5000, Max Records: 20000, Max Bin Age: 30 sec)
       │
       ▼
PutIceberg  ← 30초마다, 5000~20000건 단위 커밋

이 패턴이면 분당 2개 스냅샷 정도가 만들어지고, 데이터 가시성 지연은 30초 이내입니다. 워크로드에 따라 Max Bin Age를 10초까지 줄여도 됩니다.


5. 옵션 B 구현 — Trino JDBC 경유

5.1 DBCPConnectionPool

Database Connection URL:
  jdbc:trino://trino-coord.internal:8443/iceberg/sales

Database Driver Class Name:
  io.trino.jdbc.TrinoDriver

Database Driver Location:
  /opt/nifi/lib/trino-jdbc-447.jar

Database User:           nifi-ingest
Password:                ${trino.password}

Properties:
  SSL=true
  SSLVerification=FULL
  externalAuthentication=false
  source=nifi-ingest
  applicationName=nifi-ingest
  sessionProperties=iceberg.target_max_file_size_bytes:134217728

sourceapplicationName을 박아두면 Trino 쪽 쿼리 이력에서 NiFi 트래픽만 필터링할 수 있습니다. nifi-ingest 같은 전용 서비스 계정에 INSERT 권한만 부여합니다.

5.2 PutDatabaseRecord 설정

Statement Type:               INSERT
Record Reader:                JsonTreeReader (또는 AvroReader)
Database Type:                Generic
Table Name:                   events
Schema Name:                  sales
Catalog Name:                 iceberg
Translate Field Names:        true
Unmatched Field Behavior:     Ignore Unmatched Fields
Unmatched Column Behavior:    Ignore Unmatched Columns
Maximum Batch Size:           1000

Trino JDBC는 표준 JDBC PreparedStatement.executeBatch()를 지원하므로 Maximum Batch Size가 그대로 작동합니다. 1000 정도면 코디네이터에 부담을 주지 않으면서 처리량을 유지할 수 있는 무난한 값입니다.

5.3 INSERT가 스냅샷을 만든다

옵션 B에서도 옵션 A와 같은 원칙이 그대로 적용됩니다. PutDatabaseRecord가 한 번 호출될 때마다 Iceberg 스냅샷이 1개 생깁니다. Trino의 INSERT가 워커 쪽에서 데이터 파일을 쓰고, 코디네이터가 카탈로그에 metadata.json을 swap합니다.

따라서 옵션 A의 MergeRecord 패턴이 여기서도 똑같이 유효합니다. FlowFile을 잘게 던지지 말고 묶어서 던집니다.


6. 실시간 적재의 함정 — 작은 파일·스냅샷 폭증·커밋 충돌

"REFRESH 없이 유연하게"라는 표면 아래에는 실시간 적재 특유의 비용이 있습니다. 이걸 무시하면 며칠 안에 쿼리 성능이 두세 배로 느려집니다.

6.1 작은 파일 (small files)

Iceberg에서도 한 번 커밋되면 data file 자체는 불변입니다. 매번 5초마다 1MB짜리 Parquet을 쓰면, 하루에 17,000개의 작은 파일이 생기고 manifest 비용·쿼리 플래닝 시간이 폭증합니다.

해결책은 두 가지를 함께 씁니다.

  • 쓰는 시점에 묶기 — 위에서 본 MergeRecord 패턴. 510초의 가시성 지연을 허용하고, 한 번에 64128 MB 정도가 쓰이도록 유도합니다.
  • 쓴 뒤에 컴팩션 — 그래도 남는 작은 파일들은 주기적으로 컴팩션합니다.
ALTER TABLE iceberg.sales.events EXECUTE optimize;
 
-- 또는 특정 파티션만
ALTER TABLE iceberg.sales.events
EXECUTE optimize WHERE event_time >= TIMESTAMP '2026-05-23 00:00:00 UTC';

운영 패턴은 매 시간 직전 1시간 파티션을 컴팩션 또는 매일 새벽에 직전 1일 파티션을 컴팩션입니다. NiFi의 ExecuteSQL 또는 별도 Airflow DAG로 스케줄링합니다.

6.2 스냅샷 폭증

30초마다 커밋하면 하루에 2,880개 스냅샷이 생깁니다. 한 달이면 86,400개. metadata.json이 거대해지고, 새 스냅샷 커밋의 비용 자체도 올라갑니다.

-- 7일보다 오래된 스냅샷을 만료
ALTER TABLE iceberg.sales.events
EXECUTE expire_snapshots(retention_threshold => '7d');
 
-- 더 이상 참조되지 않는 데이터 파일 제거
ALTER TABLE iceberg.sales.events
EXECUTE remove_orphan_files(retention_threshold => '7d');

Time travel을 7일 이상 보장할 필요가 없다면, 매일 1회 expire_snapshots를 도는 것이 표준입니다. remove_orphan_files는 더 보수적으로 — 진행 중인 적재와 충돌하지 않게 — 주 1회 정도면 충분합니다.

6.3 동시 커밋 충돌

여러 NiFi 노드가 같은 테이블에 동시에 커밋하면 Iceberg의 낙관적 동시성 제어(OCC)가 충돌을 감지하고 후행 커밋이 실패합니다. PutIceberg의 Number of Commit Retries(기본 4회)가 자동 재시도를 처리하지만, 충돌이 잦으면 처리량이 깎입니다.

완화 패턴:

  • NiFi에서 같은 파티션을 같은 노드로 묶기 — PartitionRecord 프로세서로 파티셔닝 컬럼별 그룹을 만들고, 같은 그룹은 같은 PutIceberg 인스턴스가 처리하게 라우팅합니다.
  • MergeRecord로 묶음 커지게 만들기 — 충돌 빈도 자체를 줄입니다.
  • 테이블별 라이터 수 제한 — 동시에 같은 테이블에 쓰는 NiFi 라이터 수를 4~8개 이내로 둡니다.

6.4 시계 / 워터마크 / 늦게 도착하는 이벤트

NiFi PutIceberg / Trino JDBC 모두 append-only가 가장 자연스럽습니다. 늦게 도착하는 이벤트로 인한 갱신/업서트가 필요하다면 두 가지 선택지가 있습니다.

  • append + 후처리 MERGE — NiFi는 단순 append만 하고, 별도 Trino 잡이 MERGE INTO target USING staging으로 정리합니다. Iceberg V2의 row-level delete가 여기에 활용됩니다.
  • Flink Iceberg Sink로 교체 — 진정한 의미의 streaming upsert가 필요하면 Flink가 더 자연스럽습니다. NiFi의 강점은 다양한 소스/싱크의 오케스트레이션이지, ms-level streaming upsert는 아닙니다.

7. 운영 — 모니터링과 백프레셔

7.1 NiFi 측

  • 큐 깊이 알림 — ConsumeKafka 다음 큐가 지속적으로 차오르면 PutIceberg/Trino 쪽 처리 속도가 못 따라가는 것입니다. NiFi 5분 평균 큐 깊이에 임계값 알림을 둡니다.
  • PutIceberg/PutDatabaseRecord 실패 카운트 — 일시적 commit 실패는 재시도 큐로, 영구 실패는 DLQ로 라우팅합니다.
  • Provenance 이벤트 — FlowFile 단위로 입력 → 적재 시각, 행 수, 커밋된 snapshot ID가 추적됩니다.

7.2 Trino / Iceberg 측

  • 테이블 메타데이터 조회:
-- 최근 스냅샷 이력
SELECT committed_at, snapshot_id, operation, summary
FROM iceberg.sales."events$snapshots"
ORDER BY committed_at DESC
LIMIT 50;
 
-- 파일 통계
SELECT
    count(*)              AS data_file_count,
    sum(record_count)     AS total_rows,
    sum(file_size_in_bytes) / 1024 / 1024 AS total_mb,
    avg(file_size_in_bytes) / 1024 / 1024 AS avg_mb
FROM iceberg.sales."events$files";
 
-- 파티션별 크기
SELECT partition, file_count, record_count
FROM iceberg.sales."events$partitions"
ORDER BY record_count DESC
LIMIT 20;

이 세 쿼리를 대시보드(Superset / Grafana)에 박아두면, 적재 파이프라인의 건강 상태가 한눈에 보입니다. avg_mb가 64 미만으로 떨어지기 시작하면 컴팩션 주기를 당겨야 한다는 신호입니다.

7.3 알림 임계값 예시

지표임계값액션
ConsumeKafka 큐 깊이 (5분 평균)> 50,000 FlowFileNiFi 워커 증설 / Trino INSERT 튜닝
events$snapshots 마지막 커밋과 현재 시각의 차이> 5분적재 파이프라인 중단 알림
events$filesavg_mb< 32컴팩션 주기 단축
일별 스냅샷 수> 5,000MergeRecord 배치 크기 증가

8. Trino 측에서 보는 데이터 — REFRESH 없이 정말 보이는가

실제로 확인해 봅시다. NiFi가 30초 전에 커밋한 데이터를 Trino에서 조회합니다.

-- 새로 적재된 이벤트 확인
SELECT event_time, ingest_time, count(*)
FROM iceberg.sales.events
WHERE ingest_time >= current_timestamp - INTERVAL '1' MINUTE
GROUP BY 1, 2
ORDER BY 1 DESC;

이 쿼리는 항상 최신 스냅샷을 봅니다. REFRESH도, INVALIDATE METADATA도, 어떤 명시적 호출도 필요 없습니다. Trino는 매 쿼리마다 카탈로그에서 metadata.json 포인터를 읽어 새 스냅샷을 발견합니다.

Trino의 Iceberg 메타데이터 캐시가 켜져 있고 TTL이 5분이라면, 최대 5분의 지연이 있을 수 있습니다. 적재 직후 즉시 보이는 것이 중요하다면 SET SESSION iceberg.metadata_cache_enabled = false로 세션 단위로 끄거나, 카탈로그 설정에서 TTL을 30초~1분으로 줄입니다.

Time travel도 그대로 동작합니다.

-- 5분 전 시점의 테이블 스냅샷 조회
SELECT count(*)
FROM iceberg.sales.events
FOR TIMESTAMP AS OF (current_timestamp - INTERVAL '5' MINUTE);
 
-- 특정 snapshot으로 조회
SELECT count(*)
FROM iceberg.sales.events
FOR VERSION AS OF 7263921832639218321;

이 정도가 Impala + Hive에서는 불가능했던 영역입니다. 라이터의 커밋이 곧 메타데이터이고, 모든 스냅샷이 시간 좌표를 가지므로, REFRESH 없이도 일관된 가시성과 회고 분석이 동시에 됩니다.


9. 권한·카탈로그 일관성 — 가장 흔히 빠뜨리는 것

운영하면서 가장 자주 만나는 문제는 코드가 아니라 권한과 카탈로그 정합성입니다.

  • NiFi 서비스 계정에 너무 넓은 권한을 주지 않는다. PutIceberg는 카탈로그의 CREATE TABLE / ALTER 권한까지 필요로 할 수 있습니다. 정밀하게 줄이려면 카탈로그 측 정책에서 INSERTmetadata.json swap만 허용합니다.
  • Trino, NiFi, Spark가 같은 카탈로그를 보고 있는지 정기적으로 확인한다. Hive Metastore + Glue + REST Catalog가 동시에 등장하면, 어느 한쪽에서만 본 테이블이 다른 엔진에서 보이지 않는 사고가 납니다. REST Catalog로 통일하는 것을 권장합니다.
  • vended credentials을 우선 검토한다. NiFi 노드에 영속 S3 자격 증명을 두지 않을 수 있다면 그게 낫습니다. REST Catalog의 단명 자격 증명을 받는 클라이언트 구성을 NiFi IcebergCatalogService에서 지원하는지 NiFi 버전별로 확인합니다.

10. 정리

  • Iceberg는 라이터의 원자적 커밋이 곧 메타데이터다. Impala + Hive에서 필요했던 REFRESH / INVALIDATE METADATA가 본질적으로 사라집니다. Trino 측 메타데이터 캐시는 성능 최적화이지 가시성의 조건이 아닙니다.
  • NiFi → Iceberg는 두 가지 경로. 처리량이 핵심이면 PutIceberg 직접 쓰기, 권한·감사·카탈로그 다양성이 핵심이면 Trino JDBC 경유.
  • 배치 크기가 곧 스냅샷 빈도. MergeRecord로 5~30초 단위로 묶고, PutIceberg / PutDatabaseRecord를 한 번 호출 = 스냅샷 1개로 설계합니다.
  • 운영 비용은 컴팩션·스냅샷 만료·orphan 정리에서 나온다. OPTIMIZE, expire_snapshots, remove_orphan_files를 스케줄링하지 않으면 며칠 안에 쿼리가 느려집니다.
  • 동시 커밋 충돌은 PartitionRecord + 라이터 수 제한으로 완화한다. Iceberg OCC가 자동 재시도를 해주지만 충돌이 잦으면 처리량이 깎입니다.
  • events$snapshots, events$files, events$partitions 메타테이블을 대시보드에 박는다. 적재 파이프라인의 건강 상태가 SQL 하나로 보입니다.
  • append-only가 가장 자연스럽다. upsert가 필요하면 NiFi append + 야간 Trino MERGE, 또는 Flink Iceberg Sink로의 교체를 검토합니다.

Iceberg는 "운영자가 메타데이터를 손으로 갱신해야 하는" 시절의 흐름을 정리해 줍니다. 그 결과 NiFi 같은 인입 도구는 본업(다양한 소스의 라우팅·변환·재시도)에 집중하고, 가시성·시간 여행·일관성은 테이블 포맷에 맡길 수 있게 됩니다. 이것이 "Impala의 REFRESH 없이 유연하게"라는 표현이 가리키는 실제 운영의 모습입니다.

— Data Dynamics 팀