Apache Calcite
Apache Calcite 为什么能这么流行 - 知乎 (zhihu.com)
简介
途中是使用apache calcite或至少关联的项目。
那 Apache Calcite 究竟是干嘛的,又为什么能这么流行呢?
首先,摆一个应该没多少人会反对的共识:SQL 是编程领域最流行的语言。
- 有 MySQL、Oracle 之类使用 SQL 作为交互语言的数据库
- 然后有 JDBC、ODBC 之类和各种数据库交互的标准接口
- 有大量数据科学家和数据分析师等不太会编程语言但又要使用数据的人
- 第一代大数据计算引擎 MapReduce 被 Hive SQL 很大程度上替代
- 新一代大数据计算引擎 Spark 很快就推出了 Spark SQL
- 最近几年大热的流处理引擎 Flink 很快也推出了 Flink SQL
- ......
比如要给MongoDB套上一个SQL的壳子,或者想要直接用SQL查一堆CSV文件,可怕没多少人能顺利实现。
Apache Calcite的出现,让你能够很容易的给你的系统套上一个SQL的壳子,并且提供足够高效的查询性能优化。
足够简答和focus的定位
- 查询语言
- 查询优化
- 查询执行
- 数据管理
- 数据存储
通常我们可以把数据库管理系统分为上面五个组件。Calcite在设计之初九确定了自己只关注和实现图中上面三个部分,而把数据管理和数据存储留给了外部系统、计算引擎。
通常数据管理和数据存储,尤其是后者是很复杂的,也会由于数据本身的特性导致是线上的多样性。Calcite抛弃了这俩部分,而是专注于上层更加通用的模块。
另一方面,Calcite 没有去重复造各种轮子,在该用现成的东西的时候,就直接用能找到的最合适的。这是个非常好的习惯,但也是非常容易被程序员抵触的方法。
比如,作为一个SQL解决方案,关键的SQL解析这一步,Calcite没有选择造轮子,而是直接使用了开源JavaCC,来将SQL语句转换为JAVA代码,然后转换成一颗
AST供下一阶段使用。
javaCC全称为Java Compiler compiler,它是一个生成器,用于生成词法分析和语法分析器。它可以通过读取一个词法和语法描述文件,来生成一个java程序,这个java程序就包括了词法分析器和语法分析器。接着就可以用生成的词法分析器和语法分析器来对我们的输入进行判断,判断是否符合我们要求的语法规则
所以总的来说JAVACC其实就是一个解释器的生成器,它的关键在于识别模板文件来生成分析程序。
另外一个例子,为了支持后面会提到的灵活元数据功能,Calcite需要支持运行时编译java代码。默认的javaC太重,需要一个更轻量级的编译器,Calcite同样没有选择重复造轮子,而是使用了开源的Janino方案。
足够简单和focus定位,不重复造轮子,使得Calcite的实现足够简单和稳定。
当apache Spark、Flink、Presto为了加速数据处理速度,而使用代码生成技术时,都不约而同的选择了这个开源库Janino
Janino:轻巧的Java动态编译器 - 知乎 (zhihu.com)
Jaino主要有三大功能
Parser:java代码的解析器。可以解析完整的模块函数、或者是表达式等
UnParser是Parser的逆操作。也就是把抽象语法树(AST)变为Java程序。
Evaluator:最后,到了用的最多的Evaluator执行环节了。比如:为了加速表达式的执行,那我可以先使用Janino来编译表达式为JVM的bytecode,并在后续多次调用这个编译好的表达式来计算。
架构
灵活可插拔的架构
上面的图是 Calcite 官方给出的架构图。
一方面印证了我们上面提到的,Calcite 足够简单,没有做自己不该做的事;另一方面,也是更重要的,Calcite 被设计的足够模块化和可插拔。
JDBC Driver 这个模块用来支持使用 JDBC client 的应用;SQL Parser and Validator 模块用来做 SQL 解析和校验;Expressions Builder 用来支持自己做 SQL 解析和校验的框架对接;Operator Expressions 这个模块用来处理关系表达式;Metadata Providers 用来支持外部自定义元数据;Pluggable Rules 用来定义优化规则;最核心的 Query Optimizer 则专注查询优化。
功能模块的划分足够合理,足够独立,使得不用完整集成,而是可以只选择其中的一部分使用;而基本上每个模块都支持自定义,也使得用户能更多地定制系统。
上图展示里 10多种框架对Calcite的集成情况。我们看一看到像Hive就做了SQL解析,只使用了Calcite的查询优化功能;而Flink则解析到优化都使用了Calcite
Flink 对 Calcite 的使用,从这个 Flink 的架构图看的会更清楚。Flink 提供了 Table API 和 SQL API 两种形式来支持对格式化数据的处理。 SQL API 通过 Calite 的 SQL Parser 和 Validator 转成逻辑执行计划,而Table API 直接转换成 Calcite 的逻辑执行计划。二者在这里达到统一,再通过 Calcite 做优化,完了之后再转成对应的物理执行计划,Table API 对应对 DataSet 的操作,SQL API 对应对 DataStream 的操作。
以上说的集成方法,都是把 Calcite 的模块当做库使用。如果觉得太重量级,可以选择更简单的 Adapter的方式。通过类似 Spark/Flume 这些框架里自定义 Source/Sink 的方式,来实现和外部系统的数据交互操作。
上图列的就是集中典型的 Adapter,比如通过 MongoDB 的 Adapter 就能直接在应用层通过 SQL,而底层自动转换成 Java 和 MongoDB 完成交互。当然如果社区没有现成的实现,也很容易自己按照 API 规范去实现一个。
对多种异构数据源的支持
除了对标准 SQL 的支持,Calcite 还支持各种丰富的数据源。比如MongoDB和流数据
查询优化
提到查询优化,不得不提关系代数(Relational Algebra)和关系表达式(Relational Expression),这也是 Calcite 的核心概念之一。
我们看一个例子,上图的 SQL 语句通过 SQL 解析和验证后,会先被转换成下面这样的关系表达式:
很简单,就是先对两个表做 scan,完了再用 id 字段做 join,再做一个 filter,最后 project 挑出想要的字段。
如果要用一句话解释关系代数,就是定义了什么样的关系表达式是等价的。
上面的关系表达式会被 Calcite 通过关系代数转换成下面这样:
相信大家都看的懂,通过 push down filter 和 push down project,大大减少了数据量,也就是减少了IO,而 IO 通常都是性能瓶颈,自然就大大提升了性能。
这样提升性能的等价关系表达式转换,就是查询优化。
而类似 push down filter 这样的转换就叫做 planner rule。类似这样的 rule 还有很多,不一一罗列了。
知道了怎么优化,怎么样算优化完了呢,不可能一直这么转换下去吧?
当然不会,决定什么时候停止优化的是 Planner Engine。
目前 Calcite 支持两种 Planner Engine:
- HepPlanner,优化到关系表达式不再变化为止。
- VolcanoPlanner,优化到查询成本不再明显变化为止。
很显然,HepPlanner 更表象点,不一定就能得到最优解,但优点是速度快;而 VolcanoPlanner 则更准确些,有 cost 来量化做比较,缺点自然是稍慢。
用户可以自由选择用哪一种,比如 Hive 就用的 VolcanoPlanner 来做 CBO。
接着很自然能想到的问题,就是怎么计算 cost 了。
而这里又体现 Calcite 的灵活性了,除了提供默认的 cost function,还允许前面提到的 metadata provider提供自定义的 cost function。
通常会被用来计算 cost 的有行数、空间大小、笛卡尔积大小等等,这里不过多展开。