TiSpark 原理之下推丨TiDB 工具分享

TiSpark 是 PingCAP 为解决用户复杂 OLAP 需求而推出的产品。它通过 Spark 提供的拓展机制与内置的 TiKV Client Java,在 Spark 之上直连 TiKV 进行读写,具有事务性读取、事务性写入与删除等能力。其中在事务性读取中基于 Spark Extension 实现了下推(详情可见 TiSpark 用户指南)。

为了帮助读者更好地理解、运用 TiSpark,本文将详细介绍 TiSpark 中下推相关的知识,包括 Spark 中的下推含义,实现原理,及其拓展接口、TiSpark 下推策略和下推支持列表。

本文作者:施宇航,PingCAP 研发工程师。

TiSpark 架构图

理解 Spark 的下推

TiSpark 本质是 Spark 的 connector,想要了解 TiSpark 是如何下推的必须先理解 Spark 中的下推。

了解 Spark SQL

首先简单了解一下 Spark SQL 的执行过程,这有助于理解下推原理的介绍。

Spark SQL 的核心是 Catalyst,它会依次进行 SQL 的解析,校验,优化,选择物理计划。最终生成可执行 RDD,交由 Spark Core 执行任务。

在这个过程中,Spark SQL 会被解析为一颗树。树的节点称为 TreeNode,它有多个实现类,可以用来表示逻辑计划树与物理计划树中各种类型的节点。这里不过多展开,我们只需要知道由 TreeNode 组成的一颗树可以表示一条 SQL:如过滤条件会被解析为 Filter 算子节点。

Spark 中的下推

下推是一种经典的 SQL 优化手段,它会尽量将一些算子推向靠近数据源的位置,以减少上层所需处理的数据量,最终达到加快查询速度的目的。常见的下推优化有:谓词下推,聚合下推,映射下推。

在分布式计算引擎 Spark 中,下推的含义如出一辙,但需要注意在 Spark 中其实有两步下推优化:

  1. 逻辑计划优化阶段:会尽量将算子推向靠近数据源的方向,但不会推向数据源

  2. 物理计划生成阶段:将算子推到数据源,Spark 可能不会再处理该算子

举个例子,考虑如下 SQL:

select * from A join B on A.id = B.id where A.a>10 and B.b<100;

上文提到 SQL 会被解析为一颗逻辑计划树

  • filter 表示 where 条件
  • join 表示 join 操作
  • scan_a 与 scan_b 表示从数据源 A,B 表拉取数据

第一步:在逻辑计划下推优化后,过滤条件会被下推到更靠近数据源的位置,这样 join 所需处理的数据就会更少

对应的 SQL 可如下表示:

select * from (select * from A where A.a>10) a join (select * from B where B.b<100) b on a.id = b.id 

第二步:在物理计划生成时,过滤条件还可能被彻底下推到数据源。也就是说 Spark 无需处理 Filter 了,数据源返回就已完成过滤。

Spark 下推原理

该小节代码基于 Spark 3.2

逻辑计划下推优化

Spark 首先会在逻辑计划优化时进行下推优化。

在 Catalyst 的逻辑计划优化阶段,会应用各种优化规则,其中就包括了下推优化的规则。这里就对应了上文所说的逻辑计划层的下推优化。以谓词下推优化 PushDownPredicates 为例

object PushDownPredicates extends Rule[LogicalPlan] with PredicateHelper {
   
   
  def apply(plan: LogicalPlan): LogicalPlan = plan transform {
   
   
    CombineFilters.applyLocally
      .orElse(PushPredicateThroughNonJoin.applyLocally)
      .orElse(PushPredicateThroughJoin.applyLocally)
  }
}

CombineFilters 用于合并过滤条件。PushPredicateThroughNonJoin 和 PushPredicateThroughJoin 则用于分别处理不包含 join 和包含 join 时的谓词下推。由于这部分下推不是本文重点,我们不再赘述其具体实现,感兴趣的同学可以直接参考 Spark 源码

物理计划下推数据源

在完成逻辑计划阶段的下推优化后,Spark 会基于下推结果,在生成物理计划时再进行下推数据源的优化。TiSpark 主要涉及此时的下推,我们重点阐述这部分的原理。

下推接口

在 Spark 中,提供了 DataSource API 接口用于拓展数据源,其中包含了下推接口用于指定需要下推到数据源的算子。以 Spark 3.2.1 的谓词下推为例,其接口如下:

@Evolving
public interface SupportsPushDownFilters extends ScanBuilder {
   
   
  Filter[] pushFilters(Filter[] filters);
  Filter[] pushedFilters();
}
  • Filter[] pushFilters(Filter[] filters):入参是从 Catalyst expression 解析来的所有过滤条件,它是经过了第一步逻辑计划优化之后的结果。出参是 Spark 无法下推到数据源的过滤条件,被称为 postScanFilters
  • Filter[] pushedFilters():出参是能下推到数据源的过滤条件,被称为 pushedFilters

其中 postScanFilters 和 pushedFilters 中允许有相同的 filter,此时数据源和 Spark 都会进行过滤操作。在 Spark 中 parquet row group filter 就是有相同 filter 的一个例子

下推原理

那么当我们实现该接口,Spark 又是如何运作的呢?我们可以简单将其归纳为两步:

  1. 第一步:根据此接口,保留无法下推到数据源的 Filter
  2. 第二步:根据此接口,最终生成物理计划时,在获取数据源数据的 Scan 算子中处理下推部分的 Filter。这一步中,由于不同的数据源会有不同处理,当我们自定义拓展数据源时,一般由我们自己实现。

先来看第一步,第一步发生在 catalyst 的优化阶段,由 V2ScanRelationPushDown 完成。

其简化过的核心代码如下:

object V2ScanRelationPushDown extends Rule[LogicalPlan] with PredicateHelper {
   
   
  import DataSourceV2Implicits._

  def apply(plan: LogicalPlan): LogicalPlan = {
   
   
    applyColumnPruning(pushDownAggregates(pushDownFilters(createScanBuilder(plan))))
  }

  private def pushDownFilters(plan: LogicalPlan) = plan.transform {
   
   
    case Filter(condition, sHolder: ScanBuilderHolder) =>
      val (pushedFilters, postScanFiltersWithoutSubquery) = PushDownUtils.pushFilters(
        sHolder.builder, normalizedFiltersWithoutSubquery)
     
      val filterCondition = postScanFilters.reduceLeftOption(And)
      filterCondition.map(Filter(_, sHolder)
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值