Flink DataStream API詳解
12019.03.13 19:26:51字數 4158閱讀 2044
Flink API介紹
Flink提供了三層API,每層在簡潔性和表達性之間進行了不同的權衡。
flink-api
ProcessFunction是Flink提供的最具表現力的功能接口,它提供了對時間和狀態的細粒度控制,能夠任意修改狀態。所以ProcessFunction能夠爲許多有事件驅動的應用程序實現複雜的事件處理邏輯。
DataStream API爲許多通用的流處理操作提供原語,比如window。DataStream API適用於Java和Scala,它基於函數實現,比如map()、reduce()等。我們也可以自己擴展接口自定義函數。
SQL & Table API 這兩個都是關係API,是批處理和流處理統一的API。Table API和SQL利用Apache Calcite進行解析、驗證和查詢優化。它們可以與DataStream和DataSet API無縫集成,並支持用戶定義標量、聚合和表值函數。關係API(relational api)目標在於簡化數據分析、數據流水線(data pipelining)和ETL。
我們一般主要使用DataStream進行數據處理,下面介紹的API也是DataStream相關的API。
DataStream API
DataStream是Flink編寫流處理作業的API。我們前面說過一個完整的Flink處理程序應該包含三部分:數據源(Source)、轉換操作(Transformation)、結果接收(Sink)。下面我們從這三部分來看DataStream API。
數據源(Source)
Flink應用程序從數據源獲取要處理的數據,DataStream通過StreamExecutionEnvironment.addResource(SourceFunction)
來添加數據源。爲了方便使用,Flink預提幾類預定義的數據源,比如讀取文件的Source、通過Sockt讀取的Source、從內存中獲取的Source等。
基於集合的預定義Source
基於集合的數據源一般是指從內存集合中直接讀取要處理的數據,StreamExecutionEnvironment提供了4類預定義方法。
fromCollection
fromCollection是從給定的集合中創建DataStream,StreamExecutionEnvironment提供了4種重載方法:
- fromCollection(Collection<T> data):通過給定的集合創建DataStream。返回數據類型爲集合元素類型。
- fromCollection(Collection<T> data,TypeInformation<T> typeInfo):通過給定的非空集合創建DataStream。返回數據類型爲typeInfo。
- fromCollection(Iterator<T> data,Class<T> type):通過給定的迭代器創建DataStream。返回數據類型爲type。
- fromCollection(Iterator<T> data,TypeInformation<T> typeInfo):通過給定的迭代器創建DataStream。返回數據類型爲typeInfo。
fromParallelCollection
fromParallelCollection和fromCollection類似,但是是並行的從迭代器中創建DataStream。
- fromParallelCollection(SplittableIterator<T> data,Class<T> type)
- fromParallelCollection(SplittableIterator<T>,TypeInfomation typeInfo)
和Iterable中Spliterator類似,這是JDK1.8新增的特性,並行讀取集合元素。
fromElements
fromElements從給定的對象序列中創建DataStream,StreamExecutionEnvironment提供了2種重載方法:
- fromElements(T... data):從給定對象序列中創建DataStream,返回的數據類型爲該對象類型自身。
- fromElements(Class<T> type,T... data):從給定對象序列中創建DataStream,返回的數據類型type。
generateSequence
generateSequence(long from,long to)從給定間隔的數字序列中創建DataStream,比如from爲1,to爲10,則會生成1~10的序列。
基於Socket的預定義Source
我們還可以通過Socket來讀取數據,通過Sockt創建的DataStream能夠從Socket中無限接收字符串,字符編碼採用系統默認字符集。當Socket關閉時,Source停止讀取。Socket提供了5個重載方法,但是有兩個方法已經標記廢棄。
- socketTextStream(String hostname,int port):指定Socket主機和端口,默認數據分隔符爲換行符(\n)。
- socketTextStream(String hostname,int port,String delimiter):指定Socket主機和端口,數據分隔符爲delimiter。
- socketTextStream(String hostname,int port,String delimiter,long maxRetry):該重載方法能夠當與Socket斷開時進行重連,重連次數由maxRetry決定,時間間隔爲1秒。如果爲0則表示立即終止不重連,如果爲負數則表示一直重試。
基於文件的預定義Source
基於文件創建DataStream主要有兩種方式:readTextFile和readFile。(readFileStream已廢棄)。readTextFile就是簡單讀取文件,而readFile的使用方式比較靈活。
readTextFile
readTextFile提供了兩個重載方法:
- readTextFile(String filePath):逐行讀取指定文件來創建DataStream,使用系統默認字符編碼讀取。
- readTextFile(String filePath,String charsetName):逐行讀取文件來創建DataStream,使用charsetName編碼讀取。
readFile
readFile通過指定的FileInputFormat來讀取用戶指定路徑的文件。對於指定路徑文件,我們可以使用不同的處理模式來處理,FileProcessingMode.PROCESS_ONCE
模式只會處理文件數據一次,而FileProcessingMode.PROCESS_CONTINUOUSLY
會監控數據源文件是否有新數據,如果有新數據則會繼續處理。
readFile(FileInputFormat<T> inputFormat,String filePath,FileProcessingMode watchType,long interval,TypeInformation typrInfo)
參數 | 說明 | 實例 |
---|---|---|
inputFormat | 創建DataStream指定的輸入格式 | |
filePath | 讀取的文件路徑,爲URI格式。既可以讀取普通文件,可以讀取HDFS文件 | file:///some/local/file 或hdfs://host:port/file/path |
watchType | 文件數據處理方式 | FileProcessingMode.PROCESS_ONCE或FileProcessingMode.PROCESS_CONTINUOUSLY |
interval | 在週期性監控Source的模式下(PROCESS_CONTINUOUSLY),指定每次掃描的時間間隔 | 10 |
typeInformation | 返回數據流的類型 |
readFile提供了幾個便於使用的重載方法,但它們最終都是調用上面這個方法的。
- readFile(FileInputFormat<T> inputFormat,String filePath):處理方式默認使用FileProcessingMode.PROCESS_ONCE。
- readFile(FileInputFormat<T> inputFormat,String filePath,FileProcessingMode watchType,long interval):返回類型默認爲inputFormat類型。
需要注意:在使用FileProcessingMode.PROCESS_CONTINUOUSLY時,當修改讀取文件時,Flink會將文件整體內容重新處理,也就是打破了"exactly-once"。
自定義Source
除了預定義的Source外,我們還可以通過實現SourceFunction
來自定義Source,然後通過StreamExecutionEnvironment.addSource(sourceFunction)
添加進來。比如讀取Kafka數據的Source:
addSource(new FlinkKafkaConsumer08<>);
我們可以實現以下三個接口來自定義Source:
- SourceFunction:創建非並行數據源。
- ParallelSourceFunction:創建並行數據源。
- RichParallelSourceFunction:創建並行數據源。
數據轉換(Transformation)
數據處理的核心就是對數據進行各種轉化操作,在Flink上就是通過轉換將一個或多個DataStream轉換成新的DataStream。
爲了更好的理解transformation函數,下面給出匿名類的方式來實現各個函數。
所有轉換函數都是依賴以下基礎:
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStream<String> dataStream = env.readTextFile("/opt/yjz/flink/flink-1.7.2/README.txt");
基礎轉換操作
Map
接受一個元素,輸出一個元素。MapFunction<T,V>中T代表輸入數據類型(map方法的參數類型),V代表操作結果輸出類型(map方法返回數據類型)。
dataStream.map(new MapFunction<String, String>() {
@Override
public String map(String line) throws Exception {
return line.toUpperCase();
}
});
flatMap
輸入一個元素,輸出0個、1個或多個元素。FlatMapFunction<T,V>中T代表輸入元素數據類型(flatMap方法的第一個參數類型),V代表輸出集合中元素類型(flatMap中的Collector類型參數)
dataStream.flatMap(new FlatMapFunction<String, String>() {
@Override
public void flatMap(String line, Collector<String> collector) throws Exception {
for(String word : line.split(" "))
collector.collect(word);
}
});
轉換前後數據類型:DataStream->DataStream。
filter
過濾指定元素數據,如果返回true則該元素繼續向下傳遞,如果爲false則將該元素過濾掉。FilterFunction<T>中T代表輸入元素的數據類型。
dataStream.filter(new FilterFunction<String>() {
@Override
public boolean filter(String line) throws Exception {
if(line.contains("flink"))
return true;
else
return false;
}
});
轉換前後數據類型:DataStream->DataStream。
keyBy
邏輯上將數據流元素進行分區,具有相同key的記錄將被劃分到同一分區。指定Key的方式有多種,這個我們在之前說過了。返回類型KeyedStream<T,KEY>中T代表KeyedStream中元素數據類型,KEY代表虛擬KEY的數據類型。
KeyedStream<String,Tuple> keyedStream = dataStream.keyBy(0);
以下情況的元素不能作爲key使用:
- POJO類型,但沒有重寫hashCode(),而是依賴Object.hashCode()。
- 該元素是數組類型。
keyBy內部使用散列來實現的。
轉換前後數據類型:DataStream->KeyedStream。
reduce
對指定的“虛擬”key相同的記錄進行滾動合併,也就是當前元素與最後一次的reduce結果進行reduce操作。ReduceFunction<T>中的T代表KeyStream中元素的數據類型。
keyedStream.reduce(new ReduceFunction<String>() {
@Override
public String reduce(String value1, String value2) throws Exception {
return value1 + value2;
}
});
轉換前後數據類型:KeyedStream->DataStream。
Fold(Deprecated)
Fold功能和Reduce類似,但是Fold提供了初始值,從初始值開始滾動對相同的key記錄進行滾動合併。FoldFunction<T,V>中的T爲KeyStream中元素數據類型,V爲初始值類型和fold方法返回值類型。
keyedStream.fold(0, new FoldFunction<String, Integer>() {
@Override
public Integer fold(Integer accumulator, String value) throws Exception {
return accumulator + value.split(" ").length;
}
});
該方法已經標記爲廢棄!
轉換前後數據類型:KeyedStream->DataStream。
Aggregations
滾動聚合具有相同key的數據流元素,我們可以指定需要聚合的字段(field)。DataStream<T>中的T爲聚合之後的結果。
//對KeyedStream中元素的第一個Filed求和
DataStream<String> dataStream1 = keyedStream.sum(0);
//對KeyedStream中元素的“count”字段求和
keyedStream.sum("count");
//獲取keyedStream中第一個字段的最小值
keyedStream.min(0);
//獲取keyedStream中count字段的最小值的元素
keyedStream.minBy("count");
keyedStream.max("count");
keyedStream.maxBy(0);
min和minBy的區別是:min返回指定字段的最小值,而minBy返回最小值所在的元素。
轉換前後數據類型:KeyedStream->DataStream。
window
對已經分區的KeyedStream上定義窗口,Window會根據某些規則(比如在最後5s到達的數據)對虛擬“key”相同的記錄進行分組。WindowedStream<T, K, W extends Window>中的T爲KeyedStream中元素數據類型,K爲指定Key的數據類型,W爲我們所使用的窗口類型
WindowedStream<String,Tuple,TimeWindow> windowedStream = keyedStream.window(TumblingEventTimeWindows.of(Time.seconds(5)));
轉換前後的數據類型:KeyedStream->WindowedStream
關於Window之後會拿出來專門一篇文章來說。
windowAll
我們也可以在常規DataStream上使用窗口,Window根據某些條件(比如最後5s到達的數據)對所有流事件進行分組。AllWindowedStream<T,W extends Window>中的T爲DataStream中元素的數據類型,W爲我們所使用的窗口類型。
AllWindowedStream<String,TimeWindow> allWindowedStream = dataStream.windowAll(TumblingEventTimeWindows.of(Time.seconds(5)));
注意:該方法在許多時候並不是並行執行的,所有記錄都會收集到一個task中
轉換前後的數據類型:DataStream->AllWindowedStream
Window Apply
將整個窗口應用在指定函數上,可以對WindowedStream和AllWindowedStream使用。WindowFunction<IN, OUT, KEY, W extends Window>中的IN爲輸入元素類型,OUT爲輸出類型元素,KEY爲指定的key類型,W爲所使用的窗口類型。
windowedStream.apply(new WindowFunction<String, String, Tuple, TimeWindow>() {
@Override
public void apply(Tuple tuple, TimeWindow window, Iterable<String> input, Collector<String> out) throws Exception {
int sumCount = 0;
for(String line : input){
sumCount += line.split(" ").length;
}
out.collect(String.valueOf(sumCount));
}
});
AllWindowedStream使用與WindowedStream類似。
轉換前後的數據類型:WindowedStream->DataStream或AllWindowedStream->DataStream
Window Reduce/Fold/Aggregation/
對於WindowedStream數據流我們同樣也可以應用Reduce、Fold、Aggregation函數。
windowedStream.reduce(new ReduceFunction<String>() {
@Override
public String reduce(String value1, String value2) throws Exception {
return value1 + value2;
}
});
windowedStream.fold(0, new FoldFunction<String, Integer>() {
@Override
public Integer fold(Integer accumulator, String value) throws Exception {
return accumulator + value.split(" ").length;
}
});
windowedStream.sum(0);
windowedStream.max("count");
轉換前後的數據類型:WindowedStream->DataStream
Union
聯合(Union)兩個或多個DataStream,所有DataStream中的元素都會組合成一個新的DataStream。如果聯合自身,則每個元素出現兩次在新的DataStream。
dataStream.union(dataStream1);
轉換前後的數據類型:DataStream*->DataStream
Window Join
在指定key的公共窗口上連接兩個數據流。JoinFunction<IN1,IN2,OUT>中的IN1爲第一個DataStream中元素數據類型,IN2爲第二個DataStream中元素數據類型,OUT爲Join結果數據類型。
dataStream
.join(dataStream1)
.where(new MyKeySelector()).equalTo(new MyKeySelector())
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.apply(new JoinFunction<String, String, String>() {
@Override
public String join(String first, String second) throws Exception {
return first + second;
}
});
轉換前後的數據類型:DataStream,DataStream->DataStream
Interval Join
對指定的時間間隔內使用公共key來連接兩個KeyedStream。ProcessJoinFunction<IN1,IN2,OUT>中IN1爲第一個DataStream中元素數據類型,IN2爲第二個DataStream中的元素數據類型,OUT爲結果輸出類型。
keyedStream
.intervalJoin(keyedStream)
.between(Time.milliseconds(-2),Time.milliseconds(2))//間隔時間
.lowerBoundExclusive()//並不包含下限時間
.upperBoundExclusive()
.process(new ProcessJoinFunction<String, String, String>() {
@Override
public void processElement(String left, String right, Context ctx, Collector<String> out) throws Exception {
//...
}
});
Window CoGroup
對兩個指定的key的DataStream在公共窗口上執行CoGroup,和Join功能類似,但是更加靈活。CoGroupFunction<IN1,IN2,OUT>,IN1代表第一個DataStream中元素類型,IN2代表第二個DataStream中元素類型,OUT爲結果輸出集合類型。
dataStream
.coGroup(dataStream1)
.where(new MyKeySelector()).equalTo(new MyKeySelector())
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.apply(new CoGroupFunction<String, String, String>() {
@Override
public void coGroup(Iterable<String> first, Iterable<String> second, Collector<String> out) throws Exception {
}
});
轉換前後的數據類型:DataStream,DataStream->DataStream
Connect
連接(connect)兩個流,並且保留其類型。兩個數據流之間可以共享狀態。ConnectedStreams<IN1,IN2>中IN1代表第一個數據流中的數據類型,IN2代表第二個數據流中的數據類型。
ConnectedStreams<String,String> connectedStreams = dataStream.connect(dataStream);
轉換前後的數據類型:DataStream,DataStream->ConnectedDataStreams
CoFlatMap/CoMap
可以對連接流執行類似map和flatMap操作。
connectedStreams.map(new CoMapFunction<String, String, String>() {
@Override
public String map1(String value) throws Exception {
return value.toUpperCase();
}
@Override
public String map2(String value) throws Exception {
return value.toLowerCase();
}
});
轉換前後的數據類型:ConnectedDataStreams->DataStream
Split(Deprecated)
我們可以根據某些規則將數據流切分成兩個或多個數據流。
dataStream.split(new OutputSelector<String>() {
@Override
public Iterable<String> select(String value) {
List<String> outList = new ArrayList<>();
if(value.contains("flink"))
outList.add("flink");
else
outList.add("other");
return outList;
}
});
該方法底層引用以被標記爲廢棄!
轉換前後的數據類型:DataStream->SplitStream
Select
我們可以對SplitStream分開的流進行選擇,可以將其轉換成一個或多個DataStream。
splitStream.select("flink");
splitStream.select("other");
Extract Timestamps(Deprecated)
從記錄中提取時間戳,以便使用事件時間語義窗口。之後會專門來看Flink的Event Time。
dataStream.assignTimestamps(new TimestampExtractor<String>() {
@Override
public long extractTimestamp(String element, long currentTimestamp) {
return 0;
}
@Override
public long extractWatermark(String element, long currentTimestamp) {
return 0;
}
@Override
public long getCurrentWatermark() {
return 0;
}
});
該方法以被標記爲廢棄!
轉換前後的數據類型:DataStream->DataStream
Iterate
可以使用iterate方法來獲取IterativeStream。
IterativeStream<String> iterativeStream = dataStream.iterate();
轉換前後的數據類型:DataStream->IterativeStream
Project
對元組類型的DataStream可以使用Project選取子元組。
DataStream<Tuple2<String,Integer>> dataStream2 = dataStream.project(0,2);
轉換前後的數據類型:DataStream->DataStream
自定義分區(partitionCustom)
使用用戶自定義的分區函數對指定key進行分區,partitionCustom只支持單分區。
dataStream.partitionCustom(new Partitioner<String>() {
@Override
public int partition(String key, int numPartitions) {
return key.hashCode() % numPartitions;
}
},1);
轉換前後的數據類型:DataStream->DataStream
隨機分區(shuffle)
均勻隨機將元素進行分區。
dataStream.shuffle();
轉換前後的數據類型:DataStream->DataStream
rebalance
以輪詢的方式爲每個分區均衡分配元素,對於優化數據傾斜該方法非常有效。
dataStream.rebalance();
轉換前後的數據類型:DataStream->DataStream
broadcast
使用broadcast可以向每個分區廣播元素。
dataStream.broadcast();
轉換前後的數據類型:DataStream->DataStream
rescale
根據上下游task數進行分區。
rescale
dataStream.rescale();
轉換前後的數據類型:DataStream->DataStream
結果數據接收器(Data sink)
數據經過Flink處理之後,最終結果會寫到file、socket、外部系統或者直接打印出來。數據接收器定義在DataStream類下,我們通過addSink()可以來添加一個接收器。同Source,Flink也提供了一些預定義的Data Sink讓我們直接使用。
寫入文本文件
DataStream提供了兩個writeAsText重載方法,寫入格式會調用寫入對象的toString()方法。
- writeAsText(String path):將DataStream數據寫入到指定文件。
- writeAsText(String path,WriteMode writeMode):將DataStream數據寫入到指定文件,可以通過writeMode來指定如果文件已經存在應該採用什麼方式,可以指定OVERWRITE或NO_OVERWRITE。
寫入CSV文件
DataStream提供了三個寫入csv文件的重載方法,對於DataStream中的每個Filed,都會調用其對象的toString()方法作爲寫入格式。writeAsCsv只能用於元組(Tuple)的DataStream。
writeAsCsv(String path,WriteMode writeMode,String rowDelimiter,String fieldDelimiter)
參數 | 說明 | 實例 |
---|---|---|
path | 寫入文件路徑 | |
writeMode | 如果寫入文件已經存在,採用什麼方式處理 | WriteMode.NO_OVERWRITE 或WriteMode.OVERWRITE |
rowDelimiter | 定義行分隔符 | |
fieldDelimiter | 定義列分隔符 |
DataStream提供了兩個簡易重載方法:
- writeAsCsv(String path):使用"\n"作爲行分隔符,使用","作爲列分隔符。
- writeAsCsv(String path,WriteMode writeMode):使用"\n"作爲行分隔符,使用","作爲列分隔符。
寫入Socket
Flink提供了將DataStream作爲字節數組寫入Socket的方法,通過SerializationSchema
來指定輸出格式。
writeToSocket(String hostName,int port,SerializationSchema<T> schema)
指定輸出格式
DataStream提供了自定義文件輸出的類和方法,我們能夠自定義對象到字節的轉換。
writeUsingOutputFormat(OutputFormat<T> format)
結果打印
DataStream提供了print和printToErr打印標準輸出/標準錯誤流。DataStream中的每個元素都會調用其toString()方法作爲輸出格式,我們也可以指定一個前綴字符來區分不同的輸出。
- print():標準輸出
- print(String sinkIdentifier):指定輸出前綴
- printToErr():標準錯誤輸出
- printToErr(String sinkIdentifier):指定輸出前綴
對於並行度大於1的輸出,輸出結果也將輸出任務的標識符作爲前綴。
自定義輸出器
我們一般會自定義輸出器,通過實現SinkFunction
接口,然後通過DataStream.addSink(sinkFunction)
來指定數據接收器。
addSink(SinkFunction<T> sinkFunction)
注意:對於DataStream中的writeXxx()方法一般都是用於測試使用,因爲他們並沒有參與chaeckpoint,所以它們只有"at-last-once"也就是至少處理一次語義。
如果想要可靠輸出,想要使用"exactly-once"語義準確將結果寫入到文件系統中,我們需要使用flink-connector-filesystem
。此外,我們也可以通過addSink()自定義輸出器來使用Flink的checkpoint來完成"exactl-oncey"語義。
關注我
歡迎關注我的公衆號,會定期推送優質技術文章,讓我們一起進步、一起成長!
公衆號搜索:data_tc
或直接掃碼:🔽