FAIRYFAR-INTERNAL
 
  FAIRYFAR-INTERNAL  |  SITEMAP  |  ABOUT-ME  |  HOME  
GPDB查询内存规划详解

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_memgp_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_StartT_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(没有外部参数)


打赏作者以资鼓励: