先看下整个效果
此时对应的查询计划如下:
很显然, 限于黑框框的表现能力, 动画的效果还是很简陋的. 这里最适合的是与配套的前端页面集成起来才够炫酷. 就像 GPCC 那样. 实际上在我第一眼看到 GPCC 中查询执行的动画效果之后, 就一直非常好奇这一效果究竟是如何实现的. 奈何 GPCC 并不开源, 再加上当时技术储备尚且不够, 一直没能如愿, 直到今天. 实际上在对 GP 执行框架有个大致了解之后, 便能很顺其自然地实现出类似功能.
如上查询计划所示, 在优化期间, GP 会根据数据的分布情况结合查询语义在适当的位置插入适合地 motion 节点用来实现数据的 reshuffle; 之后, GP 会以 motion 节点为边界将一个查询切分为多个 slice. 每一个 slice 都可以看做是整个查询执行计划的一个片段. 在执行时, GP 会为每一个 slice 分配相应的执行资源来并行执行 slice. 也即图中的 slice1, slice2 会并行执行, slice1, slice2 之间通过 BroadcastMotion 节点来实现数据交互.
如果我们想动态展示查询的执行过程, 那么很显然需要做如下几件事:
第一个便是数据收集, 在每一个 plan node 执行期间, 收集每一个 plan node 的执行状态, 最起码需要收集 plan node 当前输入的总行数, 以及向上输出的总行数. 幸运地是 GP EXPLAN ANALYZE 链路已经加入了相应的能力. 具体来说 PlanState::instrument::tuplecount + PlanState::instrument::ntuples
便是一个 plan node 输出的总行数; 输出的总行数再加上 nfiltered1 + nfiltered2
便可看做是 plan node 执行时输入的总行数集合.
第二个便是数据汇报, 这时我们要周期性地将整个执行计划树中每一个 plan node 的执行状态汇报出去. 这又具体分为两个小问题: 何时汇报? 以及如何汇报? 首先看下何时汇报, 考虑到 GP 中整个查询的数据流是自下而上的, 我个人认为在数据流的最底层来决策何时汇报合适一点. 对于一个 slice 而言, 数据流的最底层可以是那些 Scan 节点, 也可以是 RecvMotion. 具体来说数据流的最底层每产生一个 tuple 之后都判断一下是否需要汇报, 这里我们新引入了一个 GUC: send_stat_per_rows
, 即每当最底层的节点生产出 send_stat_per_rows 这么多行数后, 便汇报一下整个查询计划树的执行状态. 为此我们稍微调整了下 SeqScan 的执行链路:
diff --git a/src/backend/executor/execScan.c b/src/backend/executor/execScan.c
index 216823beb8..1beda6e969 100644
--- a/src/backend/executor/execScan.c
+++ b/src/backend/executor/execScan.c
@@ -166,12 +166,13 @@ ExecScan(ScanState *node,
*/
if (TupIsNull(slot))
{
+ DoPrintTreeStat(&node->ps);
if (projInfo)
return ExecClearTuple(projInfo->pi_slot);
else
return slot;
}
+ PrintTreeStat(&node->ps);
/*
* place the current tuple into the expr context
*/
这里 DoPrintTreeStat()
的调用意味着当前最底层 node 执行结束了, 此时强制汇报下状态. PrintTreeStat()
函数会判断自上次汇报是否已经过了 send_stat_per_rows
行, 若是则再次汇报, 若不是则 noop 直接返回. 具体一点这俩函数实现如下:
void DoPrintTreeStat(PlanState *state)
{
if (state->instrument == NULL || send_stat_per_rows <= 0)
return;
DoDoPrintTreeStat(state->state);
}
void PrintTreeStat(PlanState *state)
{
if (state->instrument == NULL || send_stat_per_rows <= 0)
return;
Instrumentation *inst = state->instrument;
if ((inst->ntuples + (uint64)inst->nfiltered1 + (uint64)inst->nfiltered2 + inst->tuplecount) % send_stat_per_rows != 0)
return;
DoDoPrintTreeStat(state->state);
return;
}
DoDoPrintTreeStat() 函数则是用来执行实际的汇报工作, 其会以广度优先遍历的次序遍历执行计划, 之后汇报每一个执行 node 的执行状态. 这仨函数的名字起得是有了点匆忙…
我们可以为所有被认为是最底层的 node 加入如上调整, 这些最底层 node 包括 RecvMotion, Sort, IndexScan 等.
再看下如何汇报, 考虑到 GP 中会将执行节点输出的 NOTICE 日志转发给客户端, 因此这里最方便的汇报方式便是 elog(NOTICE)
. 也即整体来看, 当我们开启 send_stat_per_rows 之后, 在查询的执行过程中, 会周期性有 NOTICE 日志过来汇报查询的执行状态. 为了要将查询的执行动画起来, 我们只需要将这些执行状态重定向到用来绘制动画的脚本即可. 这也是视频中最开始出现的:
psql -d tpch10m -f tpch10m.3.sql 2>&1 | python execshow.py
这里 psql 部分会发送查询给 GP, 之后接受 GP 发来的执行状态 NOTICE 日志, 将日志重定向给 execshow.py 脚本, 该脚本会解析日志, 提取相关信息并以进度条的方式展示出来. 在这种处理架构下, 内核部分注重于执行状态的收集与发送, 其不需要关心这些执行状态最终会以怎样的方式展示出来, 简化了内核端的逻辑. 执行状态的渲染则是有第三方模块来完成, 这种解耦的模块设计方式使得执行状态渲染姿势的变更与内核无关, 便于快速迭代与更新.