探知FlinkSQL背后默默支撑的“男人们“

 

主要内容

本篇主要从FlinkSQL实现的内核与原理,工作流等的视角带大家构建一幅FlinkSQL全景图(以Blink为主介绍),探知背后支撑的“男人们”(组件)。建议收藏,仅此一份。

主要内容:

1. Table API 与 SQL

2. Apache Calcite

3. 元数据

4. SQL 函数

5. Flink Planner 与 Blink Planner

6. Blink SQL执行过程

7. SQL优化器

8. 总结

Table API 与 Table SQL

Table API 和 Table SQL 集成在同一套 API 中。这套 API 的核心概念是Table,用作查询的输入和输出。

Apache Flink 具有两个关系型 API - Table API 和 Table SQL - 用于统一的流和批处理。Table API 是 Scala 和 Java 的语言集成查询 API,它允许用非常直观的方式从关系运算符(如选择、过滤和连接)组成查询。Flink 的 SQL 支持是基于 Apache Calcite,它实现了 SQL 标准。无论输入是批处理输入(DataSet)还是流输入(DataStream),在任一接口中指定的查询都具有相同的语义,并指定相同的结果。

Table API 和 SQL 接口与 Flink 的 DataStream 和 DataSet API 紧密集成。你可以很容易地在所有 API 和建立在 API 基础上的库之间切换。

Apache Calcite

Calcite 是什么

Apache Calcite是一款开源的动态数据管理框架,它提供了标准的 SQL 语言、多种查询优化和连接各种数据源的能力,但不包括数据存储、处理数据的算法和存储元数据的存储库。

Calcite采用的是业界大数据查询框架的一种通用思路,它的目标是“one size fits all(一种方案适应所有需求场景)”,希望能为不同计算平台和数据源提供统一的查询引擎。

Calcite作为一个强大的SQL计算引擎,在Flink内部的SQL引擎模块就是基于Calcite。

Calcite 的特点

  • 支持标准SQL语言;

  • 独立于编程语言和数据源,可以支持不同的前端和后端;

  • 支持关系代数、可定制的逻辑规则和基于成本模型优化的查询引擎;

  • 支持物化视图(materialized view)的管理(创建、丢弃、持久化和自动识别);

  • 基于物化视图的Lattice和Tile机制,以应用于OLAP分析;

  • 支持对流数据的查询。

Calcite 的功能

1. SQL 解析

Calcite 的SQL解析是通过JavaCC实现的,使用JavaCC编写SQL语法描述文件,将SQL解析成未经校验的AST语法树。

2. SQL 校验

无状态的校验:验证SQL语句是否符合规范。有状态的校验:通过与元数据结合验证SQL的Schema,Field,Function是否存在,输入和输出是否符合。

3. 查询优化

对RelNode和逻辑计划树进行优化,得到优化后的生成物理执行计划。

4. SQL 生成器

将物理执行计划生成特定平台的可执行程序,比如Flink,Hive,不同规则的SQL查询语句。

5. 执行

通过各个执行平台在内存中编译,然后执行查询。

FlinkSQL 结合 Calcite

一条SQL从提交到Calcite解析,优化,到最后的Flink执行,一般分以下过程:

1. Sql Parser: 将sql语句通过java cc解析成AST(语法树),在calcite中用SqlNode表示AST;

2. Sql Validator: 结合数字字典(catalog)去验证sql语法;

3. 生成Logical Plan: 将sqlNode表示的AST转换成LogicalPlan, 用relNode表示;

4. 生成 optimized LogicalPlan: 先基于calcite rules 去优化logical Plan,基于flink定制的一些优化rules去优化logical Plan;

5. 生成Flink PhysicalPlan: 这里也是基于flink里头的rules将,将optimized LogicalPlan转成成Flink的物理执行计划;

6. 将物理执行计划转成Flink ExecutionPlan: 就是调用相应的tanslateToPlan方法转换和利用CodeGen元编程成Flink的各种算子。

Table API 来提交任务的话,基本流程和运行SQL类似,稍微不同的是:table api parser: flink会把table api表达的计算逻辑也表示成一颗树,用treeNode去表式; 在这棵树上的每个节点的计算逻辑用Expression来表示。

简单说一下SQL优化:RBO(基于规则)

RBO主要是开发人员在使用SQL的过程中,有些发现有些通用的规则,可以显著提高SQL执行的效率,比如最经典的filter下推:

将Filter下推到Join之前执行,这样做的好处是减少了Join的数量,同时降低了CPU,内存,网络等方面的开销,提高效率。

SQL优化的发展,则可以分为两个阶段,即RBO(基于规则),和CBO(基于代价)

RBO和CBO的区别大概在于: RBO只为应用提供的rule,而CBO会根据给出的Cost信息,智能应用rule,求出一个Cost最低的执行计划。需要纠正很多人误区的一点是,CBO其实也是基于rule的,接触到RBO和CBO这两个概念的时候,很容易将他们对立起来。但实际上CBO,可以理解为就是加上Cost的RBO。

元数据

Catalog 提供了元数据信息,例如数据库、表、分区、视图以及数据库或其他外部系统中存储的函数和信息。

数据处理最关键的方面之一是管理元数据。元数据可以是临时的,例如临时表、或者通过 TableEnvironment 注册的 UDF。元数据也可以是持久化的,例如 Hive Metastore 中的元数据。Catalog 提供了一个统一的API,用于管理元数据,并使其可以从 Table API 和 SQL 查询语句中来访问。

1. 目前支持的类型

(1) GenericInMemoryCatalog

是基于内存实现的 Catalog,所有元数据只在 session 的生命周期内可用。

(2) JdbcCatalog

JdbcCatalog 使得用户可以将 Flink 通过 JDBC 协议连接到关系数据库。PostgresCatalog 是当前实现的唯一一种 JDBC Catalog。

(3) HiveCatalog

HiveCatalog 有两个用途:作为原生 Flink 元数据的持久化存储,以及作为读写现有 Hive 元数据的接口。

(4) 用户自定义 Catalog

Catalog 是可扩展的,用户可以通过实现 Catalog 接口来开发自定义 Catalog。想要在 SQL CLI 中使用自定义 Catalog,用户除了需要实现自定义的 Catalog 之外,还需要为这个 Catalog 实现对应的 CatalogFactory 接口。

CatalogFactory 定义了一组属性,用于 SQL CLI 启动时配置 Catalog。这组属性集将传递给发现服务,在该服务中,服务会尝试将属性关联到 CatalogFactory 并初始化相应的 Catalog 实例。

2. 元数据分类

catalog定义主要有三种数据类型接口,也就是常用到的数据库,表&视图,函数。当然还有最上层的Catalog容器。

(1) 数据库

等同于数据库中库的实例,接口定义为CatalogDatabase,定义数据库实例的元数据,一个数据库实例中包含表,视图,函数等多种对象。

(2) 表&视图

CatalogTable对应数据库中的表,CatalogView队形数据库中的视图。

是一种存储的实体,包换了字段信息,表的分区,属性,描述信息。其实说白了字段定义和之前印象的数据库很是类似。你可以对比过来。不同的是,拿flink来说,所有的表都是外部数据源,除了上面所说的,还需要访问信息,比如IP端口,mater地址,connector连接类等等。

视图是一个虚拟概念,本质上是一条SQL查询语句,底层对应一张表或者多张表。包含SQL查询语句,视图的字段信息,视图的属性等等的信息。

(3) 函数

CatalogFunction是函数元数据的接口。函数元数据包含了所在的类信息和编程语言。

3. 数据访问

Flink的Table API和SQL程序可以连接到其他外部系统,用于读和写批处理表和流表。source table提供对存储在外部系统(如数据库、消息队列或文件系统)中的数据的访问。sink table 向外部存储系统发送表。根据source和sink器的类型,它们支持不同的格式,如CSV、Avro、Parquet或ORC。

(1) TableSchema

Table Source 和 Sink需要具备对外数据源的描述能力,所以Flink定义了TableSchema对象来定义字段名称和字段类型,存储格式等等信息

(2) 时间属性

支持处理时间和时间时间

(3) Watermark

用来处理乱序的数据。

4. Table Source & Table Sink

Flink本地支持各种连接器,可以查看往期总结

  • Filesystem

  • Elasticsearch

  • Apache Kafka

  • Amazon Kinesis Data Streams

  • JDBC

  • Apache HBase

  • Apache Hive

几个主要Table Source与Sink体系

(1) StreamTableSource

流数据抽象,区分了无界数据与有界数据。

(2) LookupableTableSource

按照Join条件中的字段进行关联。

(3) FilterableTableSource

过滤不符合条件的记录。

(4) LimitableTableSource

限制记录条数。

(5) ProjectableTableSource

过滤不会被使用的字段。

(6) AppendStreamTableSink

追加模式的TableSink 支持追加,不支持更新。

(7) RetractStreamTableSink

支持召回模式的TableSink,召回模式其实就是流上的update。

(8) UpsertStreamTableSink

有则更新,无则插入

SQL 函数

临时函数和持久化函数。临时函数始终由用户创建,它容易改变并且仅在会话的生命周期内有效。持久化函数不是由系统提供,就是存储在 Catalog 中,它在会话的整个生命周期内都有效。

内置函数

Table API和SQL为用户提供了一组用于数据转换的内置函数。如果您需要的函数还不受支持,您可以实现用户定义的函数

(1) Comparison Functions(比较型函数)

  eg:value1 = value2

(2) Logical Functions(逻辑函数)

  eg: boolean1 OR boolean2

(3) Arithmetic Functions(算术函数)

 eg: numeric1 + numeric2

(4) String Functions(字符串函数)

UPPER(string)

(5) Temporal Functions(时间函数)

YEAR(date)

(6) Conditional Functions(有条件的函数)

IF(condition, true_value, false_value)

(7) Type Conversion Functions(类型转换函数)

CAST(value AS type)

(8) Collection Functions(集合函数)

array '[' INT ']'

(9) Value Construction Functions  , Value Access Functions,Grouping Functions,Hash Functions,Auxiliary Functions,Aggregate Functions,Column Functions (不一一列举)

自定义函数

(1) 标量函数(UDF)

标量函数 将标量值转换成一个新标量值,也就是对一行数据中的一个或者多个字段返回一个单值。

(2) 聚合函数(UDAGG)

自定义聚合函数(UDAGG)是把一个表(一行或者多行,每行可以有一列或者多列)聚合成一个标量值。

(3) 表值函数(UDTF)

表值函数 将标量值转换成新的行数据。可以接收一个或者多个字段作为参数,输出多行列数据。

(4) 表值聚合函数(UDTAGG)

自定义表值聚合函数(UDTAGG)可以把一个表(一行或者多行,每行有一列或者多列)聚合成另一张表,结果中可以有多行多列。

(5) 异步表值函数

异步表值函数 是异步查询外部数据系统的特殊函数。

Planner 与 Blink Planner

Flink Table/SQL体系中的Planner(即查询处理器)是沟通Flink与Calcite的桥梁,为Table/SQL API提供完整的解析、优化和执行环境。

Flink Table 的新架构实现了查询处理器的插件化,项目完整保留原有 Flink Planner (Old Planner),同时又引入了新的 Blink Planner,用户可以自行选择使用 Old Planner 还是 Blink Planner。

主要区别:

  • Blink做到了真正的流批统一,即将批看做是特殊的流,把处理批的API和处理流的API做成了一样的。也就是说不管是批数据还是流数据,底层统统都是DataStream。所以使用Blink作为table planner的程序,Table和DataSet是不能相互转换的。

  • Blink planner是不支持BatchTableSource的,它只支持StreamTableSource。

  • Blink Planner和Old Planner的FilterableTableSource是不兼容的。Old - Planner会下推PlannerExpression到FilterableTableSource。而Blink planner下推的是Expression。

  • 基于String的键值对配置项只能用于Blink Planner

  • Blink Planner会优化多个sink到同一个TableEnvironment和StreamTableEnvironment。而Old Planner会为不同的sink优化到自己的DAG中,也就是说有几个sink就有几个DAG。

  • Old Planner 不支持 catalog统计,Blink支持。

  • Old Planner 不支持版本表(versioned Table)。版本表类似HBASE中版本表的意思,每个key可以记住过去的几个值。

Blink SQL执行过程

SQL执行过程分三个阶段

(1) 从SQL到 Operation

(2) 从Operation 到 Transformation

(3) 环境的执行阶段

从SQL到 Operation

(1) 解析SQL转换为QueryOperation;

(2) SQL解析为SqlNode;

(3) 校验SqlNode;

(4) 调用Calcite SQLToRelConvertrt将SqlNode转化为RelNode逻辑树;

(5) RelNode转化为Operation。

Operation 到 Transformation

(1) DQL(数据查询语言)转换,在flink中作为中间运算;

(2) DML(数据操作语言),DQL转换。

整个转换从Operation开始,先转换为Calcite的逻辑计划树,再转化为Flink的逻辑计划树,然后进行优化。优化后的逻辑树转换为Flink的物理执行,物理执行生成一系列的算子,udf等等,包装到Transformation中。

环境的执行阶段

有了Transformation后正式进入到StreamGraph的过程中,最终交给Flink集群去运行。

SQL优化器

查询优化器

再次提到两个优化器:RBO(基于规则的优化器) 和 CBO(基于代价的优化器)

(1) RBO(基于规则的优化器)会将原有表达式裁剪掉,遍历一系列规则(Rule),只要满足条件就转换,生成最终的执行计划。一些常见的规则包括分区裁剪(Partition Prune)、列裁剪、谓词下推(Predicate Pushdown)、投影下推(Projection Pushdown)、聚合下推、limit下推、sort下推、常量折叠(Constant Folding)、子查询内联转join等。

(2) CBO(基于代价的优化器)会将原有表达式保留,基于统计信息和代价模型,尝试探索生成等价关系表达式,最终取代价最小的执行计划。CBO的实现有两种模型,Volcano模型,Cascades模型。这两种模型思想很是相似,不同点在于Cascades模型一边遍历SQL逻辑树,一边优化,从而进一步裁剪掉一些执行计划。

目前各大数据库和计算引擎倾向于CBO。

总结

在目前情况下,在阿里对Flink社区的贡献下,Flink包含了Flink SQL 和 Blink SQL体系,Flink Planner称之为 Old Planner,Blink Planner称之为 New Planner。从中可以发现 Blink Planner是未来,Flink Planner将会被淘汰。

FlinkSQL依靠 Calcite提供了一套SQL验证,解析,优化等等操作。同时FlinkSQL提供元数据管理,SQL函数,数据源的建设。也自由化地提供了自定义函数,自定义connector连接,丰富了场景的使用。

FlinkSQL你值得拥有!!!

 

大数据左右手

技术如同手中的水有了生命似的,汇聚在了一起。作为大数据开发工作者,致力于大数据技术的学习与工作,分享大数据原理、架构、实时、离线、面试与总结,分享生活思考与读书见解。总有适合你的那一篇。

关注公众号!!!

和我联系吧,加群交流大数据知识,一起成长~~~

工厂模式定义:提供创建对象的接口. 为何使用? 工厂模式是我们最常用的模式了,著名的Jive论坛 ,就大量使用了工厂模式,工厂模式在Java程序系统可以说是随处可见。 为什么工厂模式是如此常用?因为工厂模式就相当于创建实例对象的new,我们经常要根据类Class生成实例对象,如A a=new A() 工厂模式也是用来创建实例对象的,所以以后new时就要多个心眼,是否可以考虑实用工厂模式,虽然这样做,可能多做一些工作,但会给你系统带来更大的可扩展性和尽量少的修改量。 我们以类Sample为例, 如果我们要创建Sample的实例对象: Sample sample=new Sample(); 可是,实际情况是,通常我们都要在创建sample实例时做点初始化的工作,比如赋值 查询数据库等。 首先,我们想到的是,可以使用Sample的构造函数,这样生成实例就写成: Sample sample=new Sample(参数); 但是,如果创建sample实例时所做的初始化工作不是象赋值这样简单的事,可能是很长一段代码,如果也写入构造函数中,那你的代码很难看了(就需要Refactor重整)。 为什么说代码很难看,初学者可能没有这种感觉,我们分析如下,初始化工作如果是很长一段代码,说明要做的工作很多,将很多工作装入一个方法中,相当于将很多鸡蛋放在一个篮子里,是很危险的,这也是有背于Java面向对象的原则,面向对象的封装(Encapsulation)和分派(Delegation)告诉我们,尽量将长的代码分派“切割”成每段,将每段再“封装”起来(减少段和段之间偶合联系性),这样,就会将风险分散,以后如果需要修改,只要更改每段,不会再发生牵一动百的事情。 在本例中,首先,我们需要将创建实例的工作与使用实例的工作分开, 也就是说,让创建实例所需要的大量初始化工作从Sample的构造函数中分离出去。 这时我们就需要Factory工厂模式来生成对象了,不能再用上面简单new Sample(参数)。还有,如果Sample有个继承如MySample, 按照面向接口编程,我们需要将Sample抽象成一个接口.现在Sample是接口,有两个子类MySample 和HisSample .我们要实例化他们时,如下: Sample mysample=new MySample(); Sample hissample=new HisSample(); 随着项目的深入,Sample可能还会"生出很多儿子出来", 那么我们要对这些儿子一个个实例化,更糟糕的是,可能还要对以前的代码进行修改:加入后来生出儿子的实例.这在传统程序中是无法避免的. 但如果你一开始就有意识使用了工厂模式,这些麻烦就没有了. 工厂方法 你会建立一个专门生产Sample实例的工厂: public class Factory{   public static Sample creator(int which){   //getClass 产生Sample 一般可使用动态类装载装入类。   if (which==1)     return new SampleA();   else if (which==2)     return new SampleB();   } } 那么在你的程序中,如果要实例化Sample时.就使用 Sample sampleA=Factory.creator(1); 这样,在整个就不涉及到Sample的具体子类,达到封装效果,也就减少错误修改的机会,这个原理可以用很通俗的话来比喻:就是具体事情做得越多,越容易范错误.这每个做过具体工作的人都深有体会,相反,官做得越高,说出的话越抽象越笼统,范错误可能性就越少.好象我们从编程序中也能悟出人生道理?呵呵. 使用工厂方法 要注意几个角色,首先你要定义产品接口,如上面的Sample,产品接口下有Sample接口的实现类,如SampleA,其次要有一个factory类,用来生成产品Sample,如下图,最右边是生产的对象Sample: 进一步稍微复杂一点,就是在工厂类上进行拓展,工厂类也有继承它的实现类concreteFactory了。 抽象工厂 工厂模式中有: 工厂方法(Factory Method) 抽象工厂(Abstract Factory). 这两个模式区别在于需要创建对象的复杂程度上。如果我们创建对象的方法变得复杂了,如上面工厂方法中是创建一个对象Sample,如果我们还有新的产品接口Sample2. 这里假设:Sample有两个concrete类SampleA和SamleB,而Sample2也有两个concrete类Sample2A和SampleB2 那么,我们就将上例中Factory变成抽象类,将共同部分封装在抽象类中,不同部分使用子类实现,下面就是将上例中的Factory拓展成抽象工厂: public abstract class Factory{   public abstract Sample creator();   public abstract Sample2 creator(String name); } public class SimpleFactory extends Factory{   public Sample creator(){     .........     return new SampleA   }   public Sample2 creator(String name){     .........     return new Sample2A   } } public class BombFactory extends Factory{   public Sample creator(){     ......     return new SampleB   }   public Sample2 creator(String name){     ......     return new Sample2B   } } 从上面看到两个工厂各自生产出一套Sample和Sample2,也许你会疑问,为什么我不可以使用两个工厂方法来分别生产Sample和Sample2? 抽象工厂还有另外一个关键要点,是因为 SimpleFactory内,生产Sample和生产Sample2的方法之间有一定联系,所以才要将这两个方法捆绑在一个类中,这个工厂类有其本身特征,也许制造过程是统一的,比如:制造工艺比较简单,所以名称叫SimpleFactory。 在实际应用中,工厂方法用得比较多一些,而且是和动态类装入器组合在一起应用, 举例 我们以Jive的ForumFactory为例,这个例子在前面的Singleton模式中我们讨论过,现在再讨论其工厂模式: public abstract class ForumFactory {   private static Object initLock = new Object();   private static String className = "com.jivesoftware.forum.database.DbForumFactory";   private static ForumFactory factory = null;   public static ForumFactory getInstance(Authorization authorization) {     //If no valid authorization passed in, return null.     if (authorization == null) {       return null;     }     //以下使用了Singleton 单态模式     if (factory == null) {       synchronized(initLock) {         if (factory == null) {             ......           try {               //动态转载类               Class c = Class.forName(className);               factory = (ForumFactory)c.newInstance();           }           catch (Exception e) {               return null;           }         }       }     }     //Now, 返回 proxy.用来限制授权对forum的访问     return new ForumFactoryProxy(authorization, factory,                     factory.getPermissions(authorization));   }   //真正创建forum的方法由继承forumfactory的子类去完成.   public abstract Forum createForum(String name, String description)   throws UnauthorizedException, ForumAlreadyExistsException;   .... } 因为现在的Jive是通过数据库系统存放论坛帖子等内容数据,如果希望更改为通过文件系统实现,这个工厂方法ForumFactory就提供了提供动态接口: private static String className = "com.jivesoftware.forum.database.DbForumFactory"; 你可以使用自己开发的创建forum的方法代替com.jivesoftware.forum.database.DbForumFactory就可以. 在上面的一段代码中一共用了三种模式,除了工厂模式外,还有Singleton单态模式,以及proxy模式,proxy模式主要用来授权用户对forum的访问,因为访问forum有两种人:一个是注册用户 一个是游客guest,那么那么相应的权限就不一样,而且这个权限是贯穿整个系统的,因此建立一个proxy,类似网关的概念,可以很好的达到这个效果. 看看Java宠物店中的CatalogDAOFactory: public class CatalogDAOFactory {   /**   * 本方法制定一个特别的子类来实现DAO模式。   * 具体子类定义是在J2EE的部署描述器中。   */   public static CatalogDAO getDAO() throws CatalogDAOSysException {     CatalogDAO catDao = null;     try {       InitialContext ic = new InitialContext();       //动态装入CATALOG_DAO_CLASS       //可以定义自己的CATALOG_DAO_CLASS,从而在无需变更太多代码       //的前提下,完成系统的巨大变更。       String className =(String) ic.lookup(JNDINames.CATALOG_DAO_CLASS);       catDao = (CatalogDAO) Class.forName(className).newInstance();     } catch (NamingException ne) {       throw new CatalogDAOSysException("         CatalogDAOFactory.getDAO: NamingException while           getting DAO type : \\n" + ne.getMessage());     } catch (Exception se) {       throw new CatalogDAOSysException("         CatalogDAOFactory.getDAO: Exception while getting           DAO type : \\n" + se.getMessage());     }     return catDao;   } } CatalogDAOFactory是典型的工厂方法,catDao是通过动态类装入器className获得CatalogDAOFactory具体实现子类,这个实现子类在Java宠物店是用来操作catalog数据库,用户可以根据数据库的类型不同,定制自己的具体实现子类,将自己的子类名给与CATALOG_DAO_CLASS变量就可以。 由此可见,工厂方法确实为系统结构提供了非常灵活强大的动态扩展机制,只要我们更换一下具体的工厂方法,系统其他地方无需一点变换,就有可能将系统功能进行改头换面的变化。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值