주요 컨텐츠로 이동
Lakebase

Lakebase Postgres에 몇 분 만에 수 테라바이트의 데이터 로드

LTAP 아키텍처로 더 빠른 대량 로드와 더 안전한 OLTP 워크로드 구현

작성자: Yecheng Yang, Nolan Biscaro, Szu-Po Wang , Pranav Aurora

  • LTAP(Lake Transactional/Analytical Processing) 아키텍처는 Lakebase Postgres의 기본 컴퓨팅에서 수행되는 무거운 대용량 작업을 Spark와 같은 분산 엔진으로 오프로드합니다.
  • Spark가 유효한 Postgres 페이지와 인덱스를 병렬로 생성하고 스토리지에 직접 쓸 수 있도록 함으로써, Lakebase Postgres는 실행 중인 애플리케이션 리소스를 소비하지 않고도 최대 147배 더 빠른 데이터 로드를 달성합니다.
  • Lakebase Postgres의 프라이머리 노드는 압축된 WAL 레코드를 통해 최종 매니페스트를 게시하여, 실시간 OLTP 쿼리가 영향을 받지 않고 높은 성능을 유지할 수 있도록 합니다.

Postgres와 같은 운영 데이터베이스는 데이터 서브셋 전반에서 동시성이 높고 지연 시간이 짧은 쿼리를 안정적으로 실행하도록 구축되었습니다. 이러한 안정성에 자주 영향을 미치는 것은 테라바이트 단위의 데이터를 로드하거나 전체 테이블을 스캔하는 분석 쿼리를 실행하는 것과 같은 대량(bulk) 작업입니다. 이러한 작업은 애플리케이션 워크로드와 동일한 리소스를 두고 경쟁하므로 성능 저하나 다운타임의 위험이 있습니다.

저희의 목표는 Lakebase Postgres를 운영 워크로드를 위한 가장 안전하고 신뢰할 수 있는 공간으로 만드는 것입니다. 기본 컴퓨팅에서 수행하던 무거운 배치 작업을 이러한 작업에 특화되어 설계된 Spark와 같은 분산 엔진으로 오프로드함으로써 이를 달성합니다. 이는 트랜잭션 엔진과 분석 엔진이 모두 레이크 내의 정확히 동일한 데이터에서 작동할 수 있도록 지원하는 LTAP(Lake Transactional/Analytical Processing) 아키텍처 덕분에 가능합니다.

이러한 격리가 이루어지지 않으면 팀에서는 대량 작업을 진행할 때 신중을 기할 수밖에 없습니다. 데이터 신선도를 희생하고, 혼잡하지 않은 업무 외 시간에 드물게 로드를 실행하며, 복잡한 백필과 체크포인팅을 수동으로 관리합니다. 이제 LTAP 아키텍처를 활용하면 수집 파이프라인을 Spark로 완전히 오프로드하여 라이브 시스템의 성능을 저하시키지 않고도 애플리케이션이 최신 데이터를 얻을 수 있습니다.

수치로 확인해 보기

베타 기간 동안 한 고객은 Synced Tables를 사용하여 매일 약 10억 개의 행을 Lakebase에 로드하고 있었습니다. LTAP 아키텍처를 활용함으로써 운영 워크로드에는 전혀 영향을 주지 않으면서 이러한 로드 속도를 획기적으로 향상시켰습니다.

image1.png

이전에는 대량 로드에 8시간 이상이 걸렸으며 CPU와 메모리가 완전히 포화 상태였습니다. Lakebase의 자동 확장을 사용하더라도 동기화를 감당하기 위해 OLTP 리소스를 과도하게 프로비저닝해야 했습니다. 이는 기존 Postgres 아키텍처에서 기본(primary) 인스턴스가 영구 상태(durable state)의 유일한 게이트키퍼 역할을 하기 때문입니다. 대량 로드는 이 단일 병목 지점을 통과해야 하므로, 가져온 모든 행이 라이브 애플리케이션 트래픽을 처리하는 동일한 인스턴스에서 힙 페이지를 생성하고 인덱스를 업데이트하며 WAL 레코드를 기록하게 됩니다.

LTAP 아키텍처는 기본 인스턴스의 부담을 완전히 덜어주어 라이브 애플리케이션 트래픽을 보호합니다. 데이터가 증가함에 따라 로드 처리량을 거의 선형적으로 확장하여 분산 엔진의 강력한 성능을 극대화할 수 있습니다. 내부 벤치마크 결과에 따르면 이제 1TB를 로드하는 데 5분도 걸리지 않습니다.

image6.png

참고: 이 벤치마크는 데이터 로드 시간(힙 페이지 구축)을 측정한 것입니다. 현재 로드된 데이터에 대한 인덱스 구축 역시 병렬화하는 작업을 활발히 진행 중입니다.

image7.png

이 게시물의 나머지 부분에서는 Postgres와 같은 OLTP 데이터베이스에 데이터를 대량 로드할 때 발생하는 과제를 심도 있게 살펴보고, LTAP 아키텍처를 활용하여 이를 해결하는 방법을 탐구합니다.

Postgres로 대규모 데이터를 대량 로드할 때 발생하는 문제

기본 Postgres COPY 명령은 일반적인 소규모 수집 작업에 효율적입니다.

하지만 고객이 운영 영역과 분석 영역을 보다 밀접하게 통합함에 따라 규모의 변화가 생깁니다. 대규모 Gold 계층 Lakehouse 테이블을 운영 쿼리 패턴이 있는 애플리케이션에 제공하는 워크로드가 점차 흔해지고 있습니다. 이처럼 극도로 방대한 용량의 데이터를 프로덕션 데이터베이스로 밀어 넣으면 두 가지 근본적인 한계에 봉착하게 됩니다.

  1. 가져온 모든 행이 단일 기본 작성자(primary writer)를 통과해야 하므로 로드 속도가 본질적으로 느립니다.
  2. 로드에 사용되는 동일한 컴퓨팅이 애플리케이션 쿼리도 처리하고 있습니다. 대량 로드는 과도한 CPU, I/O, 연결 및 WAL 대역폭을 요구하며 OLTP 트래픽과 리소스를 다툯니다.

대량 로드를 분산 Spark 작업으로 시작하더라도 이 문제는 해결되지 않습니다. Spark가 소스 파티션을 병렬로 읽을 수는 있지만, 각 행은 여전히 하나의 Postgres 작성자를 거쳐야 합니다.

  1. 워커가 COPY를 통해 Postgres로 행을 보냅니다.
  2. 기본 인스턴스가 해당 행을 힙 및 인덱스 페이지로 변환합니다.
  3. 기본 인스턴스가 변경 사항을 WAL(Write-Ahead Log)에 기록합니다.
  4. 로드를 커밋하기 전에 WAL을 영구 스토리지로 플러시(flush)해야 합니다.

기존 대량 로드는 단일 작성자로 인해 병목 현상이 발생합니다.

소스 측은 수평 확장이 가능하지만 대상(destination) 측은 불가능합니다. Spark 실행기(executor)를 추가하면 스캔 속도는 빨라지지만, 단일 작성자 병목 현상은 제거되지 않습니다. 또한 동일한 Postgres 기본 인스턴스가 온라인 애플리케이션 트랜잭션을 처리합니다. 대규모 가져오기는 CPU, 메모리, I/O, 연결 및 WAL 대역폭을 두고 이들과 경쟁합니다. 이로 인해 지연 시간이 증가하고, 팀은 사용량이 적은 시간대로 로드를 예약하며, 일상적인 트래픽이 아닌 가장 큰 가져오기 작업에 맞춰 기본 인스턴스를 프로비저닝하는 경우가 많습니다.

LTAP가 바꾸는 점

LTAP 아키텍처는 이러한 문제를 우회할 수 있는 길을 열어줍니다. 기본 인스턴스가 더 이상 영구 Postgres 상태를 생성하는 유일한 방법이 아니기 때문입니다. Lakebase에서 트랜잭션 컴퓨팅은 상태를 유지하지 않으며(stateless), 영구 상태는 기본 인스턴스의 로컬 디스크가 아닌 분산 스토리지 레이어에 저장됩니다. 어떤 면에서 Postgres는 스토리지의 클라이언트입니다. 쿼리와 트랜잭션을 처리하지만, 모든 새 페이지를 구체화(materialize)하는 프로세스일 필요는 없습니다.

image2.png

대량 로드의 경우 Spark가 Postgres 상태를 생성하여 스토리지에 작성하는 동안 기본 인스턴스는 해당 결과만 게시한다는 의미입니다.

이에 따른 운영상의 장점은 다음과 같습니다.

  • 대량 로드가 기본 인스턴스의 OLTP와 경쟁하지 않습니다. 라이브 컴퓨팅 엔드포인트 외부에서 실행되기 때문입니다. 애플리케이션 워크로드는 필요한 CPU, I/O, 연결 및 WAL 대역폭을 그대로 유지합니다.
  • 동일한 대상으로의 대규모 로드를 동시에 실행할 수 있습니다. 각 가져오기는 기본 인스턴스에 단일 WAL 레코드를 기록하는 것으로 완료되므로 더 이상 로드가 서로의 COPY 스트림 뒤에서 대기하지 않습니다.
  • 기본 인스턴스가 로드 크기에 맞춰 확장될 필요가 없습니다. 1 CU 기본 인스턴스가 트래픽을 계속 처리하는 동안 Spark는 별도의 컴퓨팅에서 수십억 개의 행을 로드할 수 있으며, Spark 컴퓨팅은 로드가 끝나는 즉시 종료됩니다.

유효한 Postgres 페이지를 안전하게 병렬 구축

Postgres 파일을 구축하고 기본 키(primary-key) 인덱스를 구성하기 위한 다양한 최적화 방안이 있습니다.

병렬로 힙 구축하기

Spark가 생성하는 페이지는 마치 기본 인스턴스가 구축한 것처럼 대상 데이터베이스에 대해 유효해야 합니다.

각 Spark 실행기(executor)는 메이저 버전 업그레이드 중에 카탈로그 OID를 보존하기 위해 pg_upgrade가 사용하는 것과 동일한 메커니즘인 바이너리 업그레이드 모드에서 샌드박스 처리된 Postgres 인스턴스를 시작합니다. 이를 사용하여 대상의 카탈로그 OID를 각 샌드박스에 이식함으로써 생성된 페이지에 임베드된 OID가 대상의 OID와 일치하도록 합니다. 드라이버는 또한 샌드박스에 겹치지 않는 OID 범위를 할당하여 동시에 생성된 개체가 충돌하지 않도록 합니다.

각 샌드박스 내부에서는 FREEZE가 포함된 바이너리 COPY가 해당 실행기의 힙 조각을 구축합니다. 프리징(freezing)은 가져온 튜플을 이미 커밋된 것으로 표시하므로 Postgres는 샌드박스의 트랜잭션 기록을 참조하지 않고도 이를 표시 가능(visible) 상태로 취급할 수 있습니다.

그런 다음 각 워커는 생성된 모든 페이지의 체크섬을 계산합니다. Lakebase의 페이지 서버는 파일을 수집할 때 해당 체크섬을 검증하여 가져온 페이지가 신뢰할 수 있는(authoritative) 상태가 되기 전에 손상 여부를 감지합니다.

바이너리 업그레이드 모드, 동결된(frozen) 튜플, 조율된 OID, 체크섬 검증이 결합되어 Spark가 대상에서 일반 Postgres 페이지로 읽을 수 있는 페이지를 생성하도록 보장합니다. 이는 Postgres 코어를 수정하지 않고 Postgres 확장 기능 및 테이블 액세스 방식을 통해 구현됩니다.

일회성 샌드박스를 사용하면 UNLOGGED 도우미 테이블과 같은 성능 최적화도 가능합니다. 이는 안전 메커니즘이 아닌 최적화 옵션입니다. 샌드박스를 공유하는 애플리케이션 트래픽이 없으므로 불필요한 WAL 및 잠금(lock) 경합을 방지할 수 있습니다.

이것은 여전히 "순수한 Postgres"인가요?
네, 그렇습니다. Spark가 작성하는 파일은 나중에 기본 인스턴스가 변환하는 가져오기 형식이 아니라 Postgres 페이지 자체입니다. 저희는 해당 경로 주변에 확장 프로그램과 테이블 액세스 방식을 추가했습니다. 이는 소스 코드를 수정하지 않고도 기능을 추가할 수 있는 Postgres만의 강력한 장점 중 하나입니다.

