暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

如何插件化地为openGauss添加算子

Gauss松鼠会 2022-06-14
441

1.1 openGauss执行算子汇总

openGauss的算子按类型可分为四类:控制算子、连接算子、扫描算子和物化算子。下面汇总了当前(openGauss2.0.0)已有的算子。

算子文件类型
AggnodeAgg.cpp物化算子
AppendnodeAppend.cpp控制算子
BitmapAndnodeBitmapAnd.cpp控制算子
BitmapHeapscannodeBitmapHeapscan.cpp扫描算子
BitmapIndexscannodeBitmapIndexscan.cpp扫描算子
BitmapOrnodeBitmapOr.cpp控制算子
CtescannodeCtescan.cpp扫描算子
ForeignscannodeForeignscan.cpp扫描算子
FunctionscannodeFunctionscan.cpp扫描算子
GroupnodeGroup.cpp物化算子
HashnodeHash.cpp物化算子
HashjoinnodeHashjoin.cpp连接算子
IndexonlyscannodeIndexonlyscan.cpp扫描算子
IndexscannodeIndexscan.cpp扫描算子
LimitnodeLimit.cpp物化算子
LockRowsnodeLockRows.cpp控制算子
MaterialnodeMaterial.cpp物化算子
MergeAppendnodeMergeAppend.cpp控制算子
MergejoinnodeMergejoin.cpp连接算子
ModifyTablenodeModifyTable.cpp控制算子
NestloopnodeNestloop.cpp连接算子
PartIteratornodePartIterator.cpp连接算子
RecursiveunionnodeRecursiveunion.cpp控制算子
ResultnodeResult.cpp控制算子
SamplescannodeSamplescan.cpp扫描算子
SeqscannodeSeqscan.cpp扫描算子
SetOpnodeSetOp.cpp物化算子
SortnodeSort.cpp物化算子
Stub
nodeStub.cpp控制算子
SubplannodeSubplan.cpp控制算子
SubqueryscannodeSubqueryscan.cpp扫描算子
TidscannodeTidscan.cpp扫描算子
Unique
nodeUnique.cpp物化算子
ValuesscannodeValuesscan.cpp扫描算子
WindowAggnodeWindowAgg.cpp物化算子
WorktablescannodeWorktablescan.cpp扫描算子
ExtensiblenodeExtensible.cpp用于扩展算子

1.2 PG新增算子汇总

下面列出PG(14devel)相比于openGauss多了哪些算子。

算子文件类型
CustomnodeCustom.c
GathernodeGather.c
GatherMergenodeGatherMerge.c
IncrementalSortnodeIncrementalSort.c
NamedtuplestorescannodeNamedtuplestorescan.c
ProjectSetnodeProjectSet.c
nodeProjectSet.cnodeTableFuncscan.c
nodeTableFuncscan.cnodeTidrangescan.c

1.1表格中的算子Extensible类似于PG的算子Custom,其作用是允许插件向数据库增加新的扫描类型。主要分为三步:
首先,在路径规划期间生成插件增加的扫描路径(ExtensiblePath);
然后,如果优化器选择该路径作为最优路径,那么需要生成对应的计划(ExtensiblePlan);
最后,必须提供执行该计划的能力(ExtensiblePlanState)。
下面以TidRangeScan为示例,演示如何使用Extensible通过插件化的方式为openGauss新增一个执行算子。

2.1 功能介绍

openGauss中堆表由一个个page组成,每个page包含若干个tuple。tid是tuple的寻址地址,由两个字段组成:(pageid,itemid),pageid代表第几个数据块,itemid代表这个page内的第几条记录。例如tid=(10,1)表示第11个数据块中的第一条记录(pageid从0开始,itemid从1开始)。
PostgreSQL 14 devel新增了算子TidRangeScan,可以直接通过tid来范围访问某个page的全部数据。(带来的好处:如果需要更新一张表所有数据时,可以开启多个会话并行去更新不同的page,提高效率。)
本次展示将该特性通过插件的方式移植到openGauss,插件化的增加一个执行算子。

2.2 使用说明

tidrangescan插件定义了一个bool类型的guc参数:enable_tidrangescan,控制优化器对tidrangescan扫描算子的使用,on表示使用,off表示不使用。


2.3 插件边界

本小节主要列举调用了哪些内核接口,当内核演进过程中修改了这些接口,有可能会影响插件的使用。

接口名文件模块
ExecInitExprexecQual.cpp优化层
clauselist_selectivityclausesel.cpp优化层
cost_qual_evalcostsize.cpp优化层
get_tablespace_page_costsspccache.cpp优化层
get_baserel_parampathinforelnode.cpp优化层
add_pathpathnode.cpp优化层
extract_actual_clausesrestrictinfo.cpp优化层
heap_getnextheapam.cpp执行层
ExecClearTupleexecTuples.cpp执行层
ExecStoreTupleexecTuples.cpp执行层
ExecScanReScanexecScan.cpp执行层
heap_beginscanheapam.cpp执行层
heap_rescanheapam.cpp执行层
ExecScanexecScan.cpp执行层
heap_endscanheapam.cpp执行层
make_ands_explicitclauses.cpp执行层
deparse_context_for_planstateruleutils.cpp执行层
deparse_expressionruleutils.cpp执行层
ExplainPropertyTextexplain.cpp执行层

2.4 设计实现

本节提到的hook在第3章《hook点总述》会做详细说明。

附社区PR:https://gitee.com/opengauss/Plugin/pulls/1

2.4.1 插件开发通用流程

2.4.1.1 Makefile

在openGauss源码的contrib目录下新建开发插件的目录,这里为tidrangescan。在该目录下新建Makefile文件。

# contrib/tidrangescan/Makefile
MODULES = tidrangescan # 模块名
EXTENSION = tidrangescan # 扩展的名称
REGRESS = tidrangescan # 回归测试
REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress -c 0 -d 1 --single_node # 回归测试相关的选项
DATA = tidrangescan--1.0.sql # 插件安装的SQL文件
 
override CPPFLAGS :=$(filter-out -fPIE, $(CPPFLAGS)) –fPIC # fPIC选项
 
# 以下是openGauss构建插件相关的命令,保留即可
ifdef USE_PGXS
PG_CONFIG = pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)
else
subdir = contrib/tidrangescan
top_builddir = ../..
include $(top_builddir)/src/Makefile.global
include $(top_srcdir)/contrib/contrib-global.mk
endif

2.4.1.2 control文件

新建控制文件,这里为tidrangescan.control。内容如下:

# tidrangescan extension
comment = 'example implementation for custom-scan-provider interface'  default_version = '1.0'  # 与Makefile里DATA属性的sql文件名的版本保持一致  module_pathname = '$libdir/tidrangescan'
relocatable = true  

2.4.1.3 sql文件

sql文件命名格式为extensionNameversion.sql,*version*即为上述版本号,这里为tidrangescan--1.0.sql。在这里编写所需的函数。

-- complain if script is sourced in psql, rather than via CREATE EXTENSION
\echo Use "CREATE EXTENSION tidrangescan" to load this file. \quit
 
CREATE FUNCTION pg_catalog.tidrangescan_invoke() RETURNS VOID AS '$libdir/tidrangescan','tidrangescan_invoke' LANGUAGE C STRICT;

2.4.1.4 回归测试用例
创建sql和expected目录,分别存放测试用例的sql脚本和预期输出,例如这里为tidrangescan.sql和tidrangescan.out。

2.4.1.5 源文件及插件目录总览
创建插件的头文件和cpp文件,这是实现插件的核心,下文主要介绍该插件代码层的设计与实现。
至此插件总体目录概览如下。


2.4.2 优化器

2.4.2.1 添加路径

