1、Flink批处理(DataSet API)- 基础概览和 DataSet API 编程指南

Flink 中的 DataSet 程序是在数据集上实现转换的常规程序(例如,filtering, mapping, joining, grouping)。数据集最初是从某些源创建的(例如,通过读取文件或从本地集合)。结果通过 sink 返回,例如,sink 可以将数据写入(分布式)文件,或者写入标准输出(例如命令行终端)。Flink 程序在各种上下文中运行,独立运行或嵌入到其他程序中。执行可以在本地 JVM 中进行,也可以在许多机器的集群中进行。

有关 Flink API 的基本概念的介绍,请参阅:Flink基础之API,DataSet、DataStream、批、流

为了创建您自己的 Flink DataSet 程序,我们鼓励您从剖析 Flink 程序开始,逐步添加您自己的转换。其余部分用作其他操作和高级功能的参考。

实例程序

下面的程序是一个完整的 WordCount 工作示例。

import org.apache.flink.api.scala._

object WordCount {
  def main(args: Array[String]) {

    val env = ExecutionEnvironment.getExecutionEnvironment
    val text = env.fromElements(
      "Who's there?",
      "I think I hear them. Stand, ho! Who's there?")

    val counts = text.flatMap { _.toLowerCase.split("\\W+") filter { _.nonEmpty } }
      .map { (_, 1) }
      .groupBy(0)
      .sum(1)

    counts.print()
  }
}

DataSet Transformations

数据转换将一个或多个数据集转换为新的数据集。程序可以将多个转换组合成复杂的程序集。本节简要概述可用的转换。

TransformationDescription
Map

获取一个元素并生成一个元素。

data.map { x => x.toInt }
FlatMap

获取一个元素并生成零个、一个或多个元素。

data.flatMap { str => str.split(" ") }
MapPartition

在单个函数调用中转换并行分区。该函数以“迭代器”的形式获取分区,并可以生成任意数量的结果值。每个分区中的元素数量取决于并行度和以前的算子

data.mapPartition { in => in map { (_, 1) } }
Filter

对每个元素求布尔函数的值,并保留函数返回true的元素。
重要提示:系统假设函数不修改应用谓词的元素。违反这个假设会导致不正确的结果。

data.filter { _ > 1000 }
Reduce

通过将两个元素重复组合为一个元素,将一组元素组合为单个元素。Reduce可以应用于完整的数据集,也可以应用于分组的数据集。

data.reduce { _ + _ }
ReduceGroup

将一组元素组合成一个或多个元素。ReduceGroup可以应用于完整的数据集,也可以应用于分组的数据集。

data.reduceGroup { elements => elements.sum }
Aggregate

将一组值聚合为单个值。聚合函数可以看作是内置的reduce函数。聚合可以应用于完整的数据集,也可以应用于分组的数据集。

val input: DataSet[(Int, String, Double)] = // [...]
val output: DataSet[(Int, String, Double)] = input.aggregate(SUM, 0).aggregate(MIN, 2)

您还可以对最小、最大和聚合使用简写语法。

val input: DataSet[(Int, String, Double)] = // [...]
val output: DataSet[(Int, String, Double)] = input.sum(0).min(2)
Distinct

返回数据集中不同的元素。它从输入数据集中删除与元素的所有字段或字段子集相关的重复项。

         data.distinct()
      
Join通过创建键上相等的所有元素对来连接两个数据集。可选地使用JoinFunction将元素对转换为单个元素,或使用FlatJoinFunction将元素对转换为任意多个(包括无元素)元素。参见keys部分,了解如何定义联接键。
// In this case tuple fields are used as keys. "0" is the join field on the first tuple
// "1" is the join field on the second tuple.
val result = input1.join(input2).where(0).equalTo(1)
可以通过连接提示指定运行时执行连接的方式。提示描述了连接是通过分区还是广播进行的,以及它是使用基于排序的算法还是基于散列的算法。
如果没有指定提示,系统将尝试估计输入大小并根据这些估计选择最佳策略。
// This executes a join by broadcasting the first data set
// using a hash table for the broadcast data
val result = input1.join(input2, JoinHint.BROADCAST_HASH_FIRST)
                   .where(0).equalTo(1)
注意,连接转换只对等连接起作用。其他连接类型需要使用OuterJoin或CoGroup来表示。
OuterJoin对两个数据集执行左、右或完全外连接。外部连接类似于常规(内部)连接,并创建键上相同的所有元素对。此外,如果在另一侧没有找到匹配的键,则保留“外部”侧的记录(左、右或全部)。将匹配的元素对(或一个元素和另一个输入的“null”值)提供给 JoinFunction 以将这对元素转换成单个元素,或提供给 FlatJoinFunction 以将这对元素转换成任意多个(包括none)元素。 有关key的定义:Flink基础之API,DataSet、DataStream、批、流
val joined = left.leftOuterJoin(right).where(0).equalTo(1) {
   (left, right) =>
     val a = if (left == null) "none" else left._1
     (a, right)
  }
CoGroup

简化操作的二维变体。将一个或多个字段上的每个输入分组,然后加入组。每对组调用转换函数。 有关key的定义:Flink基础之API,DataSet、DataStream、批、流

data1.coGroup(data2).where(0).equalTo(1)
Cross

构建两个输入的笛卡尔积(叉乘),创建所有的元素对。可选地使用交叉函数将一对元素转换为单个元素

val data1: DataSet[Int] = // [...]
val data2: DataSet[String] = // [...]
val result: DataSet[(Int, String)] = data1.cross(data2)

注意:Cross可能是一个非常计算密集型的操作,它甚至可以挑战大型计算集群!建议使用 crossWithTiny() 和 crossWithHuge() 来显示系统数据集的大小。

Union

生成两个数据集的并集。

data.union(data2)
Rebalance

均匀地重新平衡数据集的并行分区,以消除数据倾斜。只有类似 map 的转换才可能遵循再平衡转换。

val data1: DataSet[Int] = // [...]
val result: DataSet[(Int, String)] = data1.rebalance().map(...)
Hash-Partition

按给定键对数据集进行哈希分区。键可以指定为位置键、表达式键和键选择器函数。

val in: DataSet[(Int, String)] = // [...]
val result = in.partitionByHash(0).mapPartition { ... }
Range-Partition

按照范围对给定key 的数据进行分区。键可以指定为位置键、表达式键和键选择器函数。

val in: DataSet[(Int, String)] = // [...]
val result = in.partitionByRange(0).mapPartition { ... }
Custom Partitioning

使用自定义分区器函数基于特定分区的键分配记录。可以将键指定为位置键、表达式键和键选择器函数。
注意:此方法仅对单个字段键有效。

val in: DataSet[(Int, String)] = // [...]
val result = in
  .partitionCustom(partitioner, key).mapPartition { ... }
Sort Partition

按指定顺序对指定字段上的数据集的所有分区进行本地排序。字段可以指定为元组位置或字段表达式。对多个字段进行排序是通过链接 sortPartition()调用来完成的。

val in: DataSet[(Int, String)] = // [...]
val result = in.sortPartition(1, Order.ASCENDING).mapPartition { ... }
First-n

返回数据集的前n个(任意)元素。first -n可以应用于常规数据集、分组数据集或分组排序的数据集。分组键可以指定为键选择器函数、元组位置或case类字段。

val in: DataSet[(Int, String)] = // [...]
// regular data set
val result1 = in.first(3)
// grouped data set
val result2 = in.groupBy(0).first(3)
// grouped-sorted data set
val result3 = in.groupBy(0).sortGroup(1, Order.ASCENDING).first(3)

以下转换可用于元组的数据集:

TransformationDescription
MinBy / MaxBy

从一组元组中选择一个元组,其中一个或多个字段的值为最小值(最大值)。用于比较的字段必须是有效的关键字段,即,具有可比性。如果多个元组具有最小(最大)字段值,则返回这些元组的任意元组。MinBy (MaxBy)可以应用于完整数据集或分组数据集。

val in: DataSet[(Int, Double, String)] = // [...]
// a data set with a single tuple with minimum values for the Int and String fields.
val out: DataSet[(Int, Double, String)] = in.minBy(0, 2)
// a data set with one tuple for each group with the minimum value for the Double field.
val out2: DataSet[(Int, Double, String)] = in.groupBy(2)
                                             .minBy(1)

通过匿名模式匹配提取元组、case类和集合,如下图所示:

val data: DataSet[(Int, String, Double)] = // [...]
data.map {
  case (id, name, temperature) => // [...]
}

API 不支持开箱即用。要使用这个特性,您应该使用 Scala API 扩展

转换的并行性可以通过 setParallelism(int)定义,而 name(String)将自定义名称分配给转换,这有助于调试。对于 Data Source 和Data Sinks 也可能是相同的。

withParameters(Configuration) 传递配置对象,可以从用户函数中的 open() 方法访问这些对象。

Data Sources

数据源创建初始数据集,例如来自文件或来自Java集合。创建数据集的一般机制抽象在 InputFormat 之后。Flink 提供了几种内置格式,用于从常见的文件格式创建数据集。其中许多在 ExecutionEnvironment 上有快捷方法。

File-based:

  • readTextFile(path) / TextInputFormat - 按行读取文件并将其作为字符串返回。

  • readTextFileWithValue(path) / TextValueInputFormat - 按行读取文件并将其作为StringValues返回。StringValues是可变的字符串。

  • readCsvFile(path) / CsvInputFormat - 解析逗号(或其他字符)分隔字段的文件。返回元组、case类对象或pojo的数据集。支持基本的java类型及其对应的值作为字段类型。

  • readFileOfPrimitives(path, delimiter) / PrimitiveInputFormat - 使用给定的分隔符解析新行(或另一个字符序列)分隔的基本数据类型(如字符串或整数)。

  • readSequenceFile(Key, Value, path) / SequenceFileInputFormat - 创建JobConf并使用类型SequenceFileInputFormat、Key类和Value类从指定路径读取文件,并将它们作为Tuple2<Key, Value>返回。

Collection-based:

  • fromCollection(Iterable) - 从一个迭代创建一个数据集。由 Iterable 返回的所有元素必须是相同类型的。

  • fromCollection(Iterator) - 从迭代器创建数据集。该类指定迭代器返回的元素的数据类型。

  • fromElements(elements: _*) - 根据给定的对象序列创建数据集。所有对象必须具有相同的类型。

  • fromParallelCollection(SplittableIterator) - 从迭代器并行地创建数据集。该类指定迭代器返回的元素的数据类型。

  • generateSequence(from, to) - 并行地生成给定区间内的数字序列。

Generic:

  • readFile(inputFormat, path) / FileInputFormat - 接受文件输入格式。

  • createInput(inputFormat) / InputFormat - 接受通用输入格式。

val env  = ExecutionEnvironment.getExecutionEnvironment

// read text file from local files system
val localLines = env.readTextFile("file:///path/to/my/textfile")

// read text file from a HDFS running at nnHost:nnPort
val hdfsLines = env.readTextFile("hdfs://nnHost:nnPort/path/to/my/textfile")

// read a CSV file with three fields
val csvInput = env.readCsvFile[(Int, String, Double)]("hdfs:///the/CSV/file")

// read a CSV file with five fields, taking only two of them
val csvInput = env.readCsvFile[(String, Double)](
  "hdfs:///the/CSV/file",
  includedFields = Array(0, 3)) // take the first and the fourth field

// CSV input can also be used with Case Classes
case class MyCaseClass(str: String, dbl: Double)
val csvInput = env.readCsvFile[MyCaseClass](
  "hdfs:///the/CSV/file",
  includedFields = Array(0, 3)) // take the first and the fourth field

// read a CSV file with three fields into a POJO (Person) with corresponding fields
val csvInput = env.readCsvFile[Person](
  "hdfs:///the/CSV/file",
  pojoFields = Array("name", "age", "zipcode"))

// create a set from some given elements
val values = env.fromElements("Foo", "bar", "foobar", "fubar")

// generate a number sequence
val numbers = env.generateSequence(1, 10000000)

// read a file from the specified path of type SequenceFileInputFormat
val tuples = env.createInput(HadoopInputs.readSequenceFile(classOf[IntWritable], classOf[Text],
 "hdfs://nnHost:nnPort/path/to/file"))

CSV 解析配置

Flink为CSV解析提供了许多配置选项:

  • lineDelimiter: String 指定单个记录的分隔符。默认的行分隔符是换行字符'\n'。

  • fieldDelimiter: String 指定分隔记录字段的分隔符。默认的字段分隔符是逗号字符','。

  • includeFields: Array[Int] 定义从输入文件中读取哪些字段(以及忽略哪些字段)。默认情况下,解析前n个字段(由types()调用中的类型数量定义)。

  • pojoFields: Array[String] 指定映射到 CSV 字段的 POJO字 段。根据 POJO 字段的类型和顺序自动初始化 CSV 字段的解析器。

  • parseQuotedStrings: Character 启用引用字符串解析。如果字符串字段的第一个字符是引用字符,则将字符串解析为带引号的字符串(不修剪前导或尾随的空白)。引号内的字段分隔符将被忽略。如果引用字符串字段的最后一个字符不是引用字符,则引用字符串解析将失败。如果启用了带引号的字符串解析,并且字段的第一个字符不是带引号的字符串,则该字符串将被解析为非带引号的字符串。默认情况下,禁用带引号的字符串解析。

  • ignoreComments: String 指定注释前缀。所有以指定的注释前缀开头的行都不会被解析和忽略。默认情况下,不会忽略任何行。

  • lenient: Boolean 允许宽松的解析,即,无法正确解析的行将被忽略。默认情况下,宽松的解析是禁用的,无效的行会引发异常。

  • ignoreFirstLine: Boolean 配置 InputFormat 以忽略输入文件的第一行。默认情况下不忽略任何行。

递归遍历输入路径目录

对于基于文件的输入,当输入路径是目录时,默认情况下不会枚举嵌套文件。相反,只读取基本目录中的文件,而忽略嵌套文件。可以通过 recursive.file.enumeration 配置来启用递归遍历,如下:

// enable recursive enumeration of nested input files
val env  = ExecutionEnvironment.getExecutionEnvironment

// create a configuration object
val parameters = new Configuration

// set the recursive enumeration parameter
parameters.setBoolean("recursive.file.enumeration", true)

// pass the configuration to the data source
env.readTextFile("file:///path/with.nested/files").withParameters(parameters)

读压缩文件

Flink 目前支持输入文件的透明解压,如果这些文件用适当的文件扩展名进行了标记。特别是,这意味着不需要进一步配置输入格式,任何 FileInputFormat 都支持压缩,包括自定义输入格式。压缩文件可能无法并行读取,从而影响作业的可伸缩性。

下表列出了当前支持的压缩方法。

Compression methodFile extensionsParallelizable
DEFLATE.deflateno
GZip.gz, .gzipno
Bzip2.bz2no
XZ.xzno

Data Sinks

数据接收器使用数据集并用于存储或返回它们。数据接收操作使用 OutputFormat 进行描述。Flink 自带多种内置输出格式,封装在数据集的操作背后:

  • writeAsText() / TextOutputFormat - 将元素按行写入为字符串。通过调用每个元素的 toString()方法获得字符串。
  • writeAsCsv(...) / CsvOutputFormat - 将元组写入逗号分隔的值文件。行和字段分隔符是可配置的。每个字段的值来自对象的 toString()方法。
  • print() / printToErr() - 打印标准输出/标准错误流中每个元素的toString()值。
  • write() / FileOutputFormat - 方法和自定义文件输出的基类。支持自定义对象到字节的转换。
  • output()/ OutputFormat - 对于非基于文件的数据接收器(如将结果存储在数据库中),大多数通用输出方法。

一个数据集可以被输入到多个操作中。程序可以编写或打印数据集,同时对其运行附加的转换。

// text data
val textData: DataSet[String] = // [...]

// write DataSet to a file on the local file system
textData.writeAsText("file:///my/result/on/localFS")

// write DataSet to a file on a HDFS with a namenode running at nnHost:nnPort
textData.writeAsText("hdfs://nnHost:nnPort/my/result/on/localFS")

// write DataSet to a file and overwrite the file if it exists
textData.writeAsText("file:///my/result/on/localFS", WriteMode.OVERWRITE)

// tuples as lines with pipe as the separator "a|b|c"
val values: DataSet[(String, Int, Double)] = // [...]
values.writeAsCsv("file:///path/to/the/result/file", "\n", "|")

// this writes tuples in the text formatting "(a, b, c)", rather than as CSV lines
values.writeAsText("file:///path/to/the/result/file")

// this writes values as strings using a user-defined formatting
values map { tuple => tuple._1 + " - " + tuple._2 }
  .writeAsText("file:///path/to/the/result/file")

本地排序输出

数据接收器的输出可以使用元组字段位置或字段表达式对指定顺序中的指定字段进行本地排序。这适用于每种输出格式。

下面的例子展示了如何使用这个功能:

val tData: DataSet[(Int, String, Double)] = // [...]
val pData: DataSet[(BookPojo, Double)] = // [...]
val sData: DataSet[String] = // [...]

// sort output on String field in ascending order
tData.sortPartition(1, Order.ASCENDING).print()

// sort output on Double field in descending and Int field in ascending order
tData.sortPartition(2, Order.DESCENDING).sortPartition(0, Order.ASCENDING).print()

// sort output on the "author" field of nested BookPojo in descending order
pData.sortPartition("_1.author", Order.DESCENDING).writeAsText(...)

// sort output on the full tuple in ascending order
tData.sortPartition("_", Order.ASCENDING).writeAsCsv(...)

// sort atomic type (String) output in descending order
sData.sortPartition("_", Order.DESCENDING).writeAsText(...)

还不支持全局排序的输出。

迭代运算

迭代在Flink程序中实现循环。迭代操作符封装程序的一部分并重复执行,将一个迭代(部分解决方案)的结果反馈给下一个迭代。在Flink中有两种类型的迭代:BulkIteration 和 DeltaIteration。

Bulk Iterations

要创建 BulkIteration 调用数据集的 iterate(int)方法,迭代应该从指定一个 step 函数开始。step 函数获取当前迭代的输入数据集,并必须返回一个新的数据集。iterate 调用的参数是终止后的最大迭代次数。

还有 iterateWithTermination(int)函数,它接受一个返回两个数据集的步骤函数:迭代步骤的结果和一个终止条件。一旦终止条件数据集为空,迭代就停止。

下面的示例迭代地估计Pi的数量。目标是计算落入单位圆的随机点的数量。在每个迭代中,随机选择一个点。如果该点位于单位圆内,则增加计数。Pi然后被估计为结果计数除以迭代次数乘以4。

val env = ExecutionEnvironment.getExecutionEnvironment()

// Create initial DataSet
val initial = env.fromElements(0)

val count = initial.iterate(10000) { iterationInput: DataSet[Int] =>
  val result = iterationInput.map { i =>
    val x = Math.random()
    val y = Math.random()
    i + (if (x * x + y * y < 1) 1 else 0)
  }
  result
}

val result = count map { c => c / 10000.0 * 4 }

result.print()

env.execute("Iterative Pi Example")

Delta Iterations

增量迭代利用了这样一个事实,即某些算法不会在每次迭代中更改解决方案的每个数据点。

除了在每个迭代中反馈的部分解决方案(称为工作集)之外,增量迭代还跨迭代维护状态(称为解决方案集),可以通过增量进行更新。迭代计算的结果是最后一次迭代后的状态。

定义 DeltaIteration 类似于定义 BulkIteration。对于增量迭代,两个数据集形成每个迭代的输入(工作集和解决方案集),并且在每个迭代中产生两个数据集作为结果(新的工作集,解决方案集增量)。 

要在初始解决方案集上创建一个 DeltaIteration 调用 iterateDelta(initialWorkset, maxiteration, key)。step 函数接受两个参数:(solutionSet, workset),并且必须返回两个值:(solutionSetDelta, newWorkset)。

下面是一个增量迭代的语法示例

// read the initial data sets
val initialSolutionSet: DataSet[(Long, Double)] = // [...]

val initialWorkset: DataSet[(Long, Double)] = // [...]

val maxIterations = 100
val keyPosition = 0

val result = initialSolutionSet.iterateDelta(initialWorkset, maxIterations, Array(keyPosition)) {
  (solution, workset) =>
    val candidateUpdates = workset.groupBy(1).reduceGroup(new ComputeCandidateChanges())
    val deltas = candidateUpdates.join(solution).where(0).equalTo(0)(new CompareChangesToCurrent())

    val nextWorkset = deltas.filter(new FilterByThreshold())

    (deltas, nextWorkset)
}

result.writeAsCsv(outputPath)

env.execute()

操作函数中的数据对象

Flink 的运行时以 Java 对象的形式与用户函数交换数据。函数从运行时接收输入对象作为方法参数,并返回输出对象作为结果。因为这些对象是由用户函数和运行时代码访问的,所以理解和遵循关于用户代码如何访问的规则是非常重要的。例如,读取和修改这些对象。

用户函数从 Flink 的运行时接收对象,可以是常规方法参数(如MapFunction),也可以是迭代参数(如GroupReduceFunction)。我们将运行时传递给用户函数的对象称为输入对象。用户函数可以将对象作为方法返回值(如MapFunction)或通过收集器(如FlatMapFunction)发送到 Flink 运行时。我们将用户函数向运行时发出的对象称为输出对象。

Flink的DataSet API 具有两种模式,它们在 Flink 的运行时创建或重用输入对象的方式上有所不同。此行为影响用户函数与输入和输出对象交互方式的保证和约束。下面的部分定义了这些规则,并给出了编写安全的用户功能代码的编码指南。

重用对象关闭 (DEFAULT)

默认情况下,Flink 以对象重用禁用模式运行。此模式确保函数始终在函数调用中接收新的输入对象。禁用对象重用模式提供了更好的保证,使用起来也更安全。但是,它会带来一定的处理开销,并可能导致更高的 Java 垃圾收集活动。下表解释了用户函数如何以对象-重用禁用模式访问输入和输出对象。

OperationGuarantees and Restrictions
Reading Input Objects在方法调用中,可以保证输入对象的值不变。这包括由Iterable提供的对象。例如,在列表或映射中收集由Iterable提供的输入对象是安全的。注意,对象可以在方法调用结束后进行修改。跨函数调用记住对象是不安全的。
Modifying Input Objects您可以修改输入对象。
Emitting Input Objects您可以发出输入对象。输入对象的值在发出后可能发生了更改。在输入对象发出后读取它是不安全的。
Reading Output Objects提供给收集器或作为方法结果返回的对象可能更改了其值。读取输出对象是不安全的。
Modifying Output Objects您可以在对象发出后修改它,然后再次发出它。

对象重用禁用(默认)模式的编码准则:

  • 在方法调用之间不缓存并读取输入对象。 
  • 在发出对象之后不要读取它们。

重用对象启用

在启用对象重用模式下,Flink 的运行时最小化了对象实例化的数量。这可以提高性能并减少 Java 垃圾收集的压力。通过调用 ExecutionConfig.enableObjectReuse()激活对象重用模式。下表解释了用户函数如何在启用对象重用模式下访问输入和输出对象。

OperationGuarantees and Restrictions
Reading input objects received as regular method parameters作为常规方法参数接收的输入对象不会在函数调用中修改。方法调用结束后可以修改对象。跨函数调用缓存对象是不安全的。
Reading input objects received from an Iterable parameter从Iterable接收的输入对象只在调用下一个()方法之前有效。一个可迭代的或迭代器可以多次为同一个对象实例服务。记住从迭代中接收到的输入对象是不安全的,例如,将它们放入列表或映射中。
Modifying Input Objects除了MapFunction、FlatMapFunction、MapPartitionFunction、GroupReduceFunction、GroupCombineFunction、CoGroupFunction和InputFormat.next(重用)的输入对象外,您不能修改输入对象。
Emitting Input Objects除了MapFunction、FlatMapFunction、MapPartitionFunction、GroupReduceFunction、GroupCombineFunction、CoGroupFunction和InputFormat.next(重用)的输入对象外,您不能发出输入对象。
Reading Output Objects提供给收集器或作为方法结果返回的对象可能更改了其值。读取输出对象是不安全的。
Modifying Output Objects您可以修改输出对象并再次发出它。

支持对象重用的编码准则:

  • 不缓存从 Iterable 接收的输入对象。
  • 在方法调用之间不缓存并读取输入对象。
  • 除了 MapFunction、FlatMapFunction、MapPartitionFunction、GroupReduceFunction、GroupCombineFunction、CoGroupFunction和inputformat 的输入对象外,不要修改或发出输入对象。
  • 为了减少对象实例化,您总是可以发出一个专用的输出对象,该对象被反复修改,但从不被读取。

Debugging

在分布式集群中的大型数据集上运行数据分析程序之前,最好确保实现的算法按预期工作。因此,实现数据分析程序通常是一个检查结果、调试和改进的增量过程。

通过支持 IDE 中的本地调试、测试数据的注入和结果数据的收集,Flink 提供了一些很好的特性来显著简化数据分析程序的开发过程。本节给出了一些如何简化 Flink 程序开发的提示。

Local Execution Environment

LocalEnvironment 在创建它的JVM进程中启动一个 Flink 系统。如果从IDE启动 LocalEnvironment,可以在代码中设置断点并轻松调试程序。

LocalEnvironment 的创建和使用如下:

val env = ExecutionEnvironment.createLocalEnvironment()

val lines = env.readTextFile(pathToTextFile)
// build your program

env.execute()

Collection Data Sources and Sinks

通过创建输入文件和读取输出文件,为分析程序提供输入并检查其输出是很麻烦的。Flink提供特殊的数据源和接收器,这些数据源和接收器由Java集合支持,以简化测试。一旦对程序进行了测试,就可以很容易地将源和接收器替换为从外部数据存储(如HDFS)读写数据的源和接收器。

收集数据的来源如下:

val env = ExecutionEnvironment.createLocalEnvironment()

// Create a DataSet from a list of elements
val myInts = env.fromElements(1, 2, 3, 4, 5)

// Create a DataSet from any Collection
val data: Seq[(String, Int)] = ...
val myTuples = env.fromCollection(data)

// Create a DataSet from an Iterator
val longIt: Iterator[Long] = ...
val myLongs = env.fromCollection(longIt)

注意:目前,集合数据源要求数据类型和迭代器实现 Serializable。此外,收集数据源不能并行执行(parallelism = 1)。

语义标注

语义注释可用于向Flink提供关于函数行为的提示。它们告诉系统函数读取和计算输入的哪些字段,以及未修改的哪些字段从输入转发到输出。语义注释是加速执行的一种强大的方法,因为它允许系统在多个操作之间重用排序顺序或分区。使用语义注释可能最终将程序从不必要的数据变换或不必要的排序中拯救出来,并显著提高程序的性能。

注意:语义注释的使用是可选的。然而,在提供语义注释时保持保守是绝对重要的!不正确的语义注释将导致 Flink 对您的程序做出不正确的假设,并可能最终导致不正确的结果。如果操作符的行为不能清楚地预测,则不应提供任何注释。请仔细阅读文件。

目前支持以下语义注释。

Forwarded Fields Annotation

被转发的字段信息声明输入字段,这些字段未被函数修改,被转发到输出中的相同位置或另一个位置。优化器使用此信息来推断某个函数是否保留了排序或分区等数据属性。对于操作输入元素组(如GroupReduce、GroupCombine、CoGroup和MapPartition)的函数,定义为转发字段的所有字段必须始终联合来自相同输入元素的转发。group-wise 函数发出的每个元素的转发字段可能来自函数输入组的不同元素。

字段转发信息是使用字段表达式指定的。被转发到输出中相同位置的字段可以通过它们的位置指定。指定的位置必须对输入和输出数据类型有效,并且具有相同的类型。例如,字符串“f2”声明Java输入元组的第三个字段总是等于输出元组中的第三个字段。

未修改的字段被转发到输出中的另一个位置,通过将输入中的源字段和输出中的目标字段指定为字段表达式来声明。字符串“f0->f2”表示将Java输入元组的第一个字段不变地复制到Java输出元组的第三个字段。通配符表达式*可用于引用整个输入或输出类型,即,“f0->*”表示函数的输出总是等于其Java输入元组的第一个字段。

多个转发字段可以在一个字符串中声明,将它们用分号分隔为"f0; f2->f1; f3->f2"或在单独的字符串"f0", "f2->f1", "f3->f2"。在指定已转发的字段时,并不要求声明所有已转发的字段,但所有声明必须正确。

转发的字段信息可以通过在函数类定义上附加Java注释来声明,也可以在调用数据集上的函数后将它们作为操作符参数传递,如下所示。

函数类的注释

  • @ForwardedFields 对于单个输入函数,如Map和Reduce。
  • @ForwardedFieldsFirst 对于具有两个输入(如联接和CoGroup)的函数的第一个输入。
  • @ForwardedFieldsSecond 对于具有两个输入(如联接和CoGroup)的函数的第二个输入。

操作参数

  • data.map(myMapFnc).withForwardedFields() 对于单个输入函数,如Map和Reduce。
  • data1.join(data2).where().equalTo().with(myJoinFnc).withForwardFieldsFirst() 对于具有两个输入(如联接和CoGroup)的函数的第一个输入。
  • data1.join(data2).where().equalTo().with(myJoinFnc).withForwardFieldsSecond() 对于具有两个输入(如Join和CoGroup)的函数的第二个输入。

请注意,不可能覆盖由操作符参数指定为类注释的字段转发信息。

下面的示例演示如何使用函数类注释声明转发的字段信息:

@ForwardedFields("_1->_3")
class MyMap extends MapFunction[(Int, Int), (String, Int, Int)]{
   def map(value: (Int, Int)): (String, Int, Int) = {
    return ("foo", value._2 / 2, value._1)
  }
}

Non-Forwarded Fields

非转发字段信息声明了在函数输出中没有保存在同一位置的所有字段。所有其他字段的值都被认为保留在输出中的相同位置。因此,未转发的字段信息与转发的字段信息是相反的。组操作符(如GroupReduce、GroupCombine、CoGroup和MapPartition)的非转发字段信息必须满足与转发字段信息相同的要求。

重要提示:非转发字段信息的规范是可选的。然而,如果使用,所有!必须指定未转发的字段,因为所有其他字段都被认为是已转发的。将已转发字段声明为未转发字段是安全的。

非转发字段被指定为字段表达式的列表。该列表可以是单个字符串,字段表达式用分号分隔,也可以是多个字符串。例如"f1;f3”和“f1”、“f3”声明Java元组的第二个和第四个字段没有保留在适当的位置,而所有其他字段都保留在适当的位置。非转发的字段信息只能为具有相同输入和输出类型的函数指定。

未转发的字段信息使用以下注释指定为函数类注释:

  • @NonForwardedFields 对于单个输入函数,如Map和Reduce。
  • @NonForwardedFieldsFirst 对于具有两个输入(如联接和CoGroup)的函数的第一个输入。
  • @NonForwardedFieldsSecond 对于具有两个输入(如Join和CoGroup)的函数的第二个输入。

下面的示例演示如何声明未转发的字段信息:

@NonForwardedFields("_2") // second field is not forwarded
class MyMap extends MapFunction[(Int, Int), (Int, Int)]{
  def map(value: (Int, Int)): (Int, Int) = {
    return (value._1, value._2 / 2)
  }
}

Read Fields

Read fields信息声明所有被函数访问和计算的字段,即,该函数用于计算其结果的所有字段。例如,在指定读字段信息时,必须将条件语句中计算或用于计算的字段标记为read。只有未修改的字段被转发到输出,而没有计算它们的值或根本没有被访问的字段不被认为是读的。

重要提示:read字段信息的规范是可选的。然而,如果使用,所有!必须指定读字段。将非读字段声明为读是安全的。

读取字段被指定为字段表达式的列表。该列表可以是单个字符串,字段表达式用分号分隔,也可以是多个字符串。例如"f1;f3”和“f1”、“f3”声明Java元组的第二个和第四个字段被函数读取和计算。

read字段信息被指定为使用以下注释的函数类注释:

  • @ReadFields for single input functions such as Map and Reduce.
  • @ReadFieldsFirst for the first input of a function with two inputs such as Join and CoGroup.
  • @ReadFieldsSecond for the second input of a function with two inputs such as Join and CoGroup.
@ReadFields("_1; _4") // _1 and _4 are read and evaluated by the function.
class MyMap extends MapFunction[(Int, Int, Int, Int), (Int, Int)]{
   def map(value: (Int, Int, Int, Int)): (Int, Int) = {
    if (value._1 == 42) {
      return (value._1, value._2)
    } else {
      return (value._4 + 10, value._2)
    }
  }
}

Broadcast Variables

除了操作的常规输入之外,广播变量还允许将数据集提供给操作的所有并行实例。这对于辅助数据集或数据相关的参数化非常有用。数据集将在操作员处作为集合进行访问。

  • Broadcast: 广播集通过broadcastset(数据集、字符串)和按名称注册
  • Access: 可以通过 getRuntimeContext(). getbroadcastvariable (String)访问目标操作符。
// 1. The DataSet to be broadcast
val toBroadcast = env.fromElements(1, 2, 3)

val data = env.fromElements("a", "b")

data.map(new RichMapFunction[String, String]() {
    var broadcastSet: Traversable[String] = null

    override def open(config: Configuration): Unit = {
      // 3. Access the broadcast DataSet as a Collection
      broadcastSet = getRuntimeContext().getBroadcastVariable[String]("broadcastSetName").asScala
    }

    def map(in: String): String = {
        ...
    }
}).withBroadcastSet(toBroadcast, "broadcastSetName") // 2. Broadcast the DataSet

在注册和访问广播数据集时,确保名称(前一个示例中的 broadcastSetName)匹配.

注意:由于广播变量的内容保存在每个节点的内存中,所以它不应该变得太大。对于更简单的事情,比如标量值,您可以简单地将参数作为函数闭包的一部分,或者使用withParameters(…)方法传递配置。

分布式缓存

Flink 提供了一个分布式缓存,类似于 Apache Hadoop,使文件在本地可被用户函数的并行实例访问。此功能可用于共享包含静态外部数据(如字典或机器学习回归模型)的文件。

缓存的工作方式如下。程序将本地或远程文件系统(如HDFS或S3)的文件或目录注册到其执行环境中的特定名称下,作为缓存文件。当程序执行时,Flink自动将文件或目录复制到所有工作人员的本地文件系统。用户函数可以查找指定名称下的文件或目录,并从工作者的本地文件系统访问它。

分布式缓存的使用如下:

在 ExecutionEnvironment 中注册文件或目录。

val env = ExecutionEnvironment.getExecutionEnvironment

// register a file from HDFS
env.registerCachedFile("hdfs:///path/to/your/file", "hdfsFile")

// register a local executable file (script, executable, ...)
env.registerCachedFile("file:///path/to/exec/file", "localExecFile", true)

// define your program and execute
...
val input: DataSet[String] = ...
val result: DataSet[Integer] = input.map(new MyMapper())
...
env.execute()

在用户函数(这里是MapFunction)中访问缓存的文件。该函数必须扩展 RichFunction 类,因为它需要访问 RuntimeContext。

// extend a RichFunction to have access to the RuntimeContext
class MyMapper extends RichMapFunction[String, Int] {

  override def open(config: Configuration): Unit = {

    // access cached file via RuntimeContext and DistributedCache
    val myFile: File = getRuntimeContext.getDistributedCache.getFile("hdfsFile")
    // read the file (or navigate the directory)
    ...
  }

  override def map(value: String): Int = {
    // use content of cached file
    ...
  }
}

将参数传递给函数

可以使用构造函数或 withParameters(Configuration)方法将参数传递给函数。参数被序列化为函数对象的一部分,并传送到所有并行任务实例。

通过构造函数

val toFilter = env.fromElements(1, 2, 3)

toFilter.filter(new MyFilter(2))

class MyFilter(limit: Int) extends FilterFunction[Int] {
  override def filter(value: Int): Boolean = {
    value > limit
  }
}

通过withParameters(Configuration)

此方法将配置对象作为参数,该参数将传递给 rich 函数的open()方法。配置对象是从字符串键到不同值类型的映射。

val toFilter = env.fromElements(1, 2, 3)

val c = new Configuration()
c.setInteger("limit", 2)

toFilter.filter(new RichFilterFunction[Int]() {
    var limit = 0

    override def open(config: Configuration): Unit = {
      limit = config.getInteger("limit", 0)
    }

    def filter(in: Int): Boolean = {
        in > limit
    }
}).withParameters(c)

通过ExecutionConfig全局执行

Flink 还允许将自定义配置值传递给环境的 ExecutionConfig 接口。由于执行配置在所有(rich)用户函数中都是可访问的,所以自定义配置在所有函数中都是全局可用的。

设置自定义全局配置

val env = ExecutionEnvironment.getExecutionEnvironment
val conf = new Configuration()
conf.setString("mykey", "myvalue")
env.getConfig.setGlobalJobParameters(conf)

请注意,您还可以传递扩展 ExecutionConfig.GlobalJobParameters 类作为执行配置的全局作业参数。该接口允许实现Map<String, String> toMap()方法,该方法将依次显示来自web前端配置的值。

从全局配置中访问值

全局作业参数中的对象可以在系统中的许多地方访问。实现 RichFunction 接口的所有用户函数都可以通过运行时上下文进行访问。

public static final class Tokenizer extends RichFlatMapFunction<String, Tuple2<String, Integer>> {

    private String mykey;
    @Override
    public void open(Configuration parameters) throws Exception {
      super.open(parameters);
      ExecutionConfig.GlobalJobParameters globalParams = getRuntimeContext().getExecutionConfig().getGlobalJobParameters();
      Configuration globConf = (Configuration) globalParams;
      mykey = globConf.getString("mykey", null);
    }
    // ... more here ...

 

原文地址:https://ci.apache.org/projects/flink/flink-docs-release-1.9/dev/batch/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值