Flink 流處理流程 API詳解

目錄

流處理的簡單流程

Environment 執行環境

Source 初始化數據

transform 執行轉換操作

sink 輸出結果

execute 程序觸發


流處理API的衍變

Storm:TopologyBuilder 構建圖的工具,然後往圖中添加節點,指定節點與節點之間的有向邊是什麼。構建完成後就可以將這個圖提交到遠程的集羣或者本地的集羣運行。
Flink:不同之處是面向數據本身的,會把DataStream 抽象成一個本地集合,通過面向集合流的編程方式進行代碼編寫。兩者沒有好壞之分,Storm比較靈活自由。更好的控制。在工業界Flink會更好點。開發起來比較簡單、高效。經過一些列優化、轉化最終也會像Storm一樣回到底層的抽象。Strom API 是面向操作的,偏向底層。Flink 面向數據,相對高層次一些。

流處理的簡單流程

其他分佈式處理引擎一樣,Flink應用程序也遵循着一定的編程模式。不管是使用 DataStream API還是 DataSet API基本具有相同的程序結構,如下代碼清單所示。通過流式計算的方式實現對文本文件中的單詞數量進行統計,然後將結果輸出在給定路徑中。

public class FlinkWordCount {
    public static void main(String[] args) throws Exception {
        //  1、獲取運行環境
        final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        //  2、通過socket獲取源數據
        DataStreamSource<String> sourceData = env.socketTextStream("192.168.52.12", 9000);
        /**
         *  3、數據源進行處理
         *  flatMap方法與spark一樣,對數據進行扁平化處理
         *  將每行的單詞處理爲<word,1>
         */
        DataStream<Tuple2<String, Integer>> dataStream = sourceData.flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() {
            public void flatMap(String s, Collector<Tuple2<String, Integer>> collector) throws Exception {
                String[] words = s.split(" ");
                for (String word : words) {
                    collector.collect(new Tuple2<String, Integer>(word, 1));
                }
            }
        })
                // 相同的單詞進行分組
                .keyBy(0)
                //  聚合數據
                .sum(1);
        //  4、將數據流打印到控制檯
        dataStream.print();
        /**
         *  5、Flink與Spark相似,通過action進行出發任務執行,其他的步驟均爲lazy模式
         *  這裏env.execute就是一個action操作,觸發任務執行
         */
        env.execute("streaming word count");
    }
}

整個Flink程序一共分爲5步,分別爲設定Flink執行環境創建和加載數據集、對數據集指定轉換操作邏輯、指定計算結果輸出位置調用execute方法觸發程序執行。對於所有的Flink應用程序基本都含有這5個步驟,下面將詳細介紹每個步驟。 

操作概覽

如果給你一串數據你會怎麼去處理它?

【1】基於單條記錄進行 Filter、Map
【2】基於窗口 window 進行計算,例如小時數,看到的就不一定是單數。
【3】有時會可能會合並多條流 union(多個數據流合併成一個大的流)、Join(多條流按照一定的條件進行合併)、connect(針對多種不同類型的流進行合併)。
【4】有時候需要將一條流拆分成多個流,例如 split,然後針對特殊的流進行特殊操作。

DataStream 基本轉換


【1】對 DataStream 進行一對一轉換,輸入是SataStream 輸出也是 DataStream。比較有代表性的,例如map;
【2】將一條DataStream 拆分成多條,例如使用 split,並給劃分後的每一個結果都打上一個標籤;
【3】通過調用 SplitStream 對象的 select 方法,根據標籤抽取一個感興趣的流,它也是一個 DataStream對象。
【4】把兩條流通過 connect 合併成一個 ConnectedSteam,對 ConnectedSteam流的操作可能與 DataStream 流的操作有不太一樣的地方。ConnectedSteam 中不同類型的流在處理的時候對應不同的 process 方法,他們都位於同一個 function中,會存在一些共享的數據信息。我們在後期做一些底層的join操作的時候都會用到這個 ConnectedSteam。
【5】對ConnectedSteam 也可以做類似於 Map的一些操作,它的操作名叫coMap,但是在API中寫法是 Map。
【6】我們可以對流按照時間或者個數進行一些切分,可以理解爲將無線的流分成一個一個的單位流,怎麼切分根據用戶自定的邏輯決定的。例如調用 windowAll 生成一個 AllWindowedStream。
【7】我們對 AllWindowedStream 去應用自己的一些業務邏輯(apply),最終形成原始的 DataStream。
【8】對 DataStream 進行keyBy 進行分組操作形成 KeyedStream。
【9】我們不能對普通的 DataStream 做 reduce操作,只能對 KeyedStream 進行 reduce。主要出於計算量的考慮。
【10】我們也可以對 KeyedStream 進行 window 操作形成 WindowedStream。
【11】我們對 WindowedStream 進行 apply 操作,形成原始的 DataStream 操作。

Environment 執行環境

【1】getExecutionEnvironment:創建一個執行環境,表示當前執行程序的上下文。如果程序是獨立調用的,則此方法返回本地執行環境;如果從命令行客戶端調用程序以提交到集羣,則此方法返回此集羣的執行環境,與就是說,執行環境決定了程序執行在什麼環境 getExecutionEnvironment 會根據查詢運行的方式返回不同的運行環境,是最常用的一種創建執行環境的方式。批量處理作業和流式處理作業分別使用的是不同的ExecutionEnvironment。例如 StreamExecutionEnvironment是用來做流式數據處理環境,ExecutionEnvironment是批量數據處理環境。

//流處理
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//塊梳理
ExecutionEnvironment executionEnvironment = ExecutionEnvironment.getExecutionEnvironment();

如果沒有設置並行度,會以  flink-conf.yaml 中的配置爲準,默認爲1:parallelism.default:1

//可以設置並行度(優先級最高)
env.setParallelism(1);

  如果是本地執行環境底層調用的是 createLocalEnvironment:需要在調用時指定默認的並行度

val env = StreamExecutionEnvironment.createLocalEnvironment(1)

  如果是集羣執行環境 createRemoteEnvironment:將 Jar 提交到遠程服務器,需要在調用時指定 JobManager 的 IP和端口號,並指定要在集羣中運行的Jar包。flink 將這兩種都進行了包裝,方便我們使用。

var env = ExecutionEnvironment.createRemoteEnvironment("jobmanager-hostname",6123,"YOURPATH//wordcount.jar")

Source 初始化數據

創建完成 ExecutionEnvironment後,需要將數據引入到 Flink系統中。ExecutionEnvironment 提供不同的數據接入接口完成數據的初始化,將外部數據轉換成 DataStreamDataSet數據集。如以下代碼所示,通過調用 readTextFile()方法讀取file:///pathfile路徑中的數據並轉換成 DataStream數據集。我們可以吧 streamSource 看做一個集合進行處理。

//readTextFile讀取文本文件的連接器 streamSource 可以想象成一個集合
DataStreamSource<String> streamSource = env.readTextFile("file:///path/file");

通過讀取文件並轉換爲 DataStream[String]數據集,這樣就完成了從本地文件到分佈式數據集的轉換,同時在 Flink中提供了多種從外部讀取數據的連接器,包括批量和實時的數據連接器,能夠將Flink系統和其他第三方系統連接,直接獲取外部數據。

transform 執行轉換操作

數據從外部系統讀取並轉換成 DataStream或者 DataSet數據集後,下一步就將對數據集進行各種轉換操作。Flink 中的Transformation操作都是通過不同的 Operator來實現,每個 Operator內部通過實現 Function接口完成數據處理邏輯的定義。在DataStream API 和 DataSet API提供了大量的轉換算子,例如mapflatMapfilterkeyBy等,用戶只需要定義每種算子執行的函數邏輯,然後應用在數據轉換操作 Dperator接口中即可。如下代碼實現了對輸入的文本數據集通過 FlatMap算子轉換成數組,然後過濾非空字段,將每個單詞進行統計,得到最後的詞頻統計結果。

DataStream<Tuple2<String, Integer>> dataStream = sourceData.flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() {
	public void flatMap(String s, Collector<Tuple2<String, Integer>> collector) throws Exception {
		String[] words = s.split(" ");
		for (String word : words) {
			collector.collect(new Tuple2<String, Integer>(word, 1));
		}
	}
// keyBy 相同的單詞進行分組,sum聚合數據
}).keyBy(0).sum(1);

在上述代碼中,通過 Java接口處理數據,極大地簡化數據處理邏輯的定義,只需要通過傳入相應 Lambada計算表達式,就能完成 Function定義。特殊情況下用戶也可以通過實現 Function接口來完成定義數據處理邏輯。然後將定義好的 Function應用在對應的算子中即可。Flink中定義 Funciton的計算邏輯可以通過如下幾種方式完成定義。
【1】通過創建 Class實現 Funciton接口
Flink中提供了大量的函數供用戶使用,例如以下代碼通過定義MyMapFunction Class實現MapFunction接口,然後調用DataStream的map()方法將 MyMapFunction實現類傳入,完成對實現將數據集中字符串記錄轉換成大寫的數據處理。

public class FlinkWordCount {
    public static void main(String[] args) throws Exception {
        DataStreamSource<String> sourceData = env.socketTextStream("192.168.52.12", 9000);
        //......
        //數據源進行處理
        sourceData.map(new MyMapFunciton());
        //......
    }
}

class MyMapFunciton implements MapFunction<String, String> {

    @Override
    public String map(String s) throws Exception {
        return s.toUpperCase();
    }
}

【2】通過創建匿名類實現 Funciton接口
除了以上單獨定義 Class來實現 Function接口之處,也可以直接在 map()方法中創建匿名實現類的方式定義函數計算邏輯。

DataStreamSource<String> sourceData = env.socketTextStream("192.168.52.12", 9000);
//通過創建 MapFunction 匿名函數來定義 Map 函數計算邏輯
sourceData.map(new MapFunction<String, String>() {
	@Override
	public String map(String s) throws Exception {
		//實現字符串大寫轉換
		return s.toUpperCase();
	}
});

【3】通過實現 RichFunciton接口
前面提到的轉換操作都實現了Function接口,例如 MapFunctionFlatMapFunction接口,在 Flink中同時提供了RichFunction接口,主要用於比較高級的數據處理場景,RichFunction接口中有open、close、getRuntimeContext和setRuntimeContext等方法來獲取狀態,緩存等系統內部數據。和 MapFunction相似,RichFunction子類中也有 RichMapFunction,如下代碼通過實現RichMapFunction定義數據處理邏輯。

sourceData.map(new RichFunction() {
	@Override
	public void open(Configuration configuration) throws Exception {

	}

	@Override
	public void close() throws Exception {

	}

	@Override
	public RuntimeContext getRuntimeContext() {
		return null;
	}

	@Override
	public IterationRuntimeContext getIterationRuntimeContext() {
		return null;
	}

	@Override
	public void setRuntimeContext(RuntimeContext runtimeContext) {

	}
});

分區Key指定:在 DataStream數據經過不同的算子轉換過程中,某些算子需要根據指定的key進行轉換,常見的有 joincoGroupgroupBy類算子,需要先將 DataStream或 DataSet數據集轉換成對應的 KeyedStreamGroupedDataSet,主要目的是將相同 key值的數據路由到相同的 Pipeline中,然後進行下一步的計算操作。需要注意的是,在 Flink中這種操作並不是真正意義上將數據集轉換成 Key-Value結構,而是一種虛擬的 key,目的僅僅是幫助後面的基於 Key的算子使用,分區人 Key可以通過兩種方式指定:
【1】根據字段位置指定
DataStream API中通過 keyBy()方法將 DataStream數據集根據指定的 key轉換成重新分區的 KeyedStream,如以下代碼所示,對數據集按照相同 key進行sum()聚合操作。

// 根據第一個字段進行重分區,相同的單詞進行分組。第二個字段進行求和運算
dataStream.keyBy(0).sum(1);

DataSet API中,如果對數據根據某一條件聚合數據,對數據進行聚合時候,也需要對數據進行重新分區。如以下代碼所示,使用DataSet API對數據集根據第一個字段作爲 GroupBy的 key,然後對第二個字段進行求和運算。

// 根據第一個字段進行重分區,相同的單詞進行分組。max 求相同key下的最大值
dataStream.groupBy(0).max(1);

【2】根據字段名稱指定
KeyBy 和GroupBy 的Key 除了能夠通過字段位置來指定之外,也可以根據字段的名稱來指定。使用字段名稱需要DataStream 中的數據結構類型必須是Tuple類或者 POJOs類的。如以下代碼所示,通過指定name字段名稱來確定groupby 的key字段。

DataStreamSource<Persion> sourceData = env.fromElements(new Persion("zzx", 18));
//使用 name 屬性來確定 keyBy
sourceData.keyBy("name").sum("age");

如果程序中使用 Tuple數據類型,通常情況下字段名稱從1開始計算,字段位置索引從0開始計算,以下代碼中兩種方式是等價的。

//通過位置指定第一個字段
dataStream.keyBy(0).sum(1);
//通過名稱指定第一個字段名稱
dataStream.keyBy("_1").sum("_2");

【3】通過Key選擇器指定
另外一種方式是通過定義Key Selector來選擇數據集中的Key,如下代碼所示,定義KeySelector,然後複寫 getKey方法,從Person對象中獲取 name爲指定的Key。

DataStreamSource<Persion> persionData = env.fromElements(new Persion("zzx", 18));
persionData.keyBy("name").sum("age");
persionData.keyBy(new KeySelector<Persion, Object>() {
	@Override
	public Object getKey(Persion persion) throws Exception {
		return persion.getName();
	}
});

理解 KeyedStream


假設有一條數據流,可以利用窗口的操作,進行一些豎向的切分,得到就是一個個大的 AllWindowedStream,再根據 keyBy() 進行橫向切分,把數據流中不同類別任務輸入到不同的算子中進行處理,不同的算子之間是並行的操作。同時不同的節點只需要維護自己的狀態。前提是 key數 >> 併發度

sink 輸出結果

數據集經過轉換操作之後,形成最終的結果數據集,一般需要將數據集輸出在外部系統中或者輸出在控制檯之上。在Flink DataStream 和 DataSet接口中定義了基本的數據輸出方法,例如基於文件輸出 writeAsText(),基於控制檯輸出print()等。同時Flink在系統中定義了大量的 Connector,方便用戶和外部系統交互,用戶可以直接通過調用 addSink()添加輸出系統定義的DataSink類算子,這樣就能將數據輸出到外部系統。以下實例調用 DataStream API中的 writeAsText()和 print()方法將數據集輸出在文件和客戶端中。

//將數據流打印到控制檯
dataStream.print();
//將數據輸出到文件中
dataStream.writeAsText("file://path/to/savenfile");

execute 程序觸發

所有的計算邏輯全部操作定義好之後,需要調用 ExecutionEnvironment的 execute()方法來觸發應用程序的執行,因爲flink在執行前會先構建執行圖,再執行。其中 execute()方法返回的結果類型爲 JobExecutionResult,裏面包含了程序執行的時間和累加器等指標。需要注意的是,execute方法調用會因爲應用的類型有所不同,DataStream流式應用需要顯性地指定 execute()方法運行程序,如果不調用則 Flink流式程序不會執行,但對於 DataSet API輸出算子中已經包含對 execute()方法的調用,則不需要顯性調用 execute()方法,否則會出現程序異常。

//調用 StreamExecutionEnvironment 的 execute 方法執行流式應用程序
env.execute("App Name");

物理分組


如上,有兩個DataSource實例A1,A2。不同顏色代表不同的實例,Flink 爲我們提供了比較完整的物理分組方案:
global() 作用就是無論你下游有多少個實例(B),上游的數據(A)都會發往下游的第一個實例(B1);
broadcast() 廣播,對上游的數據(A)複製很多份發往下游的所有實例(B),數據指數級的增長,數據量大時要注意;
forward() 當上下游併發度一致的時候一對一發送,否則會報錯;
shuffle() 隨機均勻分配;
rebalance() 輪詢;
recale() 本地輪流分配,例如上圖A1只能看到兩個實例B1和B2;
partitionCustom() 自定義單播;

類型系統

Flink 它裏面的抽象都是強類型的,與它自身的序列化和反序列化機制有關。這個引擎對類型信息知道的越多,就可以對數據進行更充足的優化,序列化與反序列化就會越快。每一個DataStream 裏面都需要有一個明確的類型和TypeInformation,Flink內置瞭如下類型,都提供了對應的 TypeInfomation。

API 原理


一個 DataStream 是如何轉化成另一個 DataStream 的,其實我們調用 map方法的時候,Flink 會給我們創建一個 OneInputTransformation ,需要一個 StreamOperator 參數 Flink 內部會有預先定義好的 StreamMap轉換的算子。Operator 內部我們需要自定義一個 MapFunction,一般Function纔是我們寫代碼需要關注的點。如果需要更深一點就會寫一些 Operator。


----架構師資料,關注公衆號獲取----

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