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

Posted Gauss松鼠会

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何插件化地为openGauss添加算子相关的知识,希望对你有一定的参考价值。

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物化算子
StubnodeStub.cpp控制算子
SubplannodeSubplan.cpp控制算子
SubqueryscannodeSubqueryscan.cpp扫描算子
TidscannodeTidscan.cpp扫描算子
UniquenodeUnique.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
TableFuncscannodeTableFuncscan.c
TidrangescannodeTidrangescan.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 创建计划状态节点

调用上述ExtensiblePlanMethods中的methods定义的接口CreateExtensiblePlanState,为这个ExtensiblePlan分配一个ExtensiblePlanState。

插入位置所在的函数:ExecInitNodeByType->ExecInitExtensiblePlan

typedef struct ExtensiblePlanState 
    ScanState ss;
    uint32 flags;        /* mask of EXTENSIBLEPATH_* flags, see relation.h */
    List* extensible_ps; /* list of child PlanState nodes, if any */
    const ExtensibleExecMethods* methods;
 ExtensiblePlanState;
  • flags含义同ExtensiblePath和ExtensiblePlan一样

  • extensible_ps为该计划节点的子节点。

  • methods为包含多个执行所需接口的结构体ExtensibleExecMethods,在下文做具体介绍。

3.2.2 执行层hook

上面CustomScanState的成员CustomExecMethods定义了几个hook点

typedef struct ExtensibleExecMethods 
    const char* ExtensibleName;

    /* Executor methods: mark/restore are optional, the rest are required */
    void (*BeginExtensiblePlan)(struct ExtensiblePlanState* node, EState* estate, int eflags);
    TupleTableSlot* (*ExecExtensiblePlan)(struct ExtensiblePlanState* node);
    void (*EndExtensiblePlan)(struct ExtensiblePlanState* node);
    void (*ReScanExtensiblePlan)(struct ExtensiblePlanState* node);
    void (*ExplainExtensiblePlan)(struct ExtensiblePlanState* node, List* ancestors, struct ExplainState* es);
 ExtensibleExecMethods;

1) BeginExtensiblePlan完成所提供的ExtensiblePlanState的初始化。标准的域已经被ExecInitExtensiblePlan初始化,但是任何私有的域应该在这里被初始化。

插入位置所在的函数:ExecInitNodeByType->ExecInitExtensiblePlan

2) ExecExtensiblePlan执行扫描,取下一个扫描元组,如果还有任何元组剩余,它应该用当前扫描方向的下一个元组填充ps_ResultTupleSlot,并且接着返回该元组槽。如果没有,则用NULL填充或者返回一个空槽。

插入位置所在的函数:ExecProcNode->ExecProcNodeByType

3) EndExtensiblePlan清除任何与ExtensiblePlanState相关的私有数据。这个方法是必需的,但是如果没有相关的数据或者相关数据将被自动清除,则它不需要做任何事情。

插入位置所在的函数:ExecEndNodeByType->ExecEndExtensiblePlan

4) ReScanExtensiblePlan把当前扫描倒回到开始处,并且准备重新扫描该关系。

插入位置所在的函数:ExecReScan->ExecReScanByType

5) ExplainExtensiblePlan为一个自定义扫描计划节点的EXPLAIN输出额外的信息。这个回调函数是可选的。即使没有这个回调函数,被存储在`ScanState中的公共的数据(例如目标列表和扫描关系)也将被显示,但是该回调函数允许显示额外的信息(例如私有状态)。

插入位置所在的函数:ExplainNode->show_pushdown_qual

以上是关于如何插件化地为openGauss添加算子的主要内容,如果未能解决你的问题,请参考以下文章

openGauss数据库源码解析系列文章—— 执行器解析

如何在Sublime Text中添加代码片段

几个非常实用的JQuery代码片段

VIM 代码片段插件 ultisnips 使用教程

Word 文档的优秀代码片段工具或插件?

如何有条件地为 Catalyst 编译代码?