精通Apache Flink读书笔记--3、4

3、批处理API

流处理有其价值所在,但很多场景下用不到也没必要使用流处理。有时候,批处理也能发挥很好的作用。Flink支持批处理,而且认为批处理是流处理的一种特殊形式。

这块做下简单的解释,为什么说批是流的特殊情况?

流的简单处理形式就是来一条处理一条,但是如果将到达的数据buffer起来,当到达一定的条件时,再一次性处理这些数据,这也算是流处理;仔细想想,这其实更像批处理或微批(micro-batch)。

这也是为什么说在Flink中,批是流的一种特殊形式了。

这个问题的具体解释,见:Batch is a special case of streaming

批处理 API的流程和流处理一样,也有获得执行环境、source、transformation、sink等步骤。

3.1、数据源

批处理API的数据源可以是文件或者java collections。DataSet API提供了很多预定义的source函数,当然你也可以自定义数据源。先看看内建的数据源。

3.1.1、基于文件

基于文件的数据源,是一行一行读取,且每行读到的数据作为字符串处理。

readTextFile(Stringpath):默认读取TextInputFormat格式,每行作为一个字符串;

readTextFileWithValue(Stringpath):返回StringValues,StringValues作为mutable字符串;

readCsvFile(Stringpath):返回Java POJOS或者tuples;

readFileofPremitives(path, delimiter, class):解析一行数据到指定的class;

readHadoopFile(FileInputFormat, Key, Value, path):读取Hadoop文件,指定路径、文件格式以及key、value class;具体参见下边的图;

readSequenceFile(Key, Value, path):读取SequenceFile格式的文件,同样需指定key、value的class

关于读取HDFS文件:

这里写图片描述

这里还可以通过JDBC读取关系数据库中的表数据:
这里写图片描述

注意:基于文件的数据源,支持递归遍历,循环读取文件中的数据作为数据源。但是我们需要设置recursive.file.enumeration为true以激活此功能。

这里写图片描述

3.1.2、基于Collection

Flink DataSet API也支持读取java集合中的数据。

fromCollection(Collection)

fromCollection(Iterator, Class):也可以读取iterator,其数据本身的类型为指定的class;

fromElements(T):读取sequence对象;

fromParallelCollection(SplittableIterator, Class):读取并行iterator;

generateSequence(from, to):读取一定范围的sequnce对象。

3.1.3、通用数据源

readFile(inputFormat, path):指定路径,指定FileInputFormat;

createInput(inputFormat)

3.1.4、压缩文件

Flink支持自动读取压缩文件,扩展名为.gz、.gzip、.deflate的压缩文件。

注意读取压缩文件,不能并行处理,因此加载解压的时间会稍微有点长。

3.2、Transformations

在transformation部分,有些算子操作和流处理的是一样的,这里不做一一介绍。只介绍一些在流处理中没有的操作。

Distinct

可以讲DataSet中的元素去重,这在流处理中无法做到,因为流是无界的,要去重也必须在一定的有界范围内去重,例如窗口。但是目前Flink流处理中还不支持。

DataSet<Tuple2<Integer, Double>> output = input.distinct();

Cross

两个DataSet进行笛卡尔积操作,将会产生非常大的数据集。建议设置DataSet的大小或crossWithTiny() 和crossWithHuge()来限制一个DataSet的大小。轻易不要用。

DataSet<Integer> data1 = // [...]
DataSet<String> data2 = // [...]
DataSet<Tuple2<Integer, String>> result = data1.cross(data2);

Range partition

根据指定的key,将dataSet范围分片。

DataSet<Tuple2<String,Integer>> in = // [...]
DataSet<Integer> result = in.partitionByRange(0)
                            .mapPartition(new PartitionMapper());

Sort partition

根据key,将dataSet按照key的升序或降序重分片。

DataSet<Tuple2<String,Integer>> in = // [...]
DataSet<Integer> result = in.sortPartition(1, Order.ASCENDING)
                            .mapPartition(new PartitionMapper());

First-n

随机取出dataSet的前10个元素,first-n也可以应用在分组后的数据集上。

DataSet<Tuple2<String,Integer>> in = // [...]
// regular data set
DataSet<Tuple2<String,Integer>> result1 = in.first(3);
// grouped data set
DataSet<Tuple2<String,Integer>> result2 = in.groupBy(0)
                                            .first(3);
// grouped-sorted data set
DataSet<Tuple2<String,Integer>> result3 = in.groupBy(0)
                                            .sortGroup(1, Order.ASCENDING)
                                            .first(3);

例如应用在sortGroup集合上,first(3)将返回排序后的前3个数据。

3.3、广播变量

广播变量允许用户将特定的DataSet发送到各个节点的内存中。值得注意的是,由于是发送dataSet,因此这个dataSet的大小不能太大。

广播变量的使用主要分为2步:

1)将dataSet广播出去:withBroadcastSet(DataSet, String)
 (2)获取:在其他operator中,通过继承RichXXFunction,重写open方法来获得:  getRuntimeContext().getBroadcastVariable(String)

例如:

这里写图片描述

这里还有一个K-mean算法的例子,也用到了广播变量:K-Means Algorithm

3.4、Data Sinks

这块的内容比较简单,直接看一些例子:

这里写图片描述

自定义Data Sink的例子:

这里写图片描述

这里举了一个JDBC sink到数据库的例子。如果想sink到oracle这种不开源的数据库,则需要通过maven引入oracle的jar包,具体操作可参见:maven3 手动安装本地jar到仓库

3.4、Connectors

Flink DataSet API支持许多connectors,用于对外部存储的读写。

3.4.1 文件系统

Flink支持HDFS、S3, Google CloudStorage, Alluxio等,我们需要在pom中引入文件系统的依赖:

<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-hadoop-compatibility_2.11</artifactId>
    <version>1.1.4</version>
</dependency>

为了使用Hadoop文件系统,需要确保:

1、Flink配置文件flink-conf.yaml已经设置了fs.hdfs.hadoopconf的配置
2、在hadoop的配置文件中,要有这些组件的入口,例如S3,Alluxio等的配置
3、要将这些文件系统需要的class文件放到Flink所有节点的lib目录下,如果不方便放,则可以通过HADOOP_CLASSPATH环境变量将这些hadoop jar放到相应的类路径中。

例如S3,你需要在core-site.xml中作如下配置:

<!-- configure the file system implementation -->
<property>
  <name>fs.s3.impl</name>
  <value>org.apache.hadoop.fs.s3native.NativeS3FileSystem</value>
</property>

<!-- set your AWS ID -->
<property>
  <name>fs.s3.awsAccessKeyId</name>
  <value>putKeyHere</value>
</property>

<!-- set your AWS access key -->
<property>
  <name>fs.s3.awsSecretAccessKey</name>
  <value>putSecretHere</value>
</property>

例如Alluxio,在core-site.xml中添加:

<property>
  <name>fs.alluxio.impl</name>
  <value>alluxio.hadoop.FileSystem</value>
</property>

其他的connector这里不再一一举例,贴张官网一张connector的图:

这里写图片描述

3.4.2 mongoDB

mongoDB:Access MongoDB

最后,对于Flink DataSet Sink,我们可以通过addSink()自定义一些输出,例如输出到InfluxDB、oracle、mysql、HBase等。这里不再做详细介绍。

3.5、迭代

迭代主要用于机器学习、图计算等,Flink通过step函数来支持迭代运算。

3.5.1、迭代器算子

迭代器算子包括以下几步:

这里写图片描述

1、迭代输入:要么来自source,要么是上一个迭代的输出;
2step函数:应用在DataSet数据集上;
3Next Partial Solution:step函数都有输出,用于下一次迭代的输入;
4、output:迭代结束或者通过设置一些条件,终止迭代。

终止迭代的方式有很多,例如:

1、设置迭代次数
2、自定义迭代终止条件

例如下面计算pi的例子:

这里写图片描述

通过iterate()方法设置最大跌代次数,并将DataSet转换为IterativeDataSet;之后的step函数则是map函数,closeWith(DataSet)代表传递给下一次迭代的数据集是什么,即Next Partial Solution要表达的。

3.5.2、增量迭代

增量迭代与上一节降到的普通迭代的区别是:增量迭代是更新上一步迭代的结果,而不是全部重新计算一次。

增量迭代会使得计算更加有效,时间更短。下面展示增量迭代的数据流:

这里写图片描述

1、迭代输入
2step函数
3Next Work Set/ Update Solution:分为workSet和solutionSet,solutionSet维护这上一次迭代后的状态信息,通过step函数,更新solutionSet,并将结束(新的workSet)用于下一次迭代。
4、output

这里写图片描述

这里详细说明增量迭代如何开发:

// read the initial data sets
DataSet<Tuple2<Long, Double>> initialSolutionSet = // [...]

DataSet<Tuple2<Long, Double>> initialDeltaSet = // [...]

int maxIterations = 100;
int keyPosition = 0;

DeltaIteration<Tuple2<Long, Double>, Tuple2<Long, Double>> iteration = initialSolutionSet
    .iterateDelta(initialDeltaSet, maxIterations, keyPosition);

DataSet<Tuple2<Long, Double>> candidateUpdates = iteration.getWorkset()
    .groupBy(1)
    .reduceGroup(new ComputeCandidateChanges());

DataSet<Tuple2<Long, Double>> deltas = candidateUpdates
    .join(iteration.getSolutionSet())
    .where(0)
    .equalTo(0)
    .with(new CompareChangesToCurrent());

DataSet<Tuple2<Long, Double>> nextWorkset = deltas
    .filter(new FilterByThreshold());

iteration.closeWith(deltas, nextWorkset)
    .writeAsCsv(outputPath);

通过调用iterateDelta(DataSet, int, int)或者iterateDelta(DataSet, int, int[])来生成一个DeltaIteration。
之后通过iteration.getWorkset() 和 iteration.getSolutionSet()来获得workset 和 solution set。

通过workSet以及solutionSet的join操作,每次迭代时对workSet应用solutionSet中的状态值,实现了增量迭代的效果。

增量迭代的详细介绍,可以参考:Data Analysis with Flink: A case study and tutorial

3.6、批处理用例

这里可以参考dataArtisans的flink training的例子:flink-training-exercisesApache Flink Training

3.7、总结

本章主要介绍Flink DataSet API,下一章开始介绍Flink生态中的Table API。

4、Table API 数据处理

Flink提供了一个table接口来进行批处理和流处理,这个接口叫做Table API。一旦dataset/datastream被注册为table后,就可以引用聚合、join和select等关系型的操作了。

Table同样可以通过标准SQL来操作,操作执行后,需要将table转换为dataSet/datastream。Flink内部中使用开源框架Apache Calcite来优化这些转换操作。

为了使用Table API,我们首先需要引入依赖:

<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-table_2.11</artifactId>
    <version>1.1.4</version>
</dependency>

4.1、Tables注册

我们首先要在TableEnvironment中将dataset或datastream注册,而TableEnvironment中维护者table的基本信息,细节如下:

这里写图片描述

4.1.1、注册dataset

为了在dataset中使用SQL,我们需要将dataset注册为一个table。

ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
BatchTableEnvironment tableEnv = TableEnvironment.getTableEnvironment(env);

// register the DataSet cust as table "Customers" with fields derived from the dataset
tableEnv.registerDataSet("Customers", cust)

// register the DataSet ord as table "Orders" with fields user, product, and amount
tableEnv.registerDataSet("Orders", ord, "user, product, amount");

4.1.2、注册datastream

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
StreamTableEnvironment tableEnv = TableEnvironment.getTableEnvironment(env);

// register the DataStream cust as table "Customers" with fields derived from the datastream
tableEnv.registerDataStream("Customers", cust)

// register the DataStream ord as table "Orders" with fields user, product, and amount
tableEnv.registerDataStream("Orders", ord, "user, product, amount");

4.1.3、注册table

// works for StreamExecutionEnvironment identically
ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
BatchTableEnvironment tableEnv = TableEnvironment.getTableEnvironment(env);

// convert a DataSet into a Table
Table custT = tableEnv
  .toTable(custDs, "name, zipcode")
  .where("zipcode = '12345'")
  .select("name")

// register the Table custT as table "custNames"
tableEnv.registerTable("custNames", custT)

4.1.4、注册外部数据源

// works for StreamExecutionEnvironment identically
ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
BatchTableEnvironment tableEnv = TableEnvironment.getTableEnvironment(env);

TableSource custTS = new CsvTableSource("/path/to/file", ...)

// register a `TableSource` as external table "Customers"
tableEnv.registerTableSource("Customers", custTS)

通过TableSource 可以访问但存储在数据库(mysql、hbase等)、文件系统(CSV, Apache Parquet, Avro, ORC等)以及消息系统(Apache Kafka, RabbitMQ)中。

当前Flink预定义的TableSource如下:

这里写图片描述

可以看到,KafkaJsonTableSource还只能用在流中,将dataStream转换为TableSource。

4.1.4.1 CSV table source

CSV source默认存在flink-table的API Jar包中,因此你无需再引入其他的依赖。

CsvTableSource 可以配置以下属性:

path:文件路径
fieldNames:table的字段名
fieldTypes:table字段的类型
fieldDelim:列分隔符,默认是“,”
rowDelim:行分隔符,默认是"\n"
quoteCharacter:对于String值可选的属性,默认是null
ignoreFirstLine:忽略第一行,默认是false
ignoreComments:可选择的前缀,用于注释,默认是null
lenient:跳过错误的记录,默认是false

下面展示一个例子:

这里写图片描述

4.1.4.2 Kafka JSON table source

为了使用KafkaJsonTableSource,你需要添加依赖(这里kafka 0.9为例):

<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-connector-kafka-0.9_2.11</artifactId>
    <version>1.1.4</version>
</dependency>

你可以像下面这样创建Table Source:

// The JSON field names and types
String[] fieldNames =  new String[] { "id", "name", "score"};
Class<?>[] fieldTypes = new Class<?>[] { Integer.class, String.class, Double.class };

KafkaJsonTableSource kafkaTableSource = new Kafka08JsonTableSource(
    kafkaTopic,
    kafkaProperties,
    fieldNames,
    fieldTypes);
tableEnvironment.registerTableSource("kafka-source", kafkaTableSource);
Table result = tableEnvironment.ingest("kafka-source");

4.2、访问已经注册的表

对于BatchTableEnvironment,我们通过:

tableEnvironment.scan("tableName")

对于StreamTableEnvironment,我们通过:

tableEnvironment.ingest("tableName")

4.3、operators

Flink Table API提供了各种各样的operators,其中大部分都支持java和scala。

4.3.1 select

根SQL中的select很像,查询Table中的字段:

Table result = in.select("id, name");
Table result = in.select("*");

4.3.2 where和filter

这两个等价,过滤作用:

Table in = tableEnv.fromDataSet(ds, "a, b, c");
Table result = in.where("b = 'red'");
Table in = tableEnv.fromDataSet(ds, "a, b, c");
Table result = in.filter("a % 2 = 0");

4.3.3 as

对字段重命名:

Table result = in.select("a, c as d");

4.3.4 groupBy

和SQL的groupBy操作很像,根据某个属性分组:

Table in = tableEnv.fromDataSet(ds, "a, b, c");
Table result = in.groupBy("a").select("a, b.sum as d");

4.3.5 join

两个表join。至少指出一个连接条件,可以通过where或filter指定:

Table employee = tableEnv.fromDataSet(emp, "e_id, e_name, deptId");
Table dept = tableEnv.fromDataSet(dept, "d_id, d_name");

Table result = employee.join(dept).where("deptId =
d_id").select("e_id, e_name, d_name");

4.3.6 leftOuterJoin

在SQL中,left join等价于left Outer Join,但是在flink中,表达left join不能忽略中间的outer:

Table employee = tableEnv.fromDataSet(emp, "e_id, e_name, deptId");
Table dept = tableEnv.fromDataSet(dept, "d_id, d_name");

Table result = employee.leftOuterJoin(dept).where("deptId =
d_id").select("e_id, e_name, d_name");

当然,你也可以简写:

Table left = tableEnv.fromDataSet(ds1, "a, b, c");
Table right = tableEnv.fromDataSet(ds2, "d, e, f");
Table result = left.leftOuterJoin(right, "a = d").select("a, b, e");

4.3.7 rightOuterJoin

右连接:

Table employee = tableEnv.fromDataSet(emp, "e_id, e_name, deptId");
Table dept = tableEnv.fromDataSet(dept, "d_id, d_name");

Table result = employee.rightOuterJoin(dept).where("deptId =
d_id").select("e_id, e_name, d_name");

当然,你也可以简写:

Table left = tableEnv.fromDataSet(ds1, "a, b, c");
Table right = tableEnv.fromDataSet(ds2, "d, e, f");
Table result = left.rightOuterJoin(right, "a = d").select("a, b, e");

4.3.8 fullOuterJoin

full join,左右两边join不到时,全部保留,左边或右边用null填充。

Table employee = tableEnv.fromDataSet(emp, "e_id, e_name, deptId");
Table dept = tableEnv.fromDataSet(dept, "d_id, d_name");

Table result = employee.fullOuterJoin(dept).where("deptId =
d_id").select("e_id, e_name, d_name");

当然,你可以简写:

Table left = tableEnv.fromDataSet(ds1, "a, b, c");
Table right = tableEnv.fromDataSet(ds2, "d, e, f");
Table result = left.fullOuterJoin(right, "a = d").select("a, b, e");

4.3.9 union

跟SQL的union一样,将两个相似(字段类型和个数一样)的table union起来,但是,它起到了去重的作用:

Table employee1 = tableEnv.fromDataSet(emp, "e_id, e_name, deptId");
Table employee2 = tableEnv.fromDataSet(emp, "e_id, e_name, deptId");
Table result = employee1.union(employee2);

4.3.10 unionAll

跟SQL的union all操作一样,但是它不去重:

Table employee1 = tableEnv.fromDataSet(emp, "e_id, e_name, deptId");
Table employee2 = tableEnv.fromDataSet(emp, "e_id, e_name, deptId");
Table result = employee1.unionAll(employee2);

4.3.11 intersect

和SQL中的intersect一样,求两个表的交集,即两个table中都存在的数据。但是结果去重,即结果中没有重复的数据存在:

Table employee1 = tableEnv.fromDataSet(emp, "e_id, e_name, deptId");
Table employee2 = tableEnv.fromDataSet(emp, "e_id, e_name, deptId");
Table result = employee1.intersect(employee2);

4.3.12 intersectAll

求交集,但是不去重:

Table employee1 = tableEnv.fromDataSet(emp, "e_id, e_name, deptId");
Table employee2 = tableEnv.fromDataSet(emp, "e_id, e_name, deptId");
Table result = employee1.intersectAll(employee2);

4.3.13 minus

和SQL的minus一样,求差集。即左边table存在但右边table不存在的记录,结果去重:

Table employee1 = tableEnv.fromDataSet(emp, "e_id, e_name, deptId");
Table employee2 = tableEnv.fromDataSet(emp, "e_id, e_name, deptId");
Table result = employee1.minus(employee2);

4.3.14 minusAll

求差集,但是结果不去重,即左边如果有重复数据,结果并不去重:

Table employee1 = tableEnv.fromDataSet(emp, "e_id, e_name, deptId");
Table employee2 = tableEnv.fromDataSet(emp, "e_id, e_name, deptId");
Table result = employee1.minusAll(employee2);

4.3.15 distinct

和SQL的distinct一样:

Table employee1 = tableEnv.fromDataSet(emp, "e_id, e_name, deptId");
Table result = employee1.distinct();

4.3.16 orderBy

按照某个字段全局排序:

Table employee1 = tableEnv.fromDataSet(emp, "e_id, e_name, deptId");
Table result = employee1.orderBy("e_id.asc");

4.3.17 limit

配合orderBy一起使用,即在orderBy的结果上,从第n+1条开始取,limit支持两种参数:

第一种写法是一个参数,代表从第6个数据开始取,知道后边所有的数据:

Table employee1 = tableEnv.fromDataSet(emp, "e_id, e_name, deptId");
//returns records from 6th record
Table result = employee1.orderBy("e_id.asc").limit(5);

第二种写法是2个参数,第一个参数代表从第4个数据开始取,往后取5条数据:

//returns 5 records from 4th record
Table result1 = employee1.orderBy("e_id.asc").limit(3,5);

4.3.18 数据类型

flink使用TypeInformation来自动识别table和sql以及java中的数据类型,当前的匹配如下:

这里写图片描述

通过sql()方法注册给TableEnviroment,我们可以在Table API中使用SQL。目前SQL功能适用于DataSet批处理和DataStream流处理,但是目前流处理支持的SQL力度有限,如果SQL语句中出现flink不能识别的语法,则会抛出TableException异常。

Flink内部通过Apache Calcite对SQL进行解析、优化和执行。

4.4.1 SQL on DataSet

ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
BatchTableEnvironment tableEnv = TableEnvironment.getTableEnvironment(env);

// read a DataSet from an external source
DataSet<Tuple3<Long, String, Integer>> ds = env.readCsvFile(...);
// register the DataSet as table "Orders"
tableEnv.registerDataSet("Orders", ds, "user, product, amount");
// run a SQL query on the Table and retrieve the result as a new Table
Table result = tableEnv.sql(
  "SELECT SUM(amount) FROM Orders WHERE product LIKE '%Rubber%'");

当前版本(Flink 1.2)在DataSet中的SQL,支持的语法包括select(filter)、projection、等价join、分组、非distinct的聚合操作,排序等,但不支持以下语法:

1、毫秒精度timestamp和intervals
2、不支持interval
3、类似于COUNT(DISTINCT name)不支持
4、非等价连接和笛卡尔积不支持
5、Grouping sets操作不支持

4.4.2 SQL on DataStream

目前1.2版本中的SQL on DataStream还支持select、from、where和union操作,其他的类似聚合类的、join等操作还不支持。

通过SELECT STREAM进行:

StreamExecutionEnvironment env =
StreamExecutionEnvironment.getExecutionEnvironment();
StreamTableEnvironment tableEnv =
TableEnvironment.getTableEnvironment(env);

DataStream<Tuple3<Long, String, Integer>> ds = env.addSource(...);
// register the DataStream as table "Products"
tableEnv.registerDataStream("Products", ds, "id, name, stock");

// run a SQL query on the Table and retrieve the result as a new Table
Table result = tableEnv.sql(
"SELECT STREAM * FROM Products WHERE name LIKE '%Apple%'");

4.5、用例

这里,我单独开了一篇博客,以2016年中超联赛射手榜的榜单为源数据,对这份榜单使用Flink SQL来进行了简单的统计,详见:Apache Flink SQL示例

4.6、总结

这篇我们介绍了Table API以及基于SQL的API。通过TableEnvironment在dataset、datastream和table之间的转换以及注册。下一篇文章我们将介绍复杂事件处理:Flink CEP。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值