将set_rel_pathlist_hook赋值为SetTidRangeScanPath,该函数解析扫描表的查询条件,当存在tid范围查询时调用add_path添加ExtensiblePath,计算代价,并将创建计划的接口tidrangescan_path_methods存入path中。

static void SetTidRangeScanPath(PlannerInfo *root, RelOptInfo *baserel, Index rtindex, RangeTblEntry *rte)
{
    ...
    tidrangequals = TidRangeQualFromRestrictInfoList(baserel->baserestrictinfo, baserel);
    ...
    if (tidrangequals != NIL) {
        cpath = (ExtensiblePath*)palloc0(sizeof(ExtensiblePath));
        cpath->path.type = T_ExtensiblePath;
        cpath->path.pathtype = T_ExtensiblePlan;
        cpath->path.parent = baserel;
        cpath->extensible_private = tidrangequals;
        cpath->methods = &tidrangescan_path_methods;
      
        cost_tidrangescan(&cpath->path, root, baserel, tidrangequals, cpath->path.param_info);
        add_path(root, baserel, &cpath->path);
    }
}
 
static ExtensiblePathMethods  tidrangescan_path_methods = {
    "tidrangescan",        /* ExtensibleName */
    PlanTidRangeScanPath,    /* PlanExtensiblePath */
};

2.4.2.2 创建计划
上述的tidrangescan_path_methods定义了创建计划函数PlanTidRangeScanPath,根据最优路径生成计划ExtensiblePlan,同时将创建计划状态节点接口tidrangescan_scan_methods存入plan。

static Plan *PlanTidRangeScanPath(PlannerInfo *root, RelOptInfo *rel, ExtensiblePath *best_path, List *tlist, List *clauses, List *custom_plans)
{
    ExtensiblePlan *node = makeNode(ExtensiblePlan);
    Plan    *plan = &node->scan.plan;
    List    *tidrangequals = best_path->extensible_private;
    ...
    node->extensible_exprs = tidrangequals;
    node->scan.plan.startup_cost = best_path->path.startup_cost;
    node->scan.plan.total_cost = best_path->path.total_cost;
    node->scan.plan.plan_rows = best_path->path.rows;
    node->scan.plan.plan_width = rel->width;
    node->methods = &tidrangescan_scan_methods;
    return plan;
}
 
static ExtensiblePlanMethods tidrangescan_scan_methods = {
    "tidrangescan",        /* ExtensibleName */
    CreateTidRangeScanState,  /* CreateExtensiblePlanState */
};

2.4.3 执行器

2.4.3.1 创建计划状态节点

上述的tidrangescan_scan_methods定义了创建PlanState函数CreateTidRangeScanState,根据传入的plan返回PlanState,同样将后续执行器执行的若干方法结构体tidrangescan_exec_methods存入PlanState。

Node *CreateTidRangeScanState(ExtensiblePlan *custom_plan)
{
    TidRangeScanState *tidrangestate;
    /*
     * create state structure
     */
    tidrangestate = (TidRangeScanState*)palloc0(sizeof(TidRangeScanState));
    NodeSetTag(tidrangestate, T_ExtensiblePlanState);
    tidrangestate->css.methods = &tidrangescan_exec_methods;
    /*
     * mark scan as not in progress, and TID range as not computed yet
     */
    tidrangestate->trss_inScan = false;
    return (Node*)&tidrangestate->css;
}
 
static ExtensibleExecMethods tidrangescan_exec_methods = {
    "tidrangescan",        /* ExtensibleName */
    BeginTidRangeScan,      /* BeginExtensiblePlan */
    ExecTidRangeScan,      /* ExecExtensiblePlan */
    EndTidRangeScan,      /* EndExtensiblePlan */
    ExecReScanTidRangeScan,      /* ReScanExtensiblePlan */
    ExplainTidRangeScan       /* ExplainExtensiblePlan */
};

2.4.3.2 执行层hook
tidrangescan_exec_methods定义了五个接口,分别是执行层各个阶段的主函数:BeginTidRangeScan、ExecTidRangeScan、EndTidRangeScan、ExecReScanTidRangeScan、ExplainTidRangeScan。

static void BeginTidRangeScanScan(ExtensiblePlanState *node, EState *estate, int eflags)
{
    TidRangeScanState *ctss = (TidRangeScanState *) node;
    ExtensiblePlan    *cscan = (ExtensiblePlan *) node->ss.ps.plan;
    ctss->css.ss.ss_currentScanDesc = NULL;  /* no table scan here */
    /*
     * initialize child expressions
     */
    ctss->css.ss.ps.qual = (List*)ExecInitExpr((Expr*)cscan->scan.plan.qual, (PlanState *)ctss);
    TidExprListCreate(ctss);
}
 
static TupleTableSlot * ExecTidRangeScan(ExtensiblePlanState *pstate)
{
    return ExecScan(&pstate->ss, (ExecScanAccessMtd) TidRangeNext, (ExecScanRecheckMtd) TidRangeRecheck);
}
 
static void EndTidRangeScan(ExtensiblePlanState *node)
{
    TableScanDesc scan = node->ss.ss_currentScanDesc;
    if (scan != NULL)
        heap_endscan(scan);
    /*
     * Free the exprcontext
     */
    ExecFreeExprContext(&node->ss.ps);
 
    /*
     * clear out tuple table slots
     */
    if (node->ss.ps.ps_ResultTupleSlot)
        ExecClearTuple(node->ss.ps.ps_ResultTupleSlot);
    ExecClearTuple(node->ss.ss_ScanTupleSlot);
}
 
static void ExecReScanTidRangeScan(ExtensiblePlanState *node)
{
    /* mark scan as not in progress, and tid range list as not computed yet */
    ((TidRangeScanState*)node)->trss_inScan = false;
    /*
     * We must wait until TidRangeNext before calling table_rescan_tidrange.
     */
    ExecScanReScan(&node->ss);
}
 
static void ExplainTidRangeScan(ExtensiblePlanState *node, List *ancestors, ExplainState *es)
{
    TidRangeScanState *ctss = (TidRangeScanState *) node;
    ExtensiblePlan    *cscan = (ExtensiblePlan *) ctss->css.ss.ps.plan;
    /* logic copied from show_qual and show_expression */
    if (cscan->extensible_exprs) {
        bool  useprefix = es->verbose;
        Node  *qual;
        List  *context;
        char  *exprstr;
        /* Convert AND list to explicit AND */
        qual = (Node *) make_ands_explicit(cscan->extensible_exprs);
        /* Set up deparsing context */
        context = deparse_context_for_planstate((Node*)ctss, ancestors, es->rtable);
        /* Deparse the expression */
        exprstr = deparse_expression(qual, context, useprefix, false);
        /* And add to es->str */
        ExplainPropertyText("tid range quals", exprstr, es);
  }
}

2.4.4 改造点

2.4.4.1 无法调用内核static函数

在移植过程中,受限于插件实现的方式,无法调用内核的static函数,需要拷贝到插件侧或者对原有的代码作改造。

执行层获取单个tuple阶段,PG在heapam.c中定义了一个函数heap_getnextslot_tidrange,其中调用了static函数heapgettup_pagemode和heapgettup。在将heap_getnextslot_tidrange搬到openGauss插件时,由于无法调用这两个static函数,需要将其改为调用heap_getnext,通过heap_getnext访问heapgettup_pagemode和heapgettup。

3.1 优化器

3.1.1 添加路径

通常用来产生ExtensiblePath对象,并使用add_path把它们加入到rel中。

插入位置所在的函数:set_rel_pathlist

typedef void (*set_rel_pathlist_hook_type) (PlannerInfo *root,
                                           RelOptInfo *rel,
                                           Index rti,
                                           RangeTblEntry *rte);