힙을 스캔하지 않고 인덱스 구축하기

B-트리는 전체 키 공간에 걸쳐 정렬된 하나의 구조이며, 리프 항목에는 힙을 다시 가리키는 키 및 튜플 ID 쌍이 포함되어 있습니다.

일반적으로 Postgres는 힙을 스캔하여 이러한 쌍을 얻습니다. 그러나 여기에서는 힙이 업로드된 슬라이스 전체에 분산되어 있으며, 이를 인덱스 생성 워커에 모두 다운로드하면 병렬로 생성함으로써 얻을 수 있는 이점이 상당 부분 사라집니다.

인덱스 빌더는 실제로 힙 콘텐츠가 필요하지 않습니다. 힙 스캔이 생성하는 (key, ctid) 쌍의 스트림이 필요할 뿐입니다. 따라서 이전 단계의 각 힙 워커는 슬라이스를 빌드하는 동안 키 컬럼과 튜플 ID를 별도의 오브젝트 스토리지 파일로 내보냅니다.

이러한 레코드는 원본 행 대신 내보낸 키 컬럼과 튜플 ID만 포함하는 헬퍼 테이블로 전달됩니다. CREATE INDEX 동안, 맞춤형 테이블 액세스 메서드는 heapam와 동일하게 헬퍼 테이블을 스캔하지만, 헬퍼 행 자체의 물리적 튜플 ID 대신 각 인덱스 항목에 ctid을(를) 기록합니다. 레코드가 로드됨에 따라 각 슬라이스 로컬 ctid은 이전 힙 슬라이스의 누적 크기만큼 시프트되어 연결된 힙에서 튜플의 최종 위치를 가리키게 됩니다. 즉, Postgres의 표준 B-트리 빌더는 다운로드하지 않은 힙에 대해 이 헬퍼 테이블을 변경 없이 스캔하여 인덱스를 생성할 수 있습니다. 이는 전체 힙 이동을 훨씬 작은 키 및 튜플 ID 표현의 이동으로 대체합니다.

Postgres에 결과 전달하기

힙과 인덱스 슬라이스가 오브젝트 스토리지에 저장되면 Spark 드라이버는 매니페스트를 작성하고 대상에서 하나의 SQL 함수를 호출합니다. 프라이머리는 가져오기를 압축된 WAL 레코드로 기록합니다. 기존의 COPY은(는) 세이프키퍼 쿼럼을 통해 전체 데이터 볼륨을 전송합니다. 하지만 여기서는 가져오기 설명만 전송합니다.

페이지서버는 업로드된 파일을 신뢰할 수 있는 페이지로 간주하고 체크섬을 검증합니다. 또한 초기 읽기가 콜드 오브젝트 스토리지 가동을 발생시키지 않도록 로컬 SSD에 페이지를 프리워밍합니다. 마지막으로 프라이머리는 스테이징된 데이터를 사용자가 볼 수 있는 동기화된 테이블로 원자적으로 교체합니다.

해당 트랜잭션이 커밋될 때까지 가져온 테이블은 격리된 상태로 유지됩니다. 그 후 프라이머리는 표준 Postgres 형식 및 인터페이스를 통해 생성된 일반 힙 및 B-트리 페이지를 참조합니다.

지금 바로 시작하세요.

Lakehouse에서 골드 데이터셋을 제공할 때 LTAP 아키텍처를 사용하여 Synced Tables를 작동시키므로 훨씬 빠른 동기화가 가능합니다. Lakehouse에서 수동 ReverseETL 작업을 실행 중이라면 Lakebase 사용을 고려해 보세요.

인덱스 생성이나 마이그레이션과 같은 대규모 작업 및 유지보수를 처리할 수 있도록 이 프로토콜을 일반화하게 되어 기대가 큽니다. LTAP 아키텍처를 적용한 Lakebase는 OLTP 워크로드를 실행하기에 최적의 환경입니다.

아직 Lakebase와 Synced Tables를 사용해 보지 않으셨다면 지금 바로 시작해 보세요.

(이 글은 AI의 도움을 받아 번역되었습니다. 원문이 궁금하시다면 여기를 클릭해 주세요)

최신 게시물을 이메일로 받아보세요

블로그를 구독하고 최신 게시물을 이메일로 받아보세요.