Presto(프레스토)는 Meta(메타, 이전 Facebook)에서 개발되어 2012년에 프로덕션에 도입된 오픈 소스 분산 SQL 쿼리 엔진입니다. 현재 Uber, Netflix, Alibaba, Bloomberg, LinkedIn 등 여러 대기업에서 사용되고 있으며, Amazon Athena와 같은 서비스의 기반으로 활용되기도 합니다. GitHub에는 100명 이상의 기여자가 참여하는 활발한 오픈 소스 커뮤니티가 존재합니다
주요 특징 및 설계 원칙:
• 다양한 데이터 소스 지원: Presto는 분산 파일 시스템(Hadoop 데이터 웨어하우스), 오픈 소스 및 독점 RDBMS, NoSQL 시스템, Kafka와 같은 스트림 처리 시스템 등 여러 데이터 소스에 저장된 데이터에 대해 SQL 인터페이스를 제공합니다
• 광범위한 워크로드 처리: Meta에서는 수 엑사바이트(exabyte) 규모의 데이터 소스를 포함하는 분석 워크로드를 지원하며, 낮은 지연 시간(sub-second latency)이 요구되는 대화형 사용 사례부터 여러 시간 소요되는 배치 ETL 작업까지 다양한 용도로 사용됩니다
• 고성능 설계: Presto는 코드 생성(code generation)과 같은 여러 핵심 기능과 최적화를 통해 고성능을 목표로 구축되었습니다
• 멀티테넌트(Multi-tenant) 시스템: Presto는 수백 개의 메모리, I/O, CPU 집약적 쿼리를 동시에 실행하고 수천 개의 워커 노드로 확장하면서 클러스터 리소스를 효율적으로 활용할 수 있습니다
ARCHITECTURE OVERVIEW
Presto는 단일 코디네이터(coordinator) 노드와 하나 이상의 워커(worker) 노드로 구성된 클러스터로 이루어집니다
• 코디네이터 노드 (Coordinator Node)
◦ 클라이언트로부터 SQL 문을 포함하는 HTTP 요청을 받습니다
◦ 쿼리 정책 평가, SQL 텍스트 파싱 및 분석, 분산 실행 계획 생성 및 최적화, 쿼리 오케스트레이션(orchestration)을 담당합니다
◦ 생성된 실행 계획을 워커에 분배하고 태스크 실행을 시작합니다
◦ 코디네이터는 Presto 아키텍처의 단순화된 뷰를 보여주는 다이어그램에서도 계획자(Planner)/옵티마이저(Optimizer), 스케줄러(Scheduler), 큐(Queue)와 같은 구성 요소를 포함하고 있습니다
• 워커 노드 (Worker Nodes)
◦ 쿼리 처리를 담당합니다
◦ 코디네이터로부터 실행 계획의 일부인 태스크를 수신합니다
◦ 이 태스크들은 외부 스토리지 시스템으로부터 데이터를 가져오거나 다른 워커가 생성한 중간 결과를 처리합니다
◦ 워커 노드들은 협력적인 다중 태스킹(cooperative multi-tasking)을 사용하여 여러 쿼리에서 동시에 태스크를 처리합니다
◦ 실행은 가능한 한 파이프라인(pipelined) 방식으로 이루어지며, 데이터는 태스크 간에 가용해지는 즉시 흐릅니다
• 데이터 소스 및 커넥터 API (Data Sources and Connector API)
◦ Presto는 HDFS 데이터 웨어하우스, RDBMS, NoSQL 시스템, Kafka와 같은 스트림 처리 시스템을 포함한 수십 개의 다양한 데이터 소스에 대해 고성능 I/O 인터페이스를 제공하는 플러그인 인터페이스인 커넥터 API(Connector API)를 지원합니다
◦ 커넥터 API는 메타데이터 API(Metadata API), 데이터 위치 API(Data Location API), 데이터 소스 API(Data Source API), 데이터 싱크 API(Data Sink API)의 네 가지 부분으로 구성되어, 분산 실행 엔진 환경 내에서 커넥터를 효율적으로 구현할 수 있게 합니다
• 쿼리 실행 흐름 (Query Execution Flow)
◦ 코디네이터는 SQL 텍스트를 파싱하고 분석하여 분산 실행 task을 생성합니다
◦ 이 task은 워커 노드에 분배되고, 워커는 외부 스토리지 시스템에서 데이터 청크(splits)를 읽는 태스크 실행을 시작합니다
◦ 워커들은 데이터를 외부 시스템에서 가져오거나 다른 워커가 생성한 중간 결과를 처리합니다
◦ 중간 데이터 및 상태는 가능한 경우 메모리에 저장됩니다
◦ 노드 간 데이터 셔플(shuffling) 시에는 최소 지연 시간을 위해 버퍼링이 튜닝됩니다
• 확장성 (Extensibility)
◦ Presto는 확장 가능하도록 설계되었으며, 플러그인 인터페이스를 통해 사용자 정의 데이터 유형, 함수, 접근 제어 구현, 이벤트 컨슈머, 큐 정책 및 구성 속성을 제공할 수 있습니다
Presto는 이러한 적응성, 유연성, 확장성 있는 설계 덕분에 대용량 데이터 처리 및 다양한 사용 사례(대화형 분석, 배치 ETL, A/B 테스트 등)를 지원할 수 있습니다
SYSTEM DESIGN
Presto의 시스템 설계는 적응성, 유연성, 확장성을 바탕으로 다양한 특성을 가진 분석 워크로드를 효율적으로 처리하도록 구성되어 있습니다.
• SQL 방언 및 확장성 (SQL Dialect and Extensibility)
◦ Presto는 ANSI SQL 사양을 충실히 따릅니다
◦Map, Array와 같은 복합 데이터 타입을 위한 익명 함수(람다 표현식) 및 고차 함수(higher-order functions)를 지원하여 사용성을 향상시킵니다
• 클라이언트 인터페이스, 파싱 및 계획 (Client Interfaces, Parsing, and Planning)
◦파싱 (Parsing): ANTLR 기반 파서를 사용하여 SQL 문을 구문 트리(syntax tree)로 변환합니다
◦분석 (Analyzing): 구문 트리를 분석하여 타입, 강제 변환, 함수 및 스코프를 확인하고, 서브쿼리, 집계, 윈도우 함수와 같은 논리적 구성 요소를 추출합니다
◦논리적 계획 (Logical Planning): 구문 트리 및 분석 정보를 사용하여 논리적 연산을 나타내는 계획 노드(plan node) 트리의 중간 표현(IR)을 생성합니다
• 쿼리 최적화 (Query Optimization)
◦ 논리적 계획을 쿼리에 대한 효율적인 실행 전략을 나타내는 물리적 구조로 변환합니다
◦ 탐욕적인 평가 방식으로 일련의 변환 규칙을 적용합니다. 여기에는 조건자 푸시다운(predicate pushdown), LIMIT 푸시다운, 컬럼 가지치기(column pruning), 비상관화(decorrelation)와 같은 잘 알려진 최적화가 포함됩니다
◦ 데이터 레이아웃 속성 활용: 커넥터 API(Data Layout API)를 통해 노출되는 파티셔닝, 정렬, 그룹화, 인덱스 등의 물리적 데이터 레이아웃 정보를 활용합니다
◦ 인터-노드 병렬 처리 (Inter-node Parallelism): 계획의 일부를 워커 노드 간에 병렬로 실행할 수 있는 "단계(stages)"로 식별하고, 단계 사이에 데이터를 교환하기 위해 셔플(shuffles)을 삽입합니다
◦ 인트라-노드 병렬 처리 (Intra-node Parallelism): 단일 노드 내에서 여러 스레드에 걸쳐 병렬화될 수 있는 계획 단계의 섹션을 식별합니다. 이는 지연 시간 오버헤드가 적고 상태(예: 해시 테이블, 딕셔너리)를 효율적으로 공유할 수 있어 매우 효율적입니다
• 쿼리 실행 (Query Execution)
◦ 로컬 데이터 흐름 (Local Data Flow): 스플릿은 드라이버 루프(driver loop)에 의해 실행됩니다. 이는 협력적 다중 태스킹(cooperative multi-tasking)을 위해 설계되었으며, 페이지(page)라는 컬럼형 데이터 블록 단위로 데이터를 처리합니다
◦ 셔플 (Shuffles): 노드 간 데이터 교환을 위해 HTTP를 통한 인-메모리 버퍼링된 셔플을 사용합니다 버퍼링은 최소 지연 시간으로 튜닝됩니다. 롱 폴링(long-polling) 메커니즘을 사용하여 응답 시간을 최소화합니다
◦ 백프레셔 (Backpressure): 출력 버퍼 활용도를 모니터링하여 지속적으로 높은 경우 스플릿 실행을 줄여, 느린 클라이언트가 과도한 리소스를 소비하는 것을 방지하고 네트워크 리소스의 공정한 공유를 보장합니다
• 리소스 관리 (Resource Management)
◦ CPU 스케줄링: 전체 클러스터 처리량(집계 CPU 활용도)을 최적화하고, 노드 수준에서는 저렴한 쿼리의 짧은 처리 시간 및 유사한 CPU 요구사항을 가진 쿼리 간의 공정한 공유를 목표로 합니다
. 협력적 다중 태스킹 모델을 사용하며, 다단계 피드백 큐(multi-level feedback queue)를 기반으로 쿼리 우선순위를 결정합니다
▪ 메모리 풀 (Memory Pools): 모든 비자명한 메모리 할당은 사용자(user) 또는 시스템(system) 메모리로 분류되며 해당 풀에서 할당됩니다. 사용자 메모리는 사용자가 추론할 수 있는 데이터(예: 집계 카디널리티)에, 시스템 메모리는 구현 세부 사항(예: 셔플 버퍼)에 사용됩니다
▪ 스필링 (Spilling): 노드의 메모리가 부족할 경우, 적격한 태스크(예: 해시 조인, 집계)는 상태를 디스크로 스필할 수 있습니다
▪ 예약 풀 (Reserved Pool): 교착 상태를 방지하기 위해, 일반 풀이 소진되면 해당 워커에서 가장 많은 메모리를 사용하는 쿼리가 클러스터 전체의 예약 풀로 "승격"됩니다. 예약 풀은 한 번에 하나의 쿼리만 사용할 수 있습니다
Presto에서 스케줄링은 코디네이터(coordinator)가 쿼리 실행 단계를 워커(worker) 노드에 분배하고, 데이터 청크(splits)를 태스크(tasks)에 할당하는 과정을 의미합니다. 이 과정은 Presto의 적응성, 유연성 및 확장성을 지원하며, 다양한 분석 워크로드의 효율적인 처리를 가능하게 합니다
Presto의 스케줄링은 주로 두 가지 주요 결정으로 이루어집니다
1. 단계 스케줄링 (Stage Scheduling): 실행 단계가 어떤 순서로 스케줄링될지 결정합니다
2. 태스크 및 스플릿 스케줄링 (Task and Split Scheduling): 얼마나 많은 태스크가 스케줄링되어야 하는지, 그리고 어떤 노드에 배치되어야 하는지, 데이터 스플릿이 태스크에 어떻게 할당되는지 결정합니다
각 태스크는 단일 처리 단위를 나타내며, 여러 파이프라인(pipelines)을 포함할 수 있습니다. 태스크 스케줄러는 계획 트리를 검사하고 단계를 리프(leaf) 단계와 중간(intermediate) 단계로 분류합니다
◦ 리프 단계 (Leaf Stages): 커넥터로부터 데이터를 읽습니다
◦ 중간 단계 (Intermediate Stages): 다른 단계의 중간 결과를 처리합니다
◦ 태스크 스케줄러는 네트워크 및 커넥터 제약을 고려하여 워커 노드에 태스크를 할당합니다
Presto는 동적으로 태스크 수를 변경하여 실행 중 태스크 수를 조절할 수 있습니다
• 스플릿 스케줄링 (Split Scheduling)
◦ 스플릿 할당 (Split Assignment): 코디네이터는 워커 노드에 태스크가 설정되면 스플릿(외부 스토리지 시스템의 주소 지정 가능한 데이터 청크에 대한 불투명한 핸들)을 태스크에 할당하기 시작합니다
◦ 지연 열거 (Lazy Enumeration): Presto는 커넥터에게 스플릿을 작은 배치(batches)로 열거하도록 요청하고, 이들을 태스크에 지연적으로 할당합니다
▪ 쿼리가 모든 데이터를 처리하기 전에 결과를 반환하기 시작할 수 있게 하며 (예: LIMIT 절이 사용되거나 초기 결과 확인 후 쿼리가 취소되는 경우)
◦ 큐 관리 및 불균형 처리: 워커는 자신에게 할당된 스플릿 큐를 유지합니다 코디네이터는 가장 짧은 큐를 가진 태스크에 새로운 스플릿을 할당함으로써, CPU 처리 비용의 불균형이나 워커 간의 성능 차이로 인한 불균형을 해결하는 데 도움을 줍니다
이력 기반 쿼리 최적화기(History-Based Optimizer, HBO)
1. 전통적인 비용 기반 최적화 (CBO)
• CBO는 쿼리 계획의 계산 비용을 예측하고 가장 낮은 예상 비용을 가진 계획을 선택하여 최적화를 수행합니다
. 일반적으로 기본 테이블에 대한 통계(예: 행 수, 고유 값, 히스토그램)를 수집하고 중간 쿼리 계획 노드의 카디널리티(cardinality)를 추정하여 이루어집니다
• 한계:
◦ 오류 발생 가능성: 복잡한 쿼리 형태에서는 데이터 분포 및 카디널리티 추정이 오류가 발생하기 쉽습니다
◦ 가정 의존성: 데이터 분석 전의 데이터 요구, 데이터 균일성, 필터 및 컬럼의 독립성 등 단순화된 가정에 크게 의존합니다
◦ 복잡한 표현식 처리 어려움: 조건부 표현식, 함수 호출, 다중 키 집계 등 복잡한 표현식의 선택도를 정확히 추정하기 어렵습니다
◦ 확장성 문제: 쿼리가 복잡해질수록 오류 마진이 기하급수적으로 증가합니다
2. 이력 기반 최적화기 (HBO)
HBO는 CBO의 한계를 극복하고 반복적인 쿼리 패턴을 활용하여 더욱 정확한 최적화를 제공합니다
대부분의 데이터 웨어하우스 쿼리는 프로그램적으로 생성되며, 대시보드나 ETL 파이프라인에서 발생하는 쿼리처럼 유사한 구조를 반복적으로 사용한다는 점을 활용합니다 HBO는 이러한 쿼리의 과거 실행 통계를 수집하고, 이를 기반으로 유사한 향후 쿼리의 성능을 예측합니다
• 주요 장점 및 설계 목표:
◦ 정확성: 실제 실행 통계를 연산자 수준에서 기록하여 복잡한 쿼리 형태에 대한 정확한 추정치를 제공합니다
◦ 자동화 및 적응성: 경량 프로세스로 쿼리 실행 시마다 이력을 추적하며, 데이터 분포 변화에 자동으로 적응합니다 샘플링 오버헤드나 모델 훈련이 필요 없습니다
◦ 운영 용이성: 사용자 입력 없이 자동화된 솔루션을 제공하며, 디버깅 및 분석에 필요한 명확성을 제공합니다
◦ 원활한 통합: 이력 데이터가 없는 경우 전통적인 CBO로 폴백(fallback)할 수 있으며, 단일 쿼리 계획 내에서 HBO와 CBO 통계를 혼합하여 사용할 수 있습니다
• 작동 방식:
1. 계획 정규화 및 해싱 (Canonicalization & Hashing): 쿼리가 실행될 때마다, Presto는 쿼리 계획 노드를 표준 형식(canonical form)으로 변환합니다
▪ 테이블 스캔 정규화: WHERE date = '2023-01-01'과 같은 리터럴 파티션 필터를 WHERE date IN {X, X}와 같은 심볼릭 표현으로 대체하여 파티션 크기가 유사하다고 가정합니다
▪ 상수 제거 (Pruning Constants): 술어(predicates)나 프로젝션(projections)에 있는 상수를 일반적인 심볼 X (또는 Z for general constants)로 대체합니다. 여러 전략이 있으며, 예를 들어 등가 술어의 상수 제거는 WHERE id = 123을 WHERE id = {X}로 바꿉니다
▪ 계획 노드 정규화: 모든 내부 조인 노드를 하나로 합치고, 순서가 중요하지 않은 자식 노드(예: UNION, INNER JOIN)는 정렬하여 직렬화합니다
▪ 해싱: 정규화된 계획 노드 표현은 SHA256과 같은 해싱 알고리즘을 사용하여 고유한 키로 변환됩니다
2. 통계 수집 및 저장: 쿼리 실행 중, Presto 워커는 연산자 노드별 런타임 통계(예: 행 수, 출력 크기, NULL 키 수, 부분 집계의 입출력 크기, 테이블 라이터의 병렬 처리 수준)를 주기적으로 코디네이터에 보냅니다 코디네이터는 이들을 집계하여 쿼리 완료 시 통계 저장소에 기록합니다. 통계는 계획 노드의 해시를 키로 하는 키-값 저장소(예: Redis)에 저장됩니다
3. 최적화 규칙에 HBO 활용: HBO 통계는 여러 최적화 규칙에서 활용됩니다
▪ 조인 순서 재정렬: 과거 실행된 조인 순서에 대한 정확한 카디널리티 추정치를 제공하여 최적의 조인 트리를 선택하도록 돕습니다
▪ 조인 분배 유형 선택: 조인 입력 크기 추정치를 기반으로 브로드캐스트 조인(broadcast join)과 재분할 조인(repartition join) 중 더 효율적인 방식을 선택하도록 합니다
▪ 부분 집계 최적화: 중간 결과의 크기를 충분히 줄일 수 있는 경우에만 부분 집계를 수행하도록 결정하여 오버헤드를 줄입니다
▪ 데이터 스큐 완화: 외부 조인에서 NULL 값이 많은 경우, NULL 키 수를 추적하여 조인 키를 재작성하고 NULL 값들을 워커들 사이에 고르게 분산하여 스큐를 완화합니다
▪ 스케일드 라이터 (Scaled Writers): INSERT 쿼리에서 과거 쿼리에서 기록된 라이터 태스크 수를 기반으로, 초기 라이터 태스크 수를 조정하여 대용량 데이터 쓰기 시 병목 현상을 방지합니다
• 성능 향상: HBO는 CPU 사용량과 지연 시간 면에서 평균적으로 1.0-1.3배의 성능 향상을 가져왔으며, 메모리 사용량을 Meta에서 4%, Uber에서 16% 감소시켰습니다. 특히 복잡한 쿼리에서 CBO의 한계를 보완하여 20-40%의 쿼리 계획을 개선하고, 63-70%의 비최적화된 계획을 최적화된 계획으로 전환했습니다. HBO 자체의 오버헤드는 미미합니다.
Presto/Trino는 데이터 소스(빅쿼리, MySQL 등)에서 필요한 범위만 스트리밍으로 끌어와 워커에서 페이지(page, 보통 ~1MB) 단위로 처리하고, 워커 간 교환(exchanges) 을 HTTP 기반으로 수행합니다. 전체 테이블을 한 번에 메모리에 올리지 않습니다
조인은 크게 두 가지: 브로드캐스트(작은 테이블을 모두에게 복제) vs 파티셔닝/재분배(양쪽을 키 해시로 셔플). 어떤 방식을 쓸지는 CBO/HBO가 카디널리티·크기 추정으로 고릅니다
메모리가 모자라면 스필(spill) 로 디스크(또는 외부 익스체인지 매니저/S3 등)로 내렸다가 파티션 단위로 다시 읽어 조인을 끝냅니다
동적 필터링(dynamic filtering) 과 푸시다운으로 소스에서부터 읽는 양 자체를 줄여 네트워크·메모리 부담을 크게 깎습니다. (특히 사실 테이블 + 강하게 필터된 차원 테이블 패턴)
HBO는 실행 중 수집된 연산자 단위의 실제 런타임 통계(행 수·바이트·널 키 수·셔플 팬아웃 등)를 정규화/해시된 계획 노드 키에 매핑해 저장했다가, 유사한 향후 쿼리에 써서 위 결정을 더 정확히 해 줍니다. 처음 보는 노드/테이블이면 CBO로 폴백하거나, 한 쿼리 안에서 HBO/CBO를 혼합합니다.
query optimizer
조인 순서 재정렬: 과거 실측 카디널리티로조인 트리를 더 안정적으로 선택.
조인 분배 방식:브로드캐스트(작은 쪽 복제) vs재분할/셔플 조인(키 해시로 분배) 선택을실측 크기로 결정.
부분 집계(Partial Aggregations): “여기서 중간 결과가진짜 많이 줄어드는가?”를 과거 값으로 판단, 괜한 agg 오버헤드 회피.
스큐 완화: NULL 키 비율 같은왜곡 지표로 조인 키 재작성/분산.
스케일드 라이터: INSERT/CTAS 등 쓰기 작업에서초기 라이터 태스크 수를 과거 성공값으로 잡아 병목 방지.