extern THR_LOCAL PGDLLIMPORT set_rel_pathlist_hook_type set_rel_pathlist_hook;

ExtensiblePath定义如下。

typedef struct ExtensiblePath {
    Path path;
    uint32 flags;           /* mask of EXTENSIBLEPATH_* flags */
    List* extensible_paths; /* list of child Path nodes, if any */
    List* extensible_private;
    const struct ExtensiblePathMethods* methods;
} ExtensiblePath;
  • flags是一个标识,如果该自定义的路径支持反向扫描,则它应该包括EXTENSIBLEPATH_SUPPORT_BACKWARD_SCAN,如果支持标记和恢复则包括EXTENSIBLEPATH_SUPPORT_MARK_RESTORE。
  • extensible_paths是这个自定义路径节点的子Path节点列表
  • extensible_private可用来存储该自定义路径的私有数据。
  • methods必须包含根据该路径生成计划的方法。ExtensiblePathMethods结构如下,主要实现PlanExtensiblePath。
    typedef struct ExtensiblePathMethods {
    const char* ExtensibleName;
     
    /* Convert Path to a Plan */
    struct Plan* (*PlanExtensiblePath)(PlannerInfo* root, RelOptInfo* rel, struct ExtensiblePath* best_path,
        List* tlist, List* clauses, List* extensible_plans);
    } ExtensiblePathMethods;

3.1.2 添加连接路径

提供连接路径,同样创建ExtensiblePath路径。

插入位置所在的函数:add_paths_to_joinrel

typedef void (*set_join_pathlist_hook_type) (PlannerInfo *root,
                                               RelOptInfo *joinrel,
                                               RelOptInfo *outerrel,
                                               RelOptInfo *innerrel,
                                               JoinType jointype,
                                               SpecialJoinInfo *sjinfo,
                                               Relids param_source_rels,
                                               SemiAntiJoinFactors *semifactors,
                                               List *restrictlist);
extern THR_LOCAL PGDLLIMPORT set_join_pathlist_hook_type set_join_pathlist_hook;

3.1.3 创建计划
调用上述ExtensiblePath中的methods定义的接口PlanExtensiblePath,将自定义路径转换为一个完整的计划,返回ExtensiblePlan。
插入位置所在的函数:create_scan_plan->create_extensible_plan

typedef struct ExtensiblePlan {
    Scan scan;
 
    uint32 flags;                  /* mask of EXTENSIBLEPATH_* flags, see relation.h */
 
    List* extensible_plans;        /* list of Plan nodes, if any */
 
    List* extensible_exprs;        /* expressions that extensible code may evaluate */
 
    List* extensible_private;      /* private data for extensible code */
 
    List* extensible_plan_tlist;   /* optional tlist describing scan
                                    * tuple */
    Bitmapset* extensible_relids;  /* RTIs generated by this scan */
 
    ExtensiblePlanMethods* methods;
} ExtensiblePlan;
  • 和ExtensiblePath一样,flags同样是一个标识。
  • extensible_plans可以用来存放子Plan节点
  • extensible_exprs用来存储需要由setrefs.cpp和subselect.cpp修整的表达式树。
  • extensible_private用来存储只有该自定义算子使用的私有数据。
  • extensible_plan_tlist描述目标列
  • extensible_relids为该扫描节点要处理的关系集合
  • methods必须包含生成该计划对应的计划节点PlanState的方法。ExtensiblePlanMethods结构如下,主要实现CreateExtensiblePlanState。
    typedef struct ExtensiblePlanMethods {
    char* ExtensibleName;
     
    /* Create execution state (ExtensiblePlanState) from a ExtensiblePlan plan node */
    Node* (*CreateExtensiblePlanState)(struct ExtensiblePlan* cscan);
    } ExtensiblePlanMethods;

3.2 执行器

3.2.1 创建计划状态节点









「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论