쿼리 페더레이션이 왜 필요한가
데이터 플랫폼이 커지면 데이터가 여러 곳에 흩어진다. 운영 DB는 MySQL과 PostgreSQL에, 분석 데이터는 S3의 Parquet 파일로, 이벤트 스트림은 Kafka에, 집계 결과는 ClickHouse에. 이 데이터를 함께 분석하려면 어떻게 해야 하는가.
전통적인 접근법은 ETL로 데이터를 한 곳에 모으는 것이다. 그런데 이 방법에는 한계가 있다. 원천 시스템이 바뀔 때마다 파이프라인을 수정해야 하고, 데이터 복사본이 늘어나면 신선도 문제와 비용 문제가 생긴다. 탐색적 분석을 위해 매번 ETL을 새로 짜는 것도 비효율이다.
쿼리 페더레이션(query federation)은 다른 방향으로 접근한다. 데이터를 옮기는 대신 쿼리를 데이터 위치로 보낸다. 하나의 SQL로 MySQL, S3, PostgreSQL에 동시에 질의하고 결과를 조인할 수 있다.
Trino(구 PrestoSQL)는 이 아이디어를 대규모로 구현한 분산 SQL 쿼리 엔진이다.
Trino의 역사
Trino의 역사는 Facebook의 Presto에서 시작한다.
- 2012–2013: Facebook이 Hive의 한계(MapReduce 기반으로 인터랙티브 쿼리에 너무 느림)를 극복하기 위해 내부적으로 Presto를 개발했다.
- 2013: Presto를 오픈소스로 공개했다. Facebook의 300 PB 데이터 웨어하우스에서 실전 검증된 엔진이었다.
- 2019: Presto 원 개발팀(Martin Traverso 등)이 Facebook을 떠나 별도로 PrestoSQL 프로젝트를 설립. Facebook은 기존
prestodb브랜치를 Presto Foundation에 기증. - 2020: PrestoSQL이 Trino로 이름을 바꿨다. 현재 Trino는 prestodb와 다른 별개의 프로젝트다.
2026년 기준 Trino는 활발하게 개발 중이며, Trino Software Foundation이 관리한다. 주요 버전은 400번대를 넘어섰다. Starburst(상용 배포판)와 AWS Athena v3가 Trino를 기반으로 한다.
prestodb(Meta의 Presto)와 Trino는 초기에는 코드 베이스를 공유했지만, 이후 서로 다른 방향으로 발전해 현재는 호환되지 않는 별도 제품이다.
아키텍처 개요
Trino는 코디네이터(Coordinator)와 워커(Worker) 역할로 나뉜다. 외부에서 보면 단일 SQL 엔드포인트처럼 보이지만, 내부에서는 수십~수백 대의 워커가 병렬로 실행한다.
코디네이터(Coordinator)
클러스터에서 단일 노드다. 클라이언트 요청을 받아 쿼리를 파싱하고, 최적화하고, 실행 계획을 워커에게 배분한다. 자신도 일부 연산(마지막 집계, 결과 반환)을 수행할 수 있지만, 대부분의 I/O 작업은 워커가 담당한다.
코디네이터는 단일 장애점(SPOF)이다. 고가용성이 필요하면 두 번째 코디네이터를 대기 상태로 두거나, Kubernetes에서 재시작을 자동화한다.
워커(Worker)
실제 데이터를 읽고, 필터링하고, 셔플하고, 집계한다. 커넥터를 통해 외부 데이터소스에 연결하고, 워커 간 데이터를 교환(exchange)한다. 코디네이터가 REST API로 지시를 내리면 워커가 실행하고 결과를 반환한다.
워커는 무상태(stateless)에 가깝다. Trino의 장애 허용성(fault tolerance) 기능을 켜면 태스크 실패 시 코디네이터가 다른 워커에서 재시작할 수 있다.
커넥터 프레임워크
Trino의 핵심 강점은 커넥터(Connector) 아키텍처다. 새로운 데이터소스를 지원하려면 커넥터를 구현해 플러그인으로 추가한다.
커넥터가 구현하는 인터페이스:
- ConnectorMetadata: 스키마, 테이블, 컬럼 목록 제공. 코디네이터가 메타데이터 캐시를 채울 때 사용.
- ConnectorSplitManager: 테이블을 병렬 처리 단위(split)로 나눈다. 파일 시스템이라면 각 파일이 하나의 split, MySQL이라면 primary key range로 split.
- ConnectorPageSource: 실제 데이터를 읽어 Trino 내부 형식(Page)으로 반환.
- ConnectorRecordSetProvider: 레코드 단위 읽기(PageSource의 대안).
공식 지원 커넥터 목록(2025 기준 주요 항목):
| 커넥터 | 데이터소스 | 특이사항 |
|---|---|---|
| Hive | HDFS, S3, GCS 등의 ORC/Parquet/Avro | 가장 성숙, Metastore 필요 |
| Iceberg | S3, HDFS 등의 Iceberg 테이블 | ACID, time travel 지원 |
| Delta Lake | S3, ADLS 등의 Delta 테이블 | Delta Lake 스펙 지원 |
| Hudi | S3, HDFS 등의 Hudi 테이블 | CoW, MoR 모드 |
| MySQL | MySQL, MariaDB | Pushdown 지원 |
| PostgreSQL | PostgreSQL | Pushdown 지원 |
| ClickHouse | ClickHouse | 집계 Pushdown 일부 |
| Kafka | Kafka 토픽 | 실시간 읽기 |
| MongoDB | MongoDB | 컬렉션 |
| Redis | Redis | 키-값 |
| TPCH / TPCDS | 내장 벤치마크 데이터 | 테스트용 |
Predicate Pushdown: Trino는 WHERE 조건을 커넥터로 "밀어내려" 데이터소스에서 필터를 적용하도록 한다. MySQL 커넥터가 pushdown을 지원하면 WHERE id = 42 조건을 MySQL이 처리하고 Trino는 해당 행만 받는다. Pushdown이 없으면 전체 테이블을 Trino가 읽어서 직접 필터링한다.
쿼리 생명주기
SQL이 Trino에 제출되면 다음 단계를 거친다.
AST 생성
이름 해결·타입 검사
관계 대수 트리
조인 순서·푸시다운
Stage·Task 분해
SplitManager → 워커 배정
Scheduler → 데이터 읽기
PageSource (커넥터) → Exchange
셔플·파티셔닝 → 집계·결합
Operator 파이프라인 → 결과 반환
Stage, Task, Split
분산 실행 계획의 핵심 단위다.
- Stage: 하나의 논리적 처리 단계. 예를 들어 "S3에서 데이터 읽기", "MySQL과 조인", "집계 계산"이 각각 별개 Stage다. Stage들은 Exchange로 연결된다.
- Task: 하나의 Stage를 하나의 워커가 실행하는 단위. 같은 Stage가 여러 워커에 배분되면 여러 Task가 생긴다.
- Split: Task가 처리하는 데이터의 최소 단위. S3 파일 한 개, MySQL primary key 범위 하나가 Split이 된다. 코디네이터가 커넥터의 SplitManager를 통해 Split 목록을 얻고 워커에 배분한다.
예시: SELECT a.name, b.amount FROM mysql.customers a JOIN s3.orders b ON a.id = b.cid
Stage 0 (Final): 결과 정렬·반환 (Coordinator에서 실행)
Stage 1 (조인): Hash Join 수행 (여러 Worker에서 병렬)
├─ Task 1-1 (Worker 1)
├─ Task 1-2 (Worker 2)
└─ Task 1-3 (Worker 3)
Stage 2 (MySQL 스캔): customers 테이블 스캔
├─ Split: id BETWEEN 1 AND 10000 → Task 2-1 (Worker 1)
└─ Split: id BETWEEN 10001 AND 20000 → Task 2-2 (Worker 2)
Stage 3 (S3 스캔): orders Parquet 파일 읽기
├─ Split: s3://bucket/orders/part-0001.parquet → Task 3-1 (Worker 1)
├─ Split: s3://bucket/orders/part-0002.parquet → Task 3-2 (Worker 2)
└─ Split: s3://bucket/orders/part-0003.parquet → Task 3-3 (Worker 3)비용 기반 최적화(CBO)
Trino의 CBO는 테이블 통계(행 수, 컬럼 카디널리티, null 비율, 히스토그램)를 사용해 최적 조인 순서와 조인 알고리즘을 결정한다.
- Broadcast Join: 작은 테이블을 모든 워커에 복사(broadcast)하고, 큰 테이블 스캔 시 로컬에서 조인. 네트워크 셔플 없음. 작은 테이블 크기 제한이 있다(기본
join_distribution_type=AUTOMATIC). - Partitioned (Hash) Join: 양쪽 테이블을 조인 키 해시값으로 파티셔닝해 같은 파티션끼리 조인. 대용량 테이블 간 조인에 적합. Exchange 비용이 발생한다.
CBO가 잘 동작하려면 테이블 통계가 최신이어야 한다. Hive/Iceberg 커넥터에서 ANALYZE table_name을 주기적으로 실행해 통계를 갱신한다.
메모리 관리와 스필(Spill)
Trino는 기본적으로 메모리 내에서 연산한다. 워커 JVM 힙의 상당 부분이 쿼리 실행에 사용된다.
메모리 풀 구분
각 워커는 메모리를 두 풀로 나눈다.
- User Memory: 쿼리가 사용하는 메모리. 해시 테이블, 중간 집계 결과, 셔플 버퍼 등.
query.max-memory-per-node로 제한. - System Memory: Trino 내부 구조체, 페이지 버퍼, 네트워크 버퍼.
query.max-memory는 클러스터 전체에서 한 쿼리가 사용할 수 있는 User Memory 총합이다. 이를 초과하면 Query exceeded distributed user memory limit으로 쿼리가 실패한다.
스필(Spill to Disk)
메모리 제한을 초과할 것으로 예상되면 중간 결과를 디스크로 내린다. 이를 활성화하려면:
spill-enabled=true
spill-path=/mnt/fast-disk/trino-spill
max-spill-per-node=200GB스필은 성능 저하를 동반하지만 쿼리 실패보다는 낫다. SSD가 있는 경로를 스필 디렉터리로 설정해야 성능 영향을 최소화할 수 있다.
장애 허용(Fault Tolerance)
초기 Trino는 하나의 워커 태스크가 실패하면 전체 쿼리가 재시작되었다. 수십 분짜리 쿼리가 98% 완료 후 실패하면 처음부터 다시 실행해야 했다.
Exchange Materialization(ETM)이 이를 해결한다. Exchange(워커 간 데이터 전달) 결과를 중간 스토리지(보통 S3 또는 HDFS)에 저장한다. 특정 워커가 실패하면 그 태스크만 다른 워커에서 재시작하고 저장된 Exchange 결과를 재사용한다.
retry-policy=TASK로 설정하면 태스크 단위 재시도가 활성화된다.
retry-policy=TASK
task-retry-attempts-per-task=3
exchange.deduplication-buffer-size=32MBETM은 스토리지 쓰기·읽기 오버헤드가 있어 짧은 쿼리보다 긴 쿼리에서 이득이 크다.
보안
Trino의 보안은 인증(Authentication)과 인가(Authorization)로 나뉜다.
인증
- 파일 기반:
password-authenticator.name=file, 사용자·패스워드 파일로 단순 인증. - LDAP/Active Directory: 기업 디렉터리와 통합.
- OAuth2/OIDC: 현대적인 SSO 통합. Keycloak, Okta, Google SSO와 연동 가능.
- Kerberos: Hadoop 생태계 기업 환경에서 사용.
- JWT: 토큰 기반 인증.
TLS는 필수 설정이다(http-server.https.enabled=true). 클라이언트↔코디네이터, 코디네이터↔워커 구간 모두 TLS를 권장한다.
인가
- 파일 기반 규칙: JSON 형식으로 사용자·그룹 별 카탈로그·스키마·테이블 접근을 제어.
- Apache Ranger: 세밀한 접근 제어 정책을 중앙에서 관리.
- OPA(Open Policy Agent): 정책을 코드(Rego)로 작성하고 Trino가 외부 OPA 서버에 질의.
Row-level security는 뷰(View)를 통해 구현한다. 사용자나 그룹에 따라 다른 조건이 적용된 뷰를 만들고, 기본 테이블에 직접 접근을 차단한다.
Trino vs Spark SQL vs DuckDB
쿼리 엔진 선택은 워크로드 특성에 따라 달라진다.
| 항목 | Trino | Spark SQL | DuckDB |
|---|---|---|---|
| 실행 모델 | 분산 MPP, 메모리 내 파이프라인 | DAG 기반 배치, 스테이지 디스크 기록 | 단일 프로세스, 벡터화 실행 |
| 적합 워크로드 | 인터랙티브 쿼리 (<1분), 페더레이션 | 대규모 배치, ETL, ML 전처리 | 단일 노드 OLAP, 로컬 파일 분석 |
| 메모리 사용 | 크다 (중간 결과 메모리) | 중간 (스필 기본) | 작다 (효율적 컬럼형) |
| 지연 (latency) | 낮음 (초~분) | 중간 (분) | 매우 낮음 (밀리초) |
| 클러스터 | 필요 (수십~수백 노드) | 필요 | 불필요 (단일 서버) |
| 페더레이션 | 강점 (커넥터 생태계) | 제한적 | 제한적 (DeltaLake, Iceberg 지원) |
| 운영 복잡도 | 중간 | 높음 | 없음 |
현실적인 선택 기준:
- Trino: BI 도구(Tableau, Superset)에서 여러 데이터소스를 실시간으로 조인할 때.
- Spark SQL: 수 TB 이상 배치 ETL, 머신러닝 전처리, 복잡한 집계 파이프라인.
- DuckDB: 단일 서버에서 로컬 Parquet·CSV·Iceberg 파일 빠르게 분석할 때. 데이터 엔지니어 개인 탐색 분석.
운영 핵심 지표
| 지표 | 확인 방법 | 의미 |
|---|---|---|
| 쿼리 큐 대기 시간 | system.runtime.queries 뷰 | 실행 슬롯이 부족하면 증가 |
| 워커 메모리 사용 | JMX trino:type=Memory,* | OOM 임박 신호 |
| Split 처리 속도 | UI의 Rows/sec | 커넥터 I/O 병목 |
| GC 시간 | JVM GC 로그 | 힙 과부하 징후 |
| CPU 사용률 | 워커별 CPU | 연산 균등 분배 여부 |
Trino는 웹 UI(http://coordinator:8080)에서 실행 중인 쿼리, Stage별 진행 상황, 워커별 리소스를 실시간으로 볼 수 있다.
실무 체크리스트: Trino 운영
- 코디네이터 고가용성: 단일 코디네이터라면 k8s Deployment로 자동 재시작을 보장한다.
- 워커 JVM 힙 설정:
jvm.config에서-Xmx를 서버 RAM의 70~80%로. GC 알고리즘은 G1GC 권장. - 커넥터 별 통계 갱신: Iceberg/Hive는
ANALYZE, MySQL/PostgreSQL은 자동 통계. - 스필 경로: SSD 경로를
spill-path에 지정. 스필이 과도하면 메모리 증설 고려. - CBO 활성화:
optimizer.join-reordering-strategy=AUTOMATIC(기본값 확인). - Fault Tolerance: 30분 이상 쿼리라면
retry-policy=TASK와 ETM 스토리지 설정. - 쿼리 제한:
query.max-execution-time,query.max-memory로 폭주 쿼리 제한. - TLS 필수: 코디네이터 HTTPS, 내부 노드 간 TLS도 프로덕션에서 켠다.
References
- Trino 공식 문서, https://trino.io/docs/current/
- Trino: The Definitive Guide (O'Reilly), https://trino.io/trino-the-definitive-guide.html
- Martin Traverso et al., "Presto: SQL on Everything" (Facebook 2019 논문), https://research.fb.com/publications/presto-sql-on-everything/
- Trino GitHub 저장소, https://github.com/trinodb/trino
- Starburst Blog, "Trino Architecture Deep Dive," https://www.starburst.io/blog/
- Trino Release Notes, https://trino.io/docs/current/release.html
- AWS Athena, "Athena SQL engine versions," https://docs.aws.amazon.com/athena/latest/ug/engine-versions.html
- DuckDB 공식 문서, https://duckdb.org/docs/