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
配置算子期望内存。参阅源码以下函数:
- snippet.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。 - 剩余内存被所有内存敏感算子均分。
对应的代码为:
- snippet.c
void PolicyAutoAssignOperatorMemoryKB(PlannedStmt *stmt, uint64 memAvailableBytes);
内存敏感算子内存计算公式: $$ \frac {queryMemKB - numNonMemIntensiveOperators * nonMemIntenseOpMemKB} {numMemIntensiveOperators} $$
eager_free
:相对更优的算法。考虑到因为不是所有内存敏感算子同时活动,因此,将查询树上的算子进行分组(算子组),在一个算子组内,算子内存分派方法同auto
。对应的代码为:
- snippet.c
void PolicyEagerFreeAssignOperatorMemoryKB(PlannedStmt *stmt, uint64 memAvailableBytes);
默认使用eager_free
算法。
2.2 eager_free算法
参考PolicyEagerFreeAssignOperatorMemoryKB
函数,以下注释解释了算法过程:
- snippet.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(没有外部参数)