實例講解Flink 流處理程序編程模型

摘要:在深入瞭解 Flink 實時數據處理程序的開發之前,先通過一個簡單示例來了解使用 Flink 的 DataStream API 構建有狀態流應用程序的過程。

本文分享自華爲雲社區《Flink 實例:Flink 流處理程序編程模型》,作者:TiAmoZhang 。

在深入瞭解 Flink 實時數據處理程序的開發之前,先通過一個簡單示例來了解使用 Flink 的 DataStream API 構建有狀態流應用程序的過程。

01、流數據類型

Flink 以一種獨特的方式處理數據類型和序列化,它包含自己的類型描述符、泛型類型提取和類型序列化框架。基於 Java 和 Scala 語言,Flink 實現了一套自己的一套類型系統,它支持很多種類的類型,包括

  1. 基本類型。
  2. 數組類型。
  3. 複合類型。
  4. 輔助類型。
  5. 通用類型。

詳細的 Flink 類型系統如圖 1 所示。

■ 圖 1 Flink 類型系統

Flink 針對 Java 和 Scala 的 DataStream API 要求流數據的內容必須是可序列化的。Flink 內置了以下類型數據的序列化器:

  1. 基本數據類型:String、Long、Integer、Boolean、Array。
  2. 複合數據類型:Tuple、POJO、Scala case class。

對於其他類型,Flink 會返回 Kryo。也可以在 Flink 中使用其他序列化器。Avro 尤其得到了很好的支持。

1.java DataStream API 使用的流數據類型

對於 Java API,Flink 定義了自己的 Tuple1 到 Tuple25 類型來表示元組類型,代碼如下:

Tuple2<String, Integer> person = new Tuple2<>("王老五", 35);
//索引基於0
String name = person.f0;
Integer age = person.f1;

在 Java 中,POJO(plain old Java Object)是這樣的 Java 類:

  1. 有一個無參的默認構造器。
  2. 所有的字段要麼是 public 的,要麼有一個默認的 getter 和 setter。

例如,定義一個名爲 Person 的 POJO 類,代碼如下:

//定義一個Person POJO類public class Person{    public String name;    public Integer age;
 public Person() {};
 public Person(String name, Integer age) { this.name = name; this.age = age; };}
//創建一個實例Person person = new Person("王老五", 35);

2.Scala DataStream API 使用的流數據類型

對於元組,使用 Scala 自己的 Tuple 類型就好,代碼如下:

val person = ("王老五", 35)
//索引基於1val name = person._1val age = person._2

對於對象類型,使用 case class(相當於 Java 中的 JavaBean),代碼如下:

case class Person(name: String, age:Int)
val person = Person("王老五", 35)

3.Flink 類型系統

對於創建的任意一個 POJO 類型,看起來它是一個普通的 Java Bean,在 Java 中,可以使用 Class 來描述該類型,但其實在 Flink 引擎中,它被描述爲 PojoTypeInfo,而 PojoTypeInfo 是 TypeInformation 的子類。

TypeInformation 是 Flink 類型系統的核心類。Flink 使用 TypeInformation 來描述所有 Flink 支持的數據類型,就像 Java 中的 Class 類型一樣。每種 Flink 支持的數據類型都對應的是 TypeInformation 的子類。例如 POJO 類型對應的是 PojoTypeInfo、基礎數據類型數組對應的是 BasicArrayTypeInfo、Map 類型對應的是 MapTypeInfo、值類型對應的是 ValueTypeInfo。

除了對類型的描述,TypeInformation 還提供了序列化的支持。在 TypeInformation 中有一種方法:createSerializer 方法,它用來創建序列化器,序列化器中定義了一系列的方法,其中,通過 serialize 和 deserialize 方法,可以將指定類型進行序列化,並且 Flink 的這些序列化器會以稠密的方式來將對象寫入內存中。Flink 中也提供了非常豐富的序列化器。在我們基於 Flink 類型系統支持的數據類型進行編程時,Flink 在運行時會推斷出數據類型的信息,我們在基於 Flink 編程時,幾乎是不需要關心類型和序列化的。

4.類型與 Lambda 表達式支持

在編譯時,編譯器能夠從 Java 源代碼中讀取完整的類型信息,並強制執行類型的約束,但生成 class 字節碼時,會將參數化類型信息刪除。這就是類型擦除。類型擦除可以確保不會爲泛型創建新的 Java 類,泛型是不會產生額外的開銷的。也就是說,泛型只是在編譯器編譯時能夠理解該類型,但編譯後執行時,泛型是會被擦除掉的。

爲了全球說明,請看下面的代碼:

public static <T> boolean hasItems(T [] items, T item){ for (T i : items){ if(i.equals(item)){ return true; } } return false;}

以上是一段 Java 的泛型方法,但在編譯後,編譯器會將未綁定類型的 T 擦除掉,替換爲 Object。也就是編譯之後的代碼如下:

public static Object boolean hasItems(Object [] items, Object item){ for (Object i : items){ if(i.equals(item)){ return true; } } return false;}

泛型只是能夠防止在運行時出現類型錯誤,但運行時會出現以下異常,而且 Flink 以非常友好的方式提示:

could not be determined automatically, due to type erasure. You can give type information hints by using the returns(...) method on the result of the transformation call, or by letting your function implement the 'ResultTypeQueryable' interface.

就是因爲 Java 編譯器類型擦除的原因,所以 Flink 根本無法推斷出來算子(例如 flatMap)要輸出的類型是什麼,所以在 Flink 中使用 Lambda 表達式時,爲了防止因類型擦除而出現運行時錯誤,需要指定 TypeInformation 或者 TypeHint。

創建 TypeInformation,代碼如下:

.returns(TypeInformation.of(String.class))

創建 TypeHint,代碼如下:

.returns(new TypeHint<String>() {})

02、流應用程序實現

Flink 程序的基本構建塊是 stream 和 transformation(流和轉換)。從概念上講,stream 是數據記錄的流(可能永遠不會結束),transformation 是一個運算,它接受一個或多個流作爲輸入,經過處理/計算後生成一個或多個輸出流。

下面實現一個完整的、可工作的 Flink 流應用程序示例。

【示例 1】將有關人員的記錄流作爲輸入,並從中篩選出未成年人信息。

Scala 代碼如下:

(1) 在 IntelliJ IDEA 中創建一個 Flink 項目,使用 flink-quickstart-scala 項目模板

(2) 設置依賴。在 pom.xml 文件中添加如下依賴內容:

<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-scala_2.12</artifactId>
<version>1.13.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-streaming-scala_2.12</artifactId>
<version>1.13.2</version>
<scope>provided</scope>
</dependency>

(3) 創建主程序 StreamingJobDemo1,編輯流處理代碼如下:

import org.apache.flink.streaming.api.scala._
object StreamingJobDemo1 {//定義事件類  case class Person(name:String, age:Integer)
  def main(args: Array[String]) {
//設置流執行環境 val env = StreamExecutionEnvironment.getExecutionEnvironment
//讀取數據源,構造數據流 val peoples = env.fromElements(      Person("張三", 21),      Person("李四", 16),      Person("王老五", 35)    )
//對數據流執行filter轉換 val adults = peoples.filter(_.age>18)
//輸出結果 adults.print
//執行 env.execute("Flink Streaming Job")  }}

執行以上代碼,輸出結果如下:

7> Person(張三,21)1> Person(王老五,35)

Java 代碼如下:

(1) 在 IntelliJ IDEA 中創建一個 Flink 項目,使用 flink-quickstart-Java 項目模板

(2) 設置依賴。在 pom.xml 文件中添加如下依賴內容:

<dependency><groupId>org.apache.flink</groupId> <artifactId>flink-Java</artifactId> <version>1.13.2</version> <scope>provided</scope></dependency>dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-streaming-Java_2.12</artifactId> <version>1.13.2</version> <scope>provided</scope></dependency>
(3) 創建一個 POJO 類,用來表示流中的數據,代碼如下:

//POJO類,表示人員信息實體public class Person {  public String name; //存儲姓名  public Integer age; //存儲年齡 //空構造器  public Person() {}; //構造器,初始化屬性  public Person(String name, Integer age) {    this.name = name;    this.age = age;  }; //用於調試時輸出信息  public String toString() {    return this.name.toString() + ": age " + this.age.toString();  };}
(4) 打開項目中的 StreamingJob 對象文件,編輯流處理代碼如下:

import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;import org.apache.flink.streaming.api.datastream.DataStream;import org.apache.flink.api.common.functions.FilterFunction; public class StreamingJobDemo1 { public static void main(String[] args) throws Exception { //獲得流執行環境         final StreamExecutionEnvironment env =StreamExecutionEnvironment.getExecutionEnvironment(); //讀取數據源,構造DataStream         DataStream<Person> personDS = env.fromElements(    new Person("張三", 21),    new Person("李四", 16),    new Person("王老五", 35)         ); //執行轉換運算(這裏是過濾年齡不小於18歲的人)//注意,這裏使用了匿名函數         DataStream<Person> adults = personDS.filter(new FilterFunction<Person>() {              @Override      public boolean filter(Person person) throws Exception {      return person.age >= 18;      }          }); //將結果輸出到控制檯          adults.print(); //觸發流程序開始執行          env.execute("stream demo");  }}

(5) 執行以上程序,輸出結果如下。

張三: age 21王老五: age 35

注意

Flink 將批處理程序作爲流程序的一種特殊情況執行,其中流是有界的(有限數量的元素)。DataSet 在內部被視爲數據流,因此,上述概念同樣適用於批處理程序,也適用於流程序,只有少數例外:

  1. 批處理程序的容錯不使用檢查點。錯誤恢復是通過完全重放流實現的,這使恢復的成本更高,但是因爲它避免了檢查點,所以使常規處理更輕量。
  2. DataSet API 中的有狀態運算使用簡化的 in-memory/out-of-核數據結構,而不是 key-value 索引。
  3. DataSet API 引入了特殊的同步(基於 superstep)迭代,這隻可能在有界流上實現。

03、流應用程序剖析

所有的 Flink 應用程序都以特定的步驟來工作,這些工作步驟如圖 2 所示。

■ 圖 2 Flink 應用程序工作步驟

也就是說,每個 Flink 程序都由相同的基本部分組成:

  1. 獲取一個執行環境。
  2. 加載/創建初始數據。
  3. 指定對該數據的轉換。
  4. 指定計算結果放在哪裏。
  5. 觸發程序執行。

1.獲取一個執行環境

Flink 應用程序從其 main()方法中生成一個或多個 Flink 作業(job)。這些作業可以在本地 JVM(LocalEnvironment)中執行,也可以在具有多臺機器的集羣的遠程設置中執行(RemoteEnvironment)。對於每個程序,ExecutionEnvironment 提供了控制作業執行(例如設置並行性或容錯/檢查點參數)和與外部環境交互(數據訪問)的方法。

每個 Flink 應用程序都需要一個執行環境(本例中爲 env)。流應用程序需要的執行環境使用的是 StreamExecutionEnvironment。爲了開始編寫 Flink 程序,用戶首先需要獲得一個現有的執行環境,如果沒有,就需要先創建一個。根據目的不同,Flink 支持以下幾種方式:

  1. 獲得一個已經存在的 Flink 環境。
  2. 創建本地環境。
  3. 創建遠程環境。

Flink 流程序的入口點是 StreamExecutionEnvironment 類的一個實例,它定義了程序執行的上下文。StreamExecutionEnvironment 是所有 Flink 程序的基礎。可以通過一些靜態方法獲得一個 StreamExecutionEnvironment 的實例,代碼如下:

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

要獲得執行環境,通常只需調用 getExecutionEnvironment()方法。這將根據上下文選擇正確的執行環境。如果正在 IDE 中的本地環境上執行,則它將啓動一個本地執行環境。如果是從程序中創建了一個 JAR 文件,並通過命令行調用它,則 Flink 集羣管理器將執行 main()方法,getExecutionEnvironment()將返回用於在集羣上以分佈式方式執行程序的執行環境。

在上面的示例程序中,使用以下語句來獲得流程序的執行環境。

Scala 代碼如下:

//設置流執行環境val env = StreamExecutionEnvironment.getExecutionEnvironment

Java 代碼如下:

//獲得流執行環境final StreamExecutionEnvironment env =StreamExecutionEnvironment.getExecutionEnvironment();

StreamExecutionEnvironment 包含 ExecutionConfig,可使用它爲運行時設置特定於作業的配置值。例如,如果要設置自動水印發送間隔,可以像下面這樣在代碼進行配置。

Scala 代碼如下:

val env = StreamExecutionEnvironment.getExecutionEnvironmentenv.getConfig.setAutoWatermarkInterval(long milliseconds)

Java 代碼如下:

final StreamExecutionEnvironment env =StreamExecutionEnvironment.getExecutionEnvironment();env.getConfig().setAutoWatermarkInterval(long milliseconds);

2.加載/創建初始數據

執行環境可以從多種數據源讀取數據,包括文本文件、CSV 文件、Socket 套接字數據等,也可以使用自定義的數據輸入格式。例如,要將文本文件讀取爲行序列,代碼如下:

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

數據被逐行讀取內存後,Flink 會將它們組織到 DataStream 中,這是 Flink 中用來表示流數據的特殊類。

在示例程序【示例 1】中,使用 fromElements()方法讀取集合數據,並將讀取的數據存儲爲 DataStream 類型。

Scala 代碼如下:

//讀取數據源,構造數據流val personDS = env.fromElements(      Person("張三", 21),      Person("李四", 16),      Person("王老五", 35)    )

Java 代碼如下:

//讀取數據源,構造DataStreamDataStream<Person> personDS = env.fromElements(        new Person("張三", 21),        new Person("李四", 16),        new Person("王老五", 35));

3.對數據進行轉換

每個 Flink 程序都對分佈式數據集合執行轉換。Flink 的 DataStream API 提供了多種數據轉換功能,包括過濾、映射、連接、分組和聚合。例如,下面是一個 map 轉換應用,通過將原始集合中的每個字符串轉換爲整數來創建一個新的 DataStream,代碼如下:

在示例程序【示例 1】中使用了 filter 過濾轉換,將原始數據集轉換爲只包含成年人信息的新 DataStream 流,代碼如下:

DataStream<String> input = env.fromElements("12","3","25","5","32","6");
DataStream<Integer> parsed = input.map(new MapFunction<String, Integer>() {    @Override    public Integer map(String value) { return Integer.parseInt(value); }});

Scala 代碼如下:

//對數據流執行filter轉換val adults = personDS.filter(_.age>18)

Java 代碼如下:

//對數據流執行filter轉換DataStream<Person> adults = flintstones.filter(    new FilterFunction<Person>() {        @Override        public boolean filter(Person person) throws Exception {            return person.age >= 18;        }    });

這裏不必瞭解每個轉換的具體含義,後面我們會詳細介紹它們。需要強調的是,Flink 中的轉換是惰性的,在調用 sink 操作之前不會真正執行。

4.指定計算結果放在哪裏

一旦有了包含最終結果的 DataStream,就可以通過創建接收器(sink)將其寫入外部系統。例如,將計算結果打印輸出到屏幕上。

Scala 代碼如下:

//輸出結果adults.print

Java 代碼如下:

//輸出結果adults.print();

Flink 中的接收器(sink)操作觸發流的執行,以生成程序所需的結果,例如將結果保存到文件系統或將其打印到標準輸出。上面的示例使用 adults.print()將結果打印到任務管理器日誌中(在 IDE 中運行時,任務管理器日誌將顯示在 IDE 的控制檯中)。這將對流的每個元素調用其 toString()方法。

5.觸發流程序執行

一旦寫好了程序處理邏輯,就需要通過調用 StreamExecutionEnvironment 上的 execute()來觸發程序執行。所有的 Flink 程序都是延遲執行的:當程序的主方法執行時,數據加載和轉換不會直接發生,而是創建每個運算並添加到程序的執行計劃中。當執行環境上的 execute()調用顯式觸發執行時,這些操作才實際上被執行。程序是在本地執行還是提交到集羣中執行取決於 ExecutionEnvironment 的類型。

延遲計算可以讓用戶構建複雜的程序,然後 Flink 將其作爲一個整體計劃的單元執行。在示例程序【示例 1】中,使用如下代碼來觸發流處理程序的執行。

Scala 代碼如下:

//觸發流程序執行env.execute("Flink Streaming Job") //參數是程序名稱,會顯示在Web UI界面上

Java 代碼如下:

//觸發流程序執行env.execute("Flink Streaming Job"); //參數是程序名稱,會顯示在Web UI界面上

在應用程序中執行的 DataStream API 調用將構建一個附加到 StreamExecutionEnvironment 的作業圖(Job Graph)。調用 env.execute()時,此圖被打包併發送到 Flink Master,該 Master 並行化作業並將其片段分發給 TaskManagers 以供執行。作業的每個並行片段將在一個 task slot(任務槽)中執行,如圖 3 所示。

■圖 3 Flink 流應用程序執行原理

這個分佈式運行時要求 Flink 應用程序是可序列化的。它還要求集羣中的每個節點都可以使用所有依賴項。

StreamExecutionEnvironment 上的 execute()方法將等待作業完成,然後返回一個 JobExecutionResult,其中包含執行時間和累加器結果。注意,如果不調用 execute(),應用程序將不會運行。

如果不想等待作業完成,可以通過調用 StreamExecutionEnvironment 上的 executeAysnc()來觸發異步作業執行。它將返回一個 JobClient,可以使用它與剛纔提交的作業進行通信。例如,下面的示例代碼演示瞭如何通過 executeAsync()實現 execute()的語義。

Scala 代碼如下:

val jobClient = evn.executeAsyncval jobExecutionResult =jobClient.getJobExecutionResult(userClassloader).get

Java 代碼如下:

final JobClient jobClient = env.executeAsync();final JobExecutionResult jobExecutionResult =jobClient.getJobExecutionResult(

 

點擊關注,第一時間瞭解華爲雲新鮮技術~

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