歡迎訪問我的GitHub
https://github.com/zq2599/blog_demos
內容:所有原創文章分類彙總及配套原始碼,涉及Java、Docker、Kubernetes、DevOPS等;
關於《CoProcessFunction實戰三部曲》系列
- 《CoProcessFunction實戰三部曲》旨在通過三次實戰,由淺入深的學習和掌握Flink低階處理函式CoProcessFunction的用法;
- 整個系列的開篇先介紹CoProcessFunction,然後迅速進入實戰,瞭解CoProcessFunction的基本功能;
- 下一篇會結合狀態,讓雙流元素的處理彼此保持關係;
- 終篇的實戰會加入定時器功能,確保同一個key的資料在雙流場景下能夠及時處理;
版本資訊
- 開發環境作業系統:MacBook Pro 13寸, macOS Catalina 10.15.3
- 開發工具:IDEA ULTIMATE 2018.3
- JDK:1.8.0_211
- Maven:3.6.0
- Flink:1.9.2
系列文章連結
關於CoProcessFunction
- CoProcessFunction的作用是同時處理兩個資料來源的資料;
- 試想在面對兩個輸入流時,如果這兩個流的資料之間有業務關係,該如何編碼實現呢,例如下圖中的操作,同時監聽9998和9999埠,將收到的輸出分別處理後,再由同一個sink處理(列印):
- Flink支援的方式是擴充套件CoProcessFunction來處理,為了更清楚認識,我們把KeyedProcessFunction和CoProcessFunction的類圖擺在一起看,如下所示:
- 從上圖可見,CoProcessFunction和KeyedProcessFunction的繼承關係一樣,另外CoProcessFunction自身也很簡單,在processElement1和processElement2中分別處理兩個上游流入的資料即可,並且也支援定時器設定;
本篇實戰功能簡介
本篇我們們要開發的應用,其功能非常簡單,描述如下:
- 建兩個資料來源,資料分別來自本地9998和9999埠;
- 每個埠收到類似aaa,123這樣的資料,轉成Tuple2例項,f0是aaa,f1是123;
- 在CoProcessFunction的實現類中,對每個資料來源的資料都打日誌,然後全部傳到下游運算元;
- 下游操作是列印,因此9998和9999埠收到的所有資料都會在控制檯列印出來;
- 整個demo的功能如下圖所示:
- 接下來開始編碼;
原始碼下載
如果您不想寫程式碼,整個系列的原始碼可在GitHub下載到,地址和連結資訊如下表所示(https://github.com/zq2599/blog_demos):
名稱 | 連結 | 備註 |
---|---|---|
專案主頁 | https://github.com/zq2599/blog_demos | 該專案在GitHub上的主頁 |
git倉庫地址(https) | https://github.com/zq2599/blog_demos.git | 該專案原始碼的倉庫地址,https協議 |
git倉庫地址(ssh) | git@github.com:zq2599/blog_demos.git | 該專案原始碼的倉庫地址,ssh協議 |
這個git專案中有多個資料夾,本章的應用在flinkstudy資料夾下,如下圖紅框所示:
程式碼簡介
- 開發一個Map運算元,將字串轉成Tuple2;
- 再開發抽象類AbstractCoProcessFunctionExecutor,功能包括:flink啟動、監聽埠、呼叫運算元處理資料、雙流連線、將雙流處理結果列印出來;
- 從上面的描述可見,AbstractCoProcessFunctionExecutor做了很多事情,唯獨沒有實現雙流連線後的具體業務邏輯,這些沒有做的是留給子類來實現的,整個三部曲系列的重點都集中在AbstractCoProcessFunctionExecutor的子類上,把雙流連線後的業務邏輯做好,如下圖所示,紅色為CoProcessFunction的業務程式碼,其他的都在抽象類中完成:
Map運算元
- 做一個map運算元,用來將字串aaa,123轉成Tuple2例項,f0是aaa,f1是123;
- 運算元名為WordCountMap.java:
package com.bolingcavalry.coprocessfunction;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.util.StringUtils;
public class WordCountMap implements MapFunction<String, Tuple2<String, Integer>> {
@Override
public Tuple2<String, Integer> map(String s) throws Exception {
if(StringUtils.isNullOrWhitespaceOnly(s)) {
System.out.println("invalid line");
return null;
}
String[] array = s.split(",");
if(null==array || array.length<2) {
System.out.println("invalid line for array");
return null;
}
return new Tuple2<>(array[0], Integer.valueOf(array[1]));
}
}
抽象類
- 抽象類AbstractCoProcessFunctionExecutor.java,原始碼如下,稍後會說明幾個關鍵點:
package com.bolingcavalry.coprocessfunction;
import org.apache.flink.api.java.tuple.Tuple;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.co.CoProcessFunction;
/**
* @author will
* @email zq2599@gmail.com
* @date 2020-11-09 17:33
* @description 串起整個邏輯的執行類,用於體驗CoProcessFunction
*/
public abstract class AbstractCoProcessFunctionExecutor {
/**
* 返回CoProcessFunction的例項,這個方法留給子類實現
* @return
*/
protected abstract CoProcessFunction<
Tuple2<String, Integer>,
Tuple2<String, Integer>,
Tuple2<String, Integer>> getCoProcessFunctionInstance();
/**
* 監聽根據指定的埠,
* 得到的資料先通過map轉為Tuple2例項,
* 給元素加入時間戳,
* 再按f0欄位分割槽,
* 將分割槽後的KeyedStream返回
* @param port
* @return
*/
protected KeyedStream<Tuple2<String, Integer>, Tuple> buildStreamFromSocket(StreamExecutionEnvironment env, int port) {
return env
// 監聽埠
.socketTextStream("localhost", port)
// 得到的字串"aaa,3"轉成Tuple2例項,f0="aaa",f1=3
.map(new WordCountMap())
// 將單詞作為key分割槽
.keyBy(0);
}
/**
* 如果子類有側輸出需要處理,請重寫此方法,會在主流程執行完畢後被呼叫
*/
protected void doSideOutput(SingleOutputStreamOperator<Tuple2<String, Integer>> mainDataStream) {
}
/**
* 執行業務的方法
* @throws Exception
*/
public void execute() throws Exception {
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 並行度1
env.setParallelism(1);
// 監聽9998埠的輸入
KeyedStream<Tuple2<String, Integer>, Tuple> stream1 = buildStreamFromSocket(env, 9998);
// 監聽9999埠的輸入
KeyedStream<Tuple2<String, Integer>, Tuple> stream2 = buildStreamFromSocket(env, 9999);
SingleOutputStreamOperator<Tuple2<String, Integer>> mainDataStream = stream1
// 兩個流連線
.connect(stream2)
// 執行低階處理函式,具體處理邏輯在子類中實現
.process(getCoProcessFunctionInstance());
// 將低階處理函式輸出的元素全部列印出來
mainDataStream.print();
// 側輸出相關邏輯,子類有側輸出需求時重寫此方法
doSideOutput(mainDataStream);
// 執行
env.execute("ProcessFunction demo : CoProcessFunction");
}
}
- 關鍵點之一:一共有兩個資料來源,每個源的處理邏輯都封裝到buildStreamFromSocket方法中;
- 關鍵點之二:stream1.connect(stream2)將兩個流連線起來;
- 關鍵點之三:process接收CoProcessFunction例項,合併後的流的處理邏輯就在這裡面;
- 關鍵點之四:getCoProcessFunctionInstance是抽象方法,返回CoProcessFunction例項,交給子類實現,所以CoProcessFunction中做什麼事情完全由子類決定;
- 關鍵點之五:doSideOutput方法中啥也沒做,但是在主流程程式碼的末尾會被呼叫,如果子類有側輸出(SideOutput)的需求,重寫此方法即可,此方法的入參是處理過的資料集,可以從這裡取得側輸出;
子類,對連線後的雙流進行操作
- 本篇子類CollectEveryOne.java如下所示,邏輯很簡單,將每個源的上游資料直接輸出到下游運算元:
package com.bolingcavalry.coprocessfunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.functions.co.CoProcessFunction;
import org.apache.flink.util.Collector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class CollectEveryOne extends AbstractCoProcessFunctionExecutor {
private static final Logger logger = LoggerFactory.getLogger(CollectEveryOne.class);
@Override
protected CoProcessFunction<Tuple2<String, Integer>, Tuple2<String, Integer>, Tuple2<String, Integer>> getCoProcessFunctionInstance() {
return new CoProcessFunction<Tuple2<String, Integer>, Tuple2<String, Integer>, Tuple2<String, Integer>>() {
@Override
public void processElement1(Tuple2<String, Integer> value, Context ctx, Collector<Tuple2<String, Integer>> out) {
logger.info("處理1號流的元素:{},", value);
out.collect(value);
}
@Override
public void processElement2(Tuple2<String, Integer> value, Context ctx, Collector<Tuple2<String, Integer>> out) {
logger.info("處理2號流的元素:{}", value);
out.collect(value);
}
};
}
public static void main(String[] args) throws Exception {
new CollectEveryOne().execute();
}
}
- 上述程式碼中,CoProcessFunction後面的泛型定義很長:<Tuple2<String, Integer>, Tuple2<String, Integer>, Tuple2<String, Integer>> ,一共三個Tuple2,分別代表一號資料來源輸入、二號資料來源輸入、下游輸出的型別;
- 編碼完成,執行起來試試;
驗證
- 分別開啟本機的9998和9999埠,我這裡是MacBook,執行nc -l 9998和nc -l 9999
- 啟動Flink應用,如果您和我一樣是Mac電腦,直接執行CollectEveryOne.main方法即可(如果是windows電腦,我這沒試過,不過做成jar線上部署也是可以的);
- 在監聽9998和9999埠的控制檯分別輸入aaa,111和bbb,222
- 以下是flink控制檯輸出的內容,可見processElement1和processElement2方法的日誌程式碼已經執行,並且print方法作為最下游,將兩個資料來源的資料都列印出來了,符合預期:
12:45:38,774 INFO CollectEveryOne - 處理1號流的元素:(aaa,111),
(aaa,111)
12:45:43,816 INFO CollectEveryOne - 處理2號流的元素:(bbb,222)
(bbb,222)
- 至此,我們們的第一個雙流處理低階函式就完成了,對CoProcessFunction也有了最基本的認識,當然CoProcessFunction的作用遠不及此,下一篇我們們藉助狀態讓processElement1和processElement2分別對方處理過的狀態,讓每個元素的處理都和另一個流關聯,不再孤立;
你不孤單,欣宸原創一路相伴
歡迎關注公眾號:程式設計師欣宸
微信搜尋「程式設計師欣宸」,我是欣宸,期待與您一同暢遊Java世界...
https://github.com/zq2599/blog_demos