Flink基本API及核心概念詳解

一.概述

Flink程序是常規程序,可對分佈式集合進行轉換(例如,過濾,映射,更新狀態,聯接,分組,定義窗口,聚合)。集合最初是從源創建的(例如,通過讀取文件,kafka主題或本地內存中的集合)。結果通過接收器返回,接收器可以將數據寫入(分佈式)文件或標準輸出(例如,命令行終端)。Flink程序可以在各種上下文中運行,獨立運行或嵌入其他程序中。執行可以在本地JVM或許多計算機的羣集中進行。

根據數據源的類型(即有界或無界源),您將編寫批處理程序或流程序,其中DataSet API用於批處理,而DataStream API用於流。本指南將介紹兩個API共有的基本概念,但是請參考流式傳輸指南批處理指南,以獲取有關使用每個API編寫程序的具體信息。

二.數據集和數據流

Flink具有特殊的類DataSet,DataStream用於表示程序中的數據。可以將它們視爲包含重複項的不可變數據集合。DataSet數據是有限的,對於DataStream許多元素,它們可以是無限的。

這些集合在某些關鍵方面與常規Java集合不同。首先,它們是不可變的,這意味着一旦創建它們就不能添加或刪除元素。也不能簡單地檢查其中的元素。

集合最初通過Flink程序添加源創建或從別的集合轉換而來,通過使用API方法轉化它們衍生的map,filter等等。

三.Flink程序剖析

Flink程序看起來像轉換數據集合的常規程序。每個程序都包含相同的基本部分:

  1. 獲得execution environment
  2. 加載/創建初始數據
  3. 指定對此數據的轉換
  4. 指定將計算結果放在何處
  5. 觸發程序執行

請注意,在包org.apache.flink.api.java中可以找到Java DataSet API的所有核心類, 而在org.apache.flink.streaming.api中可以找到Java DataStream API的類 。

這StreamExecutionEnvironment是所有Flink程序的基礎。可以使用以下靜態方法獲得一個StreamExecutionEnvironment:

getExecutionEnvironment()
createLocalEnvironment()
createRemoteEnvironment(String host, int port, String... jarFiles)

通常,只需要使用getExecutionEnvironment(),因爲這將根據上下文執行正確的操作:如果是在IDE中執行程序或作爲常規Java程序執行,它將創建一個本地環境,該環境將在本地計算機上執行程序。如果是從程序創建的JAR文件,並通過命令行調用它 ,則Flink集羣管理器將執行程序中的main方法,getExecutionEnvironment()並將返回用於在集羣上執行程序的執行環境。

爲了指定數據源,執行環境有幾種使用各種方法從文件讀取的方法:可以逐行,以CSV文件的形式讀取它們,或使用完全自定義的數據輸入格式。要將文本文件讀取爲一系列行,可以使用:

final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStream<String> text = env.readTextFile("file:///path/to/file");

這將提供一個DataStream,然後可以在其上應用轉換以創建新的派生DataStream。
可以通過使用轉換函數在DataStream上調用方法來應用轉換。例如,轉換如下所示:

DataStream<String> input = ...;

DataStream<Integer> parsed = input.map(new MapFunction<String, Integer>() {
    @Override
    public Integer map(String value) {
        return Integer.parseInt(value);
    }
});

通過將原始集合中的每個String轉換爲Integer,將創建一個新的DataStream。
一旦有了包含最終結果的DataStream,就可以通過創建接收器將其寫入外部系統。這些只是創建接收器的一些示例方法:

writeAsText(String path)
print()

對於指定的完整程序,需要執行程序調用 execute()觸發執行StreamExecutionEnvironment。根據ExecutionEnvironment執行類型的不同,執行將在本地計算機上觸發或程序提交的集羣上執行。

該execute()方法返回一個JobExecutionResult,其中包含執行時間和累加器結果。

四.懶加載

所有Flink程序都是延遲執行的:執行程序的main方法時,不會直接進行執行程序的數據加載和轉換。而是將創建每個操作並將其添加到程序的計劃中。當調用execute()顯式觸發執行時,實際上纔會執行這些操作。程序是在本地執行還是在羣集上執行取決於執行環境的類型。

懶加載使您可以構建複雜的程序,Flink將其作爲一個整體計劃的單元執行。

五.鍵值對

某些轉換【join,coGroup,keyBy,groupBy】要求在dataSet上定義鍵。其他轉換【Reduce,GroupReduce,Aggregate,Windows】允許在應用鍵之前將數據分組。

dataSet分組操作:

DataSet<...> input = // [...]
DataSet<...> reduced = input.groupBy(/*define key here*/)
  .reduceGroup(/*do something*/);

dataStream分組操作:

DataStream<...> input = // [...]
DataStream<...> windowed = input.keyBy(/*define key here*/)
  .window(/*window specification*/);

Flink的數據模型不是基於鍵值對。因此,無需將數據集類型實際打包到鍵和值中。鍵是“虛擬的”:將它們定義爲對實際數據的功能,以指導分組操作。
1.元組鍵值對操作
最簡單的情況是在元組的一個或多個字段上對元組進行分組。
1.1根據元組的第一個字段(整數類型之一)進行分組。

DataStream<Tuple3<Integer,String,Long>> input = // [...]
KeyedStream<Tuple3<Integer,String,Long>,Tuple> keyed = input.keyBy(0)

1.2將元組分組在由第一字段和第二字段組成的複合鍵上。

DataStream<Tuple3<Integer,String,Long>> input = // [...]
KeyedStream<Tuple3<Integer,String,Long>,Tuple> keyed = input.keyBy(0,1)

1.3嵌套元組

DataStream<Tuple3<Tuple2<Integer, Float>,String,Long>> ds;

指定keyBy(0)將導致系統將完整字符Tuple2用作鍵(以Integer和Float作爲鍵)。如果要指向到嵌套中Tuple2,則必須使用字段表達式匹配鍵。

1.4字段表達式定義鍵
可以使用基於字符串的字段表達式來引用嵌套字段,並定義用於分組,排序,聯接或聯合分組的鍵。

字段表達式使選擇(嵌套)複合類型(例如Tuple和POJO類型)中的字段變得非常容易。

在下面的示例中,我們有一個WCPOJO,其中有兩個字段“ word”和“ count”。要按字段分組word,我們只需將其名稱傳遞給keyBy()函數即可。

case class WC(word : String, num : Int) // 樣例類
val counts = text.flatMap(_.split(" ").filter(_.nonEmpty))
    .map(WC(_, 1))
    .groupBy("word")//根據第word進行分組
    .sum("num") // 分組求和
    .setParallelism(1)  // 設置並行度
    .sortPartition("num", Order.DESCENDING) // 降序排序

執行結果:
在這裏插入圖片描述
字段表達式語法:

通過字段名稱選擇POJO字段。例如,"user"引用POJO類型的“用戶”字段。

通過其字段名稱或字段索引選擇元組字段。

可以在POJO和元組中選擇嵌套字段。使用"_“或”."。例如,“user.zip"引用存儲在POJO類型的“用戶”字段中的POJO的“ zip”字段。支持POJO和元組的任意嵌套和混合,例如”_2.user.zip"或"user._4.1.zip"。

字段表達式示例:

class WC(var complex: ComplexNestedClass, var count: Int) {
  def this() { this(null, 0) }
}

class ComplexNestedClass(
    var someNumber: Int,
    someFloat: Float,
    word: (Long, Long, String),
    hadoopCitizen: IntWritable) {
  def this() { this(0, 0, (0, 0, ""), new IntWritable(0)) }
}

六.轉換算子

大多數轉換算子需要用戶定義具體的功能。本節列出瞭如何指定它們的不同方法:
1.filter

.filter(_.endsWith("k")) // 過濾

執行結果:
在這裏插入圖片描述
2.reduce

    val counts = text.flatMap(_.split(" ").filter(_.nonEmpty))
        .map(_ => 1)
      .reduce{(x, y) => x + y}

執行結果:
在這裏插入圖片描述
3.map

class MyMapFunction extends RichMapFunction[String, Int] {
  def map(in: String):Int = { in.length }
}

應用:

    val counts = text.flatMap(_.split(" ").filter(_.nonEmpty))
      .map(new MyMapFunction())
      .reduce{(x, y) => x + y}

執行結果:
在這裏插入圖片描述
也可以定義爲匿名類:

    val counts = text.flatMap(_.split(" ").filter(_.nonEmpty))
      .map(new MyMapFunction())
      .map (new RichMapFunction[Int, Int] { // 匿名類
        def map(in: Int):Int = { in + 1 }
      })
      .reduce{(x, y) => x + y}

執行結果:
在這裏插入圖片描述

七.支持的數據類型

Flink對可以在DataSet或DataStream中的元素類型設置了一些限制。原因是系統分析類型以確定有效的執行策略。數據類型有七種不同的類別:

  • Java Tuples and Scala Case Classes
    元組是複合類型,包含固定數量的各種類型的字段。Java API提供了從Tuple1到的類Tuple25。元組的每個字段可以是任意Flink類型,包括其他元組,從而導致嵌套元組。可以使用字段名稱as tuple.f4或使用通用getter方法 直接訪問元組的字段tuple.getField(int position)。字段索引從0開始。請注意,這與Scala元組相反,但是它與Java的常規索引更加一致。
DataStream<Tuple2<String, Integer>> wordCounts = env.fromElements(
    new Tuple2<String, Integer>("hello", 1),
    new Tuple2<String, Integer>("world", 2));

wordCounts.map(new MapFunction<Tuple2<String, Integer>, Integer>() {
    @Override
    public Integer map(Tuple2<String, Integer> value) throws Exception {
        return value.f1;
    }
});

wordCounts.keyBy(0); // also valid .keyBy("f0")
  • Java POJOs
    略,下面會詳細講解
  • Primitive Types基本類型
    Flink支持所有Java和Scala的原始類型,如Integer,String和Double。
  • Regular Classes常規類類型
    Flink支持大多數Java和Scala類(API和自定義)。除包含無法序列化的字段的類外,例如文件指針,I / O流或其他本機資源。遵循Java Bean約定的類通常可以很好地工作。
    所有未標識爲POJO類型的類(請參見下面的POJO要求)都由Flink處理爲常規類類型。Flink將這些數據類型視爲黑盒,並且無法訪問它們的內容(即,進行有效排序)。通用類型使用序列化框架Kryo進行反序列化。
  • Values值類型
    值類型需要手動實現其序列化和反序列化。它們沒有通用的序列化框架,而是通過org.apache.flinktypes.Value使用方法read和實現接口爲這些操作提供了自定義代碼write。當通用序列化效率非常低時,使用Value類型是合理的。一個示例是將元素的稀疏向量實現爲數組的數據類型。知道數組大部分爲零,就可以對非零元素使用一種特殊的編碼,而通用序列化將只寫所有數組元素。
    該org.apache.flinktypes.CopyableValue接口以類似方式支持手動內部克隆邏輯。
    Flink帶有與基本數據類型相對應的預定義值類型。(ByteValue, ShortValue,IntValue,LongValue,FloatValue,DoubleValue,StringValue,CharValue, BooleanValue)。這些值類型充當基本數據類型的可變變體:可以更改它們的值,從而允許程序員重用對象並減輕垃圾收集器的壓力。
  • Hadoop Writables
    可以使用實現org.apache.hadoop.Writable接口的類型。write()和readFields()方法中定義的序列化邏輯將用於序列化。
  • Special Types
    可以使用特殊類型,包括Scala的Either,Option和Try。Java API具有自己的自定義實現Either。與Scala的類似Either,它表示兩種可能的值Left或Right。 Either對於錯誤處理或需要輸出兩種不同類型的記錄的運算符可能很有用。

八.POJO

如果Flink將Java和Scala類滿足以下要求,則它們被視爲特殊的POJO數據類型:

  • 該類必須是公開的。
  • 它必須具有不帶參數的公共構造函數(默認構造函數)。
  • 所有字段都是公共的,或者必須可以通過getter和setter函數訪問。對於稱爲foo getter和setter方法的字段,必須命名爲getFoo()和setFoo()。
  • 註冊的序列化程序必須支持字段的類型。

POJO樣例類:

case class WordWithCount(word : String, count : Int){
  def this(){
    this(null, 0)
  }
}

執行代碼:

  val input = execution.fromElements(
      new WordWithCount("hello", 1),
      new WordWithCount("word", 2))

    val result = input
      .groupBy("word")
      .sum("count") // 分組求和
      .setParallelism(1)  // 設置並行度
      .sortPartition("count", Order.DESCENDING) // 降序排序

    result.print()

執行結果:
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章