Innovating today, leading tomorrow

OpenSQL_Internals
[OpenSQL] PostgreSQL의 Parallel Query (2)

[OpenSQL] PostgreSQL의 Parallel Query (2)

Reminder

PostgreSQL의 Parallel Query에 대해 이어서 이야기 하기에 앞서 지난 챕터에서 이야기 했던 내용을 요약해보겠습니다.

Parallel Query 실행 계획에는 Gather 또는 Gather Merge 노드가 있고, 이 노드에는 항상 한 개의 자식 실행 계획이 있습니다. 메인 질의를 수행하던 프로세스(Leader Worker)가 Gather 또는 Gather Merge 노드를 만나면 실행 계획에서 제공한 Parallel Worker 개수만큼 Background Process 할당을 요청하게 됩니다. 할당된 Parallel Worker는 Gather의 자식 실행 계획을 받아서 병렬로 수행하게 됩니다.

Parallel Query 실행 계획을 만들 때에는 Parallel Safety 규칙을 준수합니다. Parallel Query 실행 계획은 Parallel Safe 또는 Parallel Restricted인 노드로만 구성되어야 하며, Gather의 자식 실행 계획은 Parallel Safe인 노드로만 구성되어야 합니다. CTE나 DML과 같은 Parallel Unsafe한 노드가 필요한 Path는 Parallel Query 실행 계획으로 만들어지지 않습니다.

더 자세한 내용은 지난 챕터를 참고하시기 바랍니다.

Overview

이번 챕터에서는 Parallel Query의 실제 수행 과정에 대해서 이야기하려고 합니다. Leader Worker가 수행하는 Gather 노드에서 어떤 일이 발생하는지 중점적으로 말씀 드리겠습니다.

Shared Context for Parallel Query

혹자는 질의를 병렬로 처리하기 위해서는 수행할 실행 계획을 Parallel Worker에게 공유하고 Leader Worker가 결과물을 공유 받을 수 있는 방법만 마련된다면 될 것이라고 생각할 수 있습니다. 하지만 실상은 다릅니다. PostgreSQL의 Parallel Worker는 전달 받은 실행 계획을 Executor Framework에 그대로 수행 시키도록 설계되어 있으며, Executor가 실행되기 위해서 기본적으로 필요로 하는 정보들을 Leader Worker가 Parallel Worker에게 전달해야만 합니다.

예를 들어 EXPLAIN ANALYZE 문을 사용하여 수행한 질의의 런타임 통계 정보를 알고 싶다고 한다면, Parallel Worker는 각자 프로세스에서 실행 계획을 수행하며 수집한 통계 정보를 Leader Worker에게 공유하고, Leader Worker는 여러 프로세스의 통계 정보를 집계하여 보여줘야 합니다.

다른 예를 들어보면, 질의에 사용되는 바인드 변수(Bind Variable)은 Executor의 런타임 최상위 컨텍스트인 QueryDesc에 포함되어 있는데, 바인드 변수를 Gather의 자식 실행 계획에서도 사용하고 있다면 Parallel Worker에게 바인드 변수를 공유해야 정상적으로 실행 계획을 수행할 수 있게 됩니다.

이렇듯 질의를 수행하기 위해서 필요로 하는 정보는 다양합니다. PostgreSQL에서는 Leader Worker와 Parallel Worker 사이에 공유해야 하는 정보를 공유 메모리를 통해 공유하여 해결합니다. Leader Worker는 동적 공유 메모리(Dynamic Shared Memory) 세그먼트 할당을 요청하고 해당 세그먼트에 공유 컨텍스트를 직렬화하여 복사합니다.

Leader Worker가 공유하는 공유 컨텍스트는 아래와 같습니다.

Shared Context EntryShared Memory Lookup KeyDescription
Sub-planPARALLEL_KEY_PLANNEDSTMTParallel Worker가 수행해야 하는 자식 실행 계획. 트리 형태의 실행 계획을 직렬화 하여 공유함.
Bind Parameter ListPARALLEL_KEY_PARAMLISTINFO질의에 포함된 바인드 변수 정보.
InstrumentationPARALLEL_KEY_INSTRUMENTATIONExecutor 런타임 통계 정보. 각 Parallel Worker가 수집하고, Leader Worker가 집계함.
Buffer UsagePARALLEL_KEY_BUFFER_USAGEParallel Worker가 사용한 Buffer Cache 사용량 정보.
WAL UsagePARALLEL_KEY_WAL_USAGEParallel Worker가 사용한 WAL 사용량 정보.
JIT InstrumentationPARALLEL_KEY_JIT_INSTRUMENTATIONJIT Compilation 관련 통계 정보.
Tuple QueuePARALLEL_KEY_TUPLE_QUEUEParallel Worker가 Leader Worker에게 결과물을 전달하는 Message Queue.
Query TextPARALLEL_KEY_QUERY_TEXT수행 중인 질의
DSAPARALLEL_KEY_DSALeader Worker와 Parallel Worker가 Executor 수행 중에 공유할 가변 길이의 정보를 공유 메모리에 할당할 수 있도록 마련해 놓은 동적 메모리 할당 공간
Fixed Parallel Executor StatePARALLEL_KEY_EXECUTOR_FIXED기타 Executor 상태 정보
Parallel Aware Plan Node’s Context각 Plan Node의 ID실행 계획에 포함된 Parallel Aware 한 노드가 공유해야 하는 컨텍스트. 키는 자신의 Plan Node ID로 등록함.

위와 같은 High-level의 정보 외에도 각 Parallel Worker가 질의 수행에 필요한 자잘하고 다양한 정보도 있습니다. 해당 정보들은 이 포스팅에선 나열 하지는 않겠지만, 궁금하신 독자는 링크를 따라가 보시기 바랍니다.

Leader Worker가 공유 메모리에 공유 컨텍스트 엔트리를 할당한 후 Parallel Worker가 해당 엔트리를 접근하기 위해서는 각 엔트리가 할당된 메모리 주소를 알아야 합니다. PostgreSQL에서는 다른 프로세스가 공유 메모리에 할당된 엔트리를 찾을 수 있도록 Lookup 키(Key)를 Table of Content(TOC)에 같이 등록해줍니다. Leader Worker가 엔트리를 할당하면서 64-bit 정수로 된 키를 등록해주고, Parallel Worker는 Lookup 함수에 필요로 하는 엔트리의 키를 주입하여 엔트리의 위치를 받아오는 방식입니다 (C++ Boost Library가 익숙한 개발자는 **boost**::**interprocess::managed_shared_memory** 라이브러리를 떠올릴 것입니다). 아래 예제 코드를 보면 이해가 더 쉽습니다.

/* Parallel Worker와 커뮤니케이션을 위한 Magic Number */
#define PARALLEL_KEY_EXECUTOR_FIXED UINT64CONST(0xE000000000000001)
#define PARALLEL_KEY_PLANNEDSTMT UINT64CONST(0xE000000000000002)
#define PARALLEL_KEY_PARAMLISTINFO UINT64CONST(0xE000000000000003)
#define PARALLEL_KEY_BUFFER_USAGE UINT64CONST(0xE000000000000004)
#define PARALLEL_KEY_TUPLE_QUEUE UINT64CONST(0xE000000000000005)
#define PARALLEL_KEY_INSTRUMENTATION UINT64CONST(0xE000000000000006)
#define PARALLEL_KEY_DSA UINT64CONST(0xE000000000000007)
#define PARALLEL_KEY_QUERY_TEXT UINT64CONST(0xE000000000000008)
#define PARALLEL_KEY_JIT_INSTRUMENTATION UINT64CONST(0xE000000000000009)
#define PARALLEL_KEY_WAL_USAGE UINT64CONST(0xE00000000000000A)

/* Buffer Usage를 공유 메모리에 할당하며 key 등록 */
bufusage_space = shm_toc_allocate(pcxt->toc,
mul_size(sizeof(BufferUsage), pcxt->nworkers));
shm_toc_insert(pcxt->toc, PARALLEL_KEY_BUFFER_USAGE, bufusage_space);
pei->buffer_usage = bufusage_space;

