本篇主要介绍了一种使用Rust语言编写的查询引擎——DataFusion,其使用了基于Arrow格式的内存模型,结合Rust语言本身的优势,达成了非常优秀的性能指标

DataFusion是一个查询引擎而非数据库,因此其本身不具备存储数据的能力。但正因为不依赖底层存储的格式,使其成为了一个灵活可扩展的查询引擎。它原生支持了查询CSV,Parquet,Avro,Json等存储格式,也支持了本地,AWS S3,Azure Blob Storage,Google Cloud Storage等多种数据源。同时还提供了丰富的扩展接口,可以方便的让我们接入自定义的数据格式和数据源。

DataFusion具有以下特性:

  • 高性能:基于Rust,不用进行垃圾回收;基于Arrow内存模型,列式存储,方便向量化计算
  • 连接简单:能够与Arrow的其他生态互通
  • 集成和定制简单:可以扩展数据源,方法和算子等
  • 完全基于Rust编写:高质量

基于DataFusion我们可以轻松构建高性能、高质量、可扩展的数据处理系统。

DBMS 与 Query Engine 的区别

DBMS: DataBase Management System

DBMS是一个包含完整数据库管理特性的系统,主要包含以下几个模块:

  • 存储系统
  • 元数据(Catalog)
  • 查询引擎(Query Engine)
  • 访问控制和权限
  • 资源管理
  • 管理工具
  • 客户端
  • 多节点管理

Query Engine

DataFusion是一种查询引擎,查询引擎属于数据库管理系统的一部分。查询引擎是用户与数据库交互的主要接口,主要作用是将面向用户的高阶查询语句翻译成可被具体执行的数据处理单元操作,然后执行操作获取数据。

DataFusion架构

架构详情

DataFusion查询引擎主要由以下几部分构成:

  1. 前端

    • 语法解析
    • 语义分析
    • Planner:语法树转换成逻辑计划

主要涉及DFParserSqlToRel这两个struct

  1. 查询中间表示

    • Expression(表达式)/ Type system(类型系统)
    • Query Plan / Relational Operators(关系算子)
    • Rewrites / Optimizations(逻辑计划优化)

主要涉及LogicalPlanExpr这两个枚举类

  1. 查询底层表示

    • Statistics(物理计划算子的统计信息,辅助物理计划优化)
    • Partitions(分块,多线程执行物理计划算子)
    • Sort orders(物理计划算子对数据是否排序)
    • Algorithms(物理计划算子的执行算法,如Hash join和Merge join)
    • Rewrites / Optimizations(物理计划优化)

主要涉及PyhsicalPlanner这个trait实现的逻辑计划到物理计划的转换,其中主要的关键点是ExecutionPlanPhysicalExpr

  1. 执行运行时(算子)

    • 分配资源
    • 向量化计算

主要涉及所有执行算子,如GroupedHashAggregateStream

扩展点

DataFusion查询引擎的架构还是比较简单的,其中的扩展点也非常清晰,我们可以从以下几个方面对DataFusion进行扩展:

用户自定义函数UDF

无状态方法

/// 逻辑表达式枚举类
pub enum Expr {
...
ScalarUDF {
/// The function
fun: Arc<ScalarUDF>,
/// List of expressions to feed to the functions as arguments
args: Vec<Expr>,
},
...
}
/// UDF的逻辑表达式
pub struct ScalarUDF {
/// 方法名
pub name: String,
/// 方法签名
pub signature: Signature,
/// 返回值类型
pub return_type: ReturnTypeFunction,
/// 方法实现
pub fun: ScalarFunctionImplementation,
}
/// UDF的物理表达式
pub struct ScalarFunctionExpr {
fun: ScalarFunctionImplementation,
name: String,
/// 参数表达式列表
args: Vec<Arc<dyn PhysicalExpr>>,
return_type: DataType,
}

用户自定义聚合函数UADF

有状态方法

/// 逻辑表达式枚举类
pub enum Expr {
...
AggregateUDF {
/// The function
fun: Arc<AggregateUDF>,
/// List of expressions to feed to the functions as arguments
args: Vec<Expr>,
/// Optional filter applied prior to aggregating
filter: Option<Box<Expr>>,
},
...
}
/// UADF的逻辑表达式
pub struct AggregateUDF {
/// 方法名
pub name: String,
/// 方法签名
pub signature: Signature,
/// 返回值类型
pub return_type: ReturnTypeFunction,
/// 方法实现
pub accumulator: AccumulatorFunctionImplementation,
/// 需要保存的状态的类型
pub state_type: StateTypeFunction,
}
/// UADF的物理表达式
pub struct AggregateFunctionExpr {
fun: AggregateUDF,
args: Vec<Arc<dyn PhysicalExpr>>,
data_type: DataType,
name: String,
}

用户自定义优化规则

Optimizer定义了承载优化规则的结构体,其中optimize方法实现了逻辑计划优化的过程。优化规则列表中的每个优化规则会被以TOP-DOWNBOTTOM-UP方式作用于逻辑计划树,优化规则列表会被实施多个轮次。我们可以通过实现OptimizerRule这个trait来实现自己的优化逻辑。

pub struct Optimizer {
/// All rules to apply
pub rules: Vec<Arc<dyn OptimizerRule + Send + Sync>>,
} pub trait OptimizerRule {
/// Try and rewrite `plan` to an optimized form, returning None if the plan cannot be
/// optimized by this rule.
fn try_optimize(
&self,
plan: &LogicalPlan,
config: &dyn OptimizerConfig,
) -> Result<Option<LogicalPlan>>; ...
}

用户自定义逻辑计划算子

/// 逻辑计划算子枚举类
pub enum LogicalPlan {
...
Extension(Extension),
...
}
/// 自定义逻辑计划算子
pub struct Extension {
/// The runtime extension operator
pub node: Arc<dyn UserDefinedLogicalNode>,
}
/// 自定义逻辑计划算子需要实现的trait
pub trait UserDefinedLogicalNode: fmt::Debug + Send + Sync { ... }

用户自定义物理计划算子

/// 为自定义的逻辑计划算子`UserDefinedLogcialNode`生成对应的物理计划算子
pub trait ExtensionPlanner {
async fn plan_extension(
&self,
planner: &dyn PhysicalPlanner,
node: &dyn UserDefinedLogicalNode,
logical_inputs: &[&LogicalPlan],
physical_inputs: &[Arc<dyn ExecutionPlan>],
session_state: &SessionState,
) -> Result<Option<Arc<dyn ExecutionPlan>>>;
}
/// DataFusion默认的逻辑计划到物理计划的转换器提供了自定义转换过程的结构体
pub struct DefaultPhysicalPlanner {
extension_planners: Vec<Arc<dyn ExtensionPlanner + Send + Sync>>,
}
/// 自定义物理计划算子需要实现的trait
pub trait ExecutionPlan: Debug + Send + Sync { ... }

用户自定义数据源

可以看出,自定义数据源其实就是生成一个对应的ExecutionPlan执行计划,这个执行计划实施的是扫表的任务。如果数据源支持下推的能力,我们在这里可以将projection filters limit等操作下推到扫表时。

/// 自定义数据源需要实现的trait
pub trait TableProvider: Sync + Send {
...
async fn scan(
&self,
state: &SessionState,
projection: Option<&Vec<usize>>,
filters: &[Expr],
limit: Option<usize>,
) -> Result<Arc<dyn ExecutionPlan>>;
...
}

用户自定义元数据

pub trait CatalogProvider: Sync + Send {
... /// 根据名称获取Schema
fn schema(&self, name: &str) -> Option<Arc<dyn SchemaProvider>>;
/// 注册Schema
fn register_schema(
&self,
name: &str,
schema: Arc<dyn SchemaProvider>,
) -> Result<Option<Arc<dyn SchemaProvider>>> {
// use variables to avoid unused variable warnings
let _ = name;
let _ = schema;
Err(DataFusionError::NotImplemented(
"Registering new schemas is not supported".to_string(),
))
}
} pub trait SchemaProvider: Sync + Send {
...
/// 根据表名获取数据源
async fn table(&self, name: &str) -> Option<Arc<dyn TableProvider>>;
/// 注册数据源
fn register_table(
&self,
name: String,
table: Arc<dyn TableProvider>,
) -> Result<Option<Arc<dyn TableProvider>>> {
Err(DataFusionError::Execution(
"schema provider does not support registering tables".to_owned(),
))
}
...
}

逻辑计划(LogicalPlan)

逻辑计划其实就是数据流图,数据从叶子节点流向根节点

let df: DataFrame = ctx.read_table("http_api_requests_total")?
.filter(col("path").eq(lit("/api/v2/write")))?
.aggregate([col("status")]), [count(lit(1))])?;

这里我们就使用DataFusion的API接口构造了一个数据流,首先read_table节点会从数据源中扫描数据到内存中,然后经过filter节点按照条件进行过滤,最后经过aggregate节点进行聚合。数据流过最后的节点时,就生成了我们需要的数据。

上述链式调用的API接口实际上并没有真正执行对数据的操作,这里实际上是使用了建造者模式构造了逻辑计划树。最终生成的DataFrame实际上只是包含了一下信息:

pub struct DataFrame {
/// 查询上下文信息,包含了元数据,用户注册的UDF和UADF,使用的优化器,使用的planner等信息
session_state: SessionState,
/// 逻辑计划树的根节点
plan: LogicalPlan,
}

支持的逻辑计划算子

点击查看代码
Projection
Filter
Window
Aggregate
Sort
Join
TableScan Repartition
Union
Subquery
Limit
Extension
Distinct Values
Explain
Analyze
SetVariable
Prepare
Dml(...) CreateExternalTable
CreateView
CreateCatalogSchema
CreateCatalog
DropTable
DropView

逻辑计划优化

目标:确保结果相同的情况下,执行更快

初始的逻辑计划,需要经过多个轮次的优化,才能生成执行效率更高的逻辑计划。DataFusion本身的优化器内置了很多优化规则,用户也可以扩展自己的优化规则。

内置优化轮次

  1. 下推(Pushdown):减少从一个节点到另一个节点的数据的行列数

    • PushDownProjection
    • PushDownFilter
    • PushDownLimit
  2. 简化(Simplify):简化表达式,减少运行时的运算。例如使用布尔代数的法则,将b > 2 AND b > 2简化成b > 2

    • SimplifyExpressions
    • UnwrapCastInComparison
  3. 简化(Simplify):删除无用的节点

  4. 平铺子查询(Flatten Subqueries):将子查询用join重写

    • DecorrelateWhereExists
    • DecorrelatedWhereIn
    • ScalarSubqueryToJoin
  5. 优化join:识别join谓词

    • ExtractEqualJoinPredicate
    • RewriteDisjunctivePredicate
    • FilterNullJoinKeys
  6. 优化distinct

    • SingleDistinctToGroupBy
    • ReplaceDistinctWithAggregate

表达式运算(Expression Evaluation)

假设现在有这样一个谓词表达式

path = '/api/v2/write' or path is null

经过语法解析和转换后,可以用如下表达式树表示:

DataFusion在实施表达式运算时,使用了Arrow提供的向量化计算方法来加速运算

物理计划(ExecutionPlan)

调用DataFusion提供的DefaultPhysicalPlanner中的create_physical_plan方法,可以将逻辑计划树转换成物理计划树。其中物理计划树中的每个节点都是一个ExecutionPlan。执行物理计划树时,会从根节点开始调用execute方法,调用该方法还没有执行对数据的操作,仅仅是将每个物理计划算子转换成一个RecordBatchStream算子,形成数据流算子树。这些RecordBatchStream算子都实现了future包提供的Stream特性,当我们最终调用RecordBatchStreamcollect方法时,才会从根节点开始poll一次来获取一下轮要处理的数据,根节点的poll方法内会调用子节点的poll方法,最终每poll一次,整棵树都会进行一次数据从叶子节点到根节点的流动,生成一个RecordBatch

DataFusion实现的物理计划算子具有以下特性:

  • 异步:避免了阻塞I/O
  • 流式:数据是流式处理的
  • 向量化:每次可以向量化地处理一个RecordBatch
  • 分片:每个算子都可以并行,可以产生多个分片
  • 多核

Apache Arrow DataFusion原理与架构的更多相关文章

  1. Zookeeper概论(对zookeeper的概论、原理、架构等的理解)

    Zookeeper概论(对zookeeper的概论.原理.架构等的理解) 一.概论 Zookeeper是一个分布式的.开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,是h ...

  2. zabbix监控的基础概念、工作原理及架构(一)

    zabbix监控的基础概念.工作原理及架构 转载于网络 一.什么是zabbix及优缺点 Zabbix能监视各种网络参数,保证服务器系统的安全运营,并提供灵活的通知机制以让系统管理员快速定位/解决存在的 ...

  3. paip.性能跟踪profile原理与架构与本质-- python扫带java php

    paip.性能跟踪profile原理与架构与本质-- python扫带java php ##背景 弄个个输入法音标转换atiEnPH工具,老是python性能不的上K,7k记录浏览过k要30分钟了. ...

  4. Apache Arrow 内存数据

    1.概述 Apache Arrow 是 Apache 基金会全新孵化的一个顶级项目.它设计的目的在于作为一个跨平台的数据层,来加快大数据分析项目的运行速度. 2.内容 现在大数据处理模型很多,用户在应 ...

  5. apache和nginx原理上的不同之处

    今天群里提到面试时问到apache和nginx原理有什么不同,一时还真没想起,想到的只是他们的优缺点,便搜索了下.记录学习下.顺便记录下优缺点吧. 原理不同之处: 为什么Nginx的性能要比Apach ...

  6. 老李推荐: 第14章2节《MonkeyRunner源码剖析》 HierarchyViewer实现原理-HierarchyViewer架构概述

    老李推荐: 第14章2节<MonkeyRunner源码剖析> HierarchyViewer实现原理-HierarchyViewer架构概述   HierarchyViewer库的引入让M ...

  7. Apache 运行PHP原理

    php,apache和mysql组合的工作过程: PHP的所有应用程序都是通过WEB服务器(如IIS或Apache)和PHP引擎程序解释执行完成的,工作过程: (1)当用户在浏览器地址中输入要访问的P ...

  8. Atitit. servlet 与 IHttpHandler  ashx  listen 和HttpModule的区别与联系 原理理论 架构设计   实现机制    java php c#.net js javascript  c++ python

    Atitit. servlet 与 IHttpHandler  ashx  listen 和HttpModule的区别与联系 原理理论 架构设计   实现机制    java php c#.net j ...

  9. Atitit.数据库表的物理存储结构原理与架构设计与实践

    Atitit.数据库表的物理存储结构原理与架构设计与实践 1. Oracle和DB2数据库的存储模型如图: 1 1.1. 2. 表数据在块中的存储以及RowId信息3 2. 数据表的物理存储结构 自然 ...

  10. php开发面试题---Apache 运行PHP原理(整理)

    php开发面试题---Apache 运行PHP原理(整理) 一.总结 一句话总结: 不要忘记 php引擎将页面静态化 和 php引擎和apache之间通讯 反思的回顾非常有用,因为决定了我的方向和技巧 ...

随机推荐

  1. windows下 mstsc 远程Ubuntu 图形界面

    安装及设置xrdp ------------------------------------------------------ touch ~/installXrdp.sh  cat > ~/ ...

  2. Hugging Face 每周速递: Chatbot Hackathon;FLAN-T5 XL 微调;构建更安全的 LLM

    每一周,我们的同事都会向社区的成员们发布一些关于 Hugging Face 相关的更新,包括我们的产品和平台更新.社区活动.学习资源和内容更新.开源库和模型更新等,我们将其称之为「Hugging Ne ...

  3. 如何使用Github创建一个仓库

    创建仓库(对我来说,这是新建) 点击这里的Create repository: 进入到这样一个界面: 其中,Repository name,是我们即将创建完成的仓库名称: 而这里: 需要填写的是对仓库 ...

  4. 在CentOS中搭建NFS

    概述 NFS是一款经典的网络文件系统,在Linux上我们可以通过创建一个NFS服务在不同的服务器之间共享磁盘文件,而不用在多个服务器之间进行不断的拷贝复制,麻烦且浪费存储空间.在k8s中我们也可以使用 ...

  5. Windows命令行备份文件

    windows命令行备份文件 0 前言 前段时间,笔者因为在C盘爆满的情况下被windows自动更新了(大概),出现了以下情况: 在试了几种方法后不起作用,无奈下只能重装系统. 在这之前要把之前的一些 ...

  6. svn提交规范

    本文档参考了Git提交规范,旨在规范使用SVN进行代码版本管理时的提交操作. 提交前的准备 1. 检查代码 在提交代码前,请先进行必要的代码检查,确保代码的正确性.可读性和可维护性.可以使用代码质量管 ...

  7. .Net7 CLR的调用函数和编译函数

    前言 .Net运行模型,无非就两个过程.一个是调用入口函数,另外一个就是编译入口函数.前者主调用,后者主编译. 概括 一:入口函数:RunMainInternal 所有的.Net程序,包括控制台,We ...

  8. 3分钟带你了解Hadoop是什么

    Hadoop是一种开源的分布式计算框架,它在Google的MapReduce论文发表后大受欢迎,并被广泛应用.Hadoop框架包括一个分布式文件系统(HDFS),它允许用户以分布式方式存储和管理大量数 ...

  9. Java面试——MyBatis

    一.MyBatis 与 JDBC 的区别 [1]JDBC 是 Java 提供操作数据库的 API:MyBatis 是一个持久层 ORM 框架,底层是对 JDBC 的封装.[2]使用 JDBC 需要连接 ...

  10. 淘宝商品信息定向爬虫.py(亲测有效)

    import requests import re def getHTMLText(url): try: kv = { 'cookie': '', #要换成自己网页的cookie 'user-agen ...