클러스터는 하나의 coordinator 인스턴스와 여러 개의 worker 인스턴스로 구성됩니다. 데이터는 worker 노드에서 분할 및 복제되며, coordinator는 이러한 분할에 대한 메타데이터를 저장합니다. 클러스터에 전달된 모든 쿼리는 coordinator를 통해 실행됩니다. coordinator는 쿼리를 더 작은 쿼리 조각으로 나누어 각 조각이 독립적으로 실행될 수 있도록 합니다. 그런 다음 조각들은 적절한 worker 노드에 배포되고, 그 결과는 최종적으로 사용자에게 반환됩니다.
다음 다이어그램은 쿼리 처리 아키텍처를 간략히 설명합니다.
Citus의 쿼리 처리 파이프라인은 두 가지 주요 구성 요소로 나뉩니다:
- 분산 쿼리 플래너 및 실행기
- PostgreSQL 플래너 및 실행기
이를 아래 섹션에서 자세히 살펴보겠습니다.
분산 쿼리 플래너
Citus의 분산 쿼리 플래너는 SQL 쿼리를 입력받아 이를 분산 실행을 위한 계획으로 변환합니다.
SELECT 쿼리의 경우, 플래너는 먼저 입력 쿼리의 계획 트리를 생성하고 이를 교환 가능하고 결합 가능한 형태로 변환하여 병렬화가 가능하도록 합니다. 또한 네트워크 I/O를 최소화하면서 확장 가능한 방식으로 쿼리를 실행하기 위해 다양한 최적화를 적용합니다.
이후 플래너는 쿼리를 두 부분으로 나눕니다: coordinator에서 실행되는 쿼리와 각 worker의 분할에서 실행되는 쿼리 조각들입니다. 이 조각들은 모든 리소스를 효율적으로 활용하도록 worker에 배포됩니다. 이후 분산 쿼리 계획은 분산 실행기에 전달되어 실행됩니다.
키 값 기반의 조회 또는 수정 쿼리는 특정 분할만 타깃으로 삼기 때문에 계획 과정이 약간 다릅니다. 플래너는 들어온 쿼리의 분산 열을 추출하고 이를 통해 적절한 분할을 결정합니다. 그 후 플래너는 원본 테이블 대신 해당 분할 테이블을 참조하는 쿼리로 다시 작성하여 이를 분산 실행기에 전달합니다.
분산 쿼리 실행기
Citus의 분산 실행기는 분산 쿼리 계획을 실행하고 실패를 처리합니다. 이 실행기는 필터링, 집계 및 공존 연결(co-location join)을 포함한 빠른 응답형 쿼리뿐만 아니라 단일 테넌트 쿼리에서도 완전한 SQL 커버리지를 제공합니다. 필요에 따라 각 분할마다 worker와의 연결을 열고 모든 조각 쿼리를 실행합니다. 이후 각 조각의 결과를 수집하여 이를 합치고 최종 결과를 사용자에게 반환합니다.
하위 쿼리/CTE Push-Pull 실행
필요한 경우 Citus는 하위 쿼리나 CTE의 결과를 coordinator 노드로 수집한 후 이를 다시 worker로 전송하여 외부 쿼리에서 사용할 수 있게 합니다. 이를 통해 Citus는 더욱 다양한 SQL 구조를 지원합니다.
예를 들어, 때때로 WHERE 절에 포함된 하위 쿼리는 메인 쿼리와 동시에 실행될 수 없으며 별도로 실행해야 합니다. 가정하자면 웹 분석 애플리케이션은 page_id별로 분할된 page_views 테이블을 유지한다고 할 때, 가장 많이 방문한 20개 페이지의 고유 방문자 수를 확인하려면 다음과 같은 쿼리를 사용할 수 있습니다.
SELECT page_id, COUNT(DISTINCT host_ip)
FROM page_views
WHERE page_id IN (
SELECT page_id
FROM page_views
GROUP BY page_id
ORDER BY COUNT(*) DESC
LIMIT 20
)
GROUP BY page_id;
실행기는 page_id별로 각 분할에서 위 쿼리의 조각을 실행하고 고유한 host_ips를 계산한 뒤 coordinator에서 결과를 결합하려고 시도합니다. 그러나 하위 쿼리의 LIMIT 덕분에 이는 조각의 일부로 실행될 수 없습니다. 이를 해결하기 위해 Citus는 재귀적으로 쿼리를 계획하여 하위 쿼리를 독립적으로 실행하고 결과를 모든 worker에 전송한 뒤 주 쿼리 조각을 실행하고 결과를 coordinator로 가져옵니다. 이러한 "Push-Pull" 설계는 위와 같은 하위 쿼리를 지원합니다.
이를 이해하기 위해 위 쿼리의 EXPLAIN 출력을 살펴보겠습니다.
GroupAggregate (cost=0.00..0.00 rows=0 width=0)
Group Key: remote_scan.page_id
-> Sort (cost=0.00..0.00 rows=0 width=0)
Sort Key: remote_scan.page_id
-> Custom Scan (Citus Adaptive) (cost=0.00..0.00 rows=0 width=0)
-> Distributed Subplan 6_1
-> Limit (cost=0.00..0.00 rows=0 width=0)
-> Sort (cost=0.00..0.00 rows=0 width=0)
Sort Key: COALESCE((pg_catalog.sum((COALESCE((pg_catalog.sum(remote_scan.worker_column_2))::bigint, '0'::bigint))))::bigint, '0'::bigint) DESC
-> HashAggregate (cost=0.00..0.00 rows=0 width=0)
Group Key: remote_scan.page_id
-> Custom Scan (Citus Adaptive) (cost=0.00..0.00 rows=0 width=0)
Task Count: 32
Tasks Shown: One of 32
-> Task
Node: host=localhost port=9701 dbname=postgres
-> HashAggregate (cost=54.70..56.70 rows=200 width=12)
Group Key: page_id
-> Seq Scan on page_views_102008 page_views (cost=0.00..43.47 rows=2247 width=4)
EXPLAIN 분석
-
Root (최상위)
coordinator노드는worker로부터 받은 결과들을 그룹화하며,GroupAggregate는 이를 정렬하기 위해 요구합니다. -
Custom Scan (Citus Adaptive) 여기에는 두 가지 큰 서브플랜이 포함됩니다. 첫 번째는 "Distributed Subplan"이며, 이는 하위 쿼리의 실행을 나타냅니다.
-
Limit 및 Sorting 각
worker는 32개의 분할 중 하나에서 작업하며, 여기에는 정렬, 그룹화 및 제한이 포함됩니다. 모든worker가 이 쿼리를 완료하면 그 결과를coordinator로 보내며, 이를 "중간 결과"로 결합합니다. -
두 번째 서브플랜
Citus는 두 번째 서브플랜에서 다른 작업을 시작합니다. 이는page_views에서 고유한 호스트를 계산하며, 중간 결과와 JOIN을 수행하여 상위 20개 페이지로 제한합니다.
Hash Join (cost=17.00..78.88 rows=1124 width=36)
Hash Cond: (page_views.page_id = intermediate_result.page_id)
-> Seq Scan on page_views_102008 page_views (cost=0.00..43.47 rows=2247 width=36)
-> Hash (cost=14.50..14.50 rows=200 width=4)
-> HashAggregate (cost=12.50..14.50 rows=200 width=4)
Group Key: intermediate_result.page_id
-> Function Scan on read_intermediate_result intermediate_result (cost=0.00..10.00 rows=1000 width=4)
worker는 내부적으로 read_intermediate_result 함수를 사용하여 중간 결과를 로드합니다.
이 예시는 Citus가 여러 단계를 거쳐 분산 쿼리를 실행하는 방법과 이를 이해하기 위한 EXPLAIN의 역할을 보여줍니다.
PostgreSQL 플래너 및 실행기
분산 실행기가 쿼리 조각을 worker로 전송하면, 이들은 일반적인 PostgreSQL 쿼리처럼 처리됩니다. 각 worker의 PostgreSQL 플래너는 해당 분할 테이블에서 쿼리를 로컬로 실행할 최적의 계획을 선택합니다. 이후 PostgreSQL 실행기는 쿼리를 실행하고 결과를 분산 실행기에 반환합니다. PostgreSQL 플래너 및 실행기에 대해 더 자세히 알아보려면 공식 문서를 참조하세요.