Flink處理函式實戰之五:CoProcessFunction(雙流處理)

程式設計師欣宸發表於2021-07-13

歡迎訪問我的GitHub

https://github.com/zq2599/blog_demos

內容:所有原創文章分類彙總及配套原始碼,涉及Java、Docker、Kubernetes、DevOPS等;

歡迎訪問我的GitHub

這裡分類和彙總了欣宸的全部原創(含配套原始碼):https://github.com/zq2599/blog_demos

Flink處理函式實戰系列連結

  1. 深入瞭解ProcessFunction的狀態操作(Flink-1.10)
  2. ProcessFunction
  3. KeyedProcessFunction類
  4. ProcessAllWindowFunction(視窗處理)
  5. CoProcessFunction(雙流處理)

本篇概覽

  • 本文是《Flink處理函式實戰》系列的第五篇,學習內容是如何同時處理兩個資料來源的資料;
  • 試想在面對兩個輸入流時,如果這兩個流的資料之間有業務關係,該如何編碼實現呢,例如下圖中的操作,同時監聽99989999埠,將收到的輸出分別處理後,再由同一個sink處理(列印):
    在這裡插入圖片描述
  • Flink支援的方式是擴充套件CoProcessFunction來處理,為了更清楚認識,我們把KeyedProcessFunctionCoProcessFunction的類圖擺在一起看,如下所示:
    在這裡插入圖片描述
  • 從上圖可見,CoProcessFunction和KeyedProcessFunction的繼承關係一樣,另外CoProcessFunction自身也很簡單,在processElement1和processElement2中分別處理兩個上游流入的資料即可,並且也支援定時器設定;

編碼實戰

接下來我們們開發一個應用來體驗CoProcessFunction,功能非常簡單,描述如下:

  1. 建兩個資料來源,資料分別來自本地99989999埠;
  2. 每個埠收到類似aaa,123這樣的資料,轉成Tuple2例項,f0是aaa,f1是123
  3. 在CoProcessFunction的實現類中,對每個資料來源的資料都打日誌,然後全部傳到下游運算元;
  4. 下游操作是列印,因此99989999埠收到的所有資料都會在控制檯列印出來;
  5. 整個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運算元

  1. 做一個map運算元,用來將字串aaa,123轉成Tuple2例項,f0是aaa,f1是123
  2. 運算元名為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]));
    }
}

便於擴充套件的抽象類

  • 開發一個抽象類,將前面圖中提到的監聽埠、map處理、keyby處理、列印都做到這個抽象類中,但是CoProcessFunction的邏輯卻不放在這裡,而是交給子類來實現,這樣如果我們想進一步實踐和擴充套件CoProcessFunction的能力,只要在子類中專注做好CoProcessFunction相關開發即可,如下圖,紅色部分交給子類實現,其餘的都是抽象類完成的:
    在這裡插入圖片描述
  • 抽象類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)的需求,重寫此方法即可,此方法的入參是處理過的資料集,可以從這裡取得側輸出;

子類決定CoProcessFunction的功能

  1. 子類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();
    }
}
  1. 上述程式碼中,CoProcessFunction後面的泛型定義很長:<Tuple2<String, Integer>, Tuple2<String, Integer>, Tuple2<String, Integer>> ,一共三個Tuple2,分別代表一號資料來源輸入、二號資料來源輸入、下游輸出的型別;

驗證

  1. 分別開啟本機的99989999埠,我這裡是MacBook,執行nc -l 9998nc -l 9999
  2. 啟動Flink應用,如果您和我一樣是Mac電腦,直接執行CollectEveryOne.main方法即可(如果是windows電腦,我這沒試過,不過做成jar線上部署也是可以的);
  3. 在監聽9998和9999埠的控制檯分別輸入aaa,111bbb,222
  4. 以下是flink控制檯輸出的內容,可見processElement1和processElement1方法的日誌程式碼已經執行,並且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獲得更多二號流的元素資訊,另外還可以結合定時器來約束兩個流協同處理的等待時間,您可以參考前面文章中的狀態和定時器來自行嘗試;

你不孤單,欣宸原創一路相伴

  1. Java系列
  2. Spring系列
  3. Docker系列
  4. kubernetes系列
  5. 資料庫+中介軟體系列
  6. DevOps系列

歡迎關注公眾號:程式設計師欣宸

微信搜尋「程式設計師欣宸」,我是欣宸,期待與您一同暢遊Java世界...
https://github.com/zq2599/blog_demos

相關文章