GPDB内存规划是指,按照当前参数配置,在Coordinator上推算各个算子的配额。所谓“配额”是指单个算子期望的最大可用内存,并非实际使用内存量,实际使用内存: * 可能小于配额:实际数据量小于代价评估数据量。 * 也可能大于配额:大部分算子会使用磁盘临时报错算子计划的中间数据,实际内存使用量可能会大于配额,但不会超出太多。 # 1. 查询内存配置参数 ## 1.1 内存敏感算子VS内存非敏感算子 从内存使用特征角度将算子分成两大类: * 内存敏感算子(Memory Intensive Operator):内存需求量相对较大,例如,Hash Join。 * 内存非敏感算子(Memory Non-intensive Operator):内存需求量很小,例如,Scan。 ## 1.2 work_mem `work_mem`是PG参数,配置内存敏感型算子期望内存,因为复杂查询算子可能会有多个,整个查询的内存可能会失控,所以,GP默认不再使用该参数,未来可能会被废弃,取而代之的是`statement_mem`。在GP重`set work_mem`会收到以下WARNING信息: > WARNING: "work_mem": setting is deprecated, and may be removed in a future release. ## 1.3 statement_mem GP使用该参数规划整个查询期望内存,根据特定算法再分派每个算子内存。当`statement_mem > 0`时,`work_mem`的功能被覆盖,否则,和PG一样,使用`work_mem`配置算子期望内存。参阅源码以下函数: ```c int64 ResourceManagerGetQueryMemoryLimit(PlannedStmt* stmt); ``` ## 1.4 planner_work_mem 使用`statement_mem`规划算子内存,需要先确定查询计划(查询计划树),而生成查询计划树,需要使用算子内存期望值,但是,`work_mem`已经不再使用,因此,GP又引入了新的参数`planner_work_mem`,该参数仅在生成查询计划时使用。 # 2. 推算算子内存 ## 2.1 两种算法 当查询计划和`query_mem`确定后,GP根据特定算法推算算子内存。推算算法有两种,由参数`gp_resqueue_memory_policy`(对应资源队列资源管理方式)或者`gp_resgroup_memory_policy`(对应资源组资源管理方式)指定。 * **`auto`**:算法最简单,在整个查询树上, * 内存非敏感型算子分配固定大小内存(`gp_resqueue_memory_policy_auto_fixed_mem`或`gp_resgroup_memory_policy_auto_fixed_mem`),默认100KB。 * 剩余内存被所有内存敏感算子均分。 对应的代码为: ```c void PolicyAutoAssignOperatorMemoryKB(PlannedStmt *stmt, uint64 memAvailableBytes); ``` 内存敏感算子内存计算公式: $$ \frac {queryMemKB - numNonMemIntensiveOperators * nonMemIntenseOpMemKB} {numMemIntensiveOperators} $$ * **`eager_free`**:相对更优的算法。考虑到因为不是所有内存敏感算子同时活动,因此,将查询树上的算子进行分组(算子组),在一个算子组内,算子内存分派方法同`auto`。对应的代码为: ```c void PolicyEagerFreeAssignOperatorMemoryKB(PlannedStmt *stmt, uint64 memAvailableBytes); ``` 默认使用`eager_free`算法。 ## 2.2 eager_free算法 参考`PolicyEagerFreeAssignOperatorMemoryKB`函数,以下注释解释了算法过程: ```c void PolicyEagerFreeAssignOperatorMemoryKB(PlannedStmt *stmt, uint64 memAvailableBytes) { // 算子分组,同时统计内存敏感算子和非敏感算子个数。 PolicyEagerFreePrelimWalker((Node *) stmt->planTree, &ctx); …… // 算子组的内存不得小于所有内存非敏感算子的内存配额, // 即,内存敏感算子需要有内存可分配。 const uint64 nonMemIntenseOpMemKB = (uint64)(*gp_resmanager_memory_policy_auto_fixed_mem); if (ctx.groupTree->groupMemKB < ctx.groupTree->numNonMemIntenseOps * nonMemIntenseOpMemKB) { ereport(……); } // 为内存非敏感算子设置内存配额,计算内存敏感算子内存配额。 PolicyEagerFreeAssignWalker((Node *) stmt->planTree, &ctx); …… } ``` 注意,内存的规划是在Coordinator节点上做的,并记录到算子数据结构上,分布式计划执行时,segment上的算子内存配额是随计划下发机制一同下发到segment节点的。 ## 2.3 关键代码说明 ### is_plan_node 判断是否为`Plan`类型的计划节点,枚举值在`T_Plan_Start`与`T_Plan_End`之间的"Plan"类型算子,这类算子是实际执行运算的算子,例如,HashJojn,Scan、Append(对应Union All)等。 ### IsMemoryIntensiveOperator 判断算子是否为内存敏感性算子。内存敏感性算子特征是:是内存需求量较大的"Plan"类型算子,包括: * Material(物化算子) * Sort * ShareInputScan(Shared Scan) * Hash * BitmapIndexScan * WindowAgg * TableFunctionScan * FunctionScan * 符合`IsAggMemoryIntensive`条件的聚集算子,包括: * Hash聚集 * DQA(Distinct-Qualified Aggregates,去重限定的聚集)聚集 * 排序聚集 * 符合`IsResultMemoryIntensive`条件的结果算子,包括: * 带表达式的Result算子 ### IsBlockingOperator 是否为阻塞算子。所谓“阻塞算子”是指,当该算子没有完成时,数据流不会继续向上一级算子传递,例如Sort算子是阻塞算子,只有Sort完成所有数据排序才能向上一级算子传递结果。阻塞算子包括: * BitmapIndexScan * Hash * Sort * 符合`IsMaterialBlockingOperator`条件的Material算子,包括: * `cdb_strict`属性为true的物化算子 * 符合`IsAggBlockingOperator`条件的Aggr算子,包括: * Scalar Agg(标量聚集) * `streaming on`时的第二阶段Hash聚集 * `streaming off`时的第一和第二阶段Hash聚集 ### IsRootOperatorInGroup 判断给定的计划节点是否是一个算子组的根节点(根算子)。需要同时满足三个条件: * 是`Plan`类型的计划节点 * 是阻塞算子 * 不需要rescan(没有外部参数)