<p align="center">
<img src="https://raw.githubusercontent.com/penca-io/penca/HEAD/docs/penca-logo.svg" alt="Penca" width="200">
</p>
CI
license
Rust
Python
데이터의 단일한 열(columnar) 복사본에서 분기 및 버전 관리가 가능한 OLTP + OLAP
객체 스토리지에 배포 가능하며, 오픈 소스입니다. 별도의 시스템도, CDC(Change Data Capture) 파이프라인도, ETL(Extract, Transform, Load) 과정도 필요 없습니다.
소개
[!WARNING]
Penca는 매우 초기 단계이며, 제품이라기보다는 개념 증명(proof of concept)에 가깝습니다. 버그와 미흡한 부분이 있을 수 있으니 유의하시고, 현재의 단점을 읽어보신 후 계획을 세우시기 바랍니다: 로드맵의 상당 부분은 아직 진행 중입니다.
Penca는 객체 스토리지에 열(columnar) 파일 형태로 저장된 데이터의 단일 복사본에서 트랜잭션 및 분석 쿼리를 처리합니다. 즉, 분석 쿼리가 읽는 파일이 프로덕션이 작성한 파일이므로, 그 사이에 별도의 시스템이나 CDC 파이프라인이 필요 없습니다. 다만, 이로 인해 단일 행(single-row) 트랜잭션 비용은 로컬 디스크의 행 저장소(row store)보다 더 많이 듭니다.
트랜잭션 지연 시간(latency)을 작업 가능한 수준으로 유지하기 위해, 쓰기 작업은 실제 ACID 트랜잭션 하에 내부 Postgres '핫 티어(hot tier)'에 기록됩니다. 백그라운드 파이프라인이 이를 영구 저장하고 스냅샷을 찍어 열 파일(기본값은 Lance이며 Parquet도 지원)로 내보냅니다. 읽기 작업은 두 티어를 병합하여 쿼리가 어떤 티어에 있든 커밋된 쓰기를 즉시 볼 수 있게 합니다.
객체 스토리지(Object storage)는 플러그인 방식으로 연결할 수 있으며, Penca가 저장하는 모든 것의 유일한 영구 거주지입니다. 테이블 데이터는 현재 그곳에 존재합니다. 카탈로그 및 브랜치 메타데이터는 지연 시간 문제로 인해 Postgres에서 제공되며, 이를 객체 스토어에 체크포인트하고 시작 시점에 해당 Postgres 테이블을 재구성하는 것은 로드맵에 있습니다. 최종 목표 상태는 로컬 디스크에 있는 어떤 것도 영구적이지 않다는 것입니다: Postgres 페이지는 항상 객체 스토어로부터 재구축 가능하며, 시스템의 기록 저장소(system of record)가 되는 경우는 없습니다.
브랜치(branch)는 행(row)을 복사하지 않는 완전한 읽기-쓰기 사본이므로, 프로덕션 환경에서 포크(forking)하는 것이 실험당 충분히 저렴합니다. 모든 변경 사항은 작성자와 타임스탬프를 포함하는 불변 로그(immutable log)에 추가되므로, 어떤 상태든 이전 커밋 시점 기준으로 감사 및 읽기가 가능합니다.
작동 방식 확인하기
세 개의 에이전트, 세 가지 전략, 하나의 실시간 데이터셋. examples/sandbox_demo.py는 main에서 각 에이전트별로 브랜치를 포크하고, 이들을 통해 공유되는 결정론적 방문자 피드(deterministic visitor feed)를 모두 구동하며, 각 에이전트가 자체 커밋된 쓰기 작업으로 조향하게 한 다음 순위를 매기고, 모든 포크를 삭제한 후 main은 손상되지 않은 상태로 보여줍니다. 그 점수판은 다음과 같습니다:
| branch | impressions | conversions | rate |
|---|
greedy | 3000 | 417 | 13.90% |
| ... | | | |
greedy와 epsilon이 even을 이긴 이유는 자신들이 방금 작성한 것에 따라 조향했기 때문입니다. 반면, even은 방문자 인덱스(visitor index)로 분할하고 읽기를 전혀 하지 않습니다. 정책들은 장난감 수준이지만, 핵심 메커니즘이 중요합니다. 제시된 수치는 2026-07-27에 시드(seed)가 지정된 실행으로, 재현 가능하지만 고정되지는 않았습니다. 포크는 행을 복사하지 않으며, 이는 데모가 출력하는 것이 아니라 단언하는 내용입니다: integration_sandbox_demo_test.py를 참조하세요. | | | |
빠른 시작
just penca-up
uv run python - <<'EOF'
...
이것은 포트 50060으로 통신하는 표준 Arrow Flight SQL 드라이버입니다. Penca 클라이언트나 사용자 지정 프로토콜이 필요 없으며, greetings는 기본 카탈로그(catalog)와 스키마에 들어오므로 먼저 생성할 것이 없습니다. ADBC, SQLAlchemy, JDBC, 그리고 ODBC 모두 같은 방식으로 연결합니다. autocommit=True가 핵심적인 역할을 합니다: DB-API는 이를 False로 기본 설정하므로 아무것도 커밋되지 않고 닫힐 때 쓰기 작업은 폐기됩니다.
배포된 Python 클라이언트는 이 표면(surface)과 gRPC를 감싸고 있으며, 브랜칭(branching), 감사(audit), 그리고 타임 트래블(time-travel) 호출에 사용되는 것이 바로 이것입니다. docs/usage.md를 참조하세요.
Docker([https://docs.docker.com/engine/install/]), uv 및 just가 필요합니다. just penca-up은 사전 구축된 서버 이미지(ghcr.io/penca-io/penca-rust-server:latest, x86-64와 Apple Silicon 모두 네이티브 지원)를 가져옵니다. 만약 풀(pull)에 실패하면 소스에서 컴파일하는 방식으로 전환되는데, 이 과정은 시간이 걸립니다. 포트는 고정되어 있으며 (Postgres 5432, Flight SQL 50060), 루프백에 바인딩됩니다. 데이터는 디렉터리를 요청하지 않는 한 휘발성입니다. 이 경우 just penca-down을 실행해도 살아남습니다:
just penca-up --db ~/.penca/data
차이점
방어 가능한 주장은 하나의 복사본에서 이루어지는 **결합(conjunction)**입니다. 각 대안은 이 중 일부만 가지고 있습니다:
- Neon. 오브젝트 스토리지 기반의 브랜치 가능한 Postgres. 브랜치와 OLTP는 지원하지만, 데이터는 Postgres 데이터 페이지로 영속화됩니다. OLAP 기능은 기본적으로 제공하지 않습니다.
- Dolt. 브랜칭, 병합 및 감사 기능을 갖춘 오픈 소스이지만, 맞춤형 행 지향(row-oriented) 형식에 기반합니다. 분석 쿼리는 행 저장소 비용을 발생시키며, 레이크하우스 도구로는 읽을 수 없습니다.
- Iceberg / Nessie. 개방형 컬럼 파일 위에서의 브랜칭을 지원하지만, 대화형으로 쓰기 내용을 읽는 기능(read-your-writes)은 없습니다. 테이블 스냅샷을 커밋할 뿐, 트랜잭션 처리를 하지는 않습니다.
- Databricks Lakebase. 레이크하우스 옆에 관리되는 Postgres를 배치하고 브랜칭을 지원합니다. 브랜치와 OLTP는 물론 OLAP까지 지원하지만, 하나의 복사본 위에서 이루어지지는 않습니다. 분석 측면은 동기화된 테이블(CDC로 최신 상태가 유지되는, 자체 문서에서 '관리형 복사본'이라고 부르는 것)이며, 브랜치는 Postgres 저장소 계층만을 포크하므로, 분석 측면은 자체적인 동기화 파이프라인을 필요로 합니다.
Databricks의 LTAP은 2026년 6월에 발표되었고 현재 배포되고 있으며, 저희가 주장하는 것과 같은 내용을 담고 있습니다: 데이터를 읽기 위해 두 번째 사본을 동기화하는 대신 열린 형식으로 한 번 저장해야 한다는 것입니다. 이보다 더 나은 검증은 있을 수 없습니다. 우리가 다를 것으로 예상되는 부분은 Penca는 Apache-2.0 라이선스이며 자체 호스팅이 가능하고, 버전 기록을 쿼리 가능한 표면(queryable surface)으로 만든다는 점입니다. 오늘날 행 수준 감사(row-level audit) 및 as_of 읽기가 가능하며, 이는 레이크하우스 리더가 결코 볼 수 없는 MVCC 회계 처리 방식이 아닙니다.
아키텍처
Penca는 두 개의 진입점 뒤에서 세 개의 gRPC 서비스로 실행됩니다. SQL 클라이언트는 Flight SQL 게이트웨이에 연결하여 SQL을 해당 호출로 변환하고, 프로그래밍 방식의 클라이언트는 서비스를 직접 호출합니다.
SQL 클라이언트 (JDBC / ODBC / ADBC) 프로그래밍 방식 클라이언트
│ │
Flight SQL gRPC (3 채널)
...
다섯 번째 프로세스인 라이프사이클 스케줄러(lifecycle scheduler)가 주기적으로 persist → snapshot → purge를 구동하여 파이프라인을 아무런 작업자 없이 진행시킵니다. 읽기 계획은 쿼리 서비스 내에서 처리되며, 서비스 간의 이동 과정이 아닙니다.
포크는 행을 복사하지 않습니다: 부모 프로세스에서의 위치를 기록하고 이를 통해 부모의 콜드 파일(cold files)을 읽습니다. 하지만 공짜는 아닙니다: 브랜치를 생성하는 것은 먼저 부모의 미저장 쓰기 내용을 콜드 상태로 플러시하므로, 포크 지연 시간은 부모가 보유한 내용이 아니라 버퍼링된 내용을 추적합니다. 자세한 내용은 docs/architecture.md를 참고하시고, 알고리즘 및 충돌 안전 불변성(crash-safety invariants)은 docs/algorithms.md를 참고하십시오.
기능
현재 단점 (Current shortcomings)
-
인증 또는 권한 부여 기능이 전혀 없음. 인증 인터셉터(auth interceptor)도 없고, TLS도 없으며, Flight SQL 핸드셰이크가 구현되지 않았습니다. 포트로 도달하는 모든 것은 완전한 접근 권한을 가지므로 루프백 바인딩(loopback bind)을 사용합니다. Penca를 통제하지 않는 네트워크에 노출시키지 마십시오.
-
브랜칭 기능이 git보다 제한적입니다. main에서 포크할 수는 있지만, 포크가 아니며, 병합 또한 fast-forward 전용입니다. 대상(target)이 포크 지점 이후의 커밋을 했다면, 재조정(reconciled)되기보다는 병합 자체가 거부됩니다. 충돌 해결, diff, revert 기능이 없습니다.
-
설정 가능한 격리 수준이 없습니다. 얻는 것은 작업별로 고정된 값입니다: 트랜잭션 내 읽기 시점의 스냅샷(BEGIN 기준), upsert 시 마지막 작성자 승리(last-writer-wins), UPDATE/DELETE 시 READ COMMITTED. 카탈로그 전체 또는 개별적으로 선택할 수 있는 설정이 없습니다.
-
OLTP는 그럭저럭 사용 가능하지만, 경쟁력이 떨어집니다. 지점 읽기나 쓰기는 하부 저장소에 의해 결정되는 고정된 명령문당 파이프라인(per-statement pipeline)의 영향을 더 많이 받으며, SQL 경로는 gRPC 경로보다 이 영향이 더 큽니다. 구축하기에는 충분하지만, 단일 클라이언트 트랜잭션 작업용으로 bare-metal Postgres를 대체할 정도는 아닙니다. examples/oltp_demo.py로 직접 측정해 보십시오.
-
OLAP은 최적화가 부족합니다. 데이터 레이크에서 트랜잭션 위험을 줄이는 데 노력이 집중되었고, 소규모 규모에서는 여전히 Postgres가 분석 쿼리에서 승리하며, 이는 벽이라기보다는 교차점(crossover)에 가깝습니다.
-
Iceberg 내보내기가 없습니다. 콜드 티어는 오픈 Lance 또는 Parquet이며 어떤 엔진이든 파일을 읽을 수 있지만, 아직 아무것도 이를 Iceberg REST 카탈로그에 등록하지 못했습니다.
-
Arrow Flight SQL만이 유일한 SQL 와이어입니다. pgwire 게이트웨이가 없기 때문에 Postgres 클라이언트와 드라이버가 수정 없이 연결될 수 없습니다.
-
전체 텍스트 검색과 벡터 인덱스가 없습니다. 보조 인덱스는 등가성 탐색(equality seeks)만 가능합니다.
로드맵 (Roadmap)
위에 언급된 모든 것이 대략적인 순서로 로드맵입니다. 그 너머로는 다음과 같은 기능들이 있습니다: 핫 티어를 우회하는 벌크 로드(bulk load)를 통해 기존 데이터 레이크 파일을 최대 속도로 수집할 수 있게 하고, 마이그레이션 없이 제자리에 Iceberg 테이블을 채택합니다. 보존성(Retention)은 부족한 절단(pruning) 기능을 추가합니다. 읽기 와이어에 구조화된 조건부 필터(structured predicate)를 적용하여 SQL 문자열의 이중 파싱(double parse)을 제거하고, 집계/제한(limit)/TopN을 스캔 단계로 밀어 넣습니다. 카탈로그 메타데이터는 오브젝트 스토리지에 체크포인트(checkpointed)되고 시작 시 Postgres 테이블로 재구성됩니다 — 여전히 정상 상태에서는 Postgres에서 서비스되지만, 오브젝트 스토리지만으로도 재구축이 가능합니다. 이것이 시스템의 모든 로컬 디스크를 일시적(ephemeral)으로 만드는 단계이며, 따라서 손실된 Postgres 볼륨은 데이터가 아닌 재시작 비용을 초래하게 됩니다.
문서 (Documentation)
라이선스 (License)
Apache-2.0. 자세한 내용은 LICENSE를 참조하세요.