PostgreSQL并行查询中的动态共享内存机制
本文适用于PostgreSQL 14。
一、概述
PosgreSQL 9.6开始支持并行查询(Parallel Query)[1],这个版本的对并行的支持还非常有限,之后的发行版本不断进行了完善。不同于多线程模式,PostgreSQL采用多进程模式,并行查询将把一条查询任务拆分给多个 worker 分布执行。因此,需要解决不同进程之间数据共享问题。为此,PostgreSQL引入了动态共享内存(DSM,Dynamic Shared Memory)机制。
二、动态共享内存
共享内存
守护进程 PostMaster 初始化阶段初始化共享内存信息,代码主要集中在 CreateSharedMemoryAndSemaphores 函数,代码片段:
- snippet.c
void CreateSharedMemoryAndSemaphores(void) { PGShmemHeader *shim = NULL; if (!IsUnderPostmaster) { Size size; size = 100000; size = add_size(size, PGSemaphoreShmemSize(numSemas)); size = add_size(size, SpinlockSemaSize()); size = add_size(size, hash_estimate_size(SHMEM_INDEX_SIZE, sizeof(ShmemIndexEnt))); size = add_size(size, dsm_estimate_size()); …… /* * Create the shmem segment */ seghdr = PGSharedMemoryCreate(size, &shim); } …… /* Initialize dynamic shared memory facilities. */ if (!IsUnderPostmaster) dsm_postmaster_startup(shim); }
- PGSharedMemoryCreate:创建一个初始大小的共享内存,并初始化header(PGShmemHeader),size是预估的初始共享内存大小。
- dsm_postmaster_startup:启动DSM。将初始化DSM段控制器 dsm_control。后续使用会创建多个DSM段,DSM段将由 dsm_control 进行维护。
DSM段
所有DSM段由 dsm_control 维护,数据结构如下:
- snippet.c
/* Shared-memory state for a dynamic shared memory segment. */ typedef struct dsm_control_item { dsm_handle handle; uint32 refcnt; /* 2+ = active, 1 = moribund, 0 = gone */ void *impl_private_pm_handle; /* only needed on Windows */ bool pinned; } dsm_control_item; /* Layout of the dynamic shared memory control segment. */ typedef struct dsm_control_header { uint32 magic; uint32 nitems; uint32 maxitems; dsm_control_item item[FLEXIBLE_ARRAY_MEMBER]; } dsm_control_header; static dsm_control_header *dsm_control;
- dsm_control_header::item:是变长数组,元素为DSM段信息。
DSM段的TOC
一个DSM段可能会被分有很多内存片(chunk),这些 chunk 使用“目录(TOC,Table of Contents)”进行检索和管理。
- snippet.c
typedef struct shm_toc_entry { uint64 key; /* Arbitrary identifier */ Size offset; /* Offset, in bytes, from TOC start */ } shm_toc_entry; struct shm_toc { uint64 toc_magic; /* Magic number identifying this TOC */ slock_t toc_mutex; /* Spinlock for mutual exclusion */ Size toc_total_bytes; /* Bytes managed by this TOC */ Size toc_allocated_bytes; /* Bytes allocated of those managed */ uint32 toc_nentry; /* Number of entries in TOC */ shm_toc_entry toc_entry[FLEXIBLE_ARRAY_MEMBER]; };
并行执行器的DSM段
关注 InitializeParallelDSM 这个函数,在算子需要并行执行时,首先会为并行上下文初始化一个DSM段。
InitializeParallelDSM 创建和使用DSM过程:
- shm_toc_estimate_chunk:使用这个函数评估将要使用的对象内存 chunk 大小,每个将要分配内存 chunk 都需要调用 shm_toc_estimate_chunk 进行评估,累计在 shm_toc_estimator::space_for_chunks 成员变量上。
- snippet.c
/* * Tools for estimating how large a chunk of shared memory will be needed * to store a TOC and its dependent objects. Note: we don't really support * large numbers of keys, but it's convenient to declare number_of_keys * as a Size anyway. */ typedef struct { Size space_for_chunks; Size number_of_keys; } shm_toc_estimator;
- shm_toc_estimate_keys:为内存 chunk 设置 key,可以通过 key 在DSM段的TOC中检索内存 chunk。key 的总个数累计在 shm_toc_estimator::number_of_keys 成员变量上。
- shm_toc_estimate:根据以上累计的大小,评估DSM段大小,设为 segsize。
- dsm_create:分配 segsize 大小的共享内存段并且 mmap 。
- shm_toc_allocate:分配一个空闲地址用于保存 chunk 内容,相对于 malloc ,不同的是,该函数通过计算内存偏移地址方式,从步骤4的DSM段里查找一个空闲内存位置。返回地址值。之后,我们就可以对这段内存进行写操作了。
- shm_toc_insert:步骤5中分配了空闲地址,但是还没有登记,本函数在段的TOC中登记,即,在目录中追加一条记录。
DSM段内存布局
并行查询的DSM段TOC保存在 ParallelContext 中:
- snippet.c
typedef struct ParallelContext { shm_toc_estimator estimator; dsm_segment *seg; void *private_memory; shm_toc *toc; …… } ParallelContext;
ParallelContext::toc 实际就是 dsm_create 函数内部 mmap 的共享内存首地址,因此,shm_toc内存布局实际就是DSM段的内存布局。如下图所示,chunk 内存使用在DSM段中从后向前分配,而TOC项是从前向后分配内存。
- shm_toc_entry::offset 指示 chunk 在段中的偏移地址,相对于 ParallelContext::toc 的偏移。
- shm_toc::toc_allocated_bytes 记录所有 chunk 大小之和。
- 当 chunk 空间与 toc 空间即将交叠时,表明当前的DSM段内存使用完,禁止继续分配。
- 可以参考 shm_toc_allocate 和 shm_toc_insert 两个函数,分析DSM段内存使用过程。
三、参考
打赏作者以资鼓励: