并行执行模块总体上可以划分为计划生成和计划执行两个阶段。
并行计划生成
OceanBase 首先总是会生成一个本地串行计划,然后根据计划中每个算子的分区信息决定是否将其转化成一个并行计划。例如,在 select 语句的计划生成代码(src/sql/optimizer/ob_select_log_plan.cpp
)中:
int ObSelectLogPlan::generate_plan()
{
int ret = OB_SUCCESS;
if (OB_FAIL(generate_raw_plan())) {
LOG_WARN("fail to generate raw plan", K(ret));
} else if (OB_ISNULL(get_plan_root())) {
ret = OB_ERR_UNEXPECTED;
LOG_WARN("null root plan", K(get_plan_root()), K(ret));
} else if (OB_FAIL(get_plan_root()->adjust_parent_child_relationship())) {
LOG_WARN("failed to adjust parent-child relationship", K(ret));
} else if (OB_FAIL(plan_traverse_loop(ALLOC_LINK,
ALLOC_EXCH,
ALLOC_GI,
ADJUST_SORT_OPERATOR,
PX_PIPE_BLOCKING,
PX_RESCAN,
RE_CALC_OP_COST,
ALLOC_MONITORING_DUMP,
OPERATOR_NUMBERING,
EXCHANGE_NUMBERING,
ALLOC_EXPR,
PROJECT_PRUNING,
ALLOC_DUMMY_OUTPUT,
CG_PREPARE,
GEN_SIGNATURE,
GEN_LOCATION_CONSTRAINT,
PX_ESTIMATE_SIZE,
GEN_LINK_STMT))) {
generate_raw_plan()
负责生成本地串行计划,plan_traverse_loop()
中的 ALLOC_EXCH 负责计划并行化。
每个算子都实现了 allocate_exchange_post()
方法,负责声明自己的分区信息。有些算子本身不会感知到分区,它会继承 child 的分区信息。例如 ob_log_sort.cpp
:
int ObLogSort::allocate_exchange_post(AllocExchContext* ctx)
{
int ret = OB_SUCCESS;
ObLogicalOperator* child = NULL;
if (OB_ISNULL(ctx) || OB_ISNULL(child = get_child(first_child))) {
ret = OB_ERR_UNEXPECTED;
LOG_WARN("get unexpected null", K(ctx), K(child), K(ret));
} else if (OB_FAIL(sharding_info_.copy_with_part_keys(child->get_sharding_info()))) {
LOG_WARN("failed to deep copy sharding info from child", K(ret));
} else { /*do nothing*/
}
return ret;
}
有些算子会根据各种信息计算自己的分区信息,例如 ObLogTableScan 算子(ob_log_table_scan.cpp
) 会检查要扫描的分区是不是在当前 server 上,并将这个信息记录到自己的 sharding_info
上。
更复杂的有 ObLogJoin 算子(ob_log_join.cpp
),它会综合左右孩子的分区信息、join 算法、连接方法等计算出一个最终的 sharding_info
。
当所有算子都计算出自己的分区信息后,就可以判断出两个算子之间是否需要添加 exchange 算子,并调用 allocate_exchange()
方法分配之。
并行计划执行
并行计划执行分为三个部分:算子、调度、执行。
算子
除了 Granule Iterator,Exchange 算子外,其余算子均可用于串行计划和并行计划。算子本身不感知是否并行,算子内也不会创建线程来执行计算任务。
一般算子的代码位于 src/sql/engine/
目录下,并行专有的算子位于 src/sql/engine/px
目录下,其中 px 是 parallel execution 的缩写。
调度
OB 的调度逻辑封装到了一个算子中:ObPxCoord 算子 (src/sql/engine/px/ob_px_coord.cpp
),它有两个子类 ObPxFifoCoord、ObPxMergeSortCoord,调度逻辑都是一样的,区别是他们收取结果数据的方式不同。
这里值得一提的是,调度逻辑封装到一个算子中,而不是实现成一段独立的控制逻辑,有非常多的好处。它使得我们可以非常容易地做到嵌套的计划调度,最典型的场景是 NLJ 右侧是一个非常复杂的分布式子计划,这方面更多的细节可以参考 ObPxCoord::rescan()
方法。
ObPxCoord 会将计划划分成多个 DFO,保存到 ObDfoMgr (src/sql/engine/px/ob_dfo_mgr.cpp
) 中,然后逐步调度执行这些 DFO。
执行
ObPxCoord 会分析每个 DFO 位于哪些机器上以及每个机器上应该分配多少个线程执行它们,然后将它们发送到对应机器上执行。为了减少 RPC 的次数,每台机器上无论启动多少个线程,都只发送一个 RPC,RPC 的 processor 我们称之为 SQC(Sub Query Coordinator,src/sql/engine/px/ob_px_sub_coord.cpp
),它会负责将 DFO 提交到多个线程上并行执行。
每个租户在每台机器上都有一个线程池 (omt::ObPxPools
) ,SQC 通过 ObPxThreadWorker 逻辑向这个线程池里提交执行任务。