2. 跟着官網學 Apache Flink 1.10.0 之實戰篇

1.1 如何使用Apache Flink?

上篇博文彙總了Apache Flink 的相關概念和理論,這篇博文講解如何使用Apache Flink 1.10.0.

1.1.1 下載Apache Flink

首先我們需要知道如何在Windows 和Mac OSX 上安裝Apache Flink.

1.1.1.1 Windows 下載安裝Flink

點擊下載Apache Flink

1.1.1.2 Mac OSX 下載安裝Flink

Mac OSX 默認自帶了HomeBrew 包管理器

因此在Mac OSX 上安裝Apache Flink 非常簡單,只需要輸入如下命令即可:

brew install apache-flink

安裝完成後,我們可以通過輸入如下命令驗證我們的安裝是否成功:

flink --version

或者輸入flink -v 也可以

如果正常的話會輸出類似如下的內容:
在這裏插入圖片描述
可能你會好奇這個命令安裝完成後我去哪裏找我的Apache Flink 呢?

我們可以通過輸入如下命令查看apache-flink 的安裝完成後的存放路徑

brew info apache-flink
  • 執行成功後我們可以看到路徑已經出來了。
    在這裏插入圖片描述
    然後進入這個文件夾輸入如下命令:
cd /usr/local/Cellar/apache-flink/1.10.0/

如果輸入ls命令

ls

然後我們可以看到當前文件夾列表如下所示:

INSTALL_RECEIPT.json	LICENSE			NOTICE			README.txt		bin			libexec

值得注意的是,如果進入bin 那麼裏面只有一個flink 文件。

而我們要啓動的flink 文件實際上是在./libexec/bin/ 文件夾裏

引入啓動Apache flink 輸入如下命令

./libexec/bin/start-cluster.sh

啓動成功後執行如下所示

Starting cluster.
Starting standalonesession daemon on host localhost.
Starting taskexecutor daemon on host localhost.
  • 打開瀏覽器輸入如下鏈接:

http://127.0.0.1:8081/

  • 顯示內容如下所示:
    在這裏插入圖片描述

1.1.2 演示示例

Apache Flink 安裝包文件夾下有一個example 文件夾裏面包含了一些例子可以供我們學習。

不過值得注意的是我們需要先進入libexec 文件夾

cd libexec

然後執行查看文件夾列表命令

ls

查看內容如下所示:
在這裏插入圖片描述
然後必須先輸入如下命令:

nc -l 9000

執行成功如下所示:
在這裏插入圖片描述

值得注意的是

  • 這個命令可不是檢查端口是否衝突, 而是開啓一個端口進行監聽
  • 如果不輸入上面這個命令,下面的例子啓動會失敗的。
  • 這個命令執行後你會發現命令提示窗口掛起了,也就是說開了一個9000端口進行監聽。

當前這個窗口已經掛起暫時沒法使用了,因此我們必須新開一個命令行窗口

  • 重新進入安裝路徑:
cd /usr/local/Cellar/apache-flink/1.10.0/
  • 進入libexec 文件夾
cd libexec
  • 輸入如下命令即可啓動這個示例:
./bin/flink run examples/streaming/SocketWindowWordCount.jar --port 9000
  • 如果成功會出現如下所示內容:
    在這裏插入圖片描述
    我們點擊下面的Job Name 列表上的超鏈接可以進入如下界面。
    在這裏插入圖片描述

1.1.2 添加Flink 依賴

如果想在maven中使用Flink,添加如下依賴即可

<dependency>
  <groupId>org.apache.flink</groupId>
  <artifactId>flink-java</artifactId>
  <version>1.10.0</version>
</dependency>
<dependency>
  <groupId>org.apache.flink</groupId>
  <artifactId>flink-streaming-java_2.11</artifactId>
  <version>1.10.0</version>
</dependency>
<dependency>
  <groupId>org.apache.flink</groupId>
  <artifactId>flink-clients_2.11</artifactId>
  <version>1.10.0</version>
</dependency>

Scala API: 爲了使用 Scala API,將 flink-java 的 artifact id 替換爲flink-scala_2.11,同時將 flink-streaming-java_2.11 替換爲 flink-streaming-scala_2.11
修改依賴如下:

<dependency>
    <groupId>org.apache.flink</groupId>  
    <artifactId>flink-scala_2.11</artifactId>
    <version>1.10.0</version>
</dependency>
<dependency>
    <groupId>org.apache.flink</groupId>  
   <artifactId>flink-streaming-scala_2.11</artifactId>  
   <version>1.10.0</version>
</dependency>
<dependency>  
   <groupId>org.apache.flink</groupId>  
   <artifactId>flink-clients_2.11</artifactId>  
   <version>1.10.0</version>
</dependency>

1.2 如何編寫一個基本的 DataStream 應用程序

Apache Flink 提供了 DataStream API 來實現穩定可靠的、有狀態的流處理應用程序。 Flink 支持對狀態和時間的細粒度控制,以此來實現複雜的事件驅動數據處理系統。 這個入門指導手冊講述瞭如何通過 Flink DataStream API 來實現一個有狀態流處理程序。

1.2.1 業務背景

在當今數字時代,信用卡欺詐行爲越來越被重視。 罪犯可以通過詐騙或者入侵安全級別較低系統來盜竊信用卡卡號。 用盜得的信用卡進行很小額度的例如一美元或者更小額度的消費進行測試。 如果測試消費成功,那麼他們就會用這個信用卡進行大筆消費,來購買一些他們希望得到的,或者可以倒賣的財物。

在這個教程中,我們將會建立一個針對可疑信用卡交易行爲的反欺詐檢測系統。 通過使用一組簡單的規則,我們將瞭解到 Flink 如何爲我們實現複雜業務邏輯並實時執行。

1.2.2 定義應用程序的數據流

接下來我們定義應用程序的數據流

FraudDetectionJob.java編碼如下:

import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.walkthrough.common.sink.AlertSink;
import org.apache.flink.walkthrough.common.entity.Alert;
import org.apache.flink.walkthrough.common.entity.Transaction;
import org.apache.flink.walkthrough.common.source.TransactionSource;

public class FraudDetectionJob {

    public static void main(String[] args) throws Exception {
        // 1. 執行環境
        //StreamExecutionEnvironment 用於設置你的執行環境。 
        //任務執行環境用於定義任務的屬性、創建數據源以及最終啓動任務的執行。
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        // 2. 創建數據源
        //數據源從外部系統例如 Apache Kafka、Rabbit MQ 或者 Apache Pulsar 接收數據,
        //然後將數據送到 Flink 程序中。 
        //這個代碼練習使用的是一個能夠無限循環生成信用卡模擬交易數據的數據源。 
        //每條交易數據包括了信用卡 ID (accountId),交易發生的時間 (timestamp) 以及交易的金額(amount)。 
        //綁定到數據源上的 name 屬性是爲了調試方便,如果發生一些異常,我們能夠通過它快速定位問題發生在哪裏。
        DataStream<Transaction> transactions = env
            .addSource(new TransactionSource())
            .name("transactions");
        //3. 對事件分區 & 欺詐檢測    
        //transactions 這個數據流包含了大量的用戶交易數據,需要被劃分到多個併發上進行欺詐檢測處理。
        //由於欺詐行爲的發生是基於某一個賬戶的,
        //所以,必須要要保證同一個賬戶的所有交易行爲數據要被同一個併發的 task 進行處理。
        //爲了保證同一個 task 處理同一個 key 的所有數據,你可以使用 DataStream#keyBy 對流進行分區。 
        //process() 函數對流綁定了一個操作,這個操作將會對流上的每一個消息調用所定義好的函數。 
        //通常,一個操作會緊跟着 keyBy 被調用,
        //在這個例子中,這個操作是FraudDetector,該操作是在一個 keyed context 上執行的。
        DataStream<Alert> alerts = transactions
            .keyBy(Transaction::getAccountId)
            .process(new FraudDetector())
            .name("fraud-detector");
        //4. 輸出結果    
        //sink 會將 DataStream 寫出到外部系統,例如 Apache Kafka、Cassandra 或者 AWS Kinesis 等。 
        //AlertSink 使用 INFO 的日誌級別打印每一個 Alert 的數據記錄,
        //而不是將其寫入持久存儲,以便你可以方便地查看結果。
        alerts
            .addSink(new AlertSink())
            .name("send-alerts");
         //5. 運行作業   
         //Flink 程序是懶加載的,並且只有在完全搭建好之後,才能夠發佈到集羣上執行。 
         //調用 StreamExecutionEnvironment#execute 時給任務傳遞一個任務名參數,就可以開始運行任務。
        env.execute("Fraud Detection");
    }
}

1.2.3 定義欺詐交易檢測的業務邏輯

欺詐檢查類 FraudDetectorKeyedProcessFunction 接口的一個實現。 他的方法 KeyedProcessFunction#processElement 將會在每個交易事件上被調用。 這個程序裏邊會對每筆交易發出警報,有人可能會說這做報過於保守了。

本教程的後續步驟將指導你對這個欺詐檢測器進行更有意義的業務邏輯擴展。

FraudDetector.java

import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
import org.apache.flink.util.Collector;
import org.apache.flink.walkthrough.common.entity.Alert;
import org.apache.flink.walkthrough.common.entity.Transaction;

public class FraudDetector extends KeyedProcessFunction<Long, Transaction, Alert> {

    private static final long serialVersionUID = 1L;
    /**
    * 如果小於$1的交易就輸出報警信息
    */
    private static final double SMALL_AMOUNT = 1.00;
    /**
    * 如果大於$500的交易就輸出報警信息 
    */
    private static final double LARGE_AMOUNT = 500.00;
    private static final long ONE_MINUTE = 60 * 1000;
   /**
     * ValueState 是一個包裝類,類似於 Java 標準庫裏邊的 AtomicReference 和 AtomicLong。
     * 它提供了三個用於交互的方法。update 用於更新狀態,value 用於獲取狀態值,還有 clear 用於清空狀態。
     *  如果一個 key 還沒有狀態,例如當程序剛啓動或者調用過 ValueState#clear 方法時,ValueState#value 將會返回 null。 
     * 如果需要更新狀態,需要調用 ValueState#update 方法,直接更改 ValueState#value 的返回值可能不會被系統識別。
     * 容錯處理將在 Flink 後臺自動管理,你可以像與常規變量那樣與狀態變量進行交互。
    */
    private transient ValueState<Boolean> flagState;
    private transient ValueState<Long> timerState;

    /**
    * 騙子們在小額交易後不會等很久就進行大額消費,這樣可以降低小額測試交易被發現的機率。 
    * 比如,假設你爲欺詐檢測器設置了一分鐘的超時,
    * 對於上邊的例子,交易 3 和 交易 4 只有間隔在一分鐘之內才被認爲是欺詐交易。
    *  Flink 中的 KeyedProcessFunction 允許您設置計時器,該計時器在將來的某個時間點執行回調函數。
    * 讓我們看看如何修改程序以符合我們的新要求:
    * 當標記狀態被設置爲 true 時,設置一個在當前時間一分鐘後觸發的定時器。
    * 當定時器被觸發時,重置標記狀態。
    * 當標記狀態被重置時,刪除定時器。
    * 要刪除一個定時器,你需要記錄這個定時器的觸發時間,
    * 這同樣需要狀態來實現,所以你需要在標記狀態後也創建一個記錄定時器時間的狀態。
    */
    @Override
    public void open(Configuration parameters) {
        ValueStateDescriptor<Boolean> flagDescriptor = new ValueStateDescriptor<>(
                "flag",
                Types.BOOLEAN);
        flagState = getRuntimeContext().getState(flagDescriptor);

        ValueStateDescriptor<Long> timerDescriptor = new ValueStateDescriptor<>(
                "timer-state",
                Types.LONG);
        timerState = getRuntimeContext().getState(timerDescriptor);
    }

    /**
    * 對於每筆交易,欺詐檢測器都會檢查該帳戶的標記狀態。 
    * 請記住,ValueState 的作用域始終限於當前的 key,即信用卡帳戶。     
    * 如果標記狀態不爲空,則該帳戶的上一筆交易是小額的,
    * 因此,如果當前這筆交易的金額很大,那麼檢測程序將輸出報警信息。
    * 在檢查之後,不論是什麼狀態,都需要被清空。 
    * 不管是當前交易觸發了欺詐報警而造成模式的結束,還是當前交易沒有觸發報警而造成模式的中斷,都需要重新開始新的模式檢測。
    * 最後,檢查當前交易的金額是否屬於小額交易。 如果是,那麼需要設置標記狀態,以便可以在下一個事件中對其進行檢查。 
    *  注意,ValueState<Boolean> 實際上有 3 種狀態:unset (null),true,和 false,ValueState 是允許空值的。
    *  我們的程序只使用了 unset (null) 和 true 兩種來判斷標記狀態被設置了與否。
    */
    @Override
    public void processElement(
            Transaction transaction,
            Context context,
            Collector<Alert> collector) throws Exception {

        // Get the current state for the current key
        Boolean lastTransactionWasSmall = flagState.value();

        // Check if the flag is set
        if (lastTransactionWasSmall != null) {
            if (transaction.getAmount() > LARGE_AMOUNT) {
                //Output an alert downstream
                Alert alert = new Alert();
                alert.setId(transaction.getAccountId());

                collector.collect(alert);
            }
            // Clean up our state
            cleanUp(context);
        }
        //KeyedProcessFunction#processElement 需要使用提供了定時器服務的 Context 來調用。
        //定時器服務可以用於查詢當前時間、註冊定時器和刪除定時器。 
        //使用它,你可以在標記狀態被設置時,也設置一個當前時間一分鐘後觸發的定時器,
        //同時,將觸發時間保存到 timerState 狀態中。
        if (transaction.getAmount() < SMALL_AMOUNT) {
            // set the flag to true
            flagState.update(true);

            long timer = context.timerService().currentProcessingTime() + ONE_MINUTE;
            context.timerService().registerProcessingTimeTimer(timer);

            timerState.update(timer);
        }
    }

    /**
    * 處理時間是本地時鐘時間,這是由運行任務的服務器的系統時間來決定的。
    * 當定時器觸發時,將會調用 KeyedProcessFunction#onTimer 方法。
    *  通過重寫這個方法來實現一個你自己的重置狀態的回調邏輯。
    */
    @Override
    public void onTimer(long timestamp, OnTimerContext ctx, Collector<Alert> out) {
        // remove flag after 1 minute
        timerState.clear();
        flagState.clear();
    }
    /**
    * 最後,如果要取消定時器,你需要刪除已經註冊的定時器,並同時清空保存定時器的狀態。
    * 我們可以把這些邏輯封裝到一個助手函數中,而不是直接調用 flagState.clear()。
    */
    private void cleanUp(Context ctx) throws Exception {
        // delete timer
        Long timer = timerState.value();
        ctx.timerService().deleteProcessingTimeTimer(timer);

        // clean up all state
        timerState.clear();
        flagState.clear();
    }
}

對於一個賬戶,如果出現小於 $1 美元的交易後緊跟着一個大於 $500 的交易,就輸出一個報警信息。

假設我們的欺詐檢測器所處理的交易數據如下:
在這裏插入圖片描述
交易 3 和交易 4 應該被標記爲欺詐行爲,因爲交易 3 是一個 $0.09 的小額交易,而緊隨着的交易 4 是一個 $510 的大額交易。 另外,交易 7、8 和 交易 9 就不屬於欺詐交易了,因爲在交易 7 這個 $0.02 的小額交易之後,並沒有跟隨一個大額交易,而是一個金額適中的交易,這使得交易 7 到 交易 9 不屬於欺詐行爲。

欺詐檢測器需要在多個交易事件之間記住一些信息。僅當一個大額的交易緊隨一個小額交易的情況發生時,這個大額交易才被認爲是欺詐交易。 在多個事件之間存儲信息就需要使用到 狀態,這也是我們選擇使用 KeyedProcessFunction 的原因。 它能夠同時提供對狀態和時間的細粒度操作,這使得我們能夠在接下來的代碼練習中實現更復雜的算法。

最直接的實現方式是使用一個 boolean 型的標記狀態來表示是否剛處理過一個小額交易。 當處理到該賬戶的一個大額交易時,你只需要檢查這個標記狀態來確認上一個交易是是否小額交易即可。

然而,僅使用一個標記作爲 FraudDetector 的類成員來記錄賬戶的上一個交易狀態是不準確的。 Flink 會在同一個 FraudDetector 的併發實例中處理多個賬戶的交易數據,假設,當賬戶 A 和賬戶 B 的數據被分發的同一個併發實例上處理時,賬戶 A 的小額交易行爲可能會將標記狀態設置爲真,隨後賬戶 B 的大額交易可能會被誤判爲欺詐交易。 當然,我們可以使用如 Map 這樣的數據結構來保存每一個賬戶的狀態,但是常規的類成員變量是無法做到容錯處理的,當任務失敗重啓後,之前的狀態信息將會丟失。 這樣的話,如果程序曾出現過失敗重啓的情況,將會漏掉一些欺詐報警。

爲了應對這個問題,Flink 提供了一套支持容錯狀態的原語,這些原語幾乎與常規成員變量一樣易於使用。

Flink 中最基礎的狀態類型是 ValueState,這是一種能夠爲被其封裝的變量添加容錯能力的類型。 ValueState 是一種 keyed state,也就是說它只能被用於 keyed context 提供的 operator 中,即所有能夠緊隨 DataStream#keyBy 之後被調用的operator。 一個 operator 中的 keyed state 的作用域默認是屬於它所屬的 key 的。 這個例子中,key 就是當前正在處理的交易行爲所屬的信用卡賬戶(key 傳入 keyBy() 函數調用),而 FraudDetector 維護了每個帳戶的標記狀態。 ValueState 需要使用 ValueStateDescriptor 來創建,ValueStateDescriptor 包含了 Flink 如何管理變量的一些元數據信息。狀態在使用之前需要先被註冊。 狀態需要使用 open() 函數來註冊狀態。

1.2.4 Transaction.java

Transaction.java編碼如下:

import java.util.Objects;

/**
 * A simple transaction.
 */
@SuppressWarnings("unused")
public final class Transaction {

	private long accountId;

	private long timestamp;

	private double amount;

	public Transaction() { }

	public Transaction(long accountId, long timestamp, double amount) {
		this.accountId = accountId;
		this.timestamp = timestamp;
		this.amount = amount;
	}

	public long getAccountId() {
		return accountId;
	}

	public void setAccountId(long accountId) {
		this.accountId = accountId;
	}

	public long getTimestamp() {
		return timestamp;
	}

	public void setTimestamp(long timestamp) {
		this.timestamp = timestamp;
	}

	public double getAmount() {
		return amount;
	}

	public void setAmount(double amount) {
		this.amount = amount;
	}

	@Override
	public boolean equals(Object o) {
		if (this == o) {
			return true;
		} else if (o == null || getClass() != o.getClass()) {
			return false;
		}
		Transaction that = (Transaction) o;
		return accountId == that.accountId &&
			timestamp == that.timestamp &&
			Double.compare(that.amount, amount) == 0;
	}

	@Override
	public int hashCode() {
		return Objects.hash(accountId, timestamp, amount);
	}

	@Override
	public String toString() {
		return "Transaction{" +
			"accountId=" + accountId +
			", timestamp=" + timestamp +
			", amount=" + amount +
			'}';
	}
}

1.2.5 Alert.java

import java.util.Objects;

/**
 * A simple alert event.
 */
@SuppressWarnings("unused")
public final class Alert {

	private long id;

	public long getId() {
		return id;
	}

	public void setId(long id) {
		this.id = id;
	}

	@Override
	public boolean equals(Object o) {
		if (this == o) {
			return true;
		} else if (o == null || getClass() != o.getClass()) {
			return false;
		}
		Alert alert = (Alert) o;
		return id == alert.id;
	}

	@Override
	public int hashCode() {
		return Objects.hash(id);
	}

	@Override
	public String toString() {
		return "Alert{" +
			"id=" + id +
			'}';
	}
}

1.2.5 查看完整源碼

關於這個例子官方文檔上沒給源碼地址,不過我在Github 上找到了

點擊查看完整源碼

1.3 如何編寫一個Table API 應用

Apache Filnk 提供 Table API 作爲批處理和流處理統一的關係型API, 即查詢在無界實時流或有界批數據集上以相同的語義執行,併產生相同的結果。 Flink 中的 Table API 通常用於簡化數據分析,數據流水線和 ETL 應用程序的定義。

在本教程中,你將學習如何構建連續的 ETL 流水線,以便按賬戶隨時跟蹤金融交易。 首先你將報表構建爲每晚執行的批處理作業,然後遷移到流式管道。

import org.apache.flink.walkthrough.common.table.SpendReportTableSink;
import org.apache.flink.walkthrough.common.table.TransactionTableSource;
import org.apache.flink.streaming.api.TimeCharacteristic;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.Tumble;
import org.apache.flink.table.api.java.StreamTableEnvironment;

public class SpendReport {

    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);

        StreamTableEnvironment tEnv = StreamTableEnvironment.create(env);

        tEnv.registerTableSource("transactions", new UnboundedTransactionTableSource());
        tEnv.registerTableSink("spend_report", new SpendReportTableSink());

        tEnv
            .scan("transactions")
            .window(Tumble.over("1.hour").on("timestamp").as("w"))
            .groupBy("accountId, w")
            .select("accountId, w.start as timestamp, amount.sum")
            .insertInto("spend_report");

        env.execute("Spend Report");
    }
}

關於這個例子官方文檔上沒給源碼地址,不過我在Github 上找到了
點擊查看完整源碼

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