ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Apache Iceberg] 아이스버그 테이블 최적화 - 컴팩션과 정렬
    개발지식 아카이브/Data - Iceberg 2025. 1. 13. 10:14

    Apache 아이스버그 테이블 최적화 - 컴팩션 편

     

     

     


     

     

    성능 최적화의 핵심 = 읽는 파일 줄이기

    Apache 아이스버그 테이블의 데이터 성능 최적화의 핵심은 바로 스캔하는 파일의 개수를 줄이는 것이다

     

    Apache Iceberg 테이블을 쿼리 할 때 일어나는 일을 생각해 보자.

    => 각 데이터 파일을 열기 → 스캔  → 닫기

     

    쿼리를 위해 스캔해야 하는 파일이 많을수록, 이러한 파일 작업은 쿼리에 더 큰 비용을 초래한다.

    이 문제는 스트리밍 또는 "실시간" 데이터의 세계에서 더욱 커진다

     

    왜냐하면 스트리밍에서 생성되는 파일들은, 주로 데이터가 생성되는 대로 수집되어, 파일 각각에 몇 개의 레코드만 있는 경우가 많기 때문이다. 이것은 줄곧 스몰파일 이슈로 이어지곤 한다.

     

    5개 파일을 읽을때는 읽기+닫기를 5배 더 하기 때문에 1개 파일을 읽을 때보다 더 많은 비용이 사용된다.

     

     

    • 스캔해야 할 파일이 많으면 성능에 영향을 미치는 이유
      • 많은 파일은 더 많은 파일 작업(열기 → 스캔 → 닫기)을 수행해야 하고
      • 읽어야 할 메타데이터가 훨씬 많으며(각 파일에 메타데이터가 있음),
      • 정리 및 유지 관리 작업을 수행할 때 더 많은 파일을 삭제해야 하기 때문에

     

    아이스버그 테이블의 성능을 최적화하기 위해서, 스캔 파일 숫자를 최대한 줄이는 전략에 대해 생각해 보자!

     

     

     

     


     

     

     

     

     

     

    일반 컴팩션 (Bin-Pack)

    스몰 파일 해결책 = 컴팩션

     

    컴팩션이란 무엇일까?

    여러 개의 스몰 파일들을 모아서 => 하나의 큰 파일에 다시 쓰는 작업을 의미한다

    (보통 데이터 파일을 다시 쓰는 것을 의미하지만, 데이터 파일 수에 비해 매니페스트가 너무 많은 경우 매니페스트를 다시 쓸 수도 있다)

     

    5개의 파일에 흩어져 있던 데이터를 1개의 파일로 모아 보자!

     

    데이터 정렬 없이 오로지 파일 병합만 수행하는 컴팩션 전략을 Bin-Pack 컴팩션이라고 부른다.

    컴팩션에는 default 전략으로 Bin-pack이 지정되어 있다.

     

     


     

    컴팩션 옵션들

    컴팩션 과정에서, 다음과 같은 옵션들을 사용할 수 있다

     

    • target-file-size-bytes
      • 병합 후 목표 출력 크기, default는 512mb

     

    • max-concurrent-file-group-rewrites
      • 동시에 쓸 수 있는 파일 그룹 수의 상한선

     

    • max-file-group-size-bytes 
      • 단일 파일 그룹의 최대 크기
      • Compaction 작업의 리소스 소비를 제어하는 데 사용될 수 있다

     

    • partial-progress-enabled
      • 부분 커밋 옵션. 다른 파일 그룹이 컴팩션되고 있는 동안, 우선 파일 그룹에 대해 먼저 커밋이 발생할 수 있도록 한다
      • 이 옵션이 false라면 커밋은 ALL OR NOTHING이지만 true라면 성공한 파일 그룹은 병합 결과로 커밋되고, 실패한 파일 그룹은 처리되지 않는다
      • default: false
      • 기본 BIN-PACK rewriter에서는 지원되지 않는 옵션

     

    • partial-progress-max-commits
      • (위)의 부분커밋옵션이 enabled 된 경우, 작업을 완료하는 데 허용되는 최대 커밋 수를 설정
      • 기본 BIN-PACK rewriter에서는 지원되지 않는 옵션

     

    • rewrite-job-order
      • 컴팩션될 대상에 대해 우선순위 전략을 선택한다
      • keywords
        • bytes-asc
        • bytes-desc
        • files-asc
        • files-desc
        • none

     

     


     

     

     

    컴팩션 자동화

    컴팩션 쿼리를 매번 휴먼이 실행할 수는 없으므로, 이러한 프로세스를 자동화하는 방법을 살펴보자!

    아이스버그 공식에서는 다음 도구들을 이용하여 주기적 간격으로 컴팩션을 수행하는 것을 추천한다.

     

    • Airflow, Dagster, Prefect, Argo 또는 Luigi와 같은 오케스트레이션 도구를 사용하여 주기적으로 컴팩션 실행
    • 서버리스 함수를 사용하여 데이터가 적재된 후 트리거
    • 특정 시간에 적절한 작업을 실행하도록 서버에 cron 작업을 설정
    • 자동화된 테이블 유지 관리기능을 제공하는 관리형 Apache Iceberg 서비스를 사용 (Tabular, Dremio Arctic)

     

    Hudi 같은 솔루션들은 MOR inline 같은 옵션을 제공하여, 자체적으로 컴팩션을 수행하도록 하여 사용자의 부담을 덜어주고 있지만...

    아이스버그의 사용자들은 컴팩션 자동화에 대한 고민과 구현이 필요하다.

     

     


     

     

     

     

     

     

    일반 컴팩션 수행

    Apache Iceberg의 Actions 패키지를 이용하거나, SparkSQL을 사용하는 것 2가지 방법이 있다. 둘 다 일반 Iceberg 트랜잭션과 동일한 ACID 보장을 유지한다.

     

     

    spark.sql("""
    CALL system.rewrite_data_files(
      table => 'temp.test_compaction',
      where => 'category = "A"',
      options => map(
        'target-file-size-bytes', '134217728',
        'rewrite-job-order', 'bytes-asc'
      )
    )
    """).show(truncate=false)

     

     

    위의 코드를 실행했더니, 데이터 폴더에, 병합된 parquet 파일이 생성되었다. (이것만으로는 기존 스몰 파일들은 삭제되지 않음 주의)

     

     

    spark.sql("SELECT file_path, file_size_in_bytes, record_count FROM temp.test_compaction.files").show()

     

    아이스버그 메타데이터에서, 현재 참조(저장)하는 파일 리스트가 어떻게 변했는지, 쿼리로 확인해 보는 것도 좋다.

     

     

    다만 여기서 주의해야 할 점은,

    컴팩션 수행 후에도 "과거 스몰 파일들"은 삭제되지 않고 스토리지에 남아있다는 것이다.

    왜냐하면 아이스버그는 스냅샷을 통한 타임트래벌 기능을 지원하기 때문에.... 아직 과거 파일들은 과거 스냅샷에서는 참조가 이어져있다.

    그렇기 때문에, "과거 스몰 파일들"까지 정리하고 싶다면......?

    우리는 "오래된 스냅샷 만료" -> "참조 끊긴 데이터 파일들 삭제" 작업도 진행해주어야 한다.

    이 부분은 다른 포스팅에서 따로 다뤄보도록 하겠다.

     

     

     

     

     

     


     

     

     

     

     

    정렬 컴팩션 (Sort)

    테이블 최적화의 또 다른 방법 = 정렬


    데이터를 정렬하거나 "클러스터링"하는 것은 쿼리와 관련하여 매우 특별한 이점이 있다.

    데이터를 미리 정렬, 클러스터링 한다면, 쿼리에 필요한 데이터를 얻기 위해 스캔해야 하는 파일 수를 제한하는 데 도움이 된다.

     

     

    => 유사한 값을 가진 데이터를 같은 곳에 몰아놓는다면, 결국 읽어야 할 파일 범위가 줄어들어, 더 효율적인 쿼리 계획을 세울 수 있는 것이다.

     

     

     


     

     

     

    CREATE & ALTER SORTED TABLE

     

    CREATE TABLE catalog.nfl_teams
    AS (SELECT * FROM non_iceberg_teams_table ORDER BY team);

     

    ALTER TABLE catalog.nfl_teams WRITE ORDERED BY team;

     

     

    ORDERED BY field 키워드를 테이블의 메타 정보로 정의한다면, 쿼리 엔진은 "정렬"한 후 데이터를 쓴다.

     

     

     


     

     

    INSERT WITH ORDERED KEYWORD

     

    INSERT INTO catalog.nfl_teams SELECT *

    FROM staging_table ORDER BY team


    이 방식은, 데이터 작성 시 정렬을 수행하기는 하나, 완벽하지는 않다. 이미 과거 데이터가 만들어져 있는 상태에서, 새 파일을 작성해야 하기 때문이다. 이때 유용하게 사용될 수 있는 것이 정렬 컴팩션이다.

    정렬 컴팩션을 수행하면, 쿼리 대상에 대해 모두 정렬을 수행한다.

     

     

     

     


     

     

     

    정렬 컴팩션 옵션

     

    CALL system.rewrite_data_files(
      table => 'temp.blogers',
      options => map('sort_order', 'category ASC, user_id ASC')
    );

     

    • NULLS LAST
      • null 값을 가진 row는 정렬의 가장 마지막에 배치한다

     

    • NULLS FIRST
      • null 값을 가진 row는 정렬의 가장 처음에 배치한다

     

    • sort_order
      • 정렬 대상 필드를 지정할 수 있다
      • 복수 필드 지정 시 가중치가 적용되어, 앞에 오는 필드 우선으로 정렬하고, 뒤에 오는 필드는 그다음으로 정렬된다

     

     

     


     

     

     

    정렬 컴팩션 사용 예시

    나는 티스토리의 블로거 정보를 저장하는 테이블을 만들어 보았다. 그리고 일반 정렬 전략을 사용하도록 테이블 프로퍼티를 주었다.

    일반 정렬은 순서에 따라 가중치가 다르다. 앞에 올 수록 가중치가 크다. 즉, CATEGORY에 따라 우선 정렬되고, 그다음에 NAME에 따라 하위 정렬된다.

     

    ALTER TABLE temp.blogers SET TBLPROPERTIES (
      'write.sort.order' = 'category ASC, user_id ASC'
    );

     

     

    만약 내가 "패션 카테고리에 속해 있으면서 && id가 g로 시작하는 블로거는 누구인가?"라는 SELECT query를 주기적으로 사용한다면, 선택한 정렬 전략은 적합하다.

     

    데이터가 category 별로 정렬되어 있고 category 안에서 유저 아이디별로 정렬이 되어 있기 때문에,

    쿼리 엔진은 먼저 패션 카테고리 섹션을 찾아간다 -> 그리고 그 안에서 정렬된 아이디를 찾는다.

     

    category 필드에 우선 가중치를 두고 있고, 다음으로 user_id 필드에 조금 더 낮은 가중치를 두는 SELECT 쿼리를 쓰고 있기 때문에 성능이 좋다.

     

     

     

     

    BUT....

    동시에 다음 쿼리도 사용해야 한다면?

    "id가 g로 시작하는 티스토리 블로거를 모두 찾아달라 (all category) "

    이 쿼리를 실행한다면, 우리는 훨씬 더 많은 파일을 스캔해야 할 것이다.

    id가 g로 시작하는 블로거들은 모든 카테고리에 다 흩어져있기 때문이다....

     

    이 문제를 풀기 위해, 다음 순서인 Z정렬로 넘어가 보자...

     

     

     


     

     

     

     

     

     

     

    Z 정렬 컴팩션 (z-order)

    Z 정렬 = 동일 가중치 쿼리

    테이블을 쿼리 할 때 여러 필드가 동일한 가중치로 우선순위가 되는 경우는 어떨까? 이때에는 Z-순서 정렬이 유용히 쓰일 수 있다.

    Z정렬의 목적은, 여러 데이터 포인트로 데이터를 물리적으로 미리 정렬해 두어서, 엔진이 최종 쿼리 계획에서 스캔하는 파일 숫자를 줄이는 것이다.

     

     

    Z정렬에서 두 개의 필드로 정렬한다고 가정하자 (나이, 키) => 이것은 사분면에, 범위에 따라 데이터를 나누어 놓는 개념인 것이다.

    위 그림처럼, 만약 우리가 파일을 4개로 분류해 놓았다면.... AGE=20, HEIGHT=7.5인 데이터를 찾을 때에, 우리는 4개 파일을 다 뒤질 필요 없이, 3 사분면의 파일로 바로 가면 된다.

    => 이 과정은 검색영역(스캔대상파일모음)에서 75%를 제거시킨다.

     

     

     

     


     

     

     

     

    Z-정렬 컴팩션 코드 예시

     

    CALL catalog.system.rewrite_data_files(
      table => 'people',
      strategy => 'sort',
      sort_order => 'zorder(age, height)'
    )

     

     

     

     

     


     

     

     

     

    Z정렬의 한계

    • 컴팩션이 실행되기 전까지 수집된 NEW 데이터는 여러 파일에 분산된 상태로 유지된다
    • 쿼리에 따라서는 비효율적일 수도 있다
      • 예) Z정렬의 경우에는, 3사분면의 파일 내용을 다 스캔해야 한다. 쿼리 내용에 따라(가중치 O) 일반정렬을 사용했다면, 어쩌면 스캔 범위를 더 줄였을 수도 있다.



     

     

     


     

     

     

     

    컴팩션 전략 3가지 비교

    컴팩션 전략으로 BIN-PACK (default) / SORT / Z-ORDER 3가지를 모두 살펴보았는데, 표로 요약 비교해 보자..!!

     

    전략 로직 장점 단점
    Bin-pack

    (DEFAULT)
    파일 병합만 수행, 전역 정렬은 하지 않는다

      - 필요한 경우 작업 내에서 로컬 정렬
    컴팩션 속도가 가장 빠름

    데이터가 클러스터링 되지 않음
    Sort 우선 가중치 필드 우선으로 정렬한 후, 파일 병합 수행

      - 예) 필드 a를 기준으로 정렬한 다음, 그 안에서 필드 b를 기준으로 정렬
    특정 필드로 필터하여 SELECT하는 경우 읽기 성능 개선 컴팩션 시간이 오래 걸림
    Z-Order 동일 가중치를 가지는 여러 필드에 대해 정렬한 후, 파일 병합 수행

      - 예) X(1~10) 및 Y(A~C) 값이 동일 파일그룹에 들어가도록 정렬
    특정 여러 필드로 필터하여 SELECT하는 경우 읽기 성능 개선 컴팩션 시간이 가장 오래 걸림

     

     

     

     


    Reference

    이 포스팅은 Apache Iceberg The Definitive Guide 4장의 내용에 기반하여 작성하였습니다.

     

     

    댓글

Copyright in 2020 (And Beyond)