레이크는 복제본이 아니라 데이터베이스의 스토리지입니다. 이것이 바로 Postgres가 빠르고, 분석이 최신 상태로 유지되며, CDC 파이프라인이 필요 없는 이유입니다.
작성자: Sergey Melnik
Reynold Xin의 게시물 "From monolith to Lakebase to LTAP"에서는 LTAP가 무엇이며 Lakebase 아키텍처가 이를 어떻게 구현하는지 설명했습니다. 즉, 운영 데이터의 거버넌스가 적용된 단일 복제본이 CDC 파이프라인이나 두 번째 데이터베이스 없이 Postgres와 분석을 모두 지원한다는 것입니다.
이어지는 많은 평론은 그 주장을 기존 시스템 전문가들이 이미 알고 있는 시각으로 해석했습니다. 흔히 네 가지 시각이 제기되었습니다.
“LTAP는 다른 이름의 CDC일 뿐이다”
“데이터의 단일 복제본은 하나의 물리적 레이아웃만 가질 수 있으므로, 포인트 조회나 스캔 중 하나는 느릴 수밖에 없다”
“오브젝트 스토리지의 Iceberg 또는 Delta 테이블은 본질적으로 최신 상태가 아니다”
“Postgres는 이미 추출 경로인 논리적 복제를 제공한다”
이러한 오해는 모두 Lakebase가 데이터를 저장하는 방식에 대한 잘못된 가정에 기반합니다. 하나씩 살펴보겠습니다.
진실: LTAP는 레이크에 각 테이블의 내구성 있는 복제본을 하나만 유지합니다. 그 앞단의 레이어들은 해당 테이블을 내구성 있고 빠르게 만들기 위해 일부 바이트를 캐시하지만, 그중 어느 것도 두 번째 데이터베이스가 아닙니다.
외부에서 볼 때 LTAP는 익숙한 파이프라인처럼 보일 수 있습니다. Postgres가 행을 쓰고, 프로세스가 로그를 추적하고, 변경 사항이 Iceberg 테이블에 반영되며, 분석 시스템이 Iceberg를 읽습니다. 이 파이프라인은 CDC입니다. 또한 이는 논리적 복제, 미러링 및 대부분의 zero-ETL 제품의 구조이기도 합니다. 따라서 LTAP를 동일한 설계로 분류하기 쉽습니다.
그렇지 않은 이유를 이해하려면 무엇을 두 번째 복제본으로 볼 것인지 정확히 정의하는 것이 도움이 됩니다. 시스템이 동일한 데이터를 둘 이상의 장소에 보유하게 되는 방식에는 두 가지가 있습니다.
따라서 테스트는 간단합니다. 이 레이어를 자체 데이터베이스로 쿼리하거나 장애 조치(failover) 대상으로 사용할 수 있습니까? 그렇다면 복제본이고, 그렇지 않다면 계층입니다.
그 기준에 따르면 Postgres는 항상 계층화되어 있었습니다. WAL, 버퍼 캐시, 인덱스 모두 힙과 겹치는 바이트를 저장합니다. 고유한 카탈로그나 타임라인이 없기 때문에 아무도 인덱스를 테이블의 복제본이라고 부르지 않습니다.
LTAP는 이와 동일한 계층 구조를 유지합니다. 이를 오브젝트 스토리지로 이동하고 레이크하우스 엔진이 하단의 내구성 있는 계층을 읽을 수 있도록 합니다.
쓰기와 읽기 과정을 거치는 Lakebase의 이 계층 구조는 다음과 같습니다.
오브젝트 스토리지 자체도 같은 방식으로 작동합니다. S3에 객체를 PUT할 때, 이 객체는 소거 코딩(erasure coding)을 사용하여 가용 영역 전반에 걸쳐 종종 둘 이상 저장됩니다. API는 여전히 하나의 객체 이름을 지정하며, 복제본 간에 동기화 작업이 실행되지 않습니다. Lakebase는 로그, 캐시, pageserver, 오브젝트 스토리지 등 동일한 개념을 레이크 앞에 적층합니다. 추가 파일들은 하나의 테이블을 내구성 있고 빠르게 만드는 방법입니다.
CDC는 실제로 두 번째 데이터베이스를 생성합니다. 논리적 변경 스트림을 외래 스키마로 디코딩하고(보통 기 본 키 기준), 원본이 다시 읽지 않는 테이블에 upsert합니다. 이 두 번째 테이블은 자체적으로 쿼리될 수 있으며 원본과 동기화가 어긋날 수 있습니다. 즉, 데이터 타입이 달라지고, DDL이 누락되며, 복제 슬롯이 행을 누락하고, 선택되지 않은 테이블은 아예 나타나지 않을 수 있습니다. 그 결과 두 개의 카탈로그가 생성됩니다.
LTAP에는 동기화를 유지해야 하는 두 번째 테이블이 없습니다. 포함할 테이블 목록을 선택할 필요도 없습니다. Postgres에 존재하는 테이블은 레이크에도 존재하며, 프라이머리가 제공하는 페이지들은 해당 저장소의 캐시입니다.
진실: 동일한 행에 대한 두 가지 무손실 인코딩은 서로 다른 액세스 경로(포인트 조회를 위한 힙 페이지, 스캔을 위한 Parquet)를 지원할 수 있습니다. 각 쿼리는 자신에게 빠른 레이아웃을 읽습니다.
추가 파일들이 계층 구조를 이룬다고 해도, 데이터 레이아웃은 여전히 별개의 문제입니다. 포인트 조회는 행 기반에서 효율적이고, 스캔은 열 기반에서 효율적입니다. 디스크에 단일 인코딩만 존재한다면 해당 액세스 패턴 중 하나는 I/O 비용을 감수해야 합니다. 테이블을 단일 논리적 복제본이라고 부른다고 해서 이 사실이 바뀌지는 않습니다.
이러한 추론은 맞지만, 하나의 복제본은 하나의 인코딩을 의미한다는 가정에 기반하고 있습니다. 두 포맷이 손실 없이 동일한 행을 표현할 수 있다면 그렇지 않습니다.
따라서 핵심 질문은 LTAP의 두 포맷이 무손실인지 여부입니다.
손실되는 데이터가 없으므로 열 기반 복제본이 레이크의 내구성 있는 저장소 역할을 할 수 있으며, 힙 페이지는 그 캐시가 됩니다. 이후 각 액세스 경로는 자신에게 적합한 포맷을 읽습니다. 조회는 컴퓨팅 노드, 로컬 디스크, 그리고 pageserver에 있는 8 KB 페이지를 타격합니다. 스캔은 OLAP 엔진을 통해 Parquet을 타격합니다. 인덱스는 핫 티어에 유지되며 Parquet으로 트랜스코딩되지 않습니다.
Postgres는 이미 이런 방식으로 작동합니다. 서로 다른 액세스 경로를 위해 동일한 행에 대해 힙과 커버링 인덱스라는 두 가지 물리적 구조를 유지합니다. 아무도 인덱스를 customer 테이블의 CDC 복제본으로 취급하지 않습니다. SELECT * FROM t WHERE id = 1과(와) SELECT count(*) FROM t은(는) 하나의 테이블에 대한 두 가지 액세스 경로입니다.
더 제한적인 관찰도 타당합니다. Postgres의 열 기반 파일들은 힙 정렬(heap-ordered)되어 있습니다. 각 압축(compaction) 시 페이지가 재작성되는 단위이기 때문에 행들은 페이지 번호 순서대로 위치합니다. 이 레이아웃은 페이지를 재구성하고 더티 페이지(dirty page)만 재작성하는 데 효율적입니다. 이는 WHERE order_date >= …에 대한 적절한 클러스터링 키가 아닙니다. 웨어하우스 쿼리에 해당 클러스터링이 필요한 경우, 운영 소스로부터 실버 테이블이 파생되는 것과 같은 방식으로 파생될 수 있습니다. 더 이상 필요하지 않은 것은 레이크 복제본을 만드는 것이 목적이었던 CDC 파이프라인뿐입니다.
진실: 모든 분석 쿼리는 일관된 스냅샷을 읽습니다. 아직 레이크에 도달하지 않은 커밋은 pageserver에서 읽어오므로, 쿼리 이전에 커밋된 내용은 누락되지 않습니다.
Iceberg 및 Delta는 오브젝트 스토리지에서 스냅샷 격리를 제공하며, 오브젝트 스토리지는 레이턴시가 높습니다. 따라서 분석은 항상 뒤처져 있고, 많은 소형 테이블은 최신 상태를 유지할 수 없으며, 테이블 간 조인은 일관성을 유지할 수 없다고 결론 내리는 것은 자연스럽습니다. 이러한 설명은 레이크로의 CDC 인젝션에 정확히 부합합니다.
이는 하나의 가정, 즉 쿼리는 오브젝트 스토리지에 이미 적재된 내용만 볼 수 있다는 가정에 의존합니다. LTAP에서는 쿼리가 아직 오브젝트 스토리지에 없는 페이지도 볼 수 있습니다. 그 점이 결과를 바꾸며, 쿼리의 흐름을 따라가 보면 가장 쉽게 이해할 수 있습니다.
분석 쿼리가 시작되면,
Postgres는 대량 읽기 트래픽을 전혀 처리하지 않으며, 단일 숫자와 최대한 그 최근 페이지들만 반환합니다. 해당 커밋이 표시되려면 레이크에 최신 커밋이 포함되어 있어야 하는 것은 아닙니다. 거의 모든 데이터와 함께, 마지막 구체화 이후에 커밋된 페이지를 가져오는 방법이 포함되어 있으면 됩니다. LSN은 기준점이 되며, 엔진에 병합을 어디까지 진행해야 하는지 알려줍니다.
동일한 LSN은 조인의 일관성도 유지해 줍니다. CDC는 테이블별로 적용되므로 하나의 릴레이션이 다른 릴레이션보다 더 나중의 슬롯 위치에 있을 수 있으며, 조인은 두 시점을 혼합하게 됩니다. LTAP는 각 파이프라인이 마지막으로 플러시한 시점이 아니라, Postgres가 REPEATABLE READ에 사용하는 것과 동일한 종류의 기준점인 단일 LSN에서 모든 테이블을 읽습니다. 해당 LSN은 공유 워터마크입니다. 워터마크 이후의 최근 변경 사항을 읽기 위한 API는 Lakebase용 기본 기능과 업스트림 Postgres용 확장 기능으로 모두 개발되고 있습니다.
소형 테이블은 최신성 제한이 아니라 메타데이터 비용입니다. Iceberg의 오버헤드는 파일과 스냅샷입니다. 몇 안 되는 행으로 이루어진 릴레이션의 경우, 그러한 장부 작업은 절약하는 것보다 비용이 더 많이 들며, 레이아웃은 스캔 성능에 중요하지 않습니다. 해당 테이블들은 페이지 캐시에 머물고, LSN 일관성 읽기에 참여하며, Iceberg 커밋을 건너뜁니다.
이로 인해 비슷하게 들리지만 실제로는 그렇지 않은 두 가지 주장이 남습니다.
구체화된 컬럼형 스냅샷은 가장 최근의 압축 주기만큼 프라이머리보다 뒤처질 수 있지만, 쿼리 자체는 그렇지 않습니다. Parquet 파일이 몇 분 정도 뒤처져 있을 수 있다고 해서 리더가 커밋된 데이터를 볼 수 없는 것은 아닙니다.
The reality: logical replication은 데이터베이스와 레이크를 동기화 상태로 유지하는 데 비용이 많이 드는 방법입니다. 모든 변경 사항을 행 단위로 다시 재생하며, 이를 따라잡아야 하는 파이프라인이 필요합니다. 로그 레코드를 독립적으로 처리할 수 없기 때문에 WAL을 병렬로 디코딩해도 이 문제를 해결할 수 없습니다.
최신 Postgres에서 분석을 수행하는 표준적인 방법은 로지컬 리플레이케이션을 활성화하는 것입니다. Debezium, PeerDB, pgoutput 및 유사한 도구들은 데이터베이스가 이미 변경 스트림을 생성한다는 점에 착안하여, 추가로 어떤 스토리지 형식이 필요한지 묻습니다.
의문점은 변경 스트림이 레이크에 테이블을 구축하는 좋은 방법인지 여부입니다. 스트림의 두 가지 속성, 즉 읽어야 하는 양과 유지해야 하는 상태의 양은 그렇지 않다고 말합니다.
Logical replication reads every change
WAL은 Postgres가 충돌 시 복구할 수 있도록 기록된 페이지 편집의 물리적 로그입니다. 로지컬 디코딩은 이를 커밋 순서의 행 이벤트로 변환합니다. 이러한 변환은 동일한 페이지에 대한 많은 업데이트를 페이지의 현재 이미지로 압축할 수 없으며, 모든 변경 사항이 통과해야 합니다. 핫 페이지에서 Postgres는 체크포인트 사이에 동일한 8KB를 백 번 로깅할 수 있습니다. 로지컬 디코딩은 백 개의벤트를 생성한 다음, 다운스트림 작업이 프라이머리 키를 기준으로 Parquet에 업서트합니다. 이와 대조적으로, 페이지 티어링 경로는 페이지를 한 번만 읽습니다. 우리가 측정형 워크로드에서, CDC는 최소한 WAL 볼륨을 읽는 반면 더티 페이지 압축은 그 일부만 읽을 수 있습니다.
Logical replication carries state
리플레이케이션 슬롯은 컨슈머가 계속해서 전진시켜야 하는 WAL의 커서입니다. 컨슈머가 뒤처지면 슬롯이 삭제되거나 디스크가 찰 때까지 WAL이 디스크에 남아 있게 됩니다. 로지컬 디코딩은 DDL을 생성하지 않으므로, ALTER TABLE, 타입 변경 및 이름 변경은 이벤트를 현재 카탈로그와 일치하지 않게 만들거나 슬롯을 무효화합니다. 프라이머리 키가 없는 테이블에는 REPLICA IDENTITY FULL이 필요합니다. 파이프라인에 비용이 많이 들기 때문에, 일반적으로 테이블의 하위 집합에만 적용됩니다. 이는 존재한다는 이유로 모든 Postgres 테이블이 레이크에 나타나는 것과 정확히 반대되는 현상입니다.
A related claim: logical decoding can be skipped and the WAL decoded in parallel
페이지서버는 이미 WAL을 소비하므로, 병렬 로그-투-Parquet 경로는 사소한 추가 단계처럼 보일 수 있습니다. 원칙적으로는 가능합니다. 프로덕션 익스트랙터로 구축된 적은 없으며, LTAP가 사용하는 경로도 아닙니다. 나중의 레코드가 이전 레코드를 가정하기 때문에 WAL 레코드의 순서는 유지됩니다. 가시성은 튜플이 아니라 해당 LSN의 pg_xact, 프로세스 배열, 그리고 카탈로그에 존재합니다. 독립적인 워커들은 각각 별개의 바이트 범위를 디코딩하여 정확한 현재 행을 생성할 수 없습니다.
LTAP가 병렬화하는 것은 로그 레코드가 아니라 페이지입니다. 시스템은 마지막 스냅샷 이후 어떤 페이지가 변경되었는지(Postgres 17+의 WAL 요약 또는 페이지서버의 로그 구조화 병합 트리) 묻고, 각 더티 페이지는 독립적인 작업이 됩니다. 해당 세트가 파티셔닝되고, 페이지가 트랜스코딩되며, 하나의 Iceberg 스냅샷이 커밋됩니다. 로그는 쓰기가 영구 저장된 방식이며, 변경된 페이지를 찾는 한 가지 방법입니다. 스냅샷은 변경을 가한 모든 레코드의 재플레이가 아니라 현재 페이지들입니다.
이 중 어떤 것도 리플레이케이션을 마이그레이션, 메이저 버전 업그레이드, 두 번째 Postgres로의 전환 등 의도된 작업에 부적합한 도구로 만들지 않습니다. 하지만 로지컬 리플레이케이션을 스토리지로 사용하면 두 번째 진실의 원천(source of truth)이 생성됩니다.
네 가지 미신 모두 데이터의 모든 구체화를 두 번째 데이터베이스로 취급하는 동일한 오해에서 비롯됩니다.
LTAP는 하나의 로지컬 테이블을 유지하며, 레이크는 그 영구 복사본입니다. 힙 페이지, 인덱스, 캐시는 마치 커버링 인덱스가 힙에 대한 쿼리를 처리하는 것과 비슷하게 Postgres가 OLTP 레이턴시로 해당 테이블을 서비스하는 방식입니다. OLAP 엔진은 Parquet을 직접 스캔하고 마지막 구체화 이후의 트랜잭션 일관성을 유지하는 변경 사항을 병합합니다.
이러한 계층 중 어느 것도 드리프트될 수 있는 두 번째 카탈로그나 두 번째 진실의 원천을 추가하지 않습니다.
Lakebase의 스토리지 아키텍처가 어떻게 LTAP로 이어지는지에 대한 자세한 내용은, 이 게시물을 참고하세요.
(이 글은 AI의 도움을 받아 번역되었습니다. 원문이 궁금하시다면 여기를 클릭해 주세요)
블로그를 구독하고 최신 게시물을 이메일로 받아보세요.