/* WAL Usage도 마찬가지로 key 등록 */
walusage_space = shm_toc_allocate(pcxt->toc,
mul_size(sizeof(WalUsage), pcxt->nworkers));
shm_toc_insert(pcxt->toc, PARALLEL_KEY_WAL_USAGE, walusage_space);
pei->wal_usage = walusage_space;

backend/executor/execParallel.c

DSM 공간은 동적으로 메모리를 할당하기에는 적합하지 않은 구조로 되어 있습니다. 필요한 메모리 공간 사이즈를 미리 구하고 그에 맞춰 DSM 세그먼트를 할당합니다. 이후 DSM 세그먼트에 엔트리를 할당할 때에는 세그먼트 내 버퍼의 오프셋 값만 조정하며 메모리 주소를 반환하는 방식으로 할당합니다. 따라서 실행 계획의 PlannedStmt와 같이 트리 구조로 되어 있는 엔트리를 일반적인 Heap 메모리 공간에 할당하듯이 DSM 공간을 사용하려고 하면, 트리 노드를 순회하기 위한 포인터 값들을 매번 교체해줘야 하는 어려움이 생깁니다. 이런 어려움을 방지하기 위해 PostgreSQL에서는 트리를 문자열 타입으로 직렬화 하여 DSM 공간에 공유합니다. 이렇게 직렬화 한 트리를 Parallel Worker는 역직렬화 하여 자신의 프로세스 메모리 공간에 다시 트리 형태로 복원하는 작업을 거쳐 실행 계획을 수행합니다.

또 하나 자세히 봐야 할 것은 Tuple Queue입니다. Leader Worker는 Parallel Worker의 결과물을 받아 상위 실행 계획을 수행합니다. 이 때 Parallel Worker에게 결과물을 받을 Message Queue를 각 Parallel Worker 마다 생성합니다. Leader Worker는 자신의 프로세스 메모리에 Message Queue 버퍼를 참조하는 Message Queue 핸들을 할당합니다. Message Queue 버퍼는 DSM 공간에 Parallel Worker 개수를 곱하여 연속된 공간을 할당하고, 각 Message Queue 핸들에 버퍼의 오프셋을 옮겨가며 참조를 만들어 놓습니다. Parallel Worker는 Executor에서 Client에 결과물을 전달할 때 사용하는 구조체인 DestReceiver 를 자신의 프로세스 메모리에 할당하고 DSM 공간에 할당된 자신의 Message Queue 버퍼를 참조하게 설정하여 결과물을 공유합니다.

Launching Parallel Workers

Parallel Worker와 공유할 정보를 DSM 세그먼트에 할당했으면 Parallel Worker를 시작할 준비가 되었습니다. Parallel Worker는 Background Process로 실행되며 일반적으로 PostgreSQL에서 Background Process를 실행시키는 방법을 동일하게 사용합니다. Leader Worker가 Background Process를 실행하는 방법은 아래 스텝을 따라갑니다.

  1. Background Process의 상태 정보를 담은 슬롯에 접근합니다.
    1. 각 슬롯에는 Background Process에 대한 다양한 정보가 있는데, 현재 소개하는 로직에서는 아래 필드만 중요하게 보면 됩니다.
      • in_use: Background Process가 현재 사용 중인지 확인할 수 있는 플래그
      • bgw_function_name: Background Process가 호출할 Entrypoint 함수 이름
      • pid: Background Process의 프로세스 아이디
  2. Leader Worker는 슬롯을 순회하며 in_use 플래그가 false 값인 공간을 찾습니다.
  3. 찾은 슬롯의 필드를 설정합니다.
    1. bgw_function_name = ParallelWorkerMain
    2. pid = InvalidPID (PostMaster가 시작되지 않은 Background Process를 찾기 위한 방법)
    3. in_use = true
  4. PostMaster에게 PMSIGNAL_BACKGROUND_WORKER_CHANGE 시그널을 보내 Background Process의 상태가 변경되었음을 알려줍니다.
  5. PostMaster는 Background Process 슬롯을 순회하며 시작되지 않은 Process를 찾아 실행시켜 줍니다.

Optimizer가 생성한 실행 계획에 명시된 Parallel Worker 수만큼 Background Process를 할당시키는 것이 가능하다면 위 방식 대로 문제 없이 수행되지만 동시에 너무 많은 Parallel Worker를 할당하게 수용했다가는 시스템에서 사용할 수 있는 컴퓨팅 자원을 초과하는 일이 발생하여 문제가 일어날 수 있습니다.

이런 상황을 우려하여 PostgreSQL에서는 max_parallel_worker 파라미터를 마련했습니다. Leader Worker가 Parallel Worker를 할당하러 함수에 진입했을 때 현재 생성한 Parallel Worker 개수가 max_parallel_worker를 초과한다면 더 이상 할당을 시도하지 않고 할당한 개수에 만족하며 수행을 이어갑니다. 만약 Parallel Worker를 할당하지 못 했을 경우 Leader Worker가 혼자서 자식 실행 계획을 수행할 수 있도록 메커니즘이 마련되어 있습니다. 하지만 이런 경우 Optimizer가 병렬 처리에 적합하게 생성한 실행 계획을 Leader Worker 혼자서 수행하게 되기 때문에 때에 따라 상당히 큰 비효율이 발생할 수 있습니다.

Receiving Tuples

마지막으로 Leader Worker가 Parallel Worker로부터 결과물을 받아오는 과정에 대해 설명하겠습니다. 앞선 설명에서 DSM 세그먼트에 Leader Worker가 Parallel Worker 개수만큼 Tuple Queue를 할당한다고 말씀 드렸습니다. Parallel Worker는 DSM 세그먼트에서 자신에게 할당된 Tuple Queue를 받아와 자신의 Executor Frame과 결합합니다. 자식 실행 계획을 수행하면서 발생한 결과물을 결합한 Tuple Queue에 삽입하면 Leader Worker가 결과물을 받아가는 방식으로 수행됩니다.

Leader Worker의 입장에서는 여러 Parallel Worker가 결과물을 만들어내어 Tuple Queue를 통해 전달하게 되는데 Gather 노드의 경우 순서와 상관 없이 라운드 로빈 방식으로 결과물을 꺼내어 상위 실행 계획으로 옮깁니다.

여기에 숨겨진 최적화 포인트가 있습니다. Leader Worker는 라운드 로빈 방식으로 Tuple Queue를 순회하되 Parallel Worker가 결과물을 만들어내기 까지 기다리지는 않습니다. 예를 들어, 총 4개의 Parallel Worker가 결과물을 만들어내고 있을 때 Leader Worker는 0번 Queue부터 순회를 하며 결과물을 받아가려고 합니다. 만약 0번 Queue에 결과물이 공유되지 않았다면 Leader Worker는 기다리지 않고 다음 Queue로 가서 결과물을 받아옵니다.

Tuple을 생성한 Tuple Queue를 찾아 결과물을 받아갑니다

만약 1번 Queue에 결과물이 있어서 해당 튜플을 상위 실행 계획으로 전달하고 다시 돌아오게 되면 다시 0번 Queue부터 순회하는 것이 아니라 1번 Queue부터 결과물이 있는지 확인합니다. 방문했던 Tuple Queue에 더 이상 Tuple이 제 시간에 나오지 않을 때까지 이를 반복하여 Tuple을 받아갑니다.이렇게 로직이 구성되어 있는 이유는 제일 빠른 속도로 작업을 수행하는 Parallel Worker 기준으로 결과물을 처리하면서 다른 Parallel Worker에게 마저 작업을 수행할 수 있는 기회를 줄 수 있게 하여 효율성을 높일 수 있기 때문입니다.

다시 돌아왔을 때는 방문했던 Tuple Queue에서 Tuple이 소진될 때까지 받아옵니다. 다른 Parallel Worker가 마저 실행 계획을 수행하여 Tuple을 생성할 수 있도록 기다려주는 효과를 가져옵니다.

만약 Queue를 모두 순회했지만 모든 Parallel Worker가 결과물을 전달하지 않았다면 Leader Worker에게는 두 가지 선택이 주어집니다. 첫 번째는 Leader Worker 본인이 직접 자식 실행 계획을 수행하여 결과물을 만들어 내는 것입니다. 두 번째는 Parallel Worker 중 하나라도 먼저 끝내는 Worker가 생길 때까지 대기하는 방법입니다. 첫 번째 선택은 parallel_leader_participation 파라미터가 켜져 있는 경우 활성화되고, 두 번째 선택은 파라미터가 꺼져 있을 경우 활성화 됩니다.

Conclusion

오늘 소개한 Parallel Query의 실행 부분을 요약하면 아래와 같습니다.

  • Parallel Query에 필요한 공용 정보를 공유하기 위해 Leader Worker는 공유 메모리(DSM) 세그먼트를 할당하고 각종 정보를 공유 메모리에 올립니다.
  • Leader Worker가 Parallel Worker를 실행시키기 위해 Background Worker Slot을 순회하여 가용 프로세스를 찾습니다. 가용 프로세스를 찾으면 실행할 Entrypoint 함수를 설정해주고 PostMaster에게 시그널을 보내 Background Worker가 실행되도록 합니다.
  • Background Process가 생성한 튜플을 받기 위해 메시지 큐를 순회합니다. 이 때 모든 메시지 큐에 생성된 튜플이 없는 경우, parallel_leader_participation 파라미터가 켜져 있으면 Leader Worker가 직접 자식 실행 계획을 수행하여 튜플을 생성합니다.

다음 Parallel Query 시리즈에서는 Parallel Worker가 어떻게 데이터를 분배하여 스캔 하는지, 그리고 조인과 집계함수와 같은 연산은 병렬로 어떻게 처리되는지 알아보도록 하고 오늘 포스팅은 이만 마치도록 하겠습니다.

지금까지 PostgreSQL의 Parallel Query (2)에 관해 알아보았습니다

PostgreSQL을 쉽게 사용할 수 있도록 유용한 팁을 가지고 돌아오겠습니다.

다음 컨텐츠도 기대해주세요! (컨텐츠는 격주 업데이트 됩니다.)

광고성 정보 수신

개인정보 수집, 활용 목적 및 기간

(주)티맥스티베로의 개인정보 수집 및 이용 목적은 다음과 같습니다.
내용을 자세히 읽어보신 후 동의 여부를 결정해 주시기 바랍니다.

  • 수집 목적: 티맥스티베로 뉴스레터 발송 및 고객 관리
  • 수집 항목: 성함, 회사명, 회사 이메일, 연락처, 부서명, 직급, 산업, 담당업무, 관계사 여부, 방문 경로
  • 보유 및 이용 기간: 동의 철회 시까지

※ 위 개인정보 수집 및 이용에 대한 동의를 거부할 권리가 있습니다.
※ 필수 수집 항목에 대한 동의를 거부하는 경우 뉴스레터 구독이 제한될 수 있습니다.

개인정보의 처리 위탁 정보
  • 업체명: 스티비 주식회사
  • 위탁 업무 목적 및 범위: 광고가 포함된 뉴스레터 발송 및 수신자 관리
 

개인정보 수집 및 이용

개인정보 수집, 활용 목적 및 기간

(주)티맥스티베로의 개인정보 수집 및 이용 목적은 다음과 같습니다. 내용을 자세히 읽어보신 후 동의 여부를 결정해 주시기 바랍니다.

  • 수집 목적: 티맥스티베로 뉴스레터 발송 및 고객 관리
  • 수집 항목: 성함, 회사명, 회사 이메일, 연락처, 부서명, 직급, 산업, 담당업무, 관계사 여부, 방문 경로
  • 보유 및 이용 기간: 동의 철회 시까지

※ 위 개인정보 수집 및 이용에 대한 동의를 거부할 권리가 있습니다.
※ 필수 수집 항목에 대한 동의를 거부하는 경우 뉴스레터 구독이 제한될 수 있습니다.

개인정보의 처리 위탁 정보

  • 업체명: 스티비 주식회사
  • 위탁 업무 목적 및 범위: 광고가 포함된 뉴스레터 발송 및 수신자 관리
  •