本文适用于PostgreSQL 14。 # 一、概述 PosgreSQL 9.6开始支持并行查询(Parallel Query)[1],这个版本的对并行的支持还非常有限,之后的发行版本不断进行了完善。不同于多线程模式,PostgreSQL采用多进程模式,并行查询将把一条查询任务拆分给多个 worker 分布执行。因此,需要解决不同进程之间数据共享问题。为此,PostgreSQL引入了动态共享内存(DSM,Dynamic Shared Memory)机制。 # 二、动态共享内存 ## 共享内存 守护进程 PostMaster 初始化阶段初始化共享内存信息,代码主要集中在 CreateSharedMemoryAndSemaphores 函数,代码片段: ```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 维护,数据结构如下: ```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)”进行检索和管理。 ```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过程: 1. shm\_toc\_estimate\_chunk:使用这个函数评估将要使用的对象内存 chunk 大小,每个将要分配内存 chunk 都需要调用 shm\_toc\_estimate\_chunk 进行评估,累计在 shm\_toc\_estimator::space\_for\_chunks 成员变量上。 ```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; ``` 2. shm\_toc\_estimate\_keys:为内存 chunk 设置 key,可以通过 key 在DSM段的TOC中检索内存 chunk。key 的总个数累计在 shm\_toc\_estimator::number\_of\_keys 成员变量上。 3. shm\_toc\_estimate:根据以上累计的大小,评估DSM段大小,设为 segsize。 4. dsm\_create:分配 segsize 大小的共享内存段并且 mmap 。 5. shm\_toc\_allocate:分配一个空闲地址用于保存 chunk 内容,相对于 malloc ,不同的是,该函数通过计算内存偏移地址方式,从步骤4的DSM段里查找一个空闲内存位置。返回地址值。之后,我们就可以对这段内存进行写操作了。 6. shm\_toc\_insert:步骤5中分配了空闲地址,但是还没有登记,本函数在段的TOC中登记,即,在目录中追加一条记录。 ## DSM段内存布局 并行查询的DSM段TOC保存在 ParallelContext 中: ```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段内存使用过程。 ![shm_toc](../../../../../ff_internal_upload/img/2021/shm_toc.png) # 三、参考 1. [PostgreSQL 9.6 Released!](https://www.postgresql.org/about/news/postgresql-96-released-1703/)