7 min read
AI assisted

PostgreSQL Lakehouse (2/2) — 분산 처리와 Citus 통합

분산 처리(Ray·Daft·Smallpond) 비교와 Citus·pg_lake postgres_fdw 통합

Series — PostgreSQL Lakehouse Experiments
  1. 1. PostgreSQL Lakehouse (1/2) — DuckLake가 막힌 자리에 pg_lake가 있었다
  2. 2. PostgreSQL Lakehouse (2/2) — 분산 처리와 Citus 통합

1편에서 DuckLake의 libpq 심볼 충돌과 pg_lake를 발견한 과정을 다뤘습니다. 이번 글은 그 이후의 실험들입니다. DuckDB 단일 노드의 처리 한계를 넘으려 할 때 어떤 분산 처리 옵션이 있는지, PostgreSQL DW(Citus)와 Lakehouse(pg_lake)를 통합하려면 어떻게 해야 하는지, 그리고 새로운 파일 포맷인 Vortex는 아직 시기상조인지를 다룹니다.


Phase 5: 분산 처리 — Ray vs Daft vs Smallpond

DuckDB는 단일 노드에서 수십 GB~수백 GB 분석을 빠르게 처리합니다. TB 스케일이 되거나 여러 노드로 부하를 분산해야 할 때는 별도 레이어가 필요합니다. 세 가지 옵션을 MinIO와 DuckLake 양쪽에서 테스트했습니다.

Ray + DuckDB

Ray는 Python 분산 컴퓨팅 프레임워크입니다. 원래 강화학습 연구에서 시작했지만 지금은 데이터 처리, ML 학습, 서빙까지 쓰입니다. DuckDB와 조합하면 각 Ray 워커 노드에서 DuckDB 인스턴스가 독립적으로 실행됩니다.

import ray
import duckdb

@ray.remote
def process_partition(s3_path: str) -> pd.DataFrame:
    conn = duckdb.connect()
    conn.execute("INSTALL httpfs; LOAD httpfs;")
    conn.execute(f"SET s3_endpoint='minio:9000'; SET s3_use_ssl=false;")
    return conn.execute(f"""
        SELECT region, SUM(amount) as total
        FROM read_parquet('{s3_path}')
        GROUP BY region
    """).df()

# 파티션별 병렬 처리
futures = [process_partition.remote(path) for path in partition_paths]
results = ray.get(futures)

MinIO S3 호환 스토리지와 DuckLake 모두 정상 동작했습니다. Ray가 워커 수명주기, 실패 복구, 결과 수집을 관리하고 DuckDB가 각 파티션의 데이터를 처리합니다. PySpark를 운영해본 팀이라면 친숙한 패턴입니다.

점수: 9/10. 단점은 Ray 클러스터 운영 오버헤드입니다.

Daft

Daft는 Eventual과 Cohere가 개발한 분산 DataFrame 프레임워크입니다. Rust로 작성됐고, Python DataFrame API(pandas/Spark 유사)를 제공합니다. Iceberg를 네이티브로 지원합니다.

import daft

# Iceberg 테이블 직접 읽기
df = daft.read_iceberg(table)

# Lazy evaluation — 실행 계획만 구성
result = (df
    .where(df["year"] == 2025)
    .groupby("region")
    .agg(daft.col("amount").sum())
)

# collect() 시점에 실제 실행
result.collect()

Daft의 Lazy evaluation은 Spark와 비슷합니다. collect() 전까지는 실행 계획만 만들고, 실행 시점에 최적화합니다. Rust 코어 덕분에 Python GIL 제약이 없어 Ray+DuckDB보다 설정이 단순합니다.

MinIO와 DuckLake 모두 PASS. 점수: 9/10.

Smallpond

MotherDuck(DuckDB 클라우드 서비스 회사)이 만든 분산 DuckDB 레이어입니다. DuckDB 위에서 투명하게 분산 처리를 추가해줄 것으로 기대했습니다.

실제로는 MinIO S3 호환 스토리지 연결에서 바로 막혔습니다.

import smallpond
sp = smallpond.init()

# MinIO S3 호환 URL 사용 시 — 실패
df = sp.read_parquet("s3://bucket/data/*.parquet",
                     endpoint_url="http://minio:9000")
# Error: unsupported S3 endpoint configuration

DuckLake도 실패했습니다. 에러 메시지가 불명확해 디버깅하기 어려웠습니다. MinIO와 DuckLake 모두 FAIL. 점수: 3/10.

Smallpond의 실패는 S3 호환 스토리지 지원이 문서와 달리 제한적이라는 것을 보여줬습니다. 상용 MotherDuck 클라우드 환경에서는 다를 수 있지만 자체 인프라에서는 아직 쓰기 어렵습니다.

프레임워크 MinIO DuckLake 점수 특징
Ray + DuckDB PASS PASS 9/10 성숙한 생태계, PySpark 유사 운영
Daft PASS PASS 9/10 Rust 코어, Iceberg 네이티브, 더 단순
Smallpond FAIL FAIL 3/10 S3 호환 제한, 불명확한 에러

Phase 6: Vortex 파일 포맷 — 아직은 이르다

Vortex는 Spiraldb가 Rust로 개발한 차세대 컬럼형 파일 포맷입니다. Parquet의 후계자를 자처하며 다음을 주장합니다.

  • Random access: Parquet 대비 100배 빠름
  • Full scan: Parquet 대비 10~20배 빠름
  • SIMD 친화적 인코딩, 더 나은 압축율

DuckDB 1.3+는 Vortex를 네이티브로 지원합니다.

import duckdb
conn = duckdb.connect()

# Parquet → Vortex 변환
conn.execute("""
    COPY (SELECT * FROM read_parquet('data.parquet'))
    TO 'data.vortex'
""")

# Vortex 직접 읽기
result = conn.execute("SELECT * FROM 'data.vortex' WHERE id = 12345").df()
# Random access가 Parquet 대비 100x 빠름

DuckDB와 Polars에서는 Vortex가 잘 동작했습니다. 문제는 Trino가 Vortex를 지원하지 않는다는 것입니다. Spark도 아직 지원하지 않습니다.

멀티엔진 Lakehouse 목표를 갖고 있다면 Vortex는 현재 시점에서 채택 불가입니다. 데이터를 Vortex로 저장하면 DuckDB에서만 읽을 수 있게 됩니다. DuckDB 단독 환경이라면 의미있는 개선이지만, Trino/Spark와 공유해야 한다면 아직 이릅니다.

Vortex를 "보류 중인 흥미로운 기술"로 분류하고 Phase 7로 넘어갔습니다.


Phase 7: Citus + pg_duckdb — 같은 프로세스에서 충돌

Citus는 PostgreSQL을 수평 분산하는 extension입니다. 여러 노드에 테이블을 샤딩해 수평 확장을 제공합니다. 기존에 Citus 기반 DW가 있는 상황에서 pg_lake Lakehouse와 통합하려면 두 가지 접근이 있습니다.

  1. 같은 PostgreSQL 인스턴스에서 Citus + pg_duckdb를 함께 로드
  2. 별도 인스턴스로 분리하고 postgres_fdw로 연결

Phase 7에서 먼저 1번을 시도했습니다.

-- PostgreSQL에 두 extension 모두 설치 후 로드
CREATE EXTENSION citus;
CREATE EXTENSION pg_duckdb;
-- → CRASH

PostgreSQL 서버 크래시. 로그를 분석하면 Citus와 pg_duckdb가 PostgreSQL 내부 훅(hook)에서 충돌합니다.

충돌의 기술적 원인:

Citus 는 PostgreSQL 쿼리 실행 경로를 깊이 수정합니다. planner_hook, executor_hook, ProcessUtility_hook 등을 등록해 분산 쿼리 계획 수립과 실행을 가로챕니다. 내부 메모리 관리도 커스텀합니다.

pg_duckdb 는 DuckDB 엔진을 PostgreSQL 프로세스 내부에 임베드합니다. DuckDB는 자체 메모리 할당자(jemalloc), 스레드 풀, 파일 I/O 시스템을 가집니다. pg_duckdb도 PostgreSQL 내부 훅들을 등록합니다.

두 extension이 같은 PostgreSQL 프로세스에 로드되면 훅 체인에서 충돌이 발생합니다. DuckDB의 jemalloc과 PostgreSQL의 palloc 메모리 관리자가 서로를 인식하지 못하고 충돌합니다. 이것은 GitHub Issue #444로 공개 등록돼 있으며, Citus와 pg_duckdb 양쪽 모두 수정 계획이 없다고 명시했습니다.

같은 인스턴스에서 Citus + pg_duckdb(따라서 pg_lake)를 함께 쓰는 것은 현재 불가능합니다.


Phase 8: Citus + pg_lake — 별도 인스턴스 + postgres_fdw

Phase 7 실패 후 접근 방식을 바꿨습니다. Citus DW와 pg_lake Lakehouse를 완전히 별도 PostgreSQL 인스턴스로 분리하고 postgres_fdw로 연결합니다.

Citus 12.1 (PostgreSQL 16, DW)   ←——postgres_fdw——→   pg_lake 3.3 (PostgreSQL 18, Lakehouse)
       [분산 쿼리, OLTP]                                    [Iceberg 테이블, 분석]
              ↑                                                     ↓
        기존 애플리케이션                                      MinIO (Parquet)
                                                                     ↑
                                                          DuckDB standalone

postgres_fdw는 PostgreSQL에 내장된 표준 FDW(Foreign Data Wrapper)입니다. 원격 PostgreSQL 서버에 연결해 테이블을 마치 로컬 테이블처럼 읽고 쓸 수 있게 합니다. 크로스 버전(PG16 → PG18)도 PostgreSQL 와이어 프로토콜이 하위 호환이므로 동작합니다.

-- Citus(PG16)에서 pg_lake(PG18) 연결 설정
CREATE SERVER pglake_server
    FOREIGN DATA WRAPPER postgres_fdw
    OPTIONS (host 'pglake-host', port '5439', dbname 'warehouse');

CREATE USER MAPPING FOR current_user
    SERVER pglake_server
    OPTIONS (user 'pg_lake_user', password '...');

IMPORT FOREIGN SCHEMA public
    FROM SERVER pglake_server
    INTO lake_schema;

-- pg_lake Iceberg 테이블을 Citus에서 직접 조회
SELECT region, SUM(amount)
FROM lake_schema.orders  -- 실제로는 pg_lake Iceberg 테이블
WHERE order_date >= '2025-01-01'
GROUP BY region;

Citus에서 이 쿼리를 실행하면 postgres_fdw가 pg_lake 서버로 쿼리를 전달합니다. pg_lake는 Iceberg Parquet 파일에서 데이터를 읽어 결과를 반환합니다.

측정 결과:

연산 응답시간 비고
SELECT (cold, 집계 포함) 102ms aggregation pushdown 동작
INSERT via FDW → Iceberg 205ms Iceberg 스냅샷 자동 생성
UPDATE via FDW → Iceberg 99ms 행 단위 갱신
DELETE via FDW → Iceberg 70ms 행 단위 삭제

aggregation pushdown이 동작한다는 점이 중요합니다. GROUP BYSUM() 같은 집계를 Citus가 pg_lake 서버로 밀어내(push down) pg_lake에서 처리합니다. 모든 행을 Citus로 가져온 뒤 집계하는 게 아닙니다.

3-way 일관성 검증

Citus FDW에서 INSERT한 데이터를 세 경로에서 읽었습니다.

-- 1. pg_lake 직접 조회
SELECT * FROM orders WHERE order_id = 999;
-- → 반환

-- 2. Citus FDW 경유 조회 (lake_schema.orders)
SELECT * FROM lake_schema.orders WHERE order_id = 999;
-- → 동일 결과

-- 3. DuckDB standalone에서 iceberg_scan()
-- (pg_lake MinIO의 동일 Parquet 파일 직접 읽기)
SELECT * FROM iceberg_scan('s3://pg-lake-warehouse/iceberg/public/orders/metadata/...');
-- → 동일 결과

세 경로 모두 동일한 데이터를 반환합니다. Iceberg 스냅샷 기반이므로 가능한 일관성입니다. Citus FDW를 통해 INSERT하면 pg_lake가 새 Iceberg 스냅샷을 생성하고, DuckDB가 그 스냅샷의 metadata.json을 참조하면 즉시 반영됩니다.


최종 아키텍처 권장

8개 Phase 결과를 종합한 프로덕션 권장 구성입니다.

[ad-hoc 분석 / OLTP-like 쿼리]    [배치 분산 처리]       [외부 엔진]
  psql, JDBC 애플리케이션            Ray + DuckDB          Spark
        ↓                             Daft                  Trino
  PostgreSQL 18 (pg_lake)  ←———————FDW——————→  Citus DW (PG16)
        ↓
  MinIO / GCS (Iceberg v2 Parquet)
        ↑
  DuckDB standalone (iceberg_scan)
시나리오 권장
PostgreSQL 중심 Lakehouse (소규모~중규모) pg_lake + Iceberg
대규모 멀티엔진 (Spark+Trino 헤비) Iceberg + Nessie + Spark/Trino
단순 분석 (1TB 이하, 단일 엔진) DuckDB + PostgreSQL 카탈로그
DuckDB 분산 처리 Ray + DuckDB 또는 Daft
기존 Citus DW + Lakehouse 연결 Citus + pg_lake + postgres_fdw
DuckDB 단독 환경 DuckLake (standalone)

이 실험에서 얻은 것들

DuckLake는 DuckDB 전용 도구입니다. PostgreSQL 카탈로그를 지원한다는 문서가 있지만, pg_duckdb 환경에서는 libpq 심볼 충돌로 동작하지 않습니다. DuckDB standalone만 쓰는 환경에서는 간단하고 좋은 선택입니다.

GCS cold read는 NVMe 대비 650배 느립니다. 클라우드 Lakehouse 아키텍처를 설계할 때 첫 번째 쿼리의 응답 시간을 반드시 고려해야 합니다. 캐싱 전략 없이 GCS에서 실시간 분석을 기대하는 건 현실적이지 않습니다.

Citus + pg_duckdb는 현재 같은 인스턴스에서 불가능합니다. 별도 인스턴스 + postgres_fdw가 유일한 현실적 경로입니다. INSERT/UPDATE/DELETE via FDW가 모두 동작하고 3-way 일관성이 확인됐습니다.

pg_lake는 2025년 11월 기준 가장 완성도 높은 PostgreSQL Lakehouse 솔루션입니다. GA 이전이므로 프로덕션 채택 전에 안정성 검증이 필요하지만, 기능적으로는 원래 목표를 가장 잘 충족했습니다.

Ray+DuckDB와 Daft는 둘 다 실용적입니다. Smallpond는 아직 자체 인프라에서 쓰기 어렵습니다. Vortex는 DuckDB 전용 환경에서만 의미있고 멀티엔진 Lakehouse에서는 시기상조입니다.