[KO] CUBRID 병렬 질의 — Heap Scan, Hash Join, Query Execute에 걸친 Intra-Query 병렬성
목차
학술적 배경
섹션 제목: “학술적 배경”관계형 실행기는 질의 계획을 튜플의 스트림으로 변환한다. 계획 자체나 그 입력이 큰 경우 그 일을 한 CPU에서만 한다는 것은 다른 코어를 놀게 두면서 응답 지연을 늘린다는 뜻이다. Intra-query 병렬성 은 계획 안의 어떤 연산자 (또는 그 leaf) 의 사본 여러 개를 동시에 돌리고, 결과를 다시 모아서 그 위의 소비자가 직렬 계획에서 보았을 동일한 출력을 보게 한다.
이 분야의 출발점은 Graefe의 Encapsulation of Parallelism in
the Volcano Query Processing System (SIGMOD 1990) 이다.
Graefe는 직렬 iterator (open/next/close) 가 exchange 라는
연산자 하나만 추가되면 병렬성에 충분한 계약이 된다고 관찰했다.
exchange가 생산자 thread와 소비자 thread 사이의 큐, 입력을
생산자에 매핑하는 분할 정책, flow control 을 모두 소유한다. 거기
서 네 가지 변종이 떨어져 나온다 — Gather (N→1), Scatter
(1→N hash 또는 range 분할), Repartition (N→N 새 함수),
Replicate (1→N broadcast). 대부분의 엔진은 이 중 일부만 고른다.
그 안에서 의미 있는 개념이 셋이다. 병렬도 (DOP, degree of
parallelism) 는 정수 N으로, 하드웨어 (코어 수), 소프트웨어
(전역 thread 예산), 데이터 (페이지 4개를 스캔하는데 worker 8개를
띄우는 것은 의미 없음) 에 의해 위로 잘린다. 분할 은 block-
range (heap의 N개의 연속 영역), hash (조인 키로 N개의 버킷),
round-robin 중 하나다. 이 선택이 위쪽 exchange가 무엇을 할 수
있는지를 제약한다 — hash는 partition-aware 라 위의 join이 파티션
별로 돌 수 있지만 block-range는 그렇지 않다. 스케줄링 은 오래
사는 worker (Postgres BackgroundWorker, Oracle PX server) 와
연산자 사이를 오가며 재활용되는 짧게 사는 task (DuckDB, CUBRID)
의 두 갈래다.
이 그림을 두 가지 정련이 마무리한다. shared-build 패턴 (Anatomy of a Database System, Red Book 4장) 은 병렬 hash join 을 위한 것이다 — inner 측을 한 번만 공유 구조로 build하고 N개의 probe worker가 동시에 읽게 한다. CUBRID은 약화된 형태를 구현한다 — 공유되는 것은 hash table 자체가 아니라 파티션 풀 이다. 2단계 parallel sort (Database Systems: The Complete Book §15.4) 는 N개의 worker가 각자 한 슬라이스를 정렬한 뒤 merge한다. CUBRID의 parallel sort가 정확히 그 slice-and-merge 다.
이 계층이 어디에 앉느냐가 책임을 결정한다. 위로는
qexec_execute_mainblock, scan_next_scan, qexec_hash_join,
sort_listfile_internal 이 직렬 경로가 만들어 내던 것과 같은
도메인 (DB_VALUE, XASL_NODE, QFILE_LIST_ID) 에서 튜플을
요구하므로 소비자는 차이를 알 수 없다. 아래로는 access method가
페이지 단위 인터페이스를 노출하므로, 병렬 계층은 직렬 latch
프로토콜을 깨지 않으면서도 N개의 thread에 걸쳐 fixing, latch,
pinning 을 조율해야 한다. thread pool, 에러 컨텍스트, perf
모니터, 인터럽트 기계 모두가 fan-out 되었다가 완료 시 fan-in
되어야 한다. CUBRID 설계의 흥미로운 부분은 정확히 그 fan-out /
fan-in 배관이다.
DBMS 공통 설계 패턴
섹션 제목: “DBMS 공통 설계 패턴”한 코어 너머로 확장하는 모든 현대 DBMS는 구현 전략 하나를 선택한다. 추상 수준에서는 일관되지만 (Graefe exchange는 어디나 있다) thread 모델, 스케줄링, 적용 범위 에서 갈라진다.
PostgreSQL 은 parallel-aware 노드 와 BackgroundWorker
풀을 쓴다. 플래너가 위에 Gather / Gather Merge 를 내고, 그
아래로는 각 병렬 가능 연산자를 별도의 Parallel* 노드
(ParallelSeqScan, ParallelIndexScan, ParallelBitmapHeapScan,
ParallelHashJoin, ParallelAppend, ParallelAggregate) 로
표현한다. 리더가 gather마다 N개의 짧게 사는 프로세스를 fork
하고, 조정은 공유 메모리로 한다. DOP는
max_parallel_workers_per_gather 로 정해지고
min_parallel_table_scan_size 로 잘린다. thread 모델은 process
-per-worker.
Oracle 은 오래 사는 PX 서버의 공유 풀에서 끌어 오는 parallel
execution slave 를 쓴다. 계획에는 Px Block Iterator / Px Partition Iterator 와 Px Send/Px Receive 쌍이 박혀 있다. DOP는
parallel_max_servers 로 막히고 11gR2 이후 Auto DOP가 질의별로
조정한다. 적용 범위는 주류 엔진 중 가장 넓다 — scan, join, sort,
group-by, top-N, parallel DML, parallel index build. shared-build
는 broadcast distribution 또는 hash-hash distribution 으로
구현된다.
SQL Server 는 SQLOS 사용자 모드 스케줄러 위에 parallel-aware
연산자를 얹는다. DOP는 max degree of parallelism 와 MAXDOP 로
막힌다. SQLOS는 컨텍스트 스위치 비용을 낮게 유지해 thread 과
배정도 견디게 한다.
MySQL/InnoDB 는 역사적으로 intra-query 병렬성을 갖지 않았다.
8.0.14부터 InnoDB가 parallel read 를 출하한다 — multi-thread로
clustered-index를 스캔하며 CHECK TABLE, SELECT COUNT(*), 일부
DDL 경로에 쓰인다. 그 위 SQL 계층은 여전히 단일 thread다 —
parallel join, sort, aggregation 은 8.4 mainline에 없다.
DuckDB 는 morsel-driven 모델 (Leis et al., SIGMOD 2014) 을 받아들인다. 스케줄러가 소스를 morsel로 쪼개고 고정 크기 풀의 thread에 할당하며, 연산자들은 데이터 청크의 push 기반 emission 으로 사슬을 이룬다.
CUBRID은 네 번째 점을 고른다 — 공유된 고정 크기 worker 풀
위에 얹은 연산자별 병렬 오케스트레이터 다. 전역 exchange 연산자
도 없고, 플래너 출력에 parallel-aware 표시도 없다. 세 연산자 —
heap scan, hash join, top-level query execute — 가 각자 자기
manager 클래스를 가지고 open / execute 시점에 병렬로 갈지
결정하고, 일을 분할하고, 전역 풀에서 N개의 worker를 예약하고, N
개의 task를 push하고, join한다. 풀은 — parallel-query 라고
이름 붙은 단일 cubthread::worker_pool 이 — 모두에게 공유된다.
오늘날의 적용 범위는 parallel sequential heap scan, parallel hash
join (Grace 식 2단계 build/probe), parallel uncorrelated subquery
(BUILDLIST_PROC / BUILDVALUE_PROC / UNION_PROC /
HASHJOIN_PROC / MERGELIST_PROC), parallel slice-and-merge
sort 이다. 절충은 분명하다 — Postgres나 Oracle에 비해 적용
범위는 좁지만 (parallel index scan 없음, heap scan 위 parallel
aggregate 없음, parallel append 없음) 조정 계층은 훨씬 얇다 —
오케스트레이터 셋, 풀 하나, DOP 함수 하나, S_PARALLEL_HEAP_SCAN
가지 하나.
CUBRID의 구현
섹션 제목: “CUBRID의 구현”parallel_query 네임스페이스와 전역 워커 풀
섹션 제목: “parallel_query 네임스페이스와 전역 워커 풀”모든 것이 세 형제 네임스페이스 (parallel_query,
parallel_query_execute, parallel_heap_scan) 아래 산다. 닻이
되는 것은 단일 클래스 — worker_manager_global 이라는 Meyers
싱글턴인데, 모든 병렬 기능이 공유하는 OS-thread 풀을 소유한다.
필드는 cubthread::worker_pool_type *m_worker_pool, std::once_flag m_init_flag, std::atomic<int> m_available, int m_capacity 이고,
공개 API는 init / destroy, 내부 API는 try_reserve_workers /
release_workers / push_task 다.
초기화는 프로젝트 전반의 워커 풀 레지스트리에 끼어든다 —
REGISTER_WORKERPOOL(parallel_query, ...) 매크로로. cubthread
의 모든 이름 붙은 풀은 서버 시작 시 그 레지스트리를 걷는 방식으로
생성되고, parallel-query 풀은 PRM_ID_MAX_PARALLEL_WORKERS 크기
의 한 항목이다. init() 안에서 std::call_once 가드가 파라미터를
읽어 기능을 비활성화하거나 (max_parallel_workers < 2)
thread_create_worker_pool(max_parallel_workers, 1, parallel-query, thread_get_entry_manager()) 로 풀을 만든다. 풀은 고정 크기
(autoscaling 없음), 단일 용도 (worker당 task_max_count = 1), 그리고
이름이 붙어 있어서 CPU 덤프가 그 이름으로 귀속된다.
흥미로운 필드는 m_available 이다 — 현재 예약되지 않은 worker
수의 단일 atomic 카운터다. 모든 연산자는 task를 push하기 전에
예약하고 (N만큼 감소시키고), release 시 다시 증가시킨다. 이
예약이 엔진 전반의 admission control 이다.
// worker_manager_global::try_reserve_workers — src/query/parallel/px_worker_manager_global.cppint worker_manager_global::try_reserve_workers (const int num_workers){ int requested = MIN (num_workers, PRM_MAX_PARALLELISM); const int min_degree = (requested == 1) ? 1 : 2; int available = m_available.load (); while (true) { if (available < min_degree) return 0; int reserved = (requested <= available) ? requested : available; if (m_available.compare_exchange_weak (available, available - reserved)) return reserved; std::this_thread::yield (); // CAS lost, retry }}여기에서 두 가지 설계 선택이 두드러진다. 첫째, CAS는 부분 예약 을 허용한다 — 8개를 요청했는데 5개만 남아 있다면 5를 반환하고, 연산자 측에서 5로 충분한지 또는 직렬로 fallback할지 결정한다. 둘째, 최소 차수가 2 다 — parallel execution (heap scan, hash join, sort) 의 경우 1 worker는 0과 같이 취급된다. main + 1 worker 는 직렬과 다를 바 없는데 오버헤드만 더하기 때문이다 — 하지만 parallel subquery (uncorrelated aptr 하나는 worker에서 돌고 main은 계속 진행) 의 경우 1 이다.
flowchart TB
subgraph "process-wide"
REG["REGISTER_WORKERPOOL(parallel_query, ...)"]
POOL["cubthread::worker_pool 'parallel-query'<br/>capacity = PRM_ID_MAX_PARALLEL_WORKERS"]
AVAIL["std::atomic<int> m_available"]
REG --> POOL --> AVAIL
end
subgraph "per-operator"
WMP1["worker_manager (heap scan)"]
WMP2["worker_manager (hash join)"]
WMP3["worker_manager (query exec)"]
WMP4["worker_manager (sort)"]
end
AVAIL -. "try_reserve_workers(N)" .-> WMP1
AVAIL -. " " .-> WMP2
AVAIL -. " " .-> WMP3
AVAIL -. " " .-> WMP4
WMP1 -. "release_workers" .-> AVAIL
WMP2 -. " " .-> AVAIL
WMP3 -. " " .-> AVAIL
WMP4 -. " " .-> AVAIL
POOL --> TASKQ["task 큐 (cubthread)"]
WMP1 -- "push_task" --> TASKQ
WMP2 -- " " --> TASKQ
WMP3 -- " " --> TASKQ
WMP4 -- " " --> TASKQ
TASKQ --> W1[worker 1]
TASKQ --> W2[worker 2]
TASKQ --> Wn[worker N]
compute_parallel_degree — 임계 페이지 수에 대한 log2
섹션 제목: “compute_parallel_degree — 임계 페이지 수에 대한 log2”DOP는 단일 함수 compute_parallel_degree (parallel_type type, UINT64 num_pages, int hint_degree) 가 결정한다. 네 개의
parallel_type 값 — HEAP_SCAN, HASH_JOIN, SORT, SUBQUERY
— 각각이 자기 페이지 수 임계값을 가진다. 임계는
PRM_ID_PARALLEL_HEAP_SCAN_PAGE_THRESHOLD,
PRM_ID_PARALLEL_HASH_JOIN_PAGE_THRESHOLD,
PRM_ID_PARALLEL_SORT_PAGE_THRESHOLD 에서 끌어 온다.
num_pages < page_threshold 이면 0을 반환해 비활성화한다.
그 외에는 다음과 같다.
// compute_parallel_degree — src/query/parallel/px_parallel.cppUINT64 x = num_pages / page_threshold;auto_degree = (63 - __builtin_clzll (x)) + start_degree; // log2(x) + 2return MIN (auto_degree, (UINT32) parallelism);공식은 log2(num_pages / threshold) + 2 이고, 전역 parallelism
파라미터와 머신의 코어 수로 위에서 잘린다. 임계 위로 입력이 두
배가 될 때마다 worker 한 명이 더 붙는다. 8 코어 머신에서 기본
임계값을 쓰면 페이지가 다하기 훨씬 전에 코어 수 근처에서 DOP가
포화한다. SUBQUERY 의 경우는 항상 1을 반환하는데, subquery
실행기는 aptr마다 N-way fan-out이 아니라 aptr마다 main + 1-
worker fan-out을 하기 때문이다. 힌트 처리는 — hint_degree == -1
→ auto-compute, >= 2 → system_core_count 로 잘라서 사용,
0 이나 1 → 비활성화.
worker_manager — 연산자별 예약 핸들
섹션 제목: “worker_manager — 연산자별 예약 핸들”전역 풀과 연산자 오케스트레이터 사이에 작은 RAII 핸들
(parallel_query::worker_manager) 이 끼어 있다.
std::atomic<int> m_active_tasks 와 int m_reserved_workers 를
들고 다닌다. 수명 주기는 — try_reserve_workers(N) 이 전역 CAS를
감싸고 (실패 시 nullptr, 연산자는 직렬로 fallback);
push_task 가 m_active_tasks 를 올리고 전역 풀에 push하며 (task
는 retire 시 pop_task() 를 불러야 한다); wait_workers 는
m_active_tasks 가 0이 될 때까지 busy-yield (std::this_thread:: yield()); release_workers 가 먼저 wait_workers 를 부르고 예약
한 worker를 전역 풀로 돌려준다. yield 루프는 의도적이다 — worker는
보통 여전히 유용한 일을 하고 있으므로 부모는 condvar에 막히면 안
된다.
공통 task 배관 — callable_task, interrupt, err_messages_with_lock
섹션 제목: “공통 task 배관 — callable_task, interrupt, err_messages_with_lock”callable_task 는 std::function<void(cubthread::entry &)> 를
cubthread::task<entry> 와 retire functor로 어댑팅한다.
대부분의 heap-scan 과 hash-join task는 cubthread::entry_task 를
직접 상속하지만, sort 모듈은 본체가 편리한 std::bind 표현이라서
callable_task 를 쓴다. retire 시 task는 pop_task() 로 부모
worker_manager 에 위임한 뒤 사용자 retire functor (보통 delete this) 를 실행한다.
interrupt 는 thread 사이에 왜 멈추는지 를 전파하는 단일
공유 atomic enum 이다. 일곱 상태 (NO_INTERRUPT,
USER_INTERRUPTED_FROM_MAIN_THREAD,
USER_INTERRUPTED_FROM_WORKER_THREAD,
ERROR_INTERRUPTED_FROM_MAIN_THREAD,
ERROR_INTERRUPTED_FROM_WORKER_THREAD, INST_NUM_SATISFIED,
JOB_ENDED) 가 방향 (수신자가 원격 er_message 를 swap-in 할지
그냥 break 할지 알 수 있도록) 과 이유 (사용자 vs 에러 vs limit
충족, 결과 핸들러가 S_END 를 반환할지 S_ERROR 를 반환할지 알
수 있도록) 를 모두 담는다. 오래 도는 worker 루프는 매 iteration
마다 한 번 m_interrupt->get_code() 를 검사한다.
err_messages_with_lock 은 cuberr::er_message 의 vector를
들고 다닌다. 에러를 만난 worker는 mutex 아래에서 자기 thread-local
에러 컨텍스트를 vector로 swap한다. main thread는 join 시 처음
나오는 non-ER_INTERRUPTED 메시지를 골라 자기 에러 컨텍스트로
다시 swap-in 한다. 짝꿍인 atomic_instnum 은 LIMIT N 을
처리한다 — N번째 튜플을 push한 worker가 동료들에게
INST_NUM_SATISFIED 인터럽트를 fan한다.
Parallel heap scan
섹션 제목: “Parallel heap scan”직렬 heap-scan iterator (HEAP_SCAN_ID 위의 scan_open_heap_scan
/ scan_next_scan) 가 병렬 버전 (PARALLEL_HEAP_SCAN_ID 위의
scan_open_parallel_heap_scan / scan_next_parallel_heap_scan)
으로 대체된다. 스캔 매니저는 표준 SCAN_TYPE switch로 dispatch
한다 — S_PARALLEL_HEAP_SCAN 이 S_HEAP_SCAN, S_INDX_SCAN 등과
나란한 가지 중 하나다.
Open: 결정과 예약
섹션 제목: “Open: 결정과 예약”scan_open_parallel_heap_scan 이 진입점이다. 옵티마이저는
TARGET_CLASS 의 access-spec에서 병렬이 그럴듯하다고 판단되면
ACCESS_SPEC_FLAG_NUM_PARALLEL_THREADS 를 세팅한다. 이 함수는
지금 병렬로 갈지를 결정한다. 먼저 scan_id->type = S_HEAP_SCAN
(낙관적 fallback) 으로 두고 다섯 개의 게이트를 닫는다 — (1) 시스템
클래스 — 너무 작고 내부에서 너무 자주 읽힘; (2) MVCC 비활성화
클래스 (catalog, transient) — 병렬 스캔은 worker별 가시성 검사가
직렬과 동일하다고 가정함; (3) select-lock-needed (serializable /
FOR UPDATE) — 병렬 스캔은 row lock을 전파하지 않음; (4)
private_heap_id == 0 으로 main-thread 가 아닌 스캔 배제 (중첩
병렬 금지); (5) HFID empty. 게이트 통과 후
file_get_num_user_pages 와 compute_parallel_degree (parallel_type::HEAP_SCAN, ...) 로 페이지 수 임계를 검사하고,
worker_manager::try_reserve_workers 로 전역 예약을 시도한다.
어느 단계에서 실패하면 scan_id->type 을 S_HEAP_SCAN 으로 둔
채 직렬 fallback. 성공하면 세 결과 타입 (MERGEABLE_LIST /
BUILDVALUE_OPT / XASL_SNAPSHOT) 중 하나를 골라서
manager<RESULT_TYPE> 을 placement-new로 짓고, 그 open() 을
부르고, scan_id->type = S_PARALLEL_HEAP_SCAN 으로 갈아끼운다.
주의 — xasl->topn_items 또는 XASL_TO_BE_CACHED 가 세팅되어
있으면 MERGEABLE_LIST 플래그는 해제된다. 이런 기능은 private-
heap shuttle 너머로 안정된 주소가 필요해서 worker 출력이 대신
XASL_SNAPSHOT 으로 가야 하기 때문이다.
세 결과 타입 이 세 가지 호출자 형태를 포착한다.
MERGEABLE_LIST 가 일반 케이스 — 각 worker가 QFILE_LIST_ID 를
쓰고 main이 읽으면서 merge한다. XASL_SNAPSHOT 은 row 단위 —
부모가 중간 list 파일 없이 한 번에 한 튜플씩 원할 때 사용 (예:
XASL_TO_BE_CACHED 또는 topn_items). BUILDVALUE_OPT 는 집계
fast path — 각 worker가 부분 집계를 만들고 main이 끝에서 부분들을
merge한다. 결과 타입은 PARALLEL_HEAP_SCAN_ID 안에 인코딩되어
있고 manager<RESULT_TYPE> / task<RESULT_TYPE> 클래스를
template-specialise 한다 (공개 표면은 open, start_tasks,
next, reset, merge_stats, end, close).
분할: ftab_set 으로 heap-file sector
섹션 제목: “분할: ftab_set 으로 heap-file sector”분할 단위는 heap-file sector 다. heap manager는 데이터 페이지를
소유하는 sector의 파일별 FILE_FTAB_COLLECTOR 를 유지한다.
input_handler_ftabs::init_on_main 이 main thread에서
file_get_all_data_sectors 를 부르고, 결과를 m_ftab_set 으로
복사하고 (ftab_set::convert 로), split (n_sets) 로 N개의 같은
크기 슬라이스로 자른다. 그 이후 페이지 수준 조정은 전적으로
worker별이다 — 공유 비트맵도 work-stealing도 없다.
각 worker는 initialize() 시 m_splited_ftab_set_idx 에
fetch_add 로 슬라이스 하나를 claim한다. worker별 상태는
input_handler_ftabs 의 정적 thread_local 멤버
(m_tl_scan_cache, m_tl_old_page_watcher, m_tl_ftab_set,
m_tl_vpid, m_tl_pgoffset, m_tl_ftab) 에 산다. 페이지-fix
watcher 패턴 (m_tl_old_page_watcher 와
m_tl_scan_cache->page_watcher 를 번갈아 쓰는 식) 이 buffer pool
의 ordered-fix 프로토콜로 페이지 단위 스캔을 구현한다 — 페이지는
다음 페이지가 fix된 뒤에야 unfix된다.
input_handler_ftabs::get_next_vpid_with_fix 이 worker hot path
다 — 다음 sector를 pop하고, 그 64-bit page_bitmap 을 걸어서
데이터 페이지를 찾고, 각 페이지를 OLD_PAGE_MAYBE_DEALLOCATED 로
ordered-fix 한다 (bitmap 빌드와 페이지 fix 사이에 deallocation과
경합하는 것은 예상되는 일이다 — 조용히 skip).
Tasks: worker별 XASL clone과 페이지별 slot iteration
섹션 제목: “Tasks: worker별 XASL clone과 페이지별 slot iteration”worker마다 자기 parallel_heap_scan::task<RESULT_TYPE> 를 worker
풀 위에서 돌린다. execute 는 세 단계다 — initialize (실패 시
에러 메시지를 m_err_messages 로 swap하고 interrupt_code:: ERROR_INTERRUPTED_FROM_WORKER_THREAD 를 세팅), loop, finalize.
initialize 는 부모의 connection / transaction 컨텍스트를
복사하고, XASL 트리를 클론하고 (캐시가 clonable XASL을 가지면
xcache_find_xasl_id_for_execute 로, 아니면
stx_map_stream_to_xasl 로), 클론한 spec 위에 worker별
HEAP_SCAN_ID 를 열고, 더 깊은 XASL 레벨이 필요로 하는 중첩 list /
index scan을 열고 (subquery, 위쪽 join 등), input handler의 sector
slice 와 result handler의 writer 상태를 세팅한다.
m_px_orig_thread_entry 는 부모 포인터로 세팅된다 — 엔진 전반의
이 thread는 병렬 worker인가? 후크다. worker 안에서 다르게
동작해야 하는 코드 (perfmon, log retry, lock-wait timeout) 가 이
값을 검사한다.
loop 의 외부 iteration 마다 — m_interrupt->get_code() 를 검사하고,
logtb_is_interrupted_tran 을 검사하고, input handler에 다음
VPID를 묻고, 성공하면 그 페이지의 모든 자격 있는 slot을 slot
iterator로 훑는다. 자격 있는 slot마다 if_pred 를 평가하고, XASL이
scan_ptr 를 가지면 (위쪽 join에 참여하는 list 또는 index scan
같은 중첩 레벨), 자격 있는 outer row마다
qexec_execute_scan_ptr 로 재귀한 뒤 result_handler_p->write(...)
를 부른다. 결과 타입 specialisation 이 m_xasl->outptr_list
(mergeable list / buildvalue) 와 m_xasl->val_list (xasl
snapshot) 사이에서 고른다. 매 row 후에는 clear_xasl_dptr_list
가 row별 동적 sub-XASL 상태를 청소한다. 에러는
ERROR_INTERRUPTED_FROM_WORKER_THREAD 를, 사용자 인터럽트는
USER_INTERRUPTED_FROM_WORKER_THREAD 를 세팅한다.
finalize 는 그 역순이다 — result handler를 write-finalise하고,
input handler와 slot iterator를 finalise하고, join 정보를 기록하고,
클론한 XASL 상태를 청소하고, XASL 클론을 retire (xcache_retire_clone)
하거나 unpacked 트리를 free한다.
읽기: main thread의 next()
섹션 제목: “읽기: main thread의 next()”worker가 fan-out 하는 동안, 부모의
scan_next_parallel_heap_scan 은 계속해서 manager->next() 를
부른다. 첫 호출은 lazy하게 worker를 띄우고 (start_tasks()), 뒤
이은 호출들은 result handler의 read() 에 위임한다.
mergeable-list는 m_xasl->list_id 에서 읽어 private-heap 경계
너머로 fetch_val_list 로 값을 다시 클론한다. xasl-snapshot은
m_xasl->val_list 로 곧장 읽고, buildvalue-opt 는
m_xasl->proc.buildvalue.agg_list 로 읽는다. 인터럽트 fan-in은
끝에서 — 어떤 worker가 에러를 raise했다면 그 er_message 를 main
thread의 컨텍스트로 swap하고 S_ERROR 를 반환한다.
MERGEABLE_LIST 경로가 가장 정교하다 — worker는 private-heap에
할당된 QFILE_LIST_ID 에 쓰지만 reader는 main thread의 private
heap으로 값을 돌려주어야 한다. shuttle은 두 개의
db_change_private_heap 경계 사이에서 일어나는 pr_clone_value
호출이다 — worker heap에서 클론 아웃, worker 측 정리, heap 스위치,
main 측으로 클론 인. 이것이 MERGEABLE_LIST 가 topn_items 또는
XASL_TO_BE_CACHED 와 결합될 수 없는 이유다 — 그 기능들은 경계
너머에서 안정된 주소를 요구한다.
sequenceDiagram
participant Main as Main thread<br/>(scan_next_scan)
participant Mgr as parallel_heap_scan::<br/>manager<result_type>
participant InH as input_handler_ftabs<br/>(thread_local sectors)
participant W1 as Worker 1
participant Wn as Worker N
participant ResH as result_handler
Main->>Mgr: scan_open_parallel_heap_scan
Mgr->>InH: init_on_main (sector 분할)
Mgr->>Mgr: try_reserve_workers (N)
Main->>Mgr: scan_next_parallel_heap_scan (1차)
Mgr->>Mgr: start_tasks() — N개 task push
par fan-out
Mgr-->>W1: push_task (XASL clone)
Mgr-->>Wn: push_task (XASL clone)
end
W1->>InH: 슬라이스 claim
Wn->>InH: 슬라이스 claim
loop VPID마다
W1->>InH: get_next_vpid_with_fix
W1->>W1: slot iterator + if_pred + write
Wn->>InH: get_next_vpid_with_fix
Wn->>Wn: slot iterator + if_pred + write
end
W1-->>ResH: writer_result_p / aggregate
Wn-->>ResH: writer_result_p / aggregate
Main->>ResH: read (merge)
Main->>Main: fetch_val_list (private-heap shuttle)
Main-->>Mgr: 호출별 SCAN_CODE
Main->>Mgr: scan_close — release_workers, free
Parallel hash join
섹션 제목: “Parallel hash join”병렬 hash join은 qexec_hash_join 안에 — 상태 switch의 새
HASHJOIN_STATUS_PARALLEL 가지에 — 자리 잡는다. 직렬 경로는 비
병렬 hash join (cubrid-hash-join.md 참조) 과 같다 — manager init,
empty-side 검사, 분할 결정, 그리고 single-pass classic build/probe
또는 partitioned build/probe. 병렬 가지는 분할이 선택되었고 and
파티션당 페이지 수가 충분히 클 때 직렬 partitioned build/probe를
worker fan-out 버전으로 대체한다.
hjoin_try_parallel — 게이트와 예약
섹션 제목: “hjoin_try_parallel — 게이트와 예약”hjoin_try_parallel 은 hjoin_try_partition 이 join에 분할이
필요하다고 결정한 다음에 돌아간다. min_page_cnt = min(outer_list_id->page_cnt, inner_list_id->page_cnt) 를 계산하고,
compute_parallel_degree(parallel_type::HASH_JOIN, min_page_cnt, manager->num_parallel_threads) 를 부르고, degree < 2 면
HASHJOIN_STATUS_PARTITION (직렬 fallback) 을 반환하고,
num_parallel_threads 를 manager->context_cnt 로 클램프한 뒤
(파티션보다 worker가 더 많을 이유는 없다)
worker_manager::try_reserve_workers 를 부른다. 성공하면 핸들을
manager->px_worker_manager 에 저장하고
HASHJOIN_STATUS_PARALLEL 을 반환한다. qexec_hash_join 의
dispatcher가 그 다음
parallel_query::hash_join::execute_partitions(*thread_p, &manager)
를 부른다.
build_partitions 와 execute_partitions
섹션 제목: “build_partitions 와 execute_partitions”build_partitions 가 phase 1 이다 — hjoin_init_shared_split_info
를 부르고, outer 와 inner 양쪽 를 qfile_collect_list_sector_info
로 데이터 sector를 열거하고, 연산자의 task_manager 위에 N개의
split_task 인스턴스를 push하고, task_manager.join() 으로 barrier
한다. 각 split_task 는 자기에게 할당된 입력 list 청크를 읽고,
join key로 hash해서 파티션별 출력 파일에 쓴다 (공유 membuf claim
으로 접근을 조정).
execute_partitions 가 phase 2 + 3 이다 — N개의 join_task 인스턴스
를 push하고 (각각 한 파티션의 inner 측을 인메모리 hash로 만들고
대응하는 outer 측 파티션을 probe), join하고, 그 다음 파티션별
list id를 hjoin_merge_qlist 로 merge한다.
split_task 와 join_task 모두 base_task : cubthread::entry_task
를 상속하고 연산자 수준 worker_manager 위에 push/wait/join을
감싸는 task_manager 를 공유한다. task_manager 의 push_task 는
m_worker_manager->push_task 를 부르기 전에 mutex 아래에서 active
-task counter를 올린다. end_task 는 그것을 내리고, pop_task 를
부르고, 0에 도달하면 m_all_tasks_done_cv.notify_all 을 보낸다.
join 은 condvar에서 기다렸다가 m_worker_manager->wait_workers()
를 부른다.
hash-join task_manager 가 heap-scan 패턴과 다른 점은 두 가지다 —
yield 루프 대신 condvar 대기를 쓴다 (heap-scan 부모와 달리 여기
서 부모는 대기 동안 할 일이 없기 때문이다 — heap-scan 부모는
worker write와 병렬로 결과를 읽는다); 그리고 명시적
handle_error 가 worker의 er_message 를 main thread의
cuberr::context 로 직접 swap한다 — err_messages_with_lock 의
중간 단계를 우회한다 — hash join은 단 하나의 에러만 표면화하면
되고 그것을 즉시 main thread에 두기를 원하기 때문이다.
task_execution_guard — thread 컨텍스트 fan-out 의 RAII
섹션 제목: “task_execution_guard — thread 컨텍스트 fan-out 의 RAII”모든 worker는 부모 thread를 흉내내는 것으로 시작한다.
task_execution_guard 의 생성자가 main thread의 entry에서
m_thread_ref.m_px_orig_thread_entry, conn_entry, tran_index,
on_trace 를 세팅하고 push_resource_tracks() 를 부른다.
소멸자는 conn_entry 와 on_trace 를 비우고
pop_resource_tracks() 를 부른다. 이게 없으면 worker마다 transaction
컨텍스트 없이 (access-method 계층의 모든 assertion이 터질 것이다)
또는 이전 task의 stale 컨텍스트로 돌게 된다.
spawn_manager — thread별 XASL 하부구조 클로닝
섹션 제목: “spawn_manager — thread별 XASL 하부구조 클로닝”hash join은 thread-local spawner 를 쓰는데, worker마다 join 시점
하부구조 (val_descr, during-join predicate, outer/inner regu
list) 를 on-demand 로 클론한다. get_* 호출 (get_val_descr,
get_during_join_pred, get_outer_regu_list_pred,
get_inner_regu_list_pred) 마다 lazy하게 worker의 db_private_alloc
heap에 클론한 하부구조를 할당-캐시하고, 다음 호출에는 캐시된
포인터를 반환한다. 이는 heap-scan task의 clone_xasl() 의 hash-join
대응물이지만 더 가는 단위에서 — worker가 실제로 필요로 하는
하부구조만 클론한다.
flowchart TB
QHJ["qexec_hash_join (status switch)"]
HTP["hjoin_try_parallel:<br/>compute_parallel_degree(HASH_JOIN)<br/>· try_reserve_workers"]
QHJ -->|HASHJOIN_STATUS_TRY| HTP
HTP -->|< 2 workers| Single[직렬: hjoin_execute]
HTP -->|>= 2 workers| EP[parallel_query::hash_join::execute_partitions]
QHJ -->|HASHJOIN_STATUS_PARTITION| BP1[hjoin_execute_partitions 직렬]
EP --> Phase1[build_partitions]
Phase1 --> SplitO["N x split_task (outer)"]
SplitO --> JoinO[task_manager::join]
JoinO --> SplitI["N x split_task (inner)"]
SplitI --> JoinI[task_manager::join]
JoinI --> Phase2[execute_partitions phase 2]
Phase2 --> JT["N x join_task (파티션별 build+probe)"]
JT --> JoinJ[task_manager::join]
JoinJ --> MergeR["파티션별 hjoin_merge_qlist"]
MergeR --> Done[xasl->list_id]
Parallel query execute
섹션 제목: “Parallel query execute”세 번째 오케스트레이터는 XASL 부분-트리 전체를 병렬로 돌린다.
표준적인 사용처는 일부 proc 타입의 aptr_list 안의 비상관
subquery 다 — 실행기가 그렇지 않으면 main 블록에 앞서 직렬로
prelude로 돌렸을 subquery들 (UNION_PROC 이 고전적인 예 — 각
가지가 독립적이다).
make_parallel_query_executor_recursively — main thread 에서의 wiring
섹션 제목: “make_parallel_query_executor_recursively — main thread 에서의 wiring”C 호출 가능 진입점은 qexec_execute_mainblock 이 호출되기 전에
병렬 구조를 wire-up 한다. !xcache_uses_clones() 면 early-out
한다 (parallel query execute는 clonable XASL을 요구한다).
thread_p->m_px_orig_thread_entry = thread_p 로 세팅하고,
선택적으로 perf-monitor 병렬 통계를 초기화하고, XASL 트리를
cubxasl::iterate_xasl_tree 로 두 번 걷는다. 첫 번째 walk는
parallel-eligible XASL 타입별로 비상관 aptr 노드를 센다. 두 번째
walk는 non-link aptr이 둘 이상인 모든 parallel-eligible XASL
노드에 query_executor 를 붙인다 — 그런 첫 번째 노드가 root
executor (m_is_root_executor == true, 큐와 worker 풀을 소유) 를
받고, 그 이후의 노드들은 root의 큐와 worker 풀을 공유하는 child
executor를 받는다.
parallel-eligible XASL 타입은 aptr_list 가 보통 독립 subquery를
담는 것들이다 — BUILDLIST_PROC / BUILDVALUE_PROC (list나 scalar
를 만드는 SELECT), UNION_PROC / INTERSECTION_PROC /
DIFFERENCE_PROC (독립 가지의 집합 연산), HASHJOIN_PROC (sub-
build 측이 그 자체로 독립일 수 있음), 그리고 MERGELIST_PROC
(파티션 프루닝 질의가 쓰는 list 파일의 parallel union-all). 따라서
질의 트리 전체가 단 하나의 작업 큐와 단 하나의 worker 집합을 공유
하고, 병렬성은 중첩된 parallel-eligible 연산자 사이에 amortise
된다.
query_executor::run_jobs — 단일 공유 큐와 main의 work-loop
섹션 제목: “query_executor::run_jobs — 단일 공유 큐와 main의 work-loop”run_jobs 는 전역 풀에 정확히 하나의 parallel-task가 있도록
보장하고 (첫 호출에서 단일 parallel_query_execute::task 를
lazy-spawn), main thread가 pre-pop된 첫 job을 돌리게 하고
(add_job 이 작은 fan-out에서 큐가 비지 않도록 m_job 으로
stash해 둔 것), m_join_context.get_running_jobs() == 0 이 될
때까지 큐에서 work-steal한다. join은 join_context::join_jobs 의
condvar 대기다. root에서는 join 후 함수가 sentinel을 push
(push_last()) 하고, worker를 release하고, 인터럽트 fan-in (heap
scan과 같은 패턴) 을 수행하고, perf 통계를 merge한다.
여기서 의미 있는 설계 선택이 — main thread도 worker로 참여한다
이다. worker 풀에 task를 하나 스케줄한 뒤 main이 큐에서 job을
pop해서 돌리기 시작한다. worker(들) 와 main이 try_pop 에서 경합
하고, job을 본 쪽이 그것을 돌린다. 이는 XASL 부분-트리 단위의
work-stealing 이고, worker 풀이 굶주릴 때조차 (worker가 없어
worker_manager 가 0을 반환할 때조차) main이 같은 코드 경로로
모든 job을 직렬로 돌리는 보너스를 가진다.
join_context 는 작은 condvar 기반 barrier 다 — add_running_jobs
/ sub_running_jobs 가 mutex 아래에서 increment / decrement (후자
는 0에 도달하면 알림); join_jobs 가 0이 될 때까지 대기. 실제
job은 execute_job_internal 이 돌리는데, XASL 상태를 클론하고,
worker thread의 컨텍스트 (conn_entry, tran_index, on_trace,
m_px_orig_thread_entry) 를 부모로 swap하고,
qexec_execute_mainblock 을 부르고, 부분 질의의 list_id 를
복사해 내고, 복원한다.
Job 큐: lock-free MPMC ring (thread_safe_queue)
섹션 제목: “Job 큐: lock-free MPMC ring (thread_safe_queue)”job 큐는 슬롯별 sequence 번호를 가진 교과서적인 bounded MPMC ring
이다. 각 슬롯은 data, std::atomic<uint64_t> sequence,
std::atomic<bool> ready 를 가진다. fast path (try_push_fast /
try_pop_fast) 는 표준적인 Vyukov 시퀀스를 실행한다 — position
load, 슬롯의 sequence를 CAS (pos → pos + m_capacity), data
write, ready = true release-store, position fetch_add. slow path
는 mutex를 잡고 condvar에서 공간 / 작업을 기다리며 iteration마다
인터럽트를 검사한다. parallel-query 한정 정련이 둘이다 —
push_completed 는 sticky shutdown 플래그, reset_queue 는 절대
position counter가 UINT64_MAX 에 가까워질 때 도는 wraparound
핸들러 (편집증적 가드 — 사실상 발화하지 않음). 더 깊은 배경은
cubrid-thread-worker-pool.md 를 보라.
sequenceDiagram
participant Main as Main thread
participant QE as query_executor
participant Q as thread_safe_queue<job>
participant Pool as parallel-query 풀
participant W as Worker
Main->>QE: make_parallel_query_executor_recursively
QE->>QE: XASL walk, px_executor 부착
Main->>QE: add_job (job_1) — m_job 으로 stash
Main->>QE: add_job (job_2..n)
QE->>Q: push job_2..n
Main->>QE: run_jobs()
QE->>Pool: push_task (query_task)
Pool->>W: dispatch
Main->>QE: execute_job_internal (m_job) /* main이 worker로 */
par main이 Q에서 worker와 경합
Main->>Q: try_pop -> job_k
Main->>QE: execute_job_internal (job_k)
W->>Q: pop -> job_m
W->>QE: execute_job_internal (job_m)
end
QE->>QE: join_context.join_jobs()
Main->>Pool: release_workers()
Main-->>QE: 통계 집계 + 인터럽트 fan-in
Parallel sort
섹션 제목: “Parallel sort”parallel sort는 넷 중 가장 오래된 것이다 — px_sort.h 의 매크로
DSL은 cubthread::entry_task / worker_manager 배관보다 먼저
있었기에 통합 방식이 parallel_query::callable_task 인스턴스를
직접 push하는 식이다. 패턴은 slice-and-merge — 입력 임시 파일을
N개의 sub-file로 쪼개고, worker 안에서 각각 정렬한 뒤, merge.
드라이버 매크로 둘은 SORT_EXECUTE_PARALLEL(num, px_sort_param, function) (각 &px_sort_param[i] 와 함께 function 에 bind된 N개
의 callable_task 를 할당해 sort_param->px_worker_manager 에
push) 와 SORT_WAIT_PARALLEL(parallel_num, sort_param, px_sort_param) (sort_param->px_mtx 아래에서 모든
px_sort_param[i].px_status 가 더 이상 PX_PROGRESS 가 아닐
때까지 condvar에서 polling하고, 그 후 wait_workers()) 이다.
DOP 게이트는 sort_check_parallelism 이 공유
compute_parallel_degree(parallel_type::SORT, ...) 를 써서 결정
한다. parallel-sort 호출자는 둘이다 — SORT_ORDER_BY (실행기의
ORDER BY 정렬, 임시 파일 위, 질의별 parallelism 힌트와 함께)
와 SORT_INDEX_LEAF (B+Tree 구축의 parallel index-leaf builder,
질의별 힌트 없음). 게이트가 통과하면 try_reserve_workers 를
부르고 실제 예약된 수를 반환한다. 어떤 실패에도 1을 반환해 호출자
가 single-thread 정렬로 fallback하게 한다.
상태 enum (PX_PROGRESS, PX_DONE, PX_ERR_FAILED) 은 parallel-
sort 한정의 interrupt_code 등가물이다. parallel sort의 독특한
부분은 merge phase 자체가 병렬 이라는 것이다 —
sort_merge_nruns_parallel 이 worker 풀에서 log2(N)-tournament 형
태의 merge task를 돌려 단일 정렬 출력만 남을 때까지 진행한다.
소스 코드 가이드
섹션 제목: “소스 코드 가이드”안정적인 심볼들 — 서브시스템별로 묶음. 절 끝의 position-hint
표는 가장 자주 인용되는 심볼을 (file, line) 으로 매핑하며, 행
수치는 updated: 시점 기준이다.
기반 — src/query/parallel/
parallel_query::compute_parallel_degree— DOP 휴리스틱.parallel_query::parallel_type—HEAP_SCAN,HASH_JOIN,SORT,SUBQUERY.parallel_query::worker_manager— 연산자별 핸들.try_reserve_workers,release_workers,wait_workers,push_task,pop_task,get_reserved_workers.parallel_query::worker_manager_global— Meyers 싱글턴.m_worker_pool(이름 붙은parallel-query풀) 과std::atomic<int> m_available을 소유.init,destroy, 내부try_reserve_workers,release_workers,push_task.parallel_query::interrupt— 7-상태 atomic enum.parallel_query::atomic_instnum—is_instnum_satisfies_after_1tuple_insert.parallel_query::err_messages_with_lock—move_top_error_message_to_this.parallel_query::callable_task—std::function/std::bind본체용cubthread::task<entry>어댑터 (sort 매크로가 사용).parallel_query::ftab_set—convert,split (n_sets),get_next,append,move_from,clear.parallel_query::thread_safe_queue<T>— Vyukov MPMC ring.push,pop,try_push,try_pop,push_last,is_empty,is_full,size,capacity,reset_queue.
Heap scan — src/query/parallel/px_heap_scan/
parallel_heap_scan::manager<RESULT_TYPE>— 스캔당 최상위 오케스트레이터.open,start_tasks,next,reset,merge_stats,end,close.RESULT_TYPE::{MERGEABLE_LIST, XASL_SNAPSHOT, BUILDVALUE_OPT}로 templated.parallel_heap_scan::task<RESULT_TYPE>—cubthread::entry_task본체.execute,retire,initialize,finalize,clone_xasl,loop.parallel_heap_scan::input_handler_ftabs— sector-slice dispatcher.init_on_main,initialize,finalize,get_next_vpid_with_fix. thread-local 페이지 watcher.parallel_heap_scan::result_handler<RESULT_TYPE>—read_initialize,read,read_finalize,write_initialize,write,write_finalize.parallel_heap_scan::slot_iterator—initialize,finalize,set_page,next_qualified_slot_with_peek.parallel_heap_scan::join_info—capture_join_info,record_join_info,apply_join_info,get_scan_info.parallel_heap_scan::trace_handler,accumulative_trace_storage— perf 카운터 집계.- C 진입점 —
scan_open_parallel_heap_scan,scan_start_parallel_heap_scan,scan_next_parallel_heap_scan,scan_reset_scan_block_parallel_heap_scan,scan_end_parallel_heap_scan,scan_close_parallel_heap_scan. 검사기 —scan_check_parallel_heap_scan_possible.
Hash join — src/query/parallel/px_hash_join/
parallel_query::hash_join::build_partitions— phase 1 (outer split, 그 다음 inner split).parallel_query::hash_join::execute_partitions— phase 2 + 3 (파티션별 build/probe, 그 다음 merge).parallel_query::hash_join::task_manager—push_task,end_task,join,handle_error,notify_stop,check_interrupt,clear_interrupt.parallel_query::hash_join::task_execution_guard— 컨텍스트 fan-out RAII.parallel_query::hash_join::base_task,split_task,join_task— task 계층.parallel_query::hash_join::spawn_manager— TLS 하부구조 cloner.get_val_descr,get_during_join_pred,get_outer_regu_list_pred,get_inner_regu_list_pred,get_instance,destroy_instance.query_hash_join.c의 C 측 —hjoin_try_parallel(게이트),qexec_hash_join의HASHJOIN_STATUS_PARALLEL가지,hjoin_init_shared_split_info,hjoin_clear_shared_split_info,hjoin_trace_*,hjoin_merge_qlist.
Query execute — src/query/parallel/px_query_execute/
parallel_query_execute::query_executor— 최상위 (root + child 생성자).add_job,run_jobs,get_parallelism,get_stats.parallel_query_execute::task—cubthread::entry_task본체.execute,retire,init,get_job,execute_job,end.parallel_query_execute::execute_job_internal— XASL 상태를 클론하고, thread 컨텍스트를 swap하고,qexec_execute_mainblock을 호출, list_id를 복사해 내고, 복원.parallel_query_execute::job—(xasl_node*, xasl_state*, join_context*, trace_context*)큐 페이로드.parallel_query_execute::join_context— 도는-job barrier (add_running_jobs,sub_running_jobs,get_running_jobs,join_jobs).parallel_query_execute::trace_context— job별 perf-stat vector.make_parallel_query_executor_recursively— 모든 parallel- eligible XASL 노드의xasl_p->px_executor를 wire.- 검사기 —
check_parallel_subquery_possible.
Sort — src/query/parallel/px_sort.{h,c} 와
src/storage/external_sort.c
- 매크로 —
SORT_IS_PARALLEL,SORT_EXECUTE_PARALLEL,SORT_WAIT_PARALLEL. - 상태 enum —
PX_ERR_FAILED,PX_DONE,PX_PROGRESS. 타입 enum —PARALLEL_TYPE::{PX_SINGLE, PX_MAIN_IN_PARALLEL, PX_THREAD_IN_PARALLEL}. - 함수 —
sort_check_parallelism,sort_start_parallelism,sort_end_parallelism,sort_listfile_execute,sort_copy_sort_param,sort_copy_sort_info,sort_split_input_temp_file,sort_merge_run_for_parallel,sort_merge_nruns,sort_put_result_for_parallel,sort_merge_nruns_parallel,sort_split_last_run,sort_put_result_from_tmpfile.
모듈 간 통합
src/query/scan_manager.c— 모든 공개 스캔 진입점이S_PARALLEL_HEAP_SCANswitch 가지가 있어px_heap_scan.cpp의 C 래퍼 (scan_start_scan,scan_reset_scan_block,scan_end_scan,scan_close_scan,scan_next_scan_local) 로 forward한다.src/query/query_hash_join.c—qexec_hash_join이parallel_query::hash_join::execute_partitions를 부르는HASHJOIN_STATUS_PARALLEL가지를 추가한다. 상태는hjoin_try_parallel이 결정한다.src/storage/external_sort.c—sort_listfile_internal이sort_check_parallelism을 부르고 ≥ 2 일 때 매크로로 dispatch.src/thread/thread_worker_pool*.{cpp,hpp}— 전역parallel- query풀이 인스턴스화하는worker_pool템플릿.- 사용된
THREAD_ENTRY필드 —m_px_orig_thread_entry(부모 포인터),m_uses_px_stats,m_px_stats(thread별 perf 버퍼),m_px_stats_mutex,m_px_lock_mutex(heap-scan task의 initialise/finalise 동안 클론된 XASL을 lock).
Position hints (updated: 기준)
섹션 제목: “Position hints (updated: 기준)”| 심볼 | 파일 | 라인 |
|---|---|---|
compute_parallel_degree | src/query/parallel/px_parallel.cpp | 36 |
worker_manager::try_reserve_workers | src/query/parallel/px_worker_manager.cpp | 49 |
worker_manager::wait_workers | src/query/parallel/px_worker_manager.cpp | 97 |
worker_manager_global::init | src/query/parallel/px_worker_manager_global.cpp | 53 |
worker_manager_global::try_reserve_workers | src/query/parallel/px_worker_manager_global.cpp | 97 |
REGISTER_WORKERPOOL(parallel_query) | src/query/parallel/px_worker_manager_global.cpp | 48 |
interrupt::interrupt_code | src/query/parallel/px_interrupt.hpp | 31 |
err_messages_with_lock::move_top_error_message_to_this | src/query/parallel/px_interrupt.hpp | 100 |
ftab_set::split | src/query/parallel/px_ftab_set.hpp | 106 |
thread_safe_queue<T>::try_push_fast | src/query/parallel/px_thread_safe_queue.cpp | 172 |
thread_safe_queue<T>::try_pop_fast | src/query/parallel/px_thread_safe_queue.cpp | 221 |
scan_open_parallel_heap_scan | px_heap_scan/px_heap_scan.cpp | 349 |
scan_next_parallel_heap_scan | px_heap_scan/px_heap_scan.cpp | 44 |
scan_close_parallel_heap_scan | px_heap_scan/px_heap_scan.cpp | 242 |
parallel_heap_scan::manager<>::open | px_heap_scan/px_heap_scan.cpp | 632 |
parallel_heap_scan::manager<>::start_tasks | px_heap_scan/px_heap_scan.cpp | 795 |
parallel_heap_scan::manager<>::next | px_heap_scan/px_heap_scan.cpp | 817 |
parallel_heap_scan::task<>::execute | px_heap_scan/px_heap_scan_task.cpp | 44 |
parallel_heap_scan::task<>::initialize | px_heap_scan/px_heap_scan_task.cpp | 71 |
parallel_heap_scan::task<>::clone_xasl | px_heap_scan/px_heap_scan_task.cpp | 439 |
parallel_heap_scan::task<>::loop | px_heap_scan/px_heap_scan_task.cpp | 510 |
input_handler_ftabs::init_on_main | px_heap_scan/px_heap_scan_input_handler_ftabs.cpp | 60 |
input_handler_ftabs::get_next_vpid_with_fix | px_heap_scan/px_heap_scan_input_handler_ftabs.cpp | 87 |
hash_join::build_partitions | px_hash_join/px_hash_join.cpp | 43 |
hash_join::execute_partitions | px_hash_join/px_hash_join.cpp | 157 |
hash_join::task_manager::push_task | px_hash_join/px_hash_join_task_manager.cpp | 58 |
hash_join::task_manager::join | px_hash_join/px_hash_join_task_manager.cpp | 81 |
hash_join::task_execution_guard | px_hash_join/px_hash_join_task_manager.hpp | 105 |
hash_join::spawn_manager::spawn | px_hash_join/px_hash_join_spawn_manager.hpp | 85 |
make_parallel_query_executor_recursively | px_query_execute/px_query_executor.cpp | 279 |
query_executor::run_jobs | px_query_execute/px_query_executor.cpp | 115 |
execute_job_internal | px_query_execute/px_query_task.cpp | 91 |
join_context::join_jobs | px_query_execute/px_query_job.hpp | 70 |
hjoin_try_parallel | src/query/query_hash_join.c | 1965 |
qexec_hash_join HASHJOIN_STATUS_PARALLEL 가지 | src/query/query_hash_join.c | 230 |
sort_check_parallelism | src/storage/external_sort.c | 4936 |
SORT_EXECUTE_PARALLEL / SORT_WAIT_PARALLEL | src/query/parallel/px_sort.h | 41, 52 |
소스 검증 노트
섹션 제목: “소스 검증 노트”parallel_heap_scan::manager는RESULT_TYPE으로 templated 되어 있지만 C API는 non-template 이고 런타임 dispatch는PARALLEL_HEAP_SCAN_ID의result_type필드로 한다. 네 번째 결과 타입을 추가하려면 다섯 곳을 손대야 한다 — enum,manager인스턴스화,task인스턴스화,scan_next_parallel_heap_scan/scan_reset_scan_block_parallel_heap_scan/scan_close_parallel_heap_scan의 런타임 switch, 그리고scan_open_parallel_heap_scan의 결과 타입 선택.- DOP 휴리스틱
auto_degree = log2(num_pages / page_threshold) + 2는compute_parallel_degree의 주석 외에는 문서화된 곳이 없다. Postgres 식 선형 ramp가 아니다. 매우 큰 테이블에서는num_pages가 다하기 훨씬 전에 DOP가parallelism에서 포화 한다 — 거대 스캔에는 보수적, 중간 스캔에는 공격적. scan_open_parallel_heap_scan안의 다섯 no parallel 게이트 (system class, MVCC-disabled, select-lock-needed,private_heap_id == 0, partitioned-class) 는 open 시 한 번만 검사된다. 주석의 “DB_PARTITION_CLASS will be parallel-heap- scanned, not DB_PARTITIONED_CLASS” 가 함의하는 단위는 부모가 아니라 leaf 파티션이다.make_parallel_query_executor_recursively가!xcache_uses_clones()에 early-out 한다는 사실 — parallel query execute는 clonable XASL을 요구한다. heap scan도 이득을 얻지만 그쪽clone_xasl은 두 가지 경로 (cache 와stx_map_stream_to_xasl를 통한 non-cache) 를 모두 가지므로 cache 없이도 동작한다. query executor는 그렇지 않다.- hash-join
task_manager는 condvar 대기를 쓰지만 heap-scanworker_manager는 yield 루프 대기를 쓴다. heap-scan main thread는 fan-out 동안 idle이 아니다 — worker write와 병렬로 결과를 읽고 있다 — 그래서 막힐 수 없다. hash-join phase 1 main thread는 진짜로 idle 이고 condvar의 이득을 본다. query- executor main thread는 worker로 참여하다가 큐를 비운 뒤에야join_context::join_jobs에서 대기한다. - 인터럽트 enum이 일곱 상태 (binary on/off 가 아닌) 인 이유는 병렬 계층이 방향 과 이유 둘 다를 필요로 하기 때문이다.
THREAD_ENTRY::m_px_lock_mutex는 특이하다 — thread별 mutex 인데 호출자가 다른 thread다 (main thread의m_px_lock_mutex는clone_xasl()과finalize()안에서 worker가 잠근다). 이는 XASL 클론 할당 / 회수를 main thread의 xasl-cache 작업을 직렬화한다. 부모 측에 둔 자식 thread 접근용 lock 이지, thread 자체용 self-lock 이 아니다.m_px_orig_thread_entry는 엔진 전반의 “이 thread는 병렬 worker인가?” 후크다. worker 안에서 다르게 동작해야 하는 코드 (perfmon, log-acquisition retry, lock-wait timeout) 가 이를 검사하고 부모의 컨텍스트를 사용한다. 가로지르는 새 기능 (예: 질의별 메모리 한도) 이 worker 경계마다 새 필드를 빼는 대신 이 같은 후크에 꽂아야 한다.
미해결 질문
섹션 제목: “미해결 질문”- 적응형 DOP.
compute_parallel_degree는 scan open / hash- join admission / sort start 에 한 번만 돌고, DOP는 그 이후 조정 될 수 없다. 질의 도중 경합이나 페이지 버퍼 압력이 치솟으면 엔진은 축소할 수 없다. Postgres에도 같은 한계가 있고, Oracle의 in-memory parallel execution 은 동적 re-sharding을 한다. - Parallel index scan. 카탈로그에
S_INDX_SCAN은 있지만 병렬 형제는 없다.S_PARALLEL_INDX_SCAN을 추가하려면 B+Tree leaf 레벨 분할과 공유 cursor 상태를 가진 worker별BTREE_SCAN클론이 필요하다. - heap scan 위 parallel aggregation.
BUILDVALUE_OPT의 부분들이 main thread의read()에서 merge된다 — 병렬이지만 연산자 수준은 아니다. cardinality가 높은GROUP BY에서는 merge가 병목이 된다. parallel merge (부분 집계의 tournament tree, 또는 hash-partitioned merge) 는 문헌이 알려진 승리지만 구현되어 있지 않다. - 중첩 병렬성. 오케스트레이터마다 같은 풀에서 예약하므로,
hash join이 또 병렬로 가고 싶어 하는 heap scan을 부르면 안쪽
호출이 고갈된 풀을 보고 조용히 직렬로 fallback한다.
private_heap_id == 0게이트가 명시적 가드다. 통제된 중첩 (예: 레벨별로 더 적은 worker 예약) 을 허용할지는 열린 선택이다. - 메모리 회계. 각 worker가 scratch (XASL 클론, 클론된
val_descr, 부분 집계) 에db_private_alloc을 쓰지만, 예산은 thread별이지 질의별이 아니다. 병렬도가 높은 질의는 자기 메모리 발자국을 N배로 늘린다. 오늘날 병렬 계층에는 질의별 메모리 한도 가 없다.
- 소스 파일 (
/data/hgryoo/references/cubrid/아래) —src/query/parallel/px_parallel.{hpp,cpp},px_worker_manager{,_global}.{hpp,cpp},px_callable_task.{hpp,cpp},px_thread_safe_queue.{hpp,cpp},px_interrupt.hpp,px_ftab_set.hpp,px_sort.{h,c},px_heap_scan/*.{hpp,cpp},px_hash_join/*.{hpp,cpp},px_query_execute/*.{hpp,cpp},src/thread/thread_worker_pool*.{hpp,cpp},src/query/scan_manager.c,src/query/query_hash_join.c,src/storage/external_sort.c. - 관련 큐레이션 문서 —
cubrid-scan-manager.md,cubrid-hash-join.md,cubrid-thread-worker-pool.md,cubrid-external-sort.md. - 이론 참고문헌 — Graefe, Encapsulation of Parallelism in the Volcano Query Processing System (SIGMOD 1990); Graefe, Query Evaluation Techniques for Large Databases (CSUR 1993) §3; Leis et al., Morsel-Driven Parallelism (SIGMOD 2014); Anatomy of a Database System (Red Book 4장) — shared build; Garcia-Molina/Ullman/Widom, Database Systems: The Complete Book §15.4 (2단계 parallel sort); Vyukov, Bounded MPMC queue.