让查询执行动画起来

Posted by w@hidva.com on June 14, 2020

先看下整个效果

此时对应的查询计划如下:

plan.svg

很显然, 限于黑框框的表现能力, 动画的效果还是很简陋的. 这里最适合的是与配套的前端页面集成起来才够炫酷. 就像 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 脚本, 该脚本会解析日志, 提取相关信息并以进度条的方式展示出来. 在这种处理架构下, 内核部分注重于执行状态的收集与发送, 其不需要关心这些执行状态最终会以怎样的方式展示出来, 简化了内核端的逻辑. 执行状态的渲染则是有第三方模块来完成, 这种解耦的模块设计方式使得执行状态渲染姿势的变更与内核无关, 便于快速迭代与更新.