一.概述
Flink程序是常規程序,可對分佈式集合進行轉換(例如,過濾,映射,更新狀態,聯接,分組,定義窗口,聚合)。集合最初是從源創建的(例如,通過讀取文件,kafka主題或本地內存中的集合)。結果通過接收器返回,接收器可以將數據寫入(分佈式)文件或標準輸出(例如,命令行終端)。Flink程序可以在各種上下文中運行,獨立運行或嵌入其他程序中。執行可以在本地JVM或許多計算機的羣集中進行。
根據數據源的類型(即有界或無界源),您將編寫批處理程序或流程序,其中DataSet API用於批處理,而DataStream API用於流。本指南將介紹兩個API共有的基本概念,但是請參考流式傳輸指南 和 批處理指南,以獲取有關使用每個API編寫程序的具體信息。
二.數據集和數據流
Flink具有特殊的類DataSet,DataStream用於表示程序中的數據。可以將它們視爲包含重複項的不可變數據集合。DataSet數據是有限的,對於DataStream許多元素,它們可以是無限的。
這些集合在某些關鍵方面與常規Java集合不同。首先,它們是不可變的,這意味着一旦創建它們就不能添加或刪除元素。也不能簡單地檢查其中的元素。
集合最初通過Flink程序添加源創建或從別的集合轉換而來,通過使用API方法轉化它們衍生的map,filter等等。
三.Flink程序剖析
Flink程序看起來像轉換數據集合的常規程序。每個程序都包含相同的基本部分:
- 獲得execution environment
- 加載/創建初始數據
- 指定對此數據的轉換
- 指定將計算結果放在何處
- 觸發程序執行
請注意,在包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()
執行結果: