大數據篇---Flink學習

第一部分 Flink 概述

第 1 節 什麼是 Flink

​ Apache Flink是一個框架和分佈式處理引擎,用於對無界和有界數據流進行有狀態計算。Flink被設計在所有常見的集羣環境中運行,以內存執行速度和任意規模來執行計算。

  • Flink起源於2008年柏林理工大學的研究性項目Stratosphere

  • 2014年該項目被捐贈給了Apache軟件基金會

  • Flink一躍成爲Apache軟件基金會的頂級項目之一

第 2 節 Flink 特點

Flink 是一個開源的流處理框架,它具有以下特點

  • 批流一體:統一批處理、流處理

  • 分佈式:Flink程序可以運行在多臺機器上

  • 高性能:處理性能比較高

  • 高可用:Flink支持高可用性(HA)

  • 準確:Flink可以保證數據處理的準確性

第 3 節 Flink 應用場景

Flink主要應用於流式數據分析場景

數據無處不在,絕大多數的企業所採取的處理數據的架構都會劃分成兩類:事務型處理、分析型處理

事務型處理

OLTP On-Line Transaction Processing :聯機事務處理過程。

流程審批、數據錄入、填報等

特點:線下工作線上化,數據保存在各自的系統中,互不相通(數據孤島)

OLTP:聯機事務處理系統是一種以事務元作爲數據處理的單位、人機交互的計算機應用系統。

它能對數據進行即時更新其他操作,系統內的數據總是保持在最新狀態。

用戶可將<u>一組保持數據一致性的操作序列</u>指定爲一個事務元,通過終端、個人計算機或其他設備輸入事務元,經系統處理後返回結果,

OLTP主要用來記錄某類業務事件的發生,如購買行爲,當行爲產生後,系統會記錄是誰在何時何地做了何事,這樣的一行(或多行)數據會以增刪改的方式在數據庫中進行數據的更新處理操作,要求實時性高、穩定性強、確保數據及時更新成功。

應用於飛機訂票、銀行出納、股票交易、超市銷售、飯店前後管理等實時系統

比如公司常見的業務系統如ERP,CRM,OA等系統都屬於OLTP

ERP: Enterprise Resource Planning 企業資源計劃

CRM:Customer Relationship Management 客戶關係管理

OA:Office Automation 辦公自動化

image-20200914170700254

期間沒處理一條事件,應用都會通過執行遠程數據庫系統的事務來讀取或更新狀態。很多時候,多個應用會共享同一個數據庫系統,有時候還會訪問相同的數據庫或表。

該設計在應用需要更新數據庫擴縮容或更改表模式的時候容易導致問題。

分析型處理

當數據積累到一定的程度,我們需要對過去發生的事情做一個總結分析時,就需要把過去一段時間內產生的數據拿出來進行統計分析,從中獲取我們想要的信息,爲公司做決策提供支持,這時候就是在做OLAP了。

因爲OLTP所產生的業務數據分散在不同的業務系統中,而OLAP往往需要將不同的業務數據集中到一起進行統一綜合的分析,這時候就需要根據業務分析需求做對應的數據清洗後存儲在數據倉庫中,然後由數據倉庫來統一提供OLAP分析

OLAP On-Line Analytical Processing :聯機分析系統

分析報表,分析決策等

根據業務分析需求做對應的數據清洗後存儲在數據倉庫中稱爲ETL

ETL:Extract-Transform-Load: 從事務型數據庫中提取數據,將其轉化成通用的表示形式(可能包含數據驗證,數據歸一化,編碼、去重、表模式轉化等工作),最終加載到分析型數據庫中。

OLAP的實現方案一:(數倉)

2717543-04110b9fe00113a6

如上圖所示,數據實時寫入 HBase,實時的數據更新也在 HBase 完成,爲了應對 OLAP 需求,我們定時(通常是 T+1 或者 T+H)將 HBase 數據寫成靜態的文件(如:Parquet)導入到 OLAP 引擎(如:HDFS,比較常見的是Impala操作Hive)。這一架構能滿足既需要隨機讀寫,又可以支持 OLAP 分析的場景,但他有如下缺點:

  • 架構複雜。從架構上看,數據在 HBase、消息隊列、HDFS 間流轉,涉及環節太多,運維成本很高。並且每個環節需要保證高可用,都需要維護多個副本,存儲空間也有一定的浪費。最後數據在多個系統上,對數據安全策略、監控等都提出了挑戰。
  • 時效性低。數據從 HBase 導出成靜態文件是週期性的,一般這個週期是一天(或一小時),在時效性上不是很高。
  • 難以應對後續的更新。真實場景中,總會有數據是「延遲」到達的。如果這些數據之前已經從 HBase 導出到 HDFS,新到的變更數據就難以處理了,一個方案是把原有數據應用上新的變更後重寫一遍,但這代價又很高。

通常數據倉庫中的查詢可以分爲兩類:

1、普通查詢:是定製的

2、即系查詢:是用戶自定義查詢條件的

image-20200914153215779

  • 實時ETL

    集成流計算現有的諸多數據通道和SQL靈活的加工能力,對流式數據進行實時清洗、歸併和結構化處理;同時,對離線數倉進行有效的補充和優化,併爲數據實時傳輸提供可計算通道。

  • 實時報表

    實時化採集、加工流式數據存儲;實時監控和展現業務、客戶各類指標,讓數據化運營實時化。

    如通過分析訂單處理系統中的數據獲知銷售增長率;

    通過分析分析運輸延遲原因或預測銷售量調整庫存;

  • 監控預警

    對系統和用戶行爲進行實時監測和分析,以便及時發現危險行爲,如計算機網絡入侵、詐騙預警等

  • 在線系統

    實時計算各類數據指標,並利用實時結果及時調整在線系統的相關策略,在各類內容投放、智能推送領域有大量的應用,如在客戶瀏覽商品的同時推薦相關商品等

第 4 節 Flink 核心組成及生態發展

Flink核心組成

image-20200722171959131

  • Deploy層:
    • 可以啓動單個JVM,讓Flink以Local模式運行
    • Flink也可以以Standalone 集羣模式運行,同時也支持Flink ON YARN,Flink應用直接提交到YARN上面運行
    • Flink還可以運行在GCE(谷歌雲服務)和EC2(亞馬遜雲服務)
  • Core層(Runtime):在Runtime之上提供了兩套核心的API,DataStream API(流處理)和DataSet API(批處理)
  • APIs & Libraries層:核心API之上又擴展了一些高階的庫和API
    • CEP流處理
    • Table API和SQL
    • Flink ML機器學習庫
    • Gelly圖計算

Flink生態發展

image-20200722171657610

  • 中間部分主要內容在上面Flink核心組成中已經提到

  • 輸入Connectors(左側部分)

    流處理方式:包含Kafka(消息隊列)、AWS kinesis(實時數據流服務)、RabbitMQ(消息隊列)、NIFI(數據管道)、Twitter(API)

    批處理方式:包含HDFS(分佈式文件系統)、HBase(分佈式列式數據庫)、Amazon S3(文件系統)、MapR FS(文件系統)、ALLuxio(基於內存分佈式文件系統)

  • 輸出Connectors(右側部分)

    流處理方式:包含Kafka(消息隊列)、AWS kinesis(實時數據流服務)、RabbitMQ(消息隊列)、NIFI(數據管道)、Cassandra(NOSQL數據庫)、ElasticSearch(全文檢索)、HDFS rolling file(滾動文件)

    批處理方式:包含HBase(分佈式列式數據庫)、HDFS(分佈式文件系統)

第 5 節 Flink 處理模型:流處理與批處理

​ Flink 專注於無限流處理,有限流處理是無限流處理的一種特殊情況

img

無限流處理:

  • 輸入的數據沒有盡頭,像水流一樣源源不斷
  • 數據處理從當前或者過去的某一個時間 點開始,持續不停地進行

有限流處理:

  • 從某一個時間點開始處理數據,然後在另一個時間點結束

  • 輸入數據可能本身是有限的(即輸入數據集並不會隨着時間增長),也可能出於分析的目的被人爲地設定爲有限集(即只分析某一個時間段內的事件)

    Flink封裝了DataStream API進行流處理,封裝了DataSet API進行批處理。

    同時,Flink也是一個批流一體的處理引擎,提供了Table API / SQL統一了批處理和流處理

有狀態的流處理應用:

image-20200915183019028

第 6 節 流處理引擎的技術選型

​ 市面上的流處理引擎不止Flink一種,其他的比如Storm、SparkStreaming、Trident等,實際應用時如何進行選型,給大家一些建議參考

  • 流數據要進行狀態管理,選擇使用Trident、Spark Streaming或者Flink
  • 消息投遞需要保證At-least-once(至少一次)或者Exactly-once(僅一次)不能選擇Storm
  • 對於小型獨立項目,有低延遲要求,可以選擇使用Storm,更簡單
  • 如果項目已經引入了大框架Spark,實時處理需求可以滿足的話,建議直接使用Spark中的Spark Streaming
  • 消息投遞要滿足Exactly-once(僅一次),數據量大、有高吞吐、低延遲要求,要進行狀態管理或窗口統計,建議使用Flink

第二部分 Flink快速應用

​ 通過一個單詞統計的案例,快速上手應用Flink,進行流處理(Streaming)和批處理(Batch)

第 1 節 單詞統計案例(批數據)

1.1 需求

​ 統計一個文件中各個單詞出現的次數,把統計結果輸出到文件

步驟:

1、讀取數據源

2、處理數據源

a、將讀到的數據源文件中的每一行根據空格切分

b、將切分好的每個單詞拼接1

c、根據單詞聚合(將相同的單詞放在一起)

d、累加相同的單詞(單詞後面的1進行累加)

3、保存處理結果

1.2 代碼實現

  • 引入依賴

    <dependencies>
            <!-- https://mvnrepository.com/artifact/org.apache.flink/flink-java -->
            <dependency>
                <groupId>org.apache.flink</groupId>
                <artifactId>flink-java</artifactId>
                <version>1.11.1</version>
            </dependency>
            <!-- https://mvnrepository.com/artifact/org.apache.flink/flink-streaming-java -->
            <dependency>
                <groupId>org.apache.flink</groupId>
                <artifactId>flink-streaming-java_2.12</artifactId>
                <version>1.11.1</version>
                <scope>provided</scope>
            </dependency>
            <!-- https://mvnrepository.com/artifact/org.apache.flink/flink-clients -->
            <dependency>
                <groupId>org.apache.flink</groupId>
                <artifactId>flink-clients_2.12</artifactId>
                <version>1.11.1</version>
            </dependency>
    
            <!-- https://mvnrepository.com/artifact/org.apache.flink/flink-scala -->
            <dependency>
                <groupId>org.apache.flink</groupId>
                <artifactId>flink-scala_2.12</artifactId>
                <version>1.11.1</version>
            </dependency>
    
            <!-- https://mvnrepository.com/artifact/org.apache.flink/flink-streaming-scala -->
            <dependency>
                <groupId>org.apache.flink</groupId>
                <artifactId>flink-streaming-scala_2.12</artifactId>
                <version>1.11.1</version>
                <scope>provided</scope>
            </dependency>	
    
  • Java程序

    package com.lagou.edu.batch;
    
    import org.apache.flink.api.common.functions.FlatMapFunction;
    import org.apache.flink.api.java.DataSet;
    import org.apache.flink.api.java.ExecutionEnvironment;
    import org.apache.flink.api.java.tuple.Tuple2;
    import org.apache.flink.util.Collector;
    
    /**
     * 單詞統計(批數據處理)
     */
    public class WordCount {
        public static void main(String[] args) throws Exception {
            // 輸入路徑和出入路徑通過參數傳入,約定第一個參數爲輸入路徑,第二個參數爲輸出路徑
            String inPath = args[0];
            String outPath = args[1];
            // 獲取Flink批處理執行環境
            ExecutionEnvironment executionEnvironment = ExecutionEnvironment.getExecutionEnvironment();
            // 獲取文件中內容
            DataSet<String> text = executionEnvironment.readTextFile(inPath);
            // 對數據進行處理
            DataSet<Tuple2<String, Integer>> dataSet = text.flatMap(new LineSplitter()).groupBy(0).sum(1);
            dataSet.writeAsCsv(outputFile,"\n","").setParallelism(1);
            // 觸發執行程序
            executionEnvironment.execute("wordcount batch process");
        }
    
    
        static class LineSplitter implements FlatMapFunction<String, Tuple2<String,Integer>> {
            @Override
            public void flatMap(String line, Collector<Tuple2<String, Integer>> collector) throws Exception {
                for (String word:line.split(" ")) {
                    collector.collect(new Tuple2<>(word,1));
                }
            }
        }
    }
    
<properties>    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>    <maven.compiler.source>11</maven.compiler.source>    <maven.compiler.target>11</maven.compiler.target></properties>

第 2 節 單詞統計案例(流數據)

nc

netcat:

2.1 需求

​ Socket模擬實時發送單詞,使用Flink實時接收數據,對指定時間窗口內(如5s)的數據進行聚合統計,每隔1s彙總計算一次,並且把時間窗口內計算結果打印出來。

2.2 代碼實現

package com.lagou.edu.stream;

import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.util.Collector;

/**
 * 	Socket模擬實時發送單詞,使用Flink實時接收數據,對指定時間窗口內(如5s)的數據進行聚合統計,每隔1s彙總計算一次,並且把時間窗口內計算結果打印出來。
 teacher2 ip : 113.31.105.128
 */
public class WordCount {

    public static void main(String[] args) throws Exception {
        // 監聽的ip和端口號,以main參數形式傳入,約定第一個參數爲ip,第二個參數爲端口
        String ip = args[0];
        int port = Integer.parseInt(args[1]);
        // 獲取Flink流執行環境
        StreamExecutionEnvironment streamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment();
        // 獲取socket輸入數據
        DataStreamSource<String> textStream = streamExecutionEnvironment.socketTextStream(ip, port, "\n");

        SingleOutputStreamOperator<Tuple2<String, Long>> tuple2SingleOutputStreamOperator = textStream.flatMap(new FlatMapFunction<String, Tuple2<String, Long>>() {
            @Override
            public void flatMap(String s, Collector<Tuple2<String, Long>> collector) throws Exception {
                String[] splits = s.split("\\s");
                for (String word : splits) {
                    collector.collect(Tuple2.of(word, 1l));
                }
            }
        });

        SingleOutputStreamOperator<Tuple2<String, Long>> word = tuple2SingleOutputStreamOperator.keyBy("word")
                .timeWindow(Time.seconds(2), Time.seconds(1))
                .sum(1);
        // 打印數據
        word.print();
        // 觸發任務執行
        streamExecutionEnvironment.execute("wordcount stream process");

    }
}

​ Flink程序開發的流程總結如下:

1)獲得一個執行環境

2)加載/創建初始化數據

3)指定數據操作的算子

4)指定結果數據存放位置

5)調用execute()觸發執行程序

注意:Flink程序是延遲計算的,只有最後調用execute()方法的時候纔會真正觸發執行程序

第三部分 Flink體系結構

第 1 節 Flink的重要角色

​ Flink是非常經典的Master/Slave結構實現,JobManager是Master,TaskManager是Slave。

  • JobManager處理器(Master)

    • 協調分佈式執行,它們用來調度task,協調檢查點(CheckPoint),協調失敗時恢復等

    • Flink運行時至少存在一個master處理器,如果配置高可用模式則會存在多個master處理器,它們其中有一個是leader,而其他的都是standby。

  • JobManager接收的應用包括jar和JobGraph

  • TaskManager處理器(Slave)

    ​ 也稱之爲Worker

    • 主要職責是從JobManager處接收任務, 並部署和啓動任務, 接收上游的數據並處理
    • Task Manager 是在 JVM 中的一個或多個線程中執行任務的工作節點
    • TaskManager在啓動的時候會向ResourceManager註冊自己的資源信息(Slot的數量等)
  • ResourceManager

    針對不同的環境和資源提供者,如(YARN,Me搜索,Kubernetes或獨立部署),Flink提供了不同的ResourceManager

    作用:負責管理Flink的處理資源單元---Slot

  • Dispatcher:

    作用:提供一個REST接口來讓我們提交需要執行的應用。

    一旦一個應用提交執行,Dispatcher會啓動一個JobManager,並將應用轉交給他。

    Dispatcher還會啓動一個webUI來提供有關作業執行信息

    注意:某些應用的提交執行的方式,有可能用不到Dispatcher

各個組件之間的關係:

image-20200917154621166

第 2 節 Flink運行架構

2.1 Flink程序結構

​ Flink程序的基本構建塊是轉換(請注意,Flink的DataSet API中使用的DataSet也是內部流 )。從概念上講,流是(可能永無止境的)數據記錄流,而轉換是將一個或多個流輸入,併產生一個或多個輸出流。

image-20200731001906709

​ 上圖表述了Flink的應用程序結構,有Source(源頭)、Transformation(轉換)、Sink(接收器)三個重要組成部分

  • Source

    ​ 數據源,定義Flink從哪裏加載數據,Flink 在流處理和批處理上的 source 大概有 4 類:基於本地集合的 source、基於文件的 source、基於網絡套接字的 source、自定義的 source。自定義的 source 常見的有 Apache kafka、RabbitMQ 等。

  • Transformation

    ​ 數據轉換的各種操作,也稱之爲算子,有 Map / FlatMap / Filter / KeyBy / Reduce / Window等,可以將數據轉換計算成你想要的數據。

  • Sink

    ​ 接收器,Flink 將轉換計算後的數據發送的地點 ,定義了結果數據的輸出方向,Flink 常見的 Sink 大概有如下幾類:寫入文件、打印出來、寫入 socket 、自定義的 sink 。自定義的 sink 常見的有 Apache kafka、RabbitMQ、MySQL、ElasticSearch、Apache Cassandra、HDFS等。

2.2 Task和SubTask

  • Task 是一個階段多個功能相同 SubTask 的集合,類似於 Spark 中的 TaskSet。

  • SubTask(子任務)

    ​ SubTask 是 Flink 中任務最小執行單元,是一個 Java 類的實例,這個 Java 類中有屬性和方法,完成具體的計算邏輯

    ​ 比如一個執行操作map,分佈式的場景下會在多個線程中同時執行,每個線程中執行的都叫做一個SubTask(在2.3節的圖中也能夠體現)

2.3 Operator chain(操作器鏈)

​ Flink的所有操作都稱之爲Operator,客戶端在提交任務的時候會對Operator進行優化操作,能進行合併的Operator會被合併爲一個Operator,合併後的Operator稱爲Operator chain,實際上就是一個執行鏈,每個執行鏈會在TaskManager上一個獨立的線程中執行。shuffle

image-20200731002729966

2.4 Flink中的數據傳輸

在運行過程中,應用中的任務會持續進行數據交換。

爲了有效利用網絡資源和提高吞吐量,Flink在處理任務間的數據傳輸過程中,採用了緩衝區機制。

2.5 任務槽和槽共享

​ 任務槽也叫做task-slot、槽共享也叫做slot sharing

· 每個TaskManager是一個JVM的進程, 可以在不同的線程中執行一個或多個子任務。

​ 爲了控制一個worker能接收多少個task。worker通過task slot來進行控制(一個worker至少有一個task slot)

  • 任務槽

    ​ 每個task slot表示TaskManager擁有資源的一個固定大小的子集。 一般來說:我們分配槽的個數都是和CPU的核數相等,比如6核,那麼就分配6個槽.

    ​ Flink將進程的內存進行了劃分到多個Slot中。假設一個TaskManager機器有3個slot,那麼每個slot佔有1/3的內存(平分)。

    ​ 內存被劃分到不同的slot之後可以獲得如下好處:

    • TaskManager最多能同時併發執行的任務是可以控制的,那就是3個,因爲不能超過slot的數量
    • slot有獨佔的內存空間,這樣在一個TaskManager中可以運行多個不同的作業,作業之間不受影響
  • 槽共享

    ​ 默認情況下,Flink允許子任務subtast(map[1] map[2] keyby[1] keyby[2] 共享插槽,即使它們是不同任務的子任務,只要它們來自同一個作業。結果是一個槽可以保存作業的整個管道。

第四部分 Flink安裝和部署

​ Flink支持多種安裝模式

  • local(本地):單機模式,一般本地開發調試使用
  • StandAlone 獨立模式:Flink自帶集羣,自己管理資源調度,生產環境也會有所應用
  • Yarn模式:計算資源統一由Hadoop YARN管理,生產環境應用較多

第 1 節 環境準備工作

1.1 基礎環境

  • jdk1.8及以上【配置JAVA_HOME環境變量】

  • ssh免密碼登錄【集羣內節點之間免密登錄】

1.2 安裝包下載

配套資料文件夾中提供,使用Flink1.11.1版本

1.3 集羣規劃

hdp-1 hdp-2 hdp-3
JobManager+TaskManager TaskManager TaskManager

1.4 StandAlone模式部署

Step1、Flink安裝包上傳到hdp-1對應目錄並解壓

Step2、修改 flink/conf/flink-conf.yaml 文件

 jobmanager.rpc.address: hdp-1
 taskmanager.numberOfTaskSlots: 2

Step3、修改 /conf/slave文件

hdp-1
hdp-2
hdp-3

Step4、standalone模式啓動

bin目錄下執行./start-cluster.sh

Step5、jps進程查看覈實

3857 TaskManagerRunner
3411 StandaloneSessionClusterEntrypoint
3914 Jps

Step6、查看Flink的web頁面 ip:8081/#/overview

web

Step7、集羣模式下運行example測試

./flink run ../examples/streaming/WordCount.jar
./flink <ACTION> [OPTIONS] [ARGUMENTS]

The following actions are available:

Action "run" compiles and runs a program.

  Syntax: run [OPTIONS] <jar-file> <arguments>
  "run" action options:
     -c,--class <classname>               Class with the program entry point
                                          ("main()" method). Only needed if the
                                          JAR file does not specify the class in
                                          its manifest.
     -C,--classpath <url>                 Adds a URL to each user code
                                          classloader  on all nodes in the
                                          cluster. The paths must specify a
                                          protocol (e.g. file://) and be
                                          accessible on all nodes (e.g. by means
                                          of a NFS share). You can use this
                                          option multiple times for specifying
                                          more than one URL. The protocol must
                                          be supported by the {@link
                                          java.net.URLClassLoader}.
     -d,--detached                        If present, runs the job in detached
                                          mode
     -n,--allowNonRestoredState           Allow to skip savepoint state that
                                          cannot be restored. You need to allow
                                          this if you removed an operator from
                                          your program that was part of the
                                          program when the savepoint was
                                          triggered.
     -p,--parallelism <parallelism>       The parallelism with which to run the
                                          program. Optional flag to override the
                                          default value specified in the
                                          configuration.
     -py,--python <pythonFile>            Python script with the program entry
                                          point. The dependent resources can be
                                          configured with the `--pyFiles`
                                          option.
     -pyarch,--pyArchives <arg>           Add python archive files for job. The
                                          archive files will be extracted to the
                                          working directory of python UDF
                                          worker. Currently only zip-format is
                                          supported. For each archive file, a
                                          target directory be specified. If the
                                          target directory name is specified,
                                          the archive file will be extracted to
                                          a name can directory with the
                                          specified name. Otherwise, the archive
                                          file will be extracted to a directory
                                          with the same name of the archive
                                          file. The files uploaded via this
                                          option are accessible via relative
                                          path. '#' could be used as the
                                          separator of the archive file path and
                                          the target directory name. Comma (',')
                                          could be used as the separator to
                                          specify multiple archive files. This
                                          option can be used to upload the
                                          virtual environment, the data files
                                          used in Python UDF (e.g.: --pyArchives
                                          file:///tmp/py37.zip,file:///tmp/data.
                                          zip#data --pyExecutable
                                          py37.zip/py37/bin/python). The data
                                          files could be accessed in Python UDF,
                                          e.g.: f = open('data/data.txt', 'r').
     -pyexec,--pyExecutable <arg>         Specify the path of the python
                                          interpreter used to execute the python
                                          UDF worker (e.g.: --pyExecutable
                                          /usr/local/bin/python3). The python
                                          UDF worker depends on Python 3.5+,
                                          Apache Beam (version == 2.19.0), Pip
                                          (version >= 7.1.0) and SetupTools
                                          (version >= 37.0.0). Please ensure
                                          that the specified environment meets
                                          the above requirements.
     -pyfs,--pyFiles <pythonFiles>        Attach custom python files for job.
                                          These files will be added to the
                                          PYTHONPATH of both the local client
                                          and the remote python UDF worker. The
                                          standard python resource file suffixes
                                          such as .py/.egg/.zip or directory are
                                          all supported. Comma (',') could be
                                          used as the separator to specify
                                          multiple files (e.g.: --pyFiles
                                          file:///tmp/myresource.zip,hdfs:///$na
                                          menode_address/myresource2.zip).
     -pym,--pyModule <pythonModule>       Python module with the program entry
                                          point. This option must be used in
                                          conjunction with `--pyFiles`.
     -pyreq,--pyRequirements <arg>        Specify a requirements.txt file which
                                          defines the third-party dependencies.
                                          These dependencies will be installed
                                          and added to the PYTHONPATH of the
                                          python UDF worker. A directory which
                                          contains the installation packages of
                                          these dependencies could be specified
                                          optionally. Use '#' as the separator
                                          if the optional parameter exists
                                          (e.g.: --pyRequirements
                                          file:///tmp/requirements.txt#file:///t
                                          mp/cached_dir).
     -s,--fromSavepoint <savepointPath>   Path to a savepoint to restore the job
                                          from (for example
                                          hdfs:///flink/savepoint-1537).
     -sae,--shutdownOnAttachedExit        If the job is submitted in attached
                                          mode, perform a best-effort cluster
                                          shutdown when the CLI is terminated
                                          abruptly, e.g., in response to a user
                                          interrupt, such as typing Ctrl + C.
  Options for Generic CLI mode:
     -D <property=value>   Generic configuration options for
                           execution/deployment and for the configured executor.
                           The available options can be found at
                           https://ci.apache.org/projects/flink/flink-docs-stabl
                           e/ops/config.html
     -e,--executor <arg>   DEPRECATED: Please use the -t option instead which is
                           also available with the "Application Mode".
                           The name of the executor to be used for executing the
                           given job, which is equivalent to the
                           "execution.target" config option. The currently
                           available executors are: "collection", "remote",
                           "local", "kubernetes-session", "yarn-per-job",
                           "yarn-session".
     -t,--target <arg>     The deployment target for the given application,
                           which is equivalent to the "execution.target" config
                           option. The currently available targets are:
                           "collection", "remote", "local",
                           "kubernetes-session", "yarn-per-job", "yarn-session",
                           "yarn-application" and "kubernetes-application".

  Options for yarn-cluster mode:
     -m,--jobmanager <arg>            Address of the JobManager to which to
                                      connect. Use this flag to connect to a
                                      different JobManager than the one
                                      specified in the configuration.
     -yid,--yarnapplicationId <arg>   Attach to running YARN session
     -z,--zookeeperNamespace <arg>    Namespace to create the Zookeeper
                                      sub-paths for high availability mode

  Options for default mode:
     -m,--jobmanager <arg>           Address of the JobManager to which to
                                     connect. Use this flag to connect to a
                                     different JobManager than the one specified
                                     in the configuration.
     -z,--zookeeperNamespace <arg>   Namespace to create the Zookeeper sub-paths
                                     for high availability mode



Action "info" shows the optimized execution plan of the program (JSON).

  Syntax: info [OPTIONS] <jar-file> <arguments>
  "info" action options:
     -c,--class <classname>           Class with the program entry point
                                      ("main()" method). Only needed if the JAR
                                      file does not specify the class in its
                                      manifest.
     -p,--parallelism <parallelism>   The parallelism with which to run the
                                      program. Optional flag to override the
                                      default value specified in the
                                      configuration.


Action "list" lists running and scheduled programs.

  Syntax: list [OPTIONS]
  "list" action options:
     -a,--all         Show all programs and their JobIDs
     -r,--running     Show only running programs and their JobIDs
     -s,--scheduled   Show only scheduled programs and their JobIDs
  Options for Generic CLI mode:
     -D <property=value>   Generic configuration options for
                           execution/deployment and for the configured executor.
                           The available options can be found at
                           https://ci.apache.org/projects/flink/flink-docs-stabl
                           e/ops/config.html
     -e,--executor <arg>   DEPRECATED: Please use the -t option instead which is
                           also available with the "Application Mode".
                           The name of the executor to be used for executing the
                           given job, which is equivalent to the
                           "execution.target" config option. The currently
                           available executors are: "collection", "remote",
                           "local", "kubernetes-session", "yarn-per-job",
                           "yarn-session".
     -t,--target <arg>     The deployment target for the given application,
                           which is equivalent to the "execution.target" config
                           option. The currently available targets are:
                           "collection", "remote", "local",
                           "kubernetes-session", "yarn-per-job", "yarn-session",
                           "yarn-application" and "kubernetes-application".

  Options for yarn-cluster mode:
     -m,--jobmanager <arg>            Address of the JobManager to which to
                                      connect. Use this flag to connect to a
                                      different JobManager than the one
                                      specified in the configuration.
     -yid,--yarnapplicationId <arg>   Attach to running YARN session
     -z,--zookeeperNamespace <arg>    Namespace to create the Zookeeper
                                      sub-paths for high availability mode

  Options for default mode:
     -m,--jobmanager <arg>           Address of the JobManager to which to
                                     connect. Use this flag to connect to a
                                     different JobManager than the one specified
                                     in the configuration.
     -z,--zookeeperNamespace <arg>   Namespace to create the Zookeeper sub-paths
                                     for high availability mode



Action "stop" stops a running program with a savepoint (streaming jobs only).

  Syntax: stop [OPTIONS] <Job ID>
  "stop" action options:
     -d,--drain                           Send MAX_WATERMARK before taking the
                                          savepoint and stopping the pipelne.
     -p,--savepointPath <savepointPath>   Path to the savepoint (for example
                                          hdfs:///flink/savepoint-1537). If no
                                          directory is specified, the configured
                                          default will be used
                                          ("state.savepoints.dir").
  Options for Generic CLI mode:
     -D <property=value>   Generic configuration options for
                           execution/deployment and for the configured executor.
                           The available options can be found at
                           https://ci.apache.org/projects/flink/flink-docs-stabl
                           e/ops/config.html
     -e,--executor <arg>   DEPRECATED: Please use the -t option instead which is
                           also available with the "Application Mode".
                           The name of the executor to be used for executing the
                           given job, which is equivalent to the
                           "execution.target" config option. The currently
                           available executors are: "collection", "remote",
                           "local", "kubernetes-session", "yarn-per-job",
                           "yarn-session".
     -t,--target <arg>     The deployment target for the given application,
                           which is equivalent to the "execution.target" config
                           option. The currently available targets are:
                           "collection", "remote", "local",
                           "kubernetes-session", "yarn-per-job", "yarn-session",
                           "yarn-application" and "kubernetes-application".

  Options for yarn-cluster mode:
     -m,--jobmanager <arg>            Address of the JobManager to which to
                                      connect. Use this flag to connect to a
                                      different JobManager than the one
                                      specified in the configuration.
     -yid,--yarnapplicationId <arg>   Attach to running YARN session
     -z,--zookeeperNamespace <arg>    Namespace to create the Zookeeper
                                      sub-paths for high availability mode

  Options for default mode:
     -m,--jobmanager <arg>           Address of the JobManager to which to
                                     connect. Use this flag to connect to a
                                     different JobManager than the one specified
                                     in the configuration.
     -z,--zookeeperNamespace <arg>   Namespace to create the Zookeeper sub-paths
                                     for high availability mode



Action "cancel" cancels a running program.

  Syntax: cancel [OPTIONS] <Job ID>
  "cancel" action options:
     -s,--withSavepoint <targetDirectory>   **DEPRECATION WARNING**: Cancelling
                                            a job with savepoint is deprecated.
                                            Use "stop" instead.
                                            Trigger savepoint and cancel job.
                                            The target directory is optional. If
                                            no directory is specified, the
                                            configured default directory
                                            (state.savepoints.dir) is used.
  Options for Generic CLI mode:
     -D <property=value>   Generic configuration options for
                           execution/deployment and for the configured executor.
                           The available options can be found at
                           https://ci.apache.org/projects/flink/flink-docs-stabl
                           e/ops/config.html
     -e,--executor <arg>   DEPRECATED: Please use the -t option instead which is
                           also available with the "Application Mode".
                           The name of the executor to be used for executing the
                           given job, which is equivalent to the
                           "execution.target" config option. The currently
                           available executors are: "collection", "remote",
                           "local", "kubernetes-session", "yarn-per-job",
                           "yarn-session".
     -t,--target <arg>     The deployment target for the given application,
                           which is equivalent to the "execution.target" config
                           option. The currently available targets are:
                           "collection", "remote", "local",
                           "kubernetes-session", "yarn-per-job", "yarn-session",
                           "yarn-application" and "kubernetes-application".

  Options for yarn-cluster mode:
     -m,--jobmanager <arg>            Address of the JobManager to which to
                                      connect. Use this flag to connect to a
                                      different JobManager than the one
                                      specified in the configuration.
     -yid,--yarnapplicationId <arg>   Attach to running YARN session
     -z,--zookeeperNamespace <arg>    Namespace to create the Zookeeper
                                      sub-paths for high availability mode

  Options for default mode:
     -m,--jobmanager <arg>           Address of the JobManager to which to
                                     connect. Use this flag to connect to a
                                     different JobManager than the one specified
                                     in the configuration.
     -z,--zookeeperNamespace <arg>   Namespace to create the Zookeeper sub-paths
                                     for high availability mode



Action "savepoint" triggers savepoints for a running job or disposes existing ones.

  Syntax: savepoint [OPTIONS] <Job ID> [<target directory>]
  "savepoint" action options:
     -d,--dispose <arg>       Path of savepoint to dispose.
     -j,--jarfile <jarfile>   Flink program JAR file.
  Options for Generic CLI mode:
     -D <property=value>   Generic configuration options for
                           execution/deployment and for the configured executor.
                           The available options can be found at
                           https://ci.apache.org/projects/flink/flink-docs-stabl
                           e/ops/config.html
     -e,--executor <arg>   DEPRECATED: Please use the -t option instead which is
                           also available with the "Application Mode".
                           The name of the executor to be used for executing the
                           given job, which is equivalent to the
                           "execution.target" config option. The currently
                           available executors are: "collection", "remote",
                           "local", "kubernetes-session", "yarn-per-job",
                           "yarn-session".
     -t,--target <arg>     The deployment target for the given application,
                           which is equivalent to the "execution.target" config
                           option. The currently available targets are:
                           "collection", "remote", "local",
                           "kubernetes-session", "yarn-per-job", "yarn-session",
                           "yarn-application" and "kubernetes-application".

  Options for yarn-cluster mode:
     -m,--jobmanager <arg>            Address of the JobManager to which to
                                      connect. Use this flag to connect to a
                                      different JobManager than the one
                                      specified in the configuration.
     -yid,--yarnapplicationId <arg>   Attach to running YARN session
     -z,--zookeeperNamespace <arg>    Namespace to create the Zookeeper
                                      sub-paths for high availability mode

  Options for default mode:
     -m,--jobmanager <arg>           Address of the JobManager to which to
                                     connect. Use this flag to connect to a
                                     different JobManager than the one specified
                                     in the configuration.
     -z,--zookeeperNamespace <arg>   Namespace to create the Zookeeper sub-paths
                                     for high availability mode

Step8、查看結果文件

注意:集羣搭建完畢後,Flink程序就可以達成Jar,在集羣環境下類似於Step7中一樣提交執行計算任務

打jar包插件:

<build>
        <plugins>
            <!-- 打jar插件 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>2.4.3</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <filters>
                                <filter>
                                    <artifact>*:*</artifact>
                                    <excludes>
                                        <exclude>META-INF/*.SF</exclude>
                                        <exclude>META-INF/*.DSA</exclude>
                                        <exclude>META-INF/*.RSA</exclude>
                                    </excludes>
                                </filter>
                            </filters>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

        </plugins>
    </build>

1.5 Yarn模式部署

(1)啓動一個YARN session(Start a long-running Flink cluster on YARN);

image-20200921141637845

修改/etc/profile

export HADOOP_CONF_DIR=$HADOOP_HOME
export YARN_CONF_DIR=$HADOOP_HOME/etc/hadoop
export HADOOP_CLASSPATH=`hadoop classpath`

配置文件 yarn-site.xml

<property>
    <name>yarn.nodemanager.pmem-check-enabled</name>
    <value>false</value>
</property>
<property>
    <name>yarn.nodemanager.vmem-check-enabled</name>
    <value>false</value>
</property>
  <property>
    <name>yarn.resourcemanager.address</name>
    <value>teacher2:8032</value>
  </property>
  <property>
    <name>yarn.resourcemanager.scheduler.address</name>
    <value>teacher2:8030</value>
  </property>
  <property>
    <name>yarn.resourcemanager.resource-tracker.address</name>
    <value>teacher2:8031</value>
  </property>

注意:yarn-site的修改需要在集羣的每一臺機器上執行

啓動hadoop (hdfs-yarn) yarn-session.sh -h

Optional
     -at,--applicationType <arg>     Set a custom application type for the application on YARN
     -D <property=value>             use value for given property
     -d,--detached                   If present, runs the job in detached mode
     -h,--help                       Help for the Yarn session CLI.
     -id,--applicationId <arg>       Attach to running YARN session
     -j,--jar <arg>                  Path to Flink jar file
     -jm,--jobManagerMemory <arg>    Memory for JobManager Container with optional unit (default: MB)
     -m,--jobmanager <arg>           Address of the JobManager to which to connect. Use this flag to connect to a different JobManager than the one specified in the configuration.
     -nl,--nodeLabel <arg>           Specify YARN node label for the YARN application
     -nm,--name <arg>                Set a custom name for the application on YARN
     -q,--query                      Display available YARN resources (memory, cores)
     -qu,--queue <arg>               Specify YARN queue.
     -s,--slots <arg>                Number of slots per TaskManager
     -t,--ship <arg>                 Ship files in the specified directory (t for transfer)
     -tm,--taskManagerMemory <arg>   Memory per TaskManager Container with optional unit (default: MB)
     -yd,--yarndetached              If present, runs the job in detached mode (deprecated; use non-YARN specific option instead)
     -z,--zookeeperNamespace <arg>   Namespace to create the Zookeeper sub-paths for high availability mode
[root@hdp-1 bin]# 

/export/servers/flink/bin/yarn-session.sh -n 2 -tm 800 -s 1 -d

申請2個CPU、1600M內存::

bin/yarn-session.sh -n 2 -tm 800 -s 1 -d

# -n 表示申請2個容器,這裏指的就是多少個taskmanager

# -s 表示每個TaskManager的slots數量

# -tm 表示每個TaskManager的內存大小

# -d 表示以後臺程序方式運行

l 解釋

上面的命令的意思是,同時向Yarn申請3個container

(即便只申請了兩個,因爲ApplicationMaster和Job Manager有一個額外的容器。一旦將Flink部署到YARN羣集中,它就會顯示Job Manager的連接詳細信息)

2 個 Container 啓動 TaskManager -n 2,每個 TaskManager 擁有1個 Task Slot -s 1,並且向每個 TaskManager 的 Container 申請 800M 的內存,以及一個ApplicationMaster--Job Manager。

如果不想讓Flink YARN客戶端始終運行,那麼也可以啓動分離的 YARN會話。該參數被稱爲-d或--detached。在這種情況下,Flink YARN客戶端只會將Flink提交給集羣,然後關閉它自己

yarn-session.sh(開闢資源) + flink run(提交任務)

- 使用Flink中的yarn-session(yarn客戶端),會啓動兩個必要服務JobManager和TaskManager

- 客戶端通過flink run提交作業

- yarn-session會一直啓動,不停地接收客戶端提交的作業

- 這種方式創建的Flink集羣會獨佔資源。

- 如果有大量的小作業/任務比較小,或者工作時間短,適合使用這種方式,減少資源創建的時間.

(2)直接在YARN上提交運行Flink作業(Run a Flink job on YARN)

image-20200921144352862

bin/flink run -m yarn-cluster -yn 2 -yjm 1024 -ytm 1024 /export/servers/flink/examples/batch/WordCount.jar

# -m jobmanager的地址

# -yn 表示TaskManager的個數

停止yarn-cluster:

yarn application -kill application_1527077715040_0003

rm -rf /tmp/.yarn-properties-root

image-20200921145741521

第五部分 Flink常用API詳解

​ DataStream API主要分爲3塊:DataSource、Transformation、Sink

  • DataSource是程序的數據源輸入,可以通過StreamExecutionEnvironment.addSource(sourceFuntion)爲程序添加一個數據源
  • Transformation是具體的操作,它對一個或多個輸入數據源進行計算處理,比如Map、FlatMap和Filter等操作
  • Sink是程序的輸出,它可以把Transformation處理之後的數據輸出到指定的存儲介質中。

第 1 節 Flink DataStream常用API

1.1 DataSource

​ Flink針對DataStream提供了大量已經實現的DataSource(數據源接口),比如如下4種

1)基於文件

​ readTextFile(path)

讀取文本文件,文件遵循TextInputFormat逐行讀取規則並返回

tip:本地Idea讀hdfs需要:

依賴:

		<dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-hadoop-compatibility_2.11</artifactId>
            <version>1.11.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-common</artifactId>
            <version>2.8.5</version>
        </dependency>	
        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-hdfs</artifactId>
            <version>2.8.5</version>
        </dependency>
        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-client</artifactId>
            <version>2.8.5</version>
        </dependency>

2)基於Socket

​ socketTextStream

從Socket中讀取數據,元素可以通過一個分隔符分開

3)基於集合

​ fromCollection(Collection)

通過Java的Collection集合創建一個數據流,集合中的所有元素必須是相同類型的

如果滿足以下條件,Flink將數據類型識別爲POJO類型(並允許“按名稱”字段引用):

  • 該類是共有且獨立的(沒有非靜態內部類)
  • 該類有共有的無參構造方法
  • 類(及父類)中所有的不被static、transient修飾的屬性要麼有公有的(且不被final修飾),要麼是包含共有的getter和setter方法,這些方法遵循java bean命名規範。

實例:

package com.lagou.edu.streamsource;

import org.apache.flink.api.common.functions.FilterFunction;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;

import java.util.ArrayList;

public class StreamFromCollection {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//        DataStreamSource<String> data = env.fromElements("spark", "flink");
        ArrayList<People> peopleList = new ArrayList<People>();
        peopleList.add(new People("lucas", 18));
        peopleList.add(new People("jack", 30));
        peopleList.add(new People("jack", 40));
        DataStreamSource<People> data = env.fromCollection(peopleList);
//        DataStreamSource<People> data = env.fromElements(new People("lucas", 18), new People("jack", 30), new People("jack", 40));
        SingleOutputStreamOperator<People> filtered = data.filter(new FilterFunction<People>() {
            public boolean filter(People people) throws Exception {
                return people.age > 20;
            }
        });
        filtered.print();
        env.execute();


    }

    public static class People{
        public String name;
        public Integer age;

        public People(String name, Integer age) {
            this.name = name;
            this.age = age;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public Integer getAge() {
            return age;
        }

        public void setAge(Integer age) {
            this.age = age;
        }

        @Override
        public String toString() {
            return "People{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
}

4)自定義輸入

可以使用StreamExecutionEnvironment.addSource(sourceFunction)將一個流式數據源加到程序中。

Flink提供了許多預先實現的源函數,但是也可以編寫自己的自定義源,方法是爲非並行源implements SourceFunction,或者爲並行源 implements ParallelSourceFunction接口,或者extends RichParallelSourceFunction。

​ Flink也提供了一批內置的Connector(連接器),如下表列了幾個主要的

連接器 是否提供Source支持 是否提供Sink支持
Apache Kafka
ElasticSearch
HDFS
Twitter Streaming PI

Kafka連接器

a、依賴:

        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-connector-kafka_2.11</artifactId>
            <version>1.11.1</version>
        </dependency>

b、代碼:

import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer;
import org.apache.flink.util.Collector;

import java.util.Properties;

public class StreamFromKafka {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        Properties properties = new Properties();
        properties.setProperty("bootstrap.servers","hdp-2:9092");
        FlinkKafkaConsumer<String> consumer = new FlinkKafkaConsumer<String>("mytopic2", new SimpleStringSchema(), properties);
        DataStreamSource<String> data = env.addSource(consumer);
        SingleOutputStreamOperator<Tuple2<String, Integer>> wordAndOne = data.flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() {
            public void flatMap(String s, Collector<Tuple2<String, Integer>> collector) throws Exception {
                for (String word : s.split(" ")) {
                    collector.collect(Tuple2.of(word, 1));
                }
            }
        });
        SingleOutputStreamOperator<Tuple2<String, Integer>> result = wordAndOne.keyBy(0).sum(1);
        result.print();
        env.execute();
    }
}

c、啓動kafka

./kafka-server-start.sh -daemon ../config/server.properties

d、創建topic

bin/kafka-topics.sh --create --zookeepper teacher1:2181 --replication-factor 1 --partitions 1 --topic mytopic2

e、啓動控制檯kafka生產者

./kafka-console-consumer.sh --bootstrap-server hdp-2:9092 --topic animal

爲非並行源implements SourceFunction,或者爲並行源 implements ParallelSourceFunction接口,或者extends RichParallelSourceFunction。

爲非並行源implements SourceFunction

package com.lagou.bak;

import org.apache.flink.streaming.api.functions.source.SourceFunction;

public class NoParalleSource implements SourceFunction<String> {
    private Long count = 1l;
    private boolean isRunning = true;

    public void run(SourceContext<String> ctx) throws Exception {
        while (isRunning) {
            count ++;
            ctx.collect(String.valueOf(count));
            Thread.sleep(1000);
        }
    }

    public void cancel() {
        isRunning = false;
    }
}

package com.lagou.bak;

import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;

public class FromNoParalleSource {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStreamSource<String> data = env.addSource(new NoParalleSource());
        data.print();
        env.execute();
    }
}

爲並行源 implements arallelSourceFunction接口

package com.lagou.bak;

import org.apache.flink.streaming.api.functions.source.ParallelSourceFunction;

public class ParalleSource implements ParallelSourceFunction<String> {
    long count = 0;
    boolean isRunning = true;
    @Override
    public void run(SourceContext<String> ctx) throws Exception {
        while(isRunning) {
            ctx.collect(String.valueOf(count));
            count ++;
            Thread.sleep(1000);
        }
    }

    @Override
    public void cancel() {
        isRunning = false;
    }
}

package com.lagou.bak;

import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;

public class FromParllelSource {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStreamSource<String> data = env.addSource(new ParalleSource());
        data.print();
        env.execute();
    }
}

extends RichParallelSourceFunction

package com.lagou.bak;

import org.apache.flink.streaming.api.functions.source.RichSourceFunction;

public class ParallelSourceRich extends RichParallelSourceFunction<String> {
    long count = 0;
    boolean isRunning = true;
    @Override
    public void run(SourceContext<String> ctx) throws Exception {
        while(isRunning) {
            ctx.collect(String.valueOf(count));
            count ++;
            Thread.sleep(1000);
        }
    }

    @Override
    public void cancel() {
        isRunning = false;
    }
}

package com.lagou.bak;

import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;

public class FromRichSourceFunction {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStreamSource<String> data = env.addSource(new ParallelSourceRich());
        data.print();
        env.execute();
    }
}

總結自定義數據源:flinkkafkaconnector源碼初探:

open方法:初始化

run方法:從kafka拉取數據

1.2 Transformation

Flink針對DataStream提供了大量的已經實現的算子

Map DataStream → DataStream Takes one element and produces one element. A map function that doubles the values of the input stream:

DataStream<Integer> dataStream = //...
dataStream.map(new MapFunction<Integer, Integer>() {
    @Override
    public Integer map(Integer value) throws Exception {
        return 2 * value;
    }
});

FlatMap DataStream → DataStream Takes one element and produces zero, one, or more elements. A flatmap function that splits sentences to words:

dataStream.flatMap(new FlatMapFunction<String, String>() {
    @Override
    public void flatMap(String value, Collector<String> out)
        throws Exception {
        for(String word: value.split(" ")){
            out.collect(word);
        }
    }
});

Filter DataStream → DataStream Evaluates a boolean function for each element and retains those for which the function returns true. A filter that filters out zero values:

dataStream.filter(new FilterFunction<Integer>() {
    @Override
    public boolean filter(Integer value) throws Exception {
        return value != 0;
    }
});

KeyBy DataStream → KeyedStream Logically partitions a stream into disjoint partitions. All records with the same key are assigned to the same partition. Internally, keyBy() is implemented with hash partitioning. There are different ways to specify keys.

This transformation returns a KeyedStream, which is, among other things, required to use keyed state.

dataStream.keyBy(value -> value.getSomeKey()) // Key by field "someKey"
dataStream.keyBy(value -> value.f0) // Key by the first element of a Tuple

Attention A type cannot be a key if:

it is a POJO type but does not override the hashCode() method and relies on the Object.hashCode() implementation. it is an array of any type. Reduce KeyedStream → DataStream A "rolling" reduce on a keyed data stream. Combines the current element with the last reduced value and emits the new value.

A reduce function that creates a stream of partial sums:

keyedStream.reduce(new ReduceFunction<Integer>() {
    @Override
    public Integer reduce(Integer value1, Integer value2)
    throws Exception {
        return value1 + value2;
    }
});

Fold KeyedStream → DataStream A "rolling" fold on a keyed data stream with an initial value. Combines the current element with the last folded value and emits the new value.

A fold function that, when applied on the sequence (1,2,3,4,5), emits the sequence "start-1", "start-1-2", "start-1-2-3", ...

DataStream<String> result =
  keyedStream.fold("start", new FoldFunction<Integer, String>() {
    @Override
    public String fold(String current, Integer value) {
        return current + "-" + value;
    }
  });

Aggregations KeyedStream → DataStream Rolling aggregations on a keyed data stream. The difference between min and minBy is that min returns the minimum value, whereas minBy returns the element that has the minimum value in this field (same for max and maxBy).

keyedStream.sum(0);
keyedStream.sum("key");
keyedStream.min(0);
keyedStream.min("key");
keyedStream.max(0);
keyedStream.max("key");
keyedStream.minBy(0);
keyedStream.minBy("key");
keyedStream.maxBy(0);
keyedStream.maxBy("key");

Window KeyedStream → WindowedStream Windows can be defined on already partitioned KeyedStreams. Windows group the data in each key according to some characteristic (e.g., the data that arrived within the last 5 seconds). See windows for a complete description of windows.

dataStream.keyBy(value -> value.f0).window(TumblingEventTimeWindows.of(Time.seconds(5))); // Last 5 seconds of data

WindowAll DataStream → AllWindowedStream Windows can be defined on regular DataStreams. Windows group all the stream events according to some characteristic (e.g., the data that arrived within the last 5 seconds). See windows for a complete description of windows.

WARNING: This is in many cases a non-parallel transformation. All records will be gathered in one task for the windowAll operator.

dataStream.windowAll(TumblingEventTimeWindows.of(Time.seconds(5))); // Last 5 seconds of data

Window Apply WindowedStream → DataStream AllWindowedStream → DataStream Applies a general function to the window as a whole. Below is a function that manually sums the elements of a window.

Note: If you are using a windowAll transformation, you need to use an AllWindowFunction instead.

windowedStream.apply (new WindowFunction<Tuple2<String,Integer>, Integer, Tuple, Window>() {
    public void apply (Tuple tuple,
            Window window,
            Iterable<Tuple2<String, Integer>> values,
            Collector<Integer> out) throws Exception {
        int sum = 0;
        for (value t: values) {
            sum += t.f1;
        }
        out.collect (new Integer(sum));
    }
});

// applying an AllWindowFunction on non-keyed window stream
allWindowedStream.apply (new AllWindowFunction<Tuple2<String,Integer>, Integer, Window>() {
    public void apply (Window window,
            Iterable<Tuple2<String, Integer>> values,
            Collector<Integer> out) throws Exception {
        int sum = 0;
        for (value t: values) {
            sum += t.f1;
        }
        out.collect (new Integer(sum));
    }
});

Window Reduce WindowedStream → DataStream Applies a functional reduce function to the window and returns the reduced value.

windowedStream.reduce (new ReduceFunction<Tuple2<String,Integer>>() {
    public Tuple2<String, Integer> reduce(Tuple2<String, Integer> value1, Tuple2<String, Integer> value2) throws Exception {
        return new Tuple2<String,Integer>(value1.f0, value1.f1 + value2.f1);
    }
});

Window Fold WindowedStream → DataStream Applies a functional fold function to the window and returns the folded value. The example function, when applied on the sequence (1,2,3,4,5), folds the sequence into the string "start-1-2-3-4-5":

windowedStream.fold("start", new FoldFunction<Integer, String>() {
    public String fold(String current, Integer value) {
        return current + "-" + value;
    }
});

Aggregations on windows WindowedStream → DataStream Aggregates the contents of a window. The difference between min and minBy is that min returns the minimum value, whereas minBy returns the element that has the minimum value in this field (same for max and maxBy).

windowedStream.sum(0);
windowedStream.sum("key");
windowedStream.min(0);
windowedStream.min("key");
windowedStream.max(0);
windowedStream.max("key");
windowedStream.minBy(0);
windowedStream.minBy("key");
windowedStream.maxBy(0);
windowedStream.maxBy("key");

Union DataStream → DataStream Union of two or more data streams creating a new stream containing all the elements from all the streams. Note: If you union a data stream with itself you will get each element twice in the resulting stream.

dataStream.union(otherStream1, otherStream2, ...);

Window Join DataStream,DataStream → DataStream Join two data streams on a given key and a common window.

dataStream.join(otherStream)
    .where(<key selector>).equalTo(<key selector>)
    .window(TumblingEventTimeWindows.of(Time.seconds(3)))
    .apply (new JoinFunction () {...});

Interval Join KeyedStream,KeyedStream → DataStream Join two elements e1 and e2 of two keyed streams with a common key over a given time interval, so that e1.timestamp + lowerBound <= e2.timestamp <= e1.timestamp + upperBound

// this will join the two streams so that
// key1 == key2 && leftTs - 2 < rightTs < leftTs + 2
keyedStream.intervalJoin(otherKeyedStream)
    .between(Time.milliseconds(-2), Time.milliseconds(2)) // lower and upper bound
    .upperBoundExclusive(true) // optional
    .lowerBoundExclusive(true) // optional
    .process(new IntervalJoinFunction() {...});

Window CoGroup DataStream,DataStream → DataStream Cogroups two data streams on a given key and a common window.

dataStream.coGroup(otherStream)
    .where(0).equalTo(1)
    .window(TumblingEventTimeWindows.of(Time.seconds(3)))
    .apply (new CoGroupFunction () {...});

Connect DataStream,DataStream → ConnectedStreams "Connects" two data streams retaining their types. Connect allowing for shared state between the two streams.

DataStream<Integer> someStream = //...
DataStream<String> otherStream = //...

ConnectedStreams<Integer, String> connectedStreams = someStream.connect(otherStream);

CoMap, CoFlatMap ConnectedStreams → DataStream Similar to map and flatMap on a connected data stream

connectedStreams.map(new CoMapFunction<Integer, String, Boolean>() {
    @Override
    public Boolean map1(Integer value) {
        return true;
    }

@Override
public Boolean map2(String value) {
    return false;
}

});
connectedStreams.flatMap(new CoFlatMapFunction<Integer, String, String>() {

   @Override
   public void flatMap1(Integer value, Collector<String> out) {
       out.collect(value.toString());
   }

   @Override
   public void flatMap2(String value, Collector<String> out) {
       for (String word: value.split(" ")) {
         out.collect(word);
       }
   }
});

Split DataStream → SplitStream Split the stream into two or more streams according to some criterion.

SplitStream<Integer> split = someDataStream.split(new OutputSelector<Integer>() {
    @Override
    public Iterable<String> select(Integer value) {
        List<String> output = new ArrayList<String>();
        if (value % 2 == 0) {
            output.add("even");
        }
        else {
            output.add("odd");
        }
        return output;
    }
});

Select SplitStream → DataStream Select one or more streams from a split stream.

SplitStream<Integer> split;
DataStream<Integer> even = split.select("even");
DataStream<Integer> odd = split.select("odd");
DataStream<Integer> all = split.select("even","odd");

Iterate DataStream → IterativeStream → DataStream Creates a "feedback" loop in the flow, by redirecting the output of one operator to some previous operator. This is especially useful for defining algorithms that continuously update a model. The following code starts with a stream and applies the iteration body continuously. Elements that are greater than 0 are sent back to the feedback channel, and the rest of the elements are forwarded downstream. See iterations for a complete description.

IterativeStream<Long> iteration = initialStream.iterate();
DataStream<Long> iterationBody = iteration.map (/*do something*/);
DataStream<Long> feedback = iterationBody.filter(new FilterFunction<Long>(){
    @Override
    public boolean filter(Long value) throws Exception {
        return value > 0;
    }
});
iteration.closeWith(feedback);
DataStream<Long> output = iterationBody.filter(new FilterFunction<Long>(){
    @Override
    public boolean filter(Long value) throws Exception {
        return value <= 0;
    }
});

1.3 Sink

Flink針對DataStream提供了大量的已經實現的數據目的地(Sink),具體如下所示

  • writeAsText():講元素以字符串形式逐行寫入,這些字符串通過調用每個元素的toString()方法來獲取

  • print()/printToErr():打印每個元素的toString()方法的值到標準輸出或者標準錯誤輸出流中

  • 自定義輸出:addSink可以實現把數據輸出到第三方存儲介質中

    Flink提供了一批內置的Connector,其中有的Connector會提供對應的Sink支持,如1.1節中表所示

案例:將流數據下沉到redis中

1、依賴:

        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-connector-redis_2.11</artifactId>
            <version>1.1.5</version>
        </dependency>

2、關鍵代碼:

//封裝
        SingleOutputStreamOperator<Tuple2<String, String>> l_wordsData = data.map(new MapFunction<String, Tuple2<String, String>>() {
            @Override
            public Tuple2<String, String> map(String value) throws Exception {
                return new Tuple2<>("l_words", value);
            }
        });

        FlinkJedisPoolConfig conf = new FlinkJedisPoolConfig.Builder().setHost("hdp-1").setPort(6379).build();

        RedisSink<Tuple2<String, String>> redisSink = new RedisSink<>(conf, new MyRedisMapper());
        l_wordsData.addSink(redisSink);
 public static class MyMapper implements RedisMapper<Tuple2<String,String>> {

        @Override
        public RedisCommandDescription getCommandDescription() {
            return new RedisCommandDescription(RedisCommand.LPUSH);
        }

        @Override
        public String getKeyFromData(Tuple2<String,String> data) {
            return data.f0;
        }

        @Override
        public String getValueFromData(Tuple2<String,String> data) {
            return data.f1;
        }
    }

案例2:將流數據下沉到redis中--自定義

package com.lagou.bak;

import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.functions.sink.RichSinkFunction;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;

public class SinkToMySql extends RichSinkFunction<Student> {
    PreparedStatement preparedStatement = null;
    Connection connection = null;

    @Override
    public void open(Configuration parameters) throws Exception {
        String url = "jdbc:mysql://localhost:3306/bigdata?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC";
        String user = "root";
        String password = "lucas";
        connection = DriverManager.getConnection(url, user, password);
        String sql = "insert into student (name,age) values (?,?)";
        preparedStatement = connection.prepareStatement(sql);

    }

    @Override
    public void invoke(Student stu, Context context) throws Exception {
        preparedStatement.setString(1,stu.getName());
        preparedStatement.setInt(2,stu.getAge());
        preparedStatement.executeUpdate();
    }

    @Override
    public void close() throws Exception {
        if(connection != null) {
            connection.close();
        }
        if (preparedStatement != null) {
            preparedStatement.close();
        }
    }
}

啓動類:

package com.lagou.bak;

import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;

public class StreamMySqlRun {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStreamSource<Student> data = env.fromElements(new Student("lucas", 18), new Student("jack", 30));
        data.addSink(new SinkToMySql());
        env.execute();
    }
}

案例3:下沉到Kafka

import org.apache.flink.api.common.serialization.SimpleStringSchema;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaProducer;

public class StreamToKafka {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStreamSource<String> data = env.socketTextStream("teacher2", 7777);
        String brokerList = "teacher2:9092";
        String topic = "mytopic2";
        FlinkKafkaProducer producer = new FlinkKafkaProducer(brokerList, topic, new SimpleStringSchema());
        data.addSink(producer);
        env.execute();

    }
}

第 2 節 Flink DataSet常用API

​ DataSet API同DataStream API一樣有三個組成部分,各部分作用對應一致,此處不再贅述

2.1 DataSource

對DataSet批處理而言,較爲頻繁的操作是讀取HDFS中的文件數據,因爲這裏主要介紹兩個DataSource組件

  • 基於集合

    fromCollection(Collection),主要是爲了方便測試使用

  • 基於文件

    readTextFile(path),基於HDFS中的數據進行計算分析

2.2 Transformation

Transformation Description
Map 在算子中得到一個元素並生成一個新元素data.map { x => x.toInt }
FlatMap 在算子中獲取一個元素, 並生成任意個數的元素data.flatMap { str => str.split(" ") }
MapPartition 類似Map, 但是一次Map一整個並行分區data.mapPartition { in => in map { (_, 1) } }
Filter 如果算子返回true則包含進數據集, 如果不是則被過濾掉data.filter { _ > 100 }
Reduce 通過將兩個元素合併爲一個元素, 從而將一組元素合併爲一個元素data.reduce { _ + _ }
ReduceGroup 將一組元素合併爲一個或者多個元素data.reduceGroup { elements => elements.sum }
Aggregate 講一組值聚合爲一個值, 聚合函數可以看作是內置的Reduce函數data.aggregate(SUM, 0).aggregate(MIN, 2)data.sum(0).min(2)
Distinct 去重
Join 按照相同的Key合併兩個數據集input1.join(input2).where(0).equalTo(1)同時也可以選擇進行合併的時候的策略, 是分區還是廣播, 是基於排序的算法還是基於哈希的算法input1.join(input2, JoinHint.BROADCAST_HASH_FIRST).where(0).equalTo(1)
OuterJoin 外連接, 包括左外, 右外, 完全外連接等left.leftOuterJoin(right).where(0).equalTo(1) { (left, right) => ... }
CoGroup 二維變量的Reduce運算, 對每個輸入數據集中的字段進行分組, 然後join這些組input1.coGroup(input2).where(0).equalTo(1)
Cross 笛卡爾積input1.cross(input2)
Union 並集input1.union(input2)
Rebalance 分區重新平衡, 以消除數據傾斜input.rebalance()
Hash-Partition 按照Hash分區input.partitionByHash(0)
Range-Partition 按照Range分區input.partitionByRange(0)
CustomParititioning 自定義分區input.partitionCustom(partitioner: Partitioner[K], key)
First-n 返回數據集中的前n個元素input.first(3)
partitionByHash 按照指定的key進行hash分區
sortPartition 指定字段對分區中的數據進行排序

​ Flink針對DataSet也提供了大量的已經實現的算子,和DataStream計算很類似

  • Map:輸入一個元素,然後返回一個元素,中間可以進行清洗轉換等操作
  • FlatMap:輸入一個元素,可以返回0個、1個或者多個元素
    • Filter:過濾函數,對傳入的數據進行判斷,符合條件的數據會被留下
    • Reduce:對數據進行聚合操作,結合當前元素和上一次Reduce返回的值進行聚合操作,然後返回一個新的值
    • Aggregations:sum()、min()、max()等

2.3 Sink

​ Flink針對DataStream提供了大量的已經實現的數據目的地(Sink),具體如下所示

  • writeAsText():將元素以字符串形式逐行寫入,這些字符串通過調用每個元素的toString()方法來獲取

  • writeAsCsv():將元組以逗號分隔寫入文件中,行及字段之間的分隔是可配置的,每個字段的值來自對象的toString()方法

  • print()/pringToErr():打印每個元素的toString()方法的值到標準輸出或者標準錯誤輸出流中

    Flink提供了一批內置的Connector,其中有的Connector會提供對應的Sink支持,如1.1節中表所示

第3節 Flink Table API和SQLAPI

Apache Flink提供了兩種頂層的關係型API,分別爲Table API和SQL,Flink通過Table API&SQL實現了批流統一。其中Table API是用於Scala和Java的語言集成查詢API,它允許以非常直觀的方式組合關係運算符(例如select,where和join)的查詢。Flink SQL基於Apache Calcite 實現了標準的SQL,用戶可以使用標準的SQL處理數據集。Table API和SQL與Flink的DataStream和DataSet API緊密集成在一起,用戶可以實現相互轉化,比如可以將DataStream或者DataSet註冊爲table進行操作數據。值得注意的是,Table API and SQL目前尚未完全完善,還在積極的開發中,所以並不是所有的算子操作都可以通過其實現。

依賴:

		<dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-table</artifactId>
            <version>1.11.1</version>
            <type>pom</type>
            <scope>provided</scope>
        </dependency>

        <!-- Either... -->
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-table-api-java-bridge_2.11</artifactId>
            <version>1.11.1</version>
            <scope>provided</scope>
        </dependency>
        <!-- or... -->
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-table-api-scala-bridge_2.11</artifactId>
            <version>1.11.1</version>
            <scope>provided</scope>
        </dependency>
        

        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-table-planner-blink_2.12</artifactId>
            <version>1.11.1</version>
            <scope>provided</scope>
        </dependency>

基於TableAPI的案例:

package com.lagou.table;

import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.source.SourceFunction;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import org.apache.flink.types.Row;

import static org.apache.flink.table.api.Expressions.$;

public class TableApiDemo {
    public static void main(String[] args) throws Exception {
        //Flink執行環境env
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        //用env,做出Table環境tEnv
        StreamTableEnvironment tEnv = StreamTableEnvironment.create(env);
        //獲取流式數據源
        DataStreamSource<Tuple2<String, Integer>> data = env.addSource(new SourceFunction<Tuple2<String, Integer>>() {
            @Override
            public void run(SourceContext<Tuple2<String, Integer>> ctx) throws Exception {
                while (true) {
                    ctx.collect(new Tuple2<>("name", 10));
                    Thread.sleep(1000);
                }
            }

            @Override
            public void cancel() {

            }
        });
        //將流式數據源做成Table
        Table table = tEnv.fromDataStream(data, $("name"), $("age"));
        //對Table中的數據做查詢
        Table name = table.select($("name"));
        //將處理結果輸出到控制檯
        DataStream<Tuple2<Boolean, Row>> result = tEnv.toRetractStream(name, Row.class);
        result.print();
        env.execute();

    }
}

第六部分 Flink Window窗口機制

Flink Window 背景

​ Flink認爲Batch是Streaming的一個特例,因此Flink底層引擎是一個流式引擎,在上面實現了流處理和批處理。而Window就是從Streaming到Batch的橋樑。

​ 通俗講,Window是用來對一個無限的流設置一個有限的集合,從而在有界的數據集上進行操作的一種機制。流上的集合由Window來劃定範圍,比如“計算過去10分鐘”或者“最後50個元素的和”。

​ Window可以由時間(Time Window)(比如每30s)或者數據(Count Window)(如每100個元素)驅動。DataStream API提供了Time和Count的Window。

Flink Window 總覽

  • Window 是flink處理無限流的核心,Windows將流拆分爲有限大小的“桶”,我們可以在其上應用計算。
  • Flink 認爲 Batch 是 Streaming 的一個特例,所以 Flink 底層引擎是一個流式引擎,在上面實現了流處理和批處理。
  • 而窗口(window)就是從 Streaming 到 Batch 的一個橋樑。
  • Flink 提供了非常完善的窗口機制。
  • 在流處理應用中,數據是連續不斷的,因此我們不可能等到所有數據都到了纔開始處理。
  • 當然我們可以每來一個消息就處理一次,但是有時我們需要做一些聚合類的處理,例如:在過去的1分鐘內有多少用戶點擊了我們的網頁。
  • 在這種情況下,我們必須定義一個窗口,用來收集最近一分鐘內的數據,並對這個窗口內的數據進行計算。
  • 窗口可以是基於時間驅動的(Time Window,例如:每30秒鐘)
  • 也可以是基於數據驅動的(Count Window,例如:每一百個元素)
  • 同時基於不同事件驅動的窗口又可以分成以下幾類:
    • 翻滾窗口 (Tumbling Window, 無重疊)
    • 滑動窗口 (Sliding Window, 有重疊)
    • 會話窗口 (Session Window, 活動間隙)
    • 全局窗口 (略)
  • Flink要操作窗口,先得將StreamSource 轉成WindowedStream

步驟:

1、獲取流數據源

2、獲取窗口

3、操作窗口數據

4、輸出窗口數據

第 1 節 時間窗口(TimeWindow)

1.1 滾動時間窗口(Tumbling Window)

image-20200731072656176

​ 將數據依據固定的窗口長度對數據進行切分

​ 特點:時間對齊,窗口長度固定,沒有重疊

代碼示例

package com.lagou.edu.flink.window;

import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.java.tuple.Tuple;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.datastream.WindowedStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.api.windowing.windows.GlobalWindow;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;

import java.text.SimpleDateFormat;
import java.util.Random;

/**
 * 翻滾窗口:窗口不重疊
 * 1、基於時間驅動
 * 2、基於事件驅動
 */
public class TumblingWindow {

    public static void main(String[] args) {
    //設置執行環境,類似spark中初始化sparkContext
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        
        env.setParallelism(1);

        DataStreamSource<String> dataStreamSource = env.socketTextStream("teache2", 7777);

        SingleOutputStreamOperator<Tuple2<String, Integer>> mapStream = dataStreamSource.map(new MapFunction<String, Tuple2<String, Integer>>() {
            @Override
            public Tuple2<String, Integer> map(String value) throws Exception {

                SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");

                long timeMillis = System.currentTimeMillis();

                int random = new Random().nextInt(10);

                System.out.println("value: " + value + " random: " + random + "timestamp: " + timeMillis + "|" + format.format(timeMillis));

                return new Tuple2<String, Integer>(value, random);
            }
        });

        KeyedStream<Tuple2<String, Integer>, Tuple> keyedStream = mapStream.keyBy(0);


        // 基於時間驅動,每隔10s劃分一個窗口
        WindowedStream<Tuple2<String, Integer>, Tuple, TimeWindow> timeWindow = keyedStream.timeWindow(Time.seconds(10));

        // 基於事件驅動, 每相隔3個事件(即三個相同key的數據), 劃分一個窗口進行計算
        // WindowedStream<Tuple2<String, Integer>, Tuple, GlobalWindow> countWindow = keyedStream.countWindow(3);

        // apply是窗口的應用函數,即apply裏的函數將應用在此窗口的數據上。
        timeWindow.apply(new MyTimeWindowFunction()).print();
        // countWindow.apply(new MyCountWindowFunction()).print();

        try {
            // 轉換算子都是lazy init的, 最後要顯式調用 執行程序
            env.execute();
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}
1.2.1 基於時間驅動

​ 場景:我們需要統計每一分鐘中用戶購買的商品的總數,需要將用戶的行爲事件按每一分鐘進行切分,這種切分被成爲翻滾時間窗口(Tumbling Time Window)

package com.lagou.edu.flink.window;

import org.apache.flink.api.java.tuple.Tuple;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.functions.windowing.WindowFunction;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.util.Collector;

import java.text.SimpleDateFormat;

public class MyTimeWindowFunction implements WindowFunction<Tuple2<String,Integer>, String, Tuple, TimeWindow> {

    @Override
    public void apply(Tuple tuple, TimeWindow window, Iterable<Tuple2<String, Integer>> input, Collector<String> out) throws Exception {
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");

        int sum = 0;

        for(Tuple2<String,Integer> tuple2 : input){
            sum +=tuple2.f1;
        }

        long start = window.getStart();
        long end = window.getEnd();

        out.collect("key:" + tuple.getField(0) + " value: " + sum + "| window_start :"
                + format.format(start) + "  window_end :" + format.format(end)
        );

    }
}
1.2.2 基於事件驅動

​ 場景:當我們想要每100個用戶的購買行爲作爲驅動,那麼每當窗口中填滿100個”相同”元素了,就會對窗口進行計算。

package com.lagou.edu.flink.window;

import org.apache.flink.api.java.tuple.Tuple;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.functions.windowing.WindowFunction;
import org.apache.flink.streaming.api.windowing.windows.GlobalWindow;
import org.apache.flink.util.Collector;

import java.text.SimpleDateFormat;

public class MyCountWindowFunction implements WindowFunction<Tuple2<String, Integer>, String, Tuple, GlobalWindow> {

    @Override
    public void apply(Tuple tuple, GlobalWindow window, Iterable<Tuple2<String, Integer>> input, Collector<String> out) throws Exception {
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");

        int sum = 0;

        for (Tuple2<String, Integer> tuple2 : input){
            sum += tuple2.f1;
        }
        //無用的時間戳,默認值爲: Long.MAX_VALUE,因爲基於事件計數的情況下,不關心時間。
        long maxTimestamp = window.maxTimestamp();

        out.collect("key:" + tuple.getField(0) + " value: " + sum + "| maxTimeStamp :"
                + maxTimestamp + "," + format.format(maxTimestamp)
        );
    }
}

1.2 滑動時間窗口(Sliding Window)

image-20200731073053682

​ 滑動窗口是固定窗口的更廣義的一種形式,滑動窗口由固定的窗口長度和滑動間隔組成

​ 特點:窗口長度固定,可以有重疊

1.2.1 基於時間的滑動窗口

​ 場景: 我們可以每30秒計算一次最近一分鐘用戶購買的商品總數

1.2.2 基於事件的滑動窗口

​ 場景: 每10個 “相同”元素計算一次最近100個元素的總和

代碼實現

package com.lagou.edu.flink.window;

import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.java.tuple.Tuple;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.datastream.WindowedStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.api.windowing.windows.GlobalWindow;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;

import java.text.SimpleDateFormat;
import java.util.Random;

/**
 * 滑動窗口:窗口可重疊
 * 1、基於時間驅動
 * 2、基於事件驅動
 */
public class SlidingWindow {

    public static void main(String[] args) {
        // 設置執行環境, 類似spark中初始化SparkContext
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        
        env.setParallelism(1);

        DataStreamSource<String> dataStreamSource = env.socketTextStream("teacher2", 7777);

        SingleOutputStreamOperator<Tuple2<String, Integer>> mapStream = dataStreamSource.map(new MapFunction<String, Tuple2<String, Integer>>() {
            @Override
            public Tuple2<String, Integer> map(String value) throws Exception {
                SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
                long timeMillis = System.currentTimeMillis();

                int random = new Random().nextInt(10);
                System.err.println("value : " + value + " random : " + random + " timestamp : " + timeMillis + "|" + format.format(timeMillis));

                return new Tuple2<String, Integer>(value, random);
            }
        });
        KeyedStream<Tuple2<String, Integer>, Tuple> keyedStream = mapStream.keyBy(0);

        //基於時間驅動,每隔5s計算一下最近10s的數據
     //   WindowedStream<Tuple2<String, Integer>, Tuple, TimeWindow> timeWindow = keyedStream.timeWindow(Time.seconds(10), Time.seconds(5));
        //基於事件驅動,每隔2個事件,觸發一次計算,本次窗口的大小爲3,代表窗口裏的每種事件最多爲3個
        WindowedStream<Tuple2<String, Integer>, Tuple, GlobalWindow> countWindow = keyedStream.countWindow(3, 2);

     //   timeWindow.sum(1).print();

        countWindow.sum(1).print();

     //   timeWindow.apply(new MyTimeWindowFunction()).print();

        try {
            env.execute();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

1.3 會話窗口(Session Window)

image-20200731073350282

​ 由一系列事件組合一個指定時間長度的timeout間隙組成,類似於web應用的session,也就是一段時間沒有接收到新數據就會生成新的窗口。

session窗口分配器通過session活動來對元素進行分組,session窗口跟滾動窗口和滑動窗口相比,不會有重疊和固定的開始時間和結束時間的情況

session窗口在一個固定的時間週期內不再收到元素,即非活動間隔產生,那麼這個窗口就會關閉。

一個session窗口通過一個session間隔來配置,這個session間隔定義了非活躍週期的長度,當這個非活躍週期產生,那麼當前的session將關閉並且後續的元素將被分配到新的session窗口中去。

特點

​ 會話窗口不重疊,沒有固定的開始和結束時間

​ 與翻滾窗口和滑動窗口相反, 當會話窗口在一段時間內沒有接收到元素時會關閉會話窗口。

​ 後續的元素將會被分配給新的會話窗口

案例描述

​ 計算每個用戶在活躍期間總共購買的商品數量,如果用戶30秒沒有活動則視爲會話斷開

代碼實現

package com.lagou.edu.flink.window;

import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.java.tuple.Tuple;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.datastream.WindowedStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.windowing.assigners.ProcessingTimeSessionWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;

import java.text.SimpleDateFormat;
import java.util.Random;

public class SessionWindow {

    public static void main(String[] args) {

        // 設置執行環境, 類似spark中初始化sparkContext

        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        env.setParallelism(1);

        DataStreamSource<String> dataStreamSource = env.socketTextStream("teacher2", 7777);

        SingleOutputStreamOperator<Tuple2<String, Integer>> mapStream = dataStreamSource.map(new MapFunction<String, Tuple2<String, Integer>>() {
            @Override
            public Tuple2<String, Integer> map(String value) throws Exception {
                SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
                long timeMillis = System.currentTimeMillis();

                int random = new Random().nextInt(10);

                System.err.println("value : " + value + " random : " + random + " timestamp : " + timeMillis + "|" + format.format(timeMillis));

                return new Tuple2<String, Integer>(value, random);
            }
        });
        KeyedStream<Tuple2<String, Integer>, Tuple> keyedStream = mapStream.keyBy(0);

        //如果連續10s內,沒有數據進來,則會話窗口斷開。
        WindowedStream<Tuple2<String, Integer>, Tuple, TimeWindow> window = keyedStream.window(ProcessingTimeSessionWindows.withGap(Time.seconds(10)));

        // window.sum(1).print();
        
        window.apply(new MyTimeWindowFunction()).print();

        try {
            env.execute();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

第七部分 Flink Time

1.1 Time

在Flink的流式處理中,會涉及到時間的不同概念,如下圖所示:

image-20200921112633884

- EventTime[事件時間]

事件發生的時間,例如:點擊網站上的某個鏈接的時間,每一條日誌都會記錄自己的生成時間

如果以EventTime爲基準來定義時間窗口那將形成EventTimeWindow,要求消息本身就應該攜帶EventTime

- IngestionTime[攝入時間]

數據進入Flink的時間,如某個Flink節點的source operator接收到數據的時間,例如:某個source消費到kafka中的數據

如果以IngesingtTime爲基準來定義時間窗口那將形成IngestingTimeWindow,以source的systemTime爲準

- ProcessingTime[處理時間]

某個Flink節點執行某個operation的時間,例如:timeWindow處理數據時的系統時間,默認的時間屬性就是Processing Time

如果以ProcessingTime基準來定義時間窗口那將形成ProcessingTimeWindow,以operator的systemTime爲準

在Flink的流式處理中,絕大部分的業務都會使用EventTime,一般只在EventTime無法使用時,纔會被迫使用ProcessingTime或者IngestionTime。

如果要使用EventTime,那麼需要引入EventTime的時間屬性,引入方式如下所示:

env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime) //設置使用事件時間

1.2.數據延遲產生的問題

l 示例1

現在假設,你正在去往地下停車場的路上,並且打算用手機點一份外賣。

選好了外賣後,你就用在線支付功能付款了,這個時候是11點50分。恰好這時,你走進了地下停車庫,而這裏並沒有手機信號。因此外賣的在線支付並沒有立刻成功,而支付系統一直在Retry重試“支付”這個操作。

當你找到自己的車並且開出地下停車場的時候,已經是12點05分了。這個時候手機重新有了信號,手機上的支付數據成功發到了外賣在線支付系統,支付完成。

在上面這個場景中你可以看到,支付數據的事件時間是11點50分,而支付數據的處理時間是12點05分

一般在實際開發中會以事件時間作爲計算標準

l 示例2

image-20200921111842449

一條日誌進入Flink的時間爲2019-08-12 10:00:01,攝入時間

到達Window的系統時間爲2019-08-12 10:00:02,處理時間

日誌的內容爲:2019-08-12 09:58:02 INFO Fail over to rm2 ,事件時間

對於業務來說,要統計1h內的故障日誌個數,哪個時間是最有意義的?---事件時間

EventTime,因爲我們要根據日誌的生成時間進行統計。

l 示例3

某 App 會記錄用戶的所有點擊行爲,並回傳日誌(在網絡不好的情況下,先保存在本地,延後回傳)。

A 用戶在 11:02 對 App 進行操作,B 用戶在 11:03 操作了 App,

但是 A 用戶的網絡不太穩定,回傳日誌延遲了,導致我們在服務端先接受到 B 用戶 11:03 的消息,然後再接受到 A 用戶 11:02 的消息,消息亂序了。

l 示例4

在實際環境中,經常會出現,因爲網絡原因,數據有可能會延遲一會纔到達Flink實時處理系統。

我們先來設想一下下面這個場景:

image-20200921111903993

  1. 使用時間窗口來統計10分鐘內的用戶流量

  2. 有一個時間窗口

- 開始時間爲:2017-03-19 10:00:00

- 結束時間爲:2017-03-19 10:10:00

  1. 有一個數據,因爲網絡延遲

- 事件發生的時間爲:2017-03-19 10:10:00

- 但進入到窗口的時間爲:2017-03-19 10:10:02,延遲了2秒中

  1. 時間窗口並沒有將59這個數據計算進來,導致數據統計不正確

這種處理方式,根據消息進入到window時間,來進行計算。在網絡有延遲的時候,會引起計算誤差。

如何解決?---使用水印解決網絡延遲問題

通過上面的例子,我們知道,在進行數據處理的時候應該按照事件時間進行處理,也就是窗口應該要考慮到事件時間

但是窗口不能無限的一直等到延遲數據的到來,需要有一個觸發窗口計算的機制

也就是我們接下來要學的watermaker水位線/水印機制

1.3 使用Watermark解決

水印(watermark)就是一個時間戳,Flink可以給數據流添加水印,

可以理解爲:收到一條消息後,額外給這個消息添加了一個時間字段,這就是添加水印。

- 水印並不會影響原有Eventtime事件時間

- 當數據流添加水印後,會按照水印時間來觸發窗口計算

也就是說watermark水印是用來觸發窗口計算的

- 一般會設置水印時間,比事件時間小几秒鐘,表示最大允許數據延遲達到多久

(即水印時間 = 事件時間 - 允許延遲時間)10:09:57 = 10:10:00 - 3s

- 當接收到的 水印時間 >= 窗口結束時間,則觸發計算 如等到一條數據的水印時間爲10:10:00 >= 10:10:00 才觸發計算,也就是要等到事件時間爲10:10:03的數據到來才觸發計算

(即事件時間 - 允許延遲時間 >= 窗口結束時間 或 事件時間 >= 窗口結束時間 + 允許延遲時間)

image-20200921111932658

總結:watermaker是用來解決延遲數據的問題

如窗口10:00:00~10:10:00

而數據到達的順序是: A 10:10:00 ,B 10:09:58

如果沒有watermaker,那麼A數據將會觸發窗口計算,B數據來了窗口已經關閉,則該數據丟失

那麼如果有了watermaker,設置允許數據遲到的閾值爲3s

那麼該窗口的結束條件則爲 水印時間>=窗口結束時間10:10:00,也就是需要有一條數據的水印時間= 10:10:00

而水印時間10:10:00= 事件時間- 延遲時間3s

也就是需要有一條事件時間爲10:10:03的數據到來,纔會真正的觸發窗口計算

而上面的 A 10:10:00 ,B 10:09:58都不會觸發計算,也就是會被窗口包含,直到10:10:03的數據到來纔會計算窗口10:00:00~10:10:00的數據

Watermark案例

步驟

1、獲取數據源

2、轉化

3、聲明水印(watermark)

4、分組聚合,調用window的操作

5、保存處理結果

注意:

當使用EventTimeWindow時,所有的Window在EventTime的時間軸上進行劃分,

也就是說,在Window啓動後,會根據初始的EventTime時間每隔一段時間劃分一個窗口,

如果Window大小是3秒,那麼1分鐘內會把Window劃分爲如下的形式:

[00:00:00,00:00:03)

[00:00:03,00:00:06)

[00:00:03,00:00:09)

[00:00:03,00:00:12)

[00:00:03,00:00:15)

[00:00:03,00:00:18)

[00:00:03,00:00:21)

[00:00:03,00:00:24)

...

[00:00:57,00:00:42)

[00:00:57,00:00:45)

[00:00:57,00:00:48)

...

如果Window大小是10秒,則Window會被分爲如下的形式:

[00:00:00,00:00:10)

[00:00:10,00:00:20)

...

[00:00:50,00:01:00)

l 注意:

1.窗口是左閉右開的,形式爲:[window_start_time,window_end_time)。

2.Window的設定基於第一條消息的事件時間,也就是說,Window會一直按照指定的時間間隔進行劃分,不論這個Window中有沒有數據,EventTime在這個Window期間的數據會進入這個Window。

3.Window會不斷產生,屬於這個Window範圍的數據會被不斷加入到Window中,所有未被觸發的Window都會等待觸發,只要Window還沒觸發,屬於這個Window範圍的數據就會一直被加入到Window中,直到Window被觸發纔會停止數據的追加,而當Window觸發之後才接受到的屬於被觸發Window的數據會被丟棄。

4.Window會在以下的條件滿足時被觸發執行:

(1)在[window_start_time,window_end_time)窗口中有數據存在

(2)watermark時間 >= window_end_time;

5.一般會設置水印時間,比事件時間小几秒鐘,表示最大允許數據延遲達到多久

(即水印時間 = 事件時間 - 允許延遲時間)

當接收到的 水印時間 >= 窗口結束時間且窗口內有數據,則觸發計算

(即事件時間 - 允許延遲時間 >= 窗口結束時間 或 事件時間 >= 窗口結束時間 + 允許延遲時間)

1.4 代碼實現

數據源:

01,1586489566000 01,1586489567000 01,1586489568000 01,1586489569000 01,1586489570000 01,1586489571000 01,1586489572000 01,1586489573000

2020-04-10 11:32:46 2020-04-10 11:32:47 2020-04-10 11:32:48 2020-04-10 11:32:49 2020-04-10 11:32:50

代碼:

package com.lagou.Time;

import org.apache.commons.math3.fitting.leastsquares.EvaluationRmsChecker;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.java.tuple.Tuple;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.TimeCharacteristic;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.AssignerWithPeriodicWatermarks;
import org.apache.flink.streaming.api.functions.windowing.WindowFunction;
import org.apache.flink.streaming.api.watermark.Watermark;
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.util.Collector;
import sun.nio.cs.StreamEncoder;

import javax.annotation.Nullable;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;

public class WaterDemo {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
        DataStreamSource<String> data = env.socketTextStream("teacher2", 7777);
        SingleOutputStreamOperator<Tuple2<String, Long>> maped = data.map(new MapFunction<String, Tuple2<String, Long>>() {
            @Override
            public Tuple2<String, Long> map(String value) throws Exception {
                String[] split = value.split(",");
                return new Tuple2<>(split[0], Long.valueOf(split[1]));
            }
        });
        SingleOutputStreamOperator<Tuple2<String, Long>> watermarks = maped.assignTimestampsAndWatermarks(new AssignerWithPeriodicWatermarks<Tuple2<String, Long>>() {
            Long currentMaxTimestamp = 0l;
            final Long maxOutOfOrderness = 10000l;
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");

            @Nullable
            @Override
            public Watermark getCurrentWatermark() {
                return new Watermark(currentMaxTimestamp - maxOutOfOrderness);
            }

            @Override
            public long extractTimestamp(Tuple2<String, Long> element, long previousElementTimestamp) {
                long timestamp = element.f1;
//                System.out.println("timestamp:" + timestamp + "..." );
//                System.out.println("..." +  sdf.format(timestamp));

                currentMaxTimestamp = Math.max(timestamp, currentMaxTimestamp);
                System.out.println("key:" + element.f0
                        + "...eventtime:[" + element.f1 + "|" + sdf.format(element.f1)
                        /*+ "],currentMaxTimestamp:[" + currentMaxTimestamp + "|" + sdf.format(currentMaxTimestamp)*/
                        /*+ "],watermark:[" + getCurrentWatermark().getTimestamp() + "| " + sdf.format(getCurrentWatermark().getTimestamp() + "]")*/);
                System.out.println("currentMaxTimestamp" + currentMaxTimestamp + "..." + sdf.format(currentMaxTimestamp));
                System.out.println("watermark:" + getCurrentWatermark().getTimestamp() + "..." + sdf.format(getCurrentWatermark().getTimestamp()));
                return timestamp;
            }
        });
        SingleOutputStreamOperator<String> res = watermarks.keyBy(0).window(TumblingEventTimeWindows.of(Time.seconds(3))).apply(new WindowFunction<Tuple2<String, Long>, String, Tuple, TimeWindow>() {
            @Override
            public void apply(Tuple tuple, TimeWindow window, Iterable<Tuple2<String, Long>> input, Collector<String> out) throws Exception {
                String key = tuple.toString();
                ArrayList<Long> list = new ArrayList<>();
                Iterator<Tuple2<String, Long>> it = input.iterator();
                while (it.hasNext()) {
                    Tuple2<String, Long> next = it.next();
                    list.add(next.f1);
                }
                Collections.sort(list);
                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
                String result = key + "," + list.size() + "," + sdf.format(list.get(0)) + "," + sdf.format(list.get(list.size() - 1)) + "," + sdf.format(window.getStart()) + "," + sdf.format(window.getEnd());
                out.collect(result);
            }
        });
        res.print();
        env.execute();
    }
}

第八部分 Flink的State--狀態原理及原理剖析

State:用來保存計算結果或緩存數據。

Sum

狀態類型

Flink根據是否需要保存中間結果,把計算分爲有狀態計算和無狀態計算

有狀態計算:依賴之前或之後的事件

無狀態計算:獨立

根據數據結構不同,Flink定義了多種state,應用於不同的場景

  • ValueState:即類型爲T的單值狀態。這個狀態與對應的key綁定,是最簡單的狀態了。它可以通過update方法更新狀態值,通過value()方法獲取狀態值。
  • ListState:即key上的狀態值爲一個列表。可以通過add方法往列表中附加值;也可以通過get()方法返回一個Iterable<T>來遍歷狀態值。
  • ReducingState:這種狀態通過用戶傳入的reduceFunction,每次調用add方法添加值的時候,會調用reduceFunction,最後合併到一個單一的狀態值。
  • FoldingState:跟ReducingState有點類似,不過它的狀態值類型可以與add方法中傳入的元素類型不同(這種狀態將會在Flink未來版本中被刪除)。
  • MapState:即狀態值爲一個map。用戶通過putputAll方法添加元素

State按照是否有key劃分爲KeyedState和OperatorState

Keyed State:KeyedStream流上的每一個Key都對應一個State

案例:利用state求平均值

原始數據:(1,3)(1,5)(1,7)(1,4)(1,2)

思路:

1、讀數據源

2、將數據源根據key分組

3、按照key分組策略,對流式數據調用狀態化處理

​ 在處理過程中:

​ a、實例化出一個狀態實例

<T> ValueState<T> getState(ValueStateDescriptor<T> stateProperties);

ValueStateDescriptor<Tuple2<Long, Long>> descriptor = new ValueStateDescriptor<>(
                        "average",
                        TypeInformation.of(new TypeHint<Tuple2<Long, Long>>() {
                        })
                        , Tuple2.of(0L, 0L)
                );
getRuntimeContext().getState(descriptor);

/**
	 * Creates a new {@code ValueStateDescriptor} with the given name and default value.
	 *
	 * @deprecated Use {@link #ValueStateDescriptor(String, TypeInformation)} instead and manually
	 * manage the default value by checking whether the contents of the state is {@code null}.
	 *
	 * @param name The (unique) name for the state.
	 * @param typeInfo The type of the values in the state.
	 * @param defaultValue The default value that will be set when requesting state without setting
	 *                     a value before.
	 */
	@Deprecated
	public ValueStateDescriptor(String name, TypeInformation<T> typeInfo, T defaultValue) {
		super(name, typeInfo, defaultValue);
	}

​ b、隨着流式數據的到來,更新狀態

sum.update(currentSum);
void update(T value) throws IOException;

4、輸出計算結果

keyed State:代碼:

package com.lagou.bak;

import org.apache.flink.api.common.functions.RichFlatMapFunction;
import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.api.common.typeinfo.TypeHint;
import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
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.util.Collector;

import javax.swing.plaf.IconUIResource;

public class StateTest1 {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStreamSource<Tuple2<Long, Long>> data = env.fromElements(Tuple2.of(1l, 3l), Tuple2.of(1l, 5l), Tuple2.of(1l, 7l), Tuple2.of(1l, 4l), Tuple2.of(1l, 2l));
        KeyedStream<Tuple2<Long, Long>, Long> keyed = data.keyBy(value -> value.f0);
//        keyed.
//        keyed.print();
        SingleOutputStreamOperator<Tuple2<Long, Long>> flatMaped = keyed.flatMap(new RichFlatMapFunction<Tuple2<Long, Long>, Tuple2<Long, Long>>() {
            private transient ValueState<Tuple2<Long, Long>> sum;

            @Override
            public void flatMap(Tuple2<Long, Long> value, Collector<Tuple2<Long, Long>> out) throws Exception {
                //獲取當前狀態值
                Tuple2<Long, Long> currentSum = sum.value();

                //更新
                currentSum.f0 += 1;
                currentSum.f1 += value.f1;
                System.out.println("...currentSum:"+ currentSum);

                //更新狀態值
                sum.update(currentSum);

                //如果count>=2 清空狀態值,重新計算
                if(currentSum.f0 >= 5) {
                    out.collect(new Tuple2<>(value.f0,currentSum.f1 / currentSum.f0));
                    sum.clear();
                }
            }

            @Override
            public void open(Configuration parameters) throws Exception {
                ValueStateDescriptor<Tuple2<Long, Long>> descriptor = new ValueStateDescriptor<>(
                        "average",
                        TypeInformation.of(new TypeHint<Tuple2<Long, Long>>() {
                        }),
                        Tuple2.of(0L, 0L)
                );
//                ValueStateDescriptor<Tuple2<Long, Long>> descriptor1 = new ValueStateDescriptor<>("average", TypeInformation.of(new TypeHint<Tuple2<Long, Long>>() {
//                }));
                sum = getRuntimeContext().getState(descriptor);
            }
        });
        flatMaped.print();
        env.execute();
    }
}

image-20201007120248948

Operator State代碼:

ListCheckPointed

CheckPointedFunction

見狀態存儲

flink_State

flink_state2

(1)Keyed State

表示和Key相關的一種State,只能用於KeydStream類型數據集對應的Functions和 Operators之上。Keyed State是 Operator State的特例,區別在於 Keyed State 事先按照key對數據集進行了分區,每個 Key State 僅對應ー個 Operator和Key的組合。Keyed State可以通過 Key Groups 進行管理,主要用於當算子並行度發生變化時,自動重新分佈Keyed State數據。在系統運行過程中,一個Keyed算子實例可能運行一個或者多個Key Groups的keys。

(2)Operator State

與 Keyed State不同的是, Operator State只和並行的算子實例綁定,和數據元素中的key無關,每個算子實例中持有所有數據元素中的一部分狀態數據。Operator State支持當算子實例並行度發生變化時自動重新分配狀態數據。

同時在 Flink中 Keyed State和 Operator State均具有兩種形式,其中一種爲**託管狀態( Managed State)形式,由 Flink Runtime中控制和管理狀態數據,並將狀態數據轉換成爲內存 Hash tables或 ROCKSDB的對象存儲,然後將這些狀態數據通過內部的接口持久化到 Checkpoints 中,任務異常時可以通過這些狀態數據恢復任務。另外一種是原生狀態(Raw State)**形式,由算子自己管理數據結構,當觸發 Checkpoint過程中, Flink並不知道狀態數據內部的數據結構,只是將數據轉換成bys數據存儲在 Checkpoints中,當從Checkpoints恢復任務時,算子自己再反序列化出狀態的數據結構。Datastream API支持使用 Managed State和 Raw State兩種狀態形式,在 Flink中推薦用戶使用 Managed State管理狀態數據,主要原因是 Managed State 能夠更好地支持狀態數據的重平衡以及更加完善的內存管理。

狀態描述

flink_StateDescriptor

State 既然是暴露給用戶的,那麼就需要有一些屬性需要指定:state 名稱、val serializer、state type info。在對應的statebackend中,會去調用對應的create方法獲取到stateDescriptor中的值。Flink通過StateDescriptor來定義一個狀態。這是一個抽象類,內部定義了狀態名稱、類型、序列化器等基礎信息。與上面的狀態對應,從StateDescriptor派生了ValueStateDescriptor, ListStateDescriptor等descriptor

  • ValueState getState(ValueStateDescriptor)
  • ReducingState getReducingState(ReducingStateDescriptor)
  • ListState getListState(ListStateDescriptor)
  • FoldingState getFoldingState(FoldingStateDescriptor)
  • MapState getMapState(MapStateDescriptor)

廣播狀態

什麼是廣播狀態?

<img src="Flink大數據講義.assets/image-20201009145618218.png" alt="image-20201009145618218" style="zoom:80%;" />

所有並行實例,這些實例將它們維持爲狀態。不廣播另一個流的事件,而是將其發送到同一運營商的各個實例,並與廣播流的事件一起處理。 新的廣播狀態非常適合需要加入低吞吐量和高吞吐量流或需要動態更新其處理邏輯的應用程序。我們將使用後一個用例的具體示例來解釋廣播狀態

廣播狀態下的動態模式評估

想象一下,一個電子商務網站將所有用戶的交互捕獲爲用戶操作流。運營該網站的公司有興趣分析交互以增加收入,改善用戶體驗,以及檢測和防止惡意行爲。 該網站實現了一個流應用程序,用於檢測用戶事件流上的模式。但是,公司希望每次模式更改時都避免修改和重新部署應用程序。相反,應用程序在從模式流接收新模式時攝取第二個模式流並更新其活動模式。在下文中,我們將逐步討論此應用程序,並展示它如何利用Apache Flink中的廣播狀態功能。 img 我們的示例應用程序攝取了兩個數據流。第一個流在網站上提供用戶操作,並在上圖的左上方顯示。用戶交互事件包括操作的類型(用戶登錄,用戶註銷,添加到購物車或完成付款)和用戶的ID,其由顏色編碼。圖示中的用戶動作事件流包含用戶1001的註銷動作,其後是用戶1003的支付完成事件,以及用戶1002的“添加到購物車”動作。

第二流提供應用將執行的動作模式。評估。模式由兩個連續的動作組成。在上圖中,模式流包含以下兩個:

  1. 模式#1:用戶登錄並立即註銷而無需瀏覽電子商務網站上的其他頁面。
  2. 模式#2:用戶將商品添加到購物車並在不完成購買的情況下注銷。

這些模式有助於企業更好地分析用戶行爲,檢測惡意行爲並改善網站體驗。例如,如果項目被添加到購物車而沒有後續購買,網站團隊可以採取適當的措施來更好地瞭解用戶未完成購買的原因並啓動特定程序以改善網站轉換(如提供折扣代碼,限時免費送貨優惠等)

在右側,該圖顯示了操作員的三個並行任務,即攝取模式和用戶操作流,評估操作流上的模式,並在下游發出模式匹配。爲簡單起見,我們示例中的運算符僅評估具有兩個後續操作的單個模式。當從模式流接收到新模式時,替換當前活動模式。原則上,還可以實現運算符以同時評估更復雜的模式或多個模式,這些模式可以單獨添加或移除。

我們將描述模式匹配應用程序如何處理用戶操作和模式流。

img

首先,將模式發送給操作員。該模式被廣播到運營商的所有三個並行任務。任務將模式存儲在其廣播狀態中。由於廣播狀態只應使用廣播數據進行更新,因此所有任務的狀態始終預期相同。

img

接下來,第一個用戶操作按用戶ID分區併發送到操作員任務。分區可確保同一用戶的所有操作都由同一任務處理。上圖顯示了操作員任務消耗第一個模式和前三個操作事件後應用程序的狀態。

當任務收到新的用戶操作時,它會通過查看用戶的最新和先前操作來評估當前活動的模式。對於每個用戶,操作員將先前的操作存儲在鍵控狀態。由於上圖中的任務到目前爲止僅爲每個用戶收到了一個操作(我們剛剛啓動了應用程序),因此不需要評估該模式。最後,用戶鍵控狀態中的先前操作被更新爲最新動作,以便能夠在同一用戶的下一個動作到達時查找它。 img

在處理前三個動作之後,下一個事件(用戶1001的註銷動作)被運送到處理用戶1001的事件的任務。當任務接收到動作時,它從廣播狀態中查找當前模式並且用戶1001的先前操作。由於模式匹配兩個動作,因此任務發出模式匹配事件。最後,任務通過使用最新操作覆蓋上一個事件來更新其鍵控狀態。 img

當新模式到達模式流時,它被廣播到所有任務,並且每個任務通過用新模式替換當前模式來更新其廣播狀態。

img

一旦用新模式更新廣播狀態,匹配邏輯就像之前一樣繼續,即,用戶動作事件由密鑰分區並由負責任務評估。

如何使用廣播狀態實現應用程序?

到目前爲止,我們在概念上討論了該應用程序並解釋了它如何使用廣播狀態來評估事件流上的動態模式。接下來,我們將展示如何使用Flink的DataStream API和廣播狀態功能實現示例應用程序。

讓我們從應用程序的輸入數據開始。我們有兩個數據流,操作和模式。在這一點上,我們並不關心流來自何處。這些流可以從Apache Kafka或Kinesis或任何其他系統中攝取。並與各兩個字段的POJO:

DataStream<Action> actions = ???`
`DataStream<Pattern> patterns = ???
Action``Pattern
  • ActionLong userIdString action
  • Pattern:,String firstAction``String secondAction

作爲第一步,我們在屬性上鍵入操作流。接下來,我們準備廣播狀態。廣播狀態始終表示爲 Flink提供的最通用的狀態原語。由於我們的應用程序一次只評估和存儲一個,我們將廣播狀態配置爲具有鍵類型和值類型。使用 廣播狀態,我們在流上應用轉換並接收 。在我們獲得了keyed Stream和廣播流之後,我們都流式傳輸並應用了一個userId

package com.lagou.state;

import org.apache.flink.api.common.state.*;
import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.BroadcastStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
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.KeyedBroadcastProcessFunction;
import org.apache.flink.util.Collector;

public class BroadCastDemo {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(4);
        //兩套數據流,1:用戶行爲   2 : 模式
        UserAction ac1 = new UserAction(1001l, "login");
        UserAction ac2 = new UserAction(1003l, "pay");
        UserAction ac3 = new UserAction(1002l, "car");
        UserAction ac4 = new UserAction(1001l, "logout");
        UserAction ac5 = new UserAction(1003l, "car");
        UserAction ac6 = new UserAction(1002l, "logout");
        DataStreamSource<UserAction> actions = env.fromElements(ac1, ac2, ac3, ac4, ac5, ac6);

        MyPattern myPattern1 = new MyPattern("login", "logout");
        MyPattern myPattern2 = new MyPattern("car", "logout");
        DataStreamSource<MyPattern> patterns = env.fromElements(myPattern1);

        KeyedStream<UserAction, Long> keyed = actions.keyBy(value -> value.getUserId());


        //將模式流廣播到下游的所有算子
        MapStateDescriptor<Void, MyPattern> bcStateDescriptor = new MapStateDescriptor<>("patterns", Types.VOID, Types.POJO(MyPattern.class));
        BroadcastStream<MyPattern> broadcastPatterns = patterns.broadcast(bcStateDescriptor);


        SingleOutputStreamOperator<Tuple2<Long, MyPattern>> process = keyed.connect(broadcastPatterns).process(new PatternEvaluator());



        //將匹配成功的結果輸出到控制檯
        process.print();
        env.execute();

    }
    public static class PatternEvaluator extends KeyedBroadcastProcessFunction<Long,UserAction,MyPattern, Tuple2<Long,MyPattern>> {
        ValueState<String> prevActionState;

        @Override
        public void open(Configuration parameters) throws Exception {
            //初始化KeyedState
            prevActionState = getRuntimeContext().getState(new ValueStateDescriptor<String>("lastAction", Types.STRING));
        }

        //沒來一個Action數據,觸發一次執行
        @Override
        public void processElement(UserAction value, ReadOnlyContext ctx, Collector<Tuple2<Long, MyPattern>> out) throws Exception {
            //把用戶行爲流和模式流中的模式進行匹配
            ReadOnlyBroadcastState<Void, MyPattern> patterns = ctx.getBroadcastState(new MapStateDescriptor<>("patterns", Types.VOID, Types.POJO(MyPattern.class)));
            MyPattern myPattern = patterns.get(null);
            String prevAction = prevActionState.value();
            if(myPattern != null && prevAction != null) {
                if (myPattern.getFirstAction().equals(prevAction) && myPattern.getSecondAction().equals(value.getUserAction())) {
                    //如果匹配成...
                    out.collect(new Tuple2<>(ctx.getCurrentKey(),myPattern));
                } else {
                    //如果匹配不成功...
                }
            }
            prevActionState.update(value.getUserAction());
        }

        //每次來一個模式Pattern的時候觸發執行
        @Override
        public void processBroadcastElement(MyPattern value, Context ctx, Collector<Tuple2<Long, MyPattern>> out) throws Exception {
            BroadcastState<Void, MyPattern> bcstate = ctx.getBroadcastState(new MapStateDescriptor<>("patterns", Types.VOID, Types.POJO(MyPattern.class)));
            bcstate.put(null,value);
        }
    }
}

  • processBroadcastElement()爲廣播流的每個記錄調用。在我們的 函數中,我們只是使用鍵將接收到的記錄放入廣播狀態(記住,我們只存儲一個模式)。PatternEvaluator Pattern``null MapState
  • processElement()爲鍵控流的每個記錄調用。它提供對廣播狀態的只讀訪問,以防止修改導致跨函數的並行實例的不同廣播狀態。 從廣播狀態檢索當前模式的方法和從鍵控狀態檢索用戶的先前動作。如果兩者都存在,則檢查先前和當前操作是否與模式匹配,並且如果是這種情況則發出模式匹配記錄。最後,它將鍵控狀態更新爲當前用戶操作。processElement()``PatternEvaluator
  • onTimer()在先前註冊的計時器觸發時調用。定時器可以在任何處理方法中註冊,並用於執行計算或將來清理狀態。我們在示例中沒有實現此方法以保持代碼簡潔。但是,當用戶在一段時間內未處於活動狀態時,它可用於刪除用戶的最後一個操作,以避免由於非活動用戶而導致狀態增長。

您可能已經注意到了處理方法的上下文對象。上下文對象提供對其他功能的訪問,例如KeyedBroadcastProcessFunction

  • 廣播狀態(讀寫或只讀,取決於方法),
  • A,可以訪問記錄的時間戳,當前的水印,以及可以註冊計時器,TimerService
  • 當前密鑰(僅適用於 ),和processElement()
  • 一種將函數應用於每個註冊密鑰的鍵控狀態的方法(僅適用於)processBroadcastElement()

在具有就像任何其他ProcessFunction完全進入狀態弗林克和時間特性,因此可以用來實現複雜的應用程序邏輯。廣播狀態被設計爲適應不同場景和用例的多功能特性。雖然我們只討論了一個相當簡單且受限制的應用程序,但您可以通過多種方式使用廣播狀態來實現應用程序的要求。KeyedBroadcastProcessFunction

結論

在這篇博文中,我們向您介紹了一個示例應用程序,以解釋Apache Flink的廣播狀態以及它如何用於評估事件流上的動態模式。我們還討論了API並展示了我們的示例應用程序的源代碼。

狀態存儲

Flink 的一個重要特性就是有狀態計算(stateful processing)。Flink 提供了簡單易用的 API 來存儲和獲取狀態。但是,我們還是要理解 API 背後的原理,才能更好的使用。

一. State 存儲方式

Flink 爲 state 提供了三種開箱即用的後端存儲方式(state backend):

  1. Memory State Backend
  2. File System (FS) State Backend
  3. RocksDB State Backend
1.1 MemoryStateBackend

MemoryStateBackend 將工作狀態數據保存在 taskmanager 的 java 內存中。key/value 狀態和 window 算子使用哈希表存儲數值和觸發器。進行快照時(checkpointing),生成的快照數據將和 checkpoint ACK 消息一起發送給 jobmanager,jobmanager 將收到的所有快照保存在 java 內存中。 MemoryStateBackend 現在被默認配置成異步的,這樣避免阻塞主線程的 pipline 處理。 MemoryStateBackend 的狀態存取的速度都非常快,但是不適合在生產環境中使用。這是因爲 MemoryStateBackend 有以下限制:

  • 每個 state 的默認大小被限制爲 5 MB(這個值可以通過 MemoryStateBackend 構造函數設置)
  • 每個 task 的所有 state 數據 (一個 task 可能包含一個 pipline 中的多個 Operator) 大小不能超過 RPC 系統的幀大小(akka.framesize,默認 10MB)
  • jobmanager 收到的 state 數據總和不能超過 jobmanager 內存

MemoryStateBackend 適合的場景:

  • 本地開發和調試
  • 狀態很小的作業

下圖表示了 MemoryStateBackend 的數據存儲位置:

img

1.2 FsStateBackend

FsStateBackend 需要配置一個 checkpoint 路徑,例如“hdfs://namenode:40010/flink/checkpoints” 或者 “file:///data/flink/checkpoints”,我們一般配置爲 hdfs 目錄 FsStateBackend 將工作狀態數據保存在 taskmanager 的 java 內存中。進行快照時,再將快照數據寫入上面配置的路徑,然後將寫入的文件路徑告知 jobmanager。jobmanager 中保存所有狀態的元數據信息(在 HA 模式下,元數據會寫入 checkpoint 目錄)。 FsStateBackend 默認使用異步方式進行快照,防止阻塞主線程的 pipline 處理。可以通過 FsStateBackend 構造函數取消該模式:

new FsStateBackend(path, false);

FsStateBackend 適合的場景:

  • 大狀態、長窗口、大鍵值(鍵或者值很大)狀態的作業
  • 適合高可用方案

@FsStateBackend state 存儲位置 | center

1.3 RocksDBStateBackend

RocksDBStateBackend 也需要配置一個 checkpoint 路徑,例如:“hdfs://namenode:40010/flink/checkpoints” 或者 “file:///data/flink/checkpoints”,一般配置爲 hdfs 路徑。 RocksDB 是一種可嵌入的持久型的 key-value 存儲引擎,提供 ACID 支持。由 Facebook 基於 levelDB 開發,使用 LSM 存儲引擎,是內存和磁盤混合存儲。 RocksDBStateBackend 將工作狀態保存在 taskmanager 的 RocksDB 數據庫中;checkpoint 時,RocksDB 中的所有數據會被傳輸到配置的文件目錄,少量元數據信息保存在 jobmanager 內存中( HA 模式下,會保存在 checkpoint 目錄)。 RocksDBStateBackend 使用異步方式進行快照。 RocksDBStateBackend 的限制:

  • 由於 RocksDB 的 JNI bridge API 是基於 byte[] 的,RocksDBStateBackend 支持的每個 key 或者每個 value 的最大值不超過 2^31 bytes((2GB))。
  • 要注意的是,有 merge 操作的狀態(例如 ListState),可能會在運行過程中超過 2^31 bytes,導致程序失敗。

RocksDBStateBackend 適用於以下場景:

  • 超大狀態、超長窗口(天)、大鍵值狀態的作業
  • 適合高可用模式

使用 RocksDBStateBackend 時,能夠限制狀態大小的是 taskmanager 磁盤空間(相對於 FsStateBackend 狀態大小限制於 taskmanager 內存 )。這也導致 RocksDBStateBackend 的吞吐比其他兩個要低一些。因爲 RocksDB 的狀態數據的讀寫都要經過反序列化/序列化。

RocksDBStateBackend 是目前三者中唯一支持增量 checkpoint 的。

img

二. Keyed State & Operator State

2.1 state 分類
  • Operator State (或者non-keyed state ) 每個 Operator state 綁定一個並行 Operator 實例。Kafka Connector 是使用 Operator state 的典型示例:每個並行的 kafka consumer 實例維護了每個 kafka topic 分區和該分區 offset 的映射關係,並將這個映射關係保存爲 Operator state。 在算子並行度改變時,Operator State 也會重新分配。

  • Keyed State 這種 State 只存在於 KeyedStream 上的函數和操作中,比如 Keyed UDF(KeyedProcessFunction…) window state 。可以把 Keyed State 想象成被分區的 Operator State。每個 Keyed State 在邏輯上可以看成與一個 <parallel-Operator-instance, key> 綁定,由於一個 key 肯定只存在於一個 Operator 實例,所以我們可以簡單的認爲一個 <operaor, key> 對應一個 Keyed State。 每個 Keyed State 在邏輯上還會被分配到一個 Key Group。分配方法如下:

// maxParallelism 爲最大並行度
MathUtils.murmurHash(key.hashCode()) % maxParallelism;

其中 maxParallelism 是 flink 程序的最大並行度,這個值一般我們不會去手動設置,使用默認的值(128)就好,這裏注意下,maxParallelism 和我們運行程序時指定的算子並行度(parallelism)不同,parallelism 不能大於 maxParallelism ,parallelism 最多隻能設置爲 maxParallelism 。 爲什麼會有 Key Group 這個概念呢?舉個栗子,我們通常寫程序,會給算子指定一個並行度,運行一段時間後,積累了一些 state ,這時候數據量大了,需要增大並行度;我們修改並行度後重新提交,那這些已經存在的 state 該如何分配到各個 Operator 呢?這就有了最大並行度(maxParallelism ) 和 Key Group 的概念。上面計算 Key Group 的公式也說明了 Key Group 的個數最多是 maxParallelism 個。當並行度更改後,我們再計算這個 key 被分配到的 Operator:

keyGroupId * parallelism / maxParallelism;

可以看到, 一個 keyGroupId 會對應到一個 Operator,當並行度更改時,新的 Operator 會去拉取對應 Key Group 的 Keyed State,這樣就把 KeyedState 儘量均勻地分配給所有的 Operator 啦!

根據 state 數據是否被 flink 託管,flink 又將 state 分類爲 managed state 和 raw state:

  • managed state: 被 flink 託管,保存爲內部的哈希表或者 RocksDB; checkpoint 時,flink 將 state 進行序列化編碼。例如 ValueState ListState…
  • raw state: Operator 自行管理的數據結構,checkpoint 時,它們只能以 byte 數組寫入 checkpoint。

當然建議使用 managed state 啦!使用 managed state 時, flink 會幫我們在更改並行度時重新分發 state,並且優化內存。

2.2 使用 managed keyed state
如何創建

上面提到,Keyed state 只能在 keyedStream 上使用,可以通過 stream.keyBy(…) 創建 keyedStream。我們可以創建以下幾種 keyed state:

  • ValueState <T>
  • ListState<T>
  • ReducingState<T>
  • AggregatingState<IN, OUT>
  • MapState<UK, UV>
  • FoldingState<T, ACC>

每種 state 都對應各自的描述符,通過描述符從 RuntimeContext 中獲取對應的 State,而 RuntimeContext 只有 RichFunction 才能獲取,所以要想使用 keyed state,用戶編寫的類必須繼承 RichFunction 或者其子類。

  • ValueState<T> getState(ValueStateDescriptor<T>)
  • ReducingState<T> getReducingState(ReducingStateDescriptor<T>)
  • ListState<T> getListState(ListStateDescriptor<T>)
  • AggregatingState<IN, OUT> getAggregatingState(AggregatingStateDescriptor<IN, ACC, OUT>)
  • FoldingState<T, ACC> getFoldingState(FoldingStateDescriptor<T, ACC>)
  • MapState<UK, UV> getMapState(MapStateDescriptor<UK, UV>)

示例:


給 keyed state 設置過期時間

flink-1.6.0 以後,我們還可以給 Keyed state 設置 TTL(Time-To-Live),當某一個 key 的 state 數據過期時,會被 statebackend 盡力刪除。 官方給出了使用示例:

import org.apache.flink.api.common.state.StateTtlConfig;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.api.common.time.Time;

StateTtlConfig ttlConfig = StateTtlConfig
    .newBuilder(Time.seconds(1)) // 狀態存活時間
    .setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite) // TTL 何時被更新,這裏配置的 state 創建和寫入時
    .setStateVisibility(StateTtlConfig.StateVisibility.NeverReturnExpired)
    .build();// 設置過期的 state 不被讀取
    
ValueStateDescriptor<String> stateDescriptor = new ValueStateDescriptor<>("text state", String.class);
stateDescriptor.enableTimeToLive(ttlConfig);

簡單來說就是在創建狀態描述符時,添加 StateTtlConfig 配置,

state 的 TTL 何時被更新?

可以進行以下配置,默認只在 key 的 state 被 modify(創建或更新) 的時候才更新 TTL:

  • StateTtlConfig.UpdateType.OnCreateAndWrite: 只在一個 key 的 state 創建和寫入時更新 TTL(默認)
  • StateTtlConfig.UpdateType.OnReadAndWrite: 讀取 state 時仍然更新 TTL
當 state 過期但是還未刪除時,這個狀態是否還可見?

可以進行以下配置,默認是不可見的:

  • StateTtlConfig.StateVisibility.NeverReturnExpired: 不可見(默認)
  • StateTtlConfig.StateVisibility.ReturnExpiredIfNotCleanedUp: 可見

注意:

  • 狀態的最新訪問時間會和狀態數據保存在一起,所以開啓 TTL 特性會增大 state 的大小。Heap state backend 會額外存儲一個包括用戶狀態以及時間戳的 Java 對象,RocksDB state backend 會在每個狀態值(list 或者 map 的每個元素)序列化後增加 8 個字節。
  • 暫時只支持基於 processing time 的 TTL。
  • 嘗試從 checkpoint/savepoint 進行恢復時,TTL 的狀態(是否開啓)必須和之前保持一致,否則會遇到 “StateMigrationException”。
  • TTL 的配置並不會保存在 checkpoint/savepoint 中,僅對當前 Job 有效。
  • 當前開啓 TTL 的 map state 僅在用戶值序列化器支持 null 的情況下,才支持用戶值爲 null。如果用戶值序列化器不支持 null, 可以用 NullableSerializer 包裝一層。
過期的 state 何時被刪除?

默認情況下,過期的 state 數據只有被顯示讀取的時候纔會被刪除,例如,調用 ValueState.value() 時。 注意:如果過期的數據如果之後不被讀取,那麼這個過期數據就不會被刪除,可能導致狀態不斷增大。目前有兩種方式解決這個問題:

1. 從全量快照恢復時刪除

可以配置從全量快照恢復時刪除過期數據:

import org.apache.flink.api.common.state.StateTtlConfig;
import org.apache.flink.api.common.time.Time;

StateTtlConfig ttlConfig = StateTtlConfig
    .newBuilder(Time.seconds(1)) // state 存活時間,這裏設置的 1 秒過期
    .cleanupFullSnapshot()
    .build();

侷限是正常運行的程序的過期狀態還是無法刪除,全量快照時,過期狀態還是被備份了,只是在從上一個快照恢復時會過濾掉過期數據。

  • 注意:使用 RocksDB 增量快照時,該配置無效。
  • 這種清理方式可以在任何時候通過 StateTtlConfig 啓用或者關閉,比如在從 savepoint 恢復時。

2. 後臺程序刪除(flink-1.8 之後的版本支持)

flink-1.8 引入了後臺清理過期 state 的特性,通過 StateTtlConfig 開啓,顯式調用 cleanupInBackground(),使用示例如下:

import org.apache.flink.api.common.state.StateTtlConfig;
StateTtlConfig ttlConfig = StateTtlConfig
    .newBuilder(Time.seconds(1)) // state 存活時間,這裏設置的 1 秒過期
    .cleanupInBackground()
    .build();

官方介紹,使用 cleanupInBackground() 時,可以讓不同 statebackend 自動選擇 cleanupIncrementally(heap state backend) 或者 cleanupInRocksdbCompactFilter(rocksdb state backend) 策略進行後臺清理。也就是說,不同的 statebackend 的具體清理過期 state 原理也是不一樣的。而且,配置爲 cleanupInBackground() 時,只能使用默認配置的參數。想要更改參數時,需要顯式配置上面提到的兩種清理方式,並且要和 statebackend 對應:

  • heap state backend 支持的增量清理 在狀態訪問或處理時進行。如果某個狀態開啓了該清理策略,則會在存儲後端保留一個所有狀態的惰性全局迭代器。 每次觸發增量清理時,從迭代器中選擇已經過期的進行清理。通過 StateTtlConfig 配置,顯式調用 cleanupIncrementally():
import org.apache.flink.api.common.state.StateTtlConfig;
 StateTtlConfig ttlConfig = StateTtlConfig
    .newBuilder(Time.seconds(1))
    .cleanupIncrementally(10, true)
    .build();
12345

使用 cleanupIncrementally() 策略時,當 state 被訪問時會觸發清理邏輯。 cleanupIncrementally() 包含兩個參數:第一個參數表示每次清理被觸發時,要檢查的 state 條目個數;第二個參數表示是否在每條數據被處理時都觸發清理邏輯。如果使用 cleanupInBackground() 的話,這裏的默認值是(5, false)。 還有以下幾點需要注意: a. 如果沒有 state 訪問,也沒有處理數據,則不會清理過期數據。 b. 增量清理會增加數據處理的耗時。 c. 現在僅 Heap state backend 支持增量清除機制。在 RocksDB state backend 上啓用該特性無效。 d. 如果 Heap state backend 使用同步快照方式,則會保存一份所有 key 的拷貝,從而防止併發修改問題,因此會增加內存的使用。但異步快照則沒有這個問題。 e. 對已有的作業,這個清理方式可以在任何時候通過 StateTtlConfig 啓用或禁用該特性,比如從 savepoint 重啓後。

  • RocksDB 進行 compaction(壓縮合並) 時清理 如果使用 RocksDB state backend,可以使用 Flink 爲 RocksDB 定製的 compaction filter。RocksDB 會週期性的對數據進行異步合併壓縮從而減少存儲空間。 Flink 壓縮過濾器會在壓縮時過濾掉已經過期的狀態數據。 該特性默認是關閉的,可以通過 Flink 的配置項 state.backend.rocksdb.ttl.compaction.filter.enabled 或者調用 RocksDBStateBackend::enableTtlCompactionFilter 啓用該特性。然後通過如下方式讓任何具有 TTL 配置的狀態使用過濾器:
import org.apache.flink.api.common.state.StateTtlConfig;

StateTtlConfig ttlConfig = StateTtlConfig
    .newBuilder(Time.seconds(1))
    .cleanupInRocksdbCompactFilter(1000)
    .build();
123456

使用這種策略需要注意: a. 壓縮時調用 TTL 過濾器會降低速度。TTL 過濾器需要解析上次訪問的時間戳,並對每個將參與壓縮的狀態進行是否過期檢查。 對於集合型狀態類型(比如 list 和 map),會對集合中每個元素進行檢查。 b. 對於元素序列化後長度不固定的列表狀態,TTL 過濾器需要在每次 JNI 調用過程中,額外調用 Flink 的 java 序列化器, 從而確定下一個未過期數據的位置。 c. 對已有的作業,這個清理方式可以在任何時候通過 StateTtlConfig 啓用或禁用該特性,比如從 savepoint 重啓後。

2.3 使用 managed operator state

我們可以通過實現 CheckpointedFunctionListCheckpointed<T extends Serializable> 接口來使用 managed operator state。

CheckpointedFunction

CheckpointedFunction 接口提供了訪問 non-keyed state 的方法,需要實現如下兩個方法:

void snapshotState(FunctionSnapshotContext context) throws Exception;

void initializeState(FunctionInitializationContext context) throws Exception;

進行 checkpoint 時會調用 snapshotState()。 用戶自定義函數初始化時會調用 initializeState(),初始化包括第一次自定義函數初始化和從之前的 checkpoint 恢復。 因此 initializeState() 不僅是定義不同狀態類型初始化的地方,也需要包括狀態恢復的邏輯。

當前,managed operator state 以 list 的形式存在。這些狀態是一個 可序列化 對象的集合 List,彼此獨立,方便在改變併發後進行狀態的重新分派。 換句話說,這些對象是重新分配 non-keyed state 的最細粒度。根據狀態的不同訪問方式,有如下幾種重新分配的模式:

  • Even-split redistribution: 每個算子都保存一個列表形式的狀態集合,整個狀態由所有的列表拼接而成。當作業恢復或重新分配的時候,整個狀態會按照算子的併發度進行均勻分配。 比如說,算子 A 的併發讀爲 1,包含兩個元素 element1 和 element2,當併發讀增加爲 2 時,element1 會被分到併發 0 上,element2 則會被分到併發 1 上。
  • Union redistribution: 每個算子保存一個列表形式的狀態集合。整個狀態由所有的列表拼接而成。當作業恢復或重新分配時,每個算子都將獲得所有的狀態數據。
ListCheckpointed

ListCheckpointed 接口是 CheckpointedFunction 的精簡版,僅支持 even-split redistributuion 的 list state。同樣需要實現兩個方法:

List<T> snapshotState(long checkpointId, long timestamp) throws Exception;

void restoreState(List<T> state) throws Exception;

snapshotState() 需要返回一個將寫入到 checkpoint 的對象列表,restoreState 則需要處理恢復回來的對象列表。如果狀態不可切分, 則可以在 snapshotState() 中返回 Collections.singletonList(MY_STATE)。

OperatorState 示例:實現帶狀態的 Sink Function

下面的例子中的 SinkFunction 在 CheckpointedFunction 中進行數據緩存,然後統一發送到下游,這個例子演示了列表狀態數據的 event-split redistribution。

package com.lagou.bak;

import org.apache.flink.api.common.functions.RichFlatMapFunction;
import org.apache.flink.api.common.state.ListState;
import org.apache.flink.api.common.state.ListStateDescriptor;
import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.api.common.typeinfo.TypeHint;
import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.api.common.typeutils.base.LongSerializer;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.runtime.state.FunctionInitializationContext;
import org.apache.flink.runtime.state.FunctionSnapshotContext;
import org.apache.flink.streaming.api.checkpoint.CheckpointedFunction;
import org.apache.flink.streaming.api.checkpoint.ListCheckpointed;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
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.sink.SinkFunction;
import org.apache.flink.streaming.api.functions.source.RichParallelSourceFunction;
import org.apache.flink.util.Collector;

import javax.swing.plaf.IconUIResource;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * 求平均值
 * (1,3)(1,5)(1,7)(1,4)(1,2)
 */
public class StateTest1 {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();


        DataStreamSource<Tuple2<Long, Long>> data = env.fromElements(Tuple2.of(1l, 3l), Tuple2.of(1l, 5l), Tuple2.of(1l, 7l), Tuple2.of(1l, 4l), Tuple2.of(1l, 2l));
        KeyedStream<Tuple2<Long, Long>, Long> keyed = data.keyBy(value -> value.f0);
//        keyed.
//        keyed.print();
        /*
        * 爲什麼用RichFlatMapFunction?  首先需求是調用flatMap方法,所以應該用FlatMapFunction,但是FlatMapFunction跟源碼發現只有flatMap方法。此處需要初始化一些東西,RichFlatMapFunction
        繼承自AbstractRichFunction,有open方法.並且實現了FlatMapFunction接口。是FlatMapFunction的功能豐富的變體(比如多了open方法)
        在說說AbstractRichFunction,繼承自RichFunction,又繼承自Function。
         Function是用戶自定義函數UDF的基礎接口
        RichFunction提供了兩個功能:1、Function的生命週期方法 2、提供了訪問Function運行時上下文
        AbstractRichFunction顧名思義Abstract即爲RichFunction接口的抽象實現類,功能爲實現類提供基類功能
        兩個待深入點:1、UDf 2、運行時上下文
        UDF:開發人員實現業務邏輯就是UDF
        RuntimeContext:對於每個Task而言,有更細節的配置信息,所以Flink又抽象出了RuntimeContext,每一個Task實例有自己的RuntimeContext,StreamExecutionEnvironment中配置信息和算子級別信息的綜合。
        */

        SingleOutputStreamOperator<Tuple2<Long, Long>> flatMaped = keyed.flatMap(new RichFlatMapFunction<Tuple2<Long, Long>, Tuple2<Long, Long>>() {
            private transient ValueState<Tuple2<Long, Long>> sum;

            @Override
            public void open(Configuration parameters) throws Exception {

//                ValueStateDescriptor<Long> count = new ValueStateDescriptor<>("count", LongSerializer.INSTANCE, 0L);
                System.out.println("...open");
                ValueStateDescriptor<Tuple2<Long, Long>> descriptor = new ValueStateDescriptor<>(
                        "average",
                        TypeInformation.of(new TypeHint<Tuple2<Long, Long>>() {
                        })
                        , Tuple2.of(0L, 0L)
                );
//                ValueStateDescriptor<Tuple2<Long, Long>> descriptor1 = new ValueStateDescriptor<>("average", TypeInformation.of(new TypeHint<Tuple2<Long, Long>>() {
//                }));
                //RuntimeContext是Function運行時的上下文,包含了Function在運行時需要的所有信息,如並行度相關信息、Task名稱、執行配置信息ExecutionConfig、State等
                sum = getRuntimeContext().getState(descriptor);
//                sum.update(new Tuple2<>(0L,0L));

            }

            @Override
            public void flatMap(Tuple2<Long, Long> value, Collector<Tuple2<Long, Long>> out) throws Exception {

                //獲取當前狀態值
                Tuple2<Long, Long> currentSum = sum.value();

                //更新
                currentSum.f0 += 1;
                currentSum.f1 += value.f1;

                //更新狀態值
                sum.update(currentSum);

                //如果count>=2 清空狀態值,重新計算
                if(currentSum.f0 == 2) {
                    out.collect(new Tuple2<>(value.f0,currentSum.f1 / currentSum.f0));
                    sum.clear();
                }
            }


        });

        flatMaped.print();

//        flatMaped.addSink(new BufferingSink(1));

        env.execute();
    }
}

class BufferingSink implements SinkFunction<Tuple2<Long,Long>>, CheckpointedFunction{
    ListState<Tuple2<Long, Long>> checkpointedState;
    private List<Tuple2<Long,Long>> bufferedElements;
    private final int threshold;

    public BufferingSink(int threshold) {
        this.threshold = threshold;
        this.bufferedElements = new ArrayList<Tuple2<Long,Long>>();
    }

    // checkpoint 時會調用 snapshotState() 函數
    @Override
    public void snapshotState(FunctionSnapshotContext context) throws Exception {
        System.out.println("...snapshotState");
        // 清空 ListState,我們要放入最新的數據啦
        checkpointedState.clear();
        // 把當前局部變量中的所有元素寫入到 checkpoint 中
        for (Tuple2<Long,Long> element : bufferedElements) {
            checkpointedState.add(element);
        }
    }

    // 需要處理第一次自定義函數初始化和從之前的 checkpoint 恢復兩種情況
    // initializeState 方法接收一個 FunctionInitializationContext 參數,會用來初始化 non-keyed state 的 “容器”。這些容器是一個 ListState, 用於在 checkpoint 時保存 non-keyed state 對象。
    // 就是說我們可以通過 FunctionInitializationContext 獲取 ListState 狀態
    @Override
    public void initializeState(FunctionInitializationContext context) throws Exception {
        System.out.println("...initializeState");
        // StateDescriptor 會包括狀態名字、以及狀態類型相關信息
        ListStateDescriptor<Tuple2<Long, Long>> descriptor = new ListStateDescriptor<>("buffered-elements", TypeInformation.of(new TypeHint<Tuple2<Long, Long>>() {
        }));
        // context.getOperatorStateStore().getListState(descriptor) 使用 even-split redistribution 算法
        // 我們還可以通過 context.getKeyedStateStore() 獲取 keyed state,當然要在 keyedStream 上使用啦!
        checkpointedState = context.getOperatorStateStore().getListState(descriptor);
        // 需要處理從 checkpoint/savepoint 恢復的情況
        // 通過 isRestored() 方法判斷是否從之前的故障中恢復回來,如果該方法返回 true 則表示從故障中進行恢復,會執行接下來的恢復邏輯
        if(context.isRestored()) {
            for(Tuple2<Long,Long> element : checkpointedState.get()) {
                bufferedElements.add(element);
            }
            System.out.println("....initializeState.bufferedElements:" + bufferedElements);
        }
    }

    @Override
    public void invoke(Tuple2<Long, Long> value, Context context) throws Exception {
        System.out.println("...invoke...value:" + value);
        // 把數據加入局部變量中
        bufferedElements.add(value);
        // 達到閾值啦!快發送
        if(bufferedElements.size() == threshold) {
            for (Tuple2<Long,Long> element : bufferedElements) {
                //// 這裏實現發送邏輯
                System.out.println("...out:" + element);
            }
            // 發送完注意清空緩存
            bufferedElements.clear();
        }
    }
}

class CounterSource extends RichParallelSourceFunction<Long> implements ListCheckpointed<Long> {

    /**  current offset for exactly once semantics */
    private Long offset = 0L;

    /** flag for job cancellation */
    private volatile boolean isRunning = true;

    @Override
    public void run(SourceContext<Long> ctx) {
        final Object lock = ctx.getCheckpointLock();

        while (isRunning) {
            // output and state update are atomic
            synchronized (lock) {
                ctx.collect(offset);
                offset += 1;
            }
        }
    }

    @Override
    public void cancel() {
        isRunning = false;
    }

    @Override
    public List<Long> snapshotState(long checkpointId, long checkpointTimestamp) {
        return Collections.singletonList(offset);
    }

    @Override
    public void restoreState(List<Long> state) {
        for (Long s : state)
            offset = s;
    }
}



希望訂閱 checkpoint 成功消息的算子,可以參考 org.apache.flink.runtime.state.CheckpointListener 接口。

2.4 statebackend 如何保存 managed keyed/operator state

上面我們詳細介紹了三種 statebackend,那麼這三種 statebackend 是如何託管 keyed state 和 Operator state 的呢? 參考很多資料並查閱源碼後,感覺下面的圖能簡單明瞭的表示當前 flink state 的存儲方式。

img

在 flink 的實際實現中,對於同一種 statebackend,不同的 state 在運行時會有細分的 statebackend 託管,例如 MemeoryStateBackend,就有 DefaultOperatorStateBackend 管理 Operator state,HeapKeydStateBackend 管理 Keyed state。我們看到 MemoryStateBackend 和 FsStateBackend 對於 keyed state 和 Operator state 的存儲都符合我們之前的理解,運行時 state 數據保存於內存,checkpoint 時分別將數據備份在 jobmanager 內存和磁盤; RocksDBStateBackend 運行時 Operator state 的保存位置需要注意下,並不是保存在 RocksDB 中,而是通過 DefaultOperatorStateBackend 保存在 taskmanager 內存,創建源碼如下:

// RocksDBStateBackend.java
// 創建 keyed statebackend
public <K> AbstractKeyedStateBackend<K> createKeyedStateBackend(...){
...
return new RocksDBKeyedStateBackend<>(
				...);
}
// 創建 Operator statebackend
public OperatorStateBackend createOperatorStateBackend(
			Environment env,
			String operatorIdentifier) throws Exception {

		//the default for RocksDB; eventually there can be a operator state backend based on RocksDB, too.
		final boolean asyncSnapshots = true;
		return new DefaultOperatorStateBackend(
				...);
	}

源碼中也標註了,未來會提供基於 RocksDB 存儲的 Operator state。所以當前即使使用 RocksDBStateBackend, Operator state 也不能超過內存限制。

Operator State 在內存中對應兩種數據結構:

  • ListState: 對應的實際實現類爲 PartitionableListState,創建並註冊的代碼如下
// DefaultOperatorStateBackend.java
private <S> ListState<S> getListState(...){
    partitionableListState = new PartitionableListState<>(
				new RegisteredOperatorStateBackendMetaInfo<>(
					name,
					partitionStateSerializer,
					mode));
	registeredOperatorStates.put(name, partitionableListState);
}
123456789

PartitionableListState 中通過 ArrayList 來保存 state 數據:

// PartitionableListState.java
/**
	 * The internal list the holds the elements of the state
	 */
	private final ArrayList<S> internalList;
12345
  • BroadcastState:對應的實際實現類爲 HeapBroadcastState,創建並註冊的代碼如下
public <K, V> BroadcastState<K, V> getBroadcastState(...) {
    broadcastState = new HeapBroadcastState<>(
					new RegisteredBroadcastStateBackendMetaInfo<>(
							name,
							OperatorStateHandle.Mode.BROADCAST,
							broadcastStateKeySerializer,
							broadcastStateValueSerializer));
	registeredBroadcastStates.put(name, broadcastState);
}
123456789

HeapBroadcastState 中通過 HashMap 來保存 state 數據:

/**
	 * The internal map the holds the elements of the state.
	 */
	private final Map<K, V> backingMap;
	HeapBroadcastState(RegisteredBroadcastStateBackendMetaInfo<K, V> stateMetaInfo) {
		this(stateMetaInfo, new HashMap<>());
	}
1234567

我們對比下 HeapKeydStateBackend 和 RocksDBKeyedStateBackend 是如何保存 keyed state 的: @HeapKeydStateBackend | center

對於 HeapKeydStateBackend , state 數據被保存在一個由多層 java Map 嵌套而成的數據結構中。這個圖表示的是 window 中的 keyed state 保存方式,而 window-contents 是 flink 中 window 數據的 state 描述符的名稱,當然描述符類型是根據實際情況變化的。比如我們經常在 window 後執行聚合操作 (aggregate),flink 就有可能創建一個名字爲 window-contents 的 AggregatingStateDescriptor:

// WindowedStream.java
AggregatingStateDescriptor<T, ACC, V> stateDesc = new AggregatingStateDescriptor<>("window-contents", aggregateFunction, accumulatorType.createSerializer(getExecutionEnvironment().getConfig()));
12

HeadKeyedStateBackend 會通過一個叫 StateTable 的數據結構,查找 key 對應的 StateMap:

// StateTable.java
/**
 * Map for holding the actual state objects. The outer array represents the key-groups.
 * All array positions will be initialized with an empty state map.
 */
protected final StateMap<K, N, S>[] keyGroupedStateMaps;
123456

根據是否開啓異步 checkpoint,StateMap 會分別對應兩個實現類:CopyOnWriteStateMap<K, N, S> 和 NestedStateMap<K, N, S>。 對於 NestedStateMap,實際存儲數據如下:

// NestedStateMap.java
private final Map<N, Map<K, S>> namespaceMap;
12

CopyOnWriteStateMap 是一個支持 Copy-On-Write 的 StateMap 子類,實際上參考了 HashMap 的實現,它支持漸進式哈希(incremental rehashing) 和異步快照特性。

對於 RocksDBKeyedStateBackend,每個 state 存儲在一個單獨的 column family 內,KeyGroup、key、namespace 進行序列化存儲在 DB 作爲 key,狀態數據作爲 value。

三. 配置 state backend

我們知道 flink 提供了三個 state backend,那麼如何配置使用某個 state backend 呢? 默認的配置在 conf/flink-conf.yaml 文件中 state.backend 指定,如果沒有配置該值,就會使用 MemoryStateBackend。默認的 state backend 可以被代碼中的配置覆蓋。

3.1 Per-job 設置

我們可以通過 StreamExecutionEnvironment 設置:

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setStateBackend(new FsStateBackend("hdfs://namenode:40010/flink/checkpoints"));
12

如果想使用 RocksDBStateBackend,你需要將相關依賴加入你的 flink 程序中:

<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-statebackend-rocksdb_2.11</artifactId>
    <version>${flink.version}</version>
    <scope>provided</scope>
</dependency>
123456
3.2 默認設置

如果沒有在程序中指定,flink 將使用 conf/flink-conf.yaml 文件中的 state.backend 指定的 state backend,這個值有三種配置:

  • jobmanager (代表 MemoryStateBackend)
  • filesystem (代表 FsStateBackend)
  • rocksdb (代表 RocksDBStateBackend)

state.checkpoints.dir 定義了 checkpoint 時,state backend 將快照數據備份的目錄

四. 開啓 checkpoint

開啓 checkpoint 後,state backend 管理的 taskmanager 上的狀態數據纔會被定期備份到 jobmanager 或 外部存儲,這些狀態數據在作業失敗恢復時會用到。我們可以通過以下代碼開啓和配置 checkpoint:

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//env.getConfig().disableSysoutLogging();
//每 30 秒觸發一次 checkpoint,checkpoint 時間應該遠小於(該值 + MinPauseBetweenCheckpoints),否則程序會一直做checkpoint,影響數據處理速度
env.enableCheckpointing(30000); // create a checkpoint every 30 seconds

// set mode to exactly-once (this is the default)
// flink 框架內保證 EXACTLY_ONCE 
env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);

// make sure 30 s of progress happen between checkpoints
// 兩個 checkpoints之間最少有 30s 間隔(上一個checkpoint完成到下一個checkpoint開始,默認爲0,這裏建議設置爲非0值)
env.getCheckpointConfig().setMinPauseBetweenCheckpoints(30000);

// checkpoints have to complete within one minute, or are discarded
// checkpoint 超時時間(默認 600 s)
env.getCheckpointConfig().setCheckpointTimeout(600000);

// allow only one checkpoint to be in progress at the same time
// 同時只有一個checkpoint運行(默認)
env.getCheckpointConfig().setMaxConcurrentCheckpoints(1);

// enable externalized checkpoints which are retained after job cancellation
// 取消作業時是否保留 checkpoint (默認不保留)
env.getCheckpointConfig().enableExternalizedCheckpoints(CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);

// checkpoint失敗時 task 是否失敗( 默認 true, checkpoint失敗時,task會失敗)
env.getCheckpointConfig().setFailOnCheckpointingErrors(true);

// 對 FsStateBackend 刷出去的文件進行文件壓縮,減小 checkpoint 體積
env.getConfig().setUseSnapshotCompression(true);
123456789101112131415161718192021222324252627282930

FsStateBackend 和 RocksDBStateBackend checkpoint 完成後最終保存到下面的目錄:

 hdfs:///your/checkpoint/path/{JOB_ID}/chk-{CHECKPOINT_ID}/
1

JOB_ID 是應用的唯一 ID,CHECKPOINT_ID 是每次 checkpoint 時自增的數字 ID 我們可以從備份的 checkpoint 數據恢復當時的作業狀態:

flink-1x.x/bin/flink run -s  hdfs:///your/checkpoint/path/{JOB_ID}/chk-{CHECKPOINT_ID}/ path/to//your/jar
1

我們可以實現 CheckpointedFunction 方法,在程序初始化或者 checkpoint 時修改狀態:

public class StatefulProcess extends KeyedProcessFunction<String, KeyValue, KeyValue> implements CheckpointedFunction {
    ValueState<Integer> processedInt;


    @Override
    public void open(Configuration parameters) throws Exception {
        super.open(parameters);
    }

    @Override
    public void processElement(KeyValue keyValue, Context context, Collector<KeyValue> collector) throws Exception {
        try{
            Integer a =  Integer.parseInt(keyValue.getValue());
            processedInt.update(a);
            collector.collect(keyValue);
        }catch(Exception e){
            e.printStackTrace();
        }
    }

    @Override
    public void initializeState(FunctionInitializationContext functionInitializationContext) throws Exception {
        processedInt = functionInitializationContext.getKeyedStateStore().getState(new ValueStateDescriptor<>("processedInt", Integer.class));
        if(functionInitializationContext.isRestored()){
            //Apply logic to restore the data
        }
    }

    @Override
    public void snapshotState(FunctionSnapshotContext functionSnapshotContext) throws Exception {
        processedInt.clear();
    }
}
123456789101112131415161718192021222324252627282930313233

五. state 文件格式

當我們創建 state 時,數據是如何保存的呢? 對於不同的 statebackend,有不同的存儲格式。但是都是使用 flink 序列化器,將鍵值轉化爲字節數組保存起來。這裏使用 RocksDBStateBackend 示例。 每個 taskmanager 會創建多個 RocksDB 目錄,每個目錄保存一個 RocksDB 數據庫;每個數據庫包含多個 column famiilies,這些 column families 由 state descriptors 定義。 每個 column family 包含多個 key-value 對,key 是 Operator 的 key, value 是對應的狀態數據。 讓我們看個例子程序:

// TestFlink.java
public static void main(String[] args) throws Exception {
    StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
    env.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime);
    ParameterTool configuration = ParameterTool.fromArgs(args);
    
    FlinkKafkaConsumer010<String> kafkaConsumer010 = new FlinkKafkaConsumer010<String>("test", new SimpleStringSchema(), getKafkaConsumerProperties("testing123"));
    
    DataStream<String> srcStream = env.addSource(kafkaConsumer010);
    
    Random random = new Random();
    
    DataStream<String> outStream =  srcStream
            .map(row -> new KeyValue("testing" + random.nextInt(100000), row))
            .keyBy(row -> row.getKey())
            .process(new StatefulProcess()).name("stateful_process").uid("stateful_process")
            .keyBy(row -> row.getKey())
            .flatMap(new StatefulMapTest()).name("stateful_map_test").uid("stateful_map_test");
    
    outStream.print();
    env.execute("Test Job");
}

public static Properties getKafkaConsumerProperties(String groupId){
    Properties props = new Properties();
    props.setProperty("bootstrap.servers", "localhost:9092"
    );
    props.setProperty("group.id", groupId);

    return props;
}
12345678910111213141516171819202122232425262728293031

這個程序包含兩個有狀態的算子:

//StatefulMapTest.java
public class StatefulMapTest extends RichFlatMapFunction<KeyValue, String> {
    ValueState<Integer> previousInt;
    ValueState<Integer> nextInt;

    @Override
    public void open(Configuration parameters) throws Exception {
        super.open(parameters);
        previousInt = getRuntimeContext().getState(new ValueStateDescriptor<Integer>("previousInt", Integer.class));
        nextInt = getRuntimeContext().getState(new ValueStateDescriptor<Integer>("nextInt", Integer.class));
    }

    @Override
    public void flatMap(KeyValue s, Collector<String> collector) throws Exception {
        try{
            Integer oldInt = Integer.parseInt(s.getValue());
            Integer newInt;
            if(previousInt.value() == null){
                newInt = oldInt;
                collector.collect("OLD INT: " + oldInt.toString());
            }else{
                newInt = oldInt - previousInt.value();
                collector.collect("NEW INT: " + newInt.toString());
            }
            nextInt.update(newInt);
            previousInt.update(oldInt);
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}
// StatefulProcess.java
public class StatefulProcess extends KeyedProcessFunction<String, KeyValue, KeyValue> {
    ValueState<Integer> processedInt;

    @Override
    public void open(Configuration parameters) throws Exception {
        super.open(parameters);
        processedInt = getRuntimeContext().getState(new ValueStateDescriptor<>("processedInt", Integer.class));
    }

    @Override
    public void processElement(KeyValue keyValue, Context context, Collector<KeyValue> collector) throws Exception {
        try{
            Integer a =  Integer.parseInt(keyValue.getValue());
            processedInt.update(a);
            collector.collect(keyValue);
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}

在 flink-conf.yaml 文件中設置 rocksdb 作爲 state backend。每個 taskmanager 將在指定的 tmp 目錄下(對於 onyarn 模式,tmp 目錄由 yarn 指定,一般爲 /path/to/nm-local-dir/usercache/user/appcache/application_xxx/flink-io-xxx),生成下面的目錄:

drwxr-xr-x   4 abc  74715970   128B Sep 23 03:19 job_127b2b84f80b368b8edfe02b2762d10d_op_KeyedProcessOperator_0d49016af99997646695a030f69aa7ee__1_1__uuid_65b50444-5857-4940-9f8c-77326cc79279/db
drwxr-xr-x   4 abc  74715970   128B Sep 23 03:20 job_127b2b84f80b368b8edfe02b2762d10d_op_StreamFlatMap_11f49afc24b1cce91c7169b1e5140284__1_1__uuid_19b333d3-3278-4e51-93c8-ac6c3608507c/db

目錄名含義如下: @rocksdb-dir | center 大致分爲 3 部分:

  1. JOB_ID: JobGraph 創建時分配的隨機 id
  2. OPERATOR_ID: 由 4 部分組成, 算子基類_Murmur3(算子 uid)_task索引_task總並行度。對於 StatefulMapTest 這個算子,4 個 部分分別爲:
    • StreamFlatMap
    • Murmur3_128(“stateful_map_test”) -> 11f49afc24b1cce91c7169b1e5140284
    • 1,因爲總並行度指定了1,所以只有這一個 task
    • 1,因爲總並行度指定了1
  3. UUID: 隨機的 UUID 值 每個目錄都包含一個 RocksDB 實例,其文件結構如下:
-rw-r--r--  1 abc  74715970    21K Sep 23 03:20 000011.sst
-rw-r--r--  1 abc  74715970    21K Sep 23 03:20 000012.sst
-rw-r--r--  1 abc  74715970     0B Sep 23 03:36 000015.log
-rw-r--r--  1 abc  74715970    16B Sep 23 03:36 CURRENT
-rw-r--r--  1 abc  74715970    33B Sep 23 03:18 IDENTITY
-rw-r--r--  1 abc  74715970     0B Sep 23 03:33 LOCK
-rw-r--r--  1 abc  74715970    34K Sep 23 03:36 LOG
-rw-r--r--  1 abc  74715970   339B Sep 23 03:36 MANIFEST-000014
-rw-r--r--  1 abc  74715970    10K Sep 23 03:36 OPTIONS-000017
  • .sst 文件是 RocksDB 生成的 SSTable,包含真實的狀態數據。
  • LOG 文件包含 commit log
  • MANIFEST 文件包含元數據信息,例如 column families
  • OPTIONS 文件包含創建 RocksDB 實例時使用的配置

我們通過 RocksDB java API 打開這些文件:

//FlinkRocksDb.java
public class FlinkRocksDb {
    public static void main(String[] args) throws Exception {
        RocksDB.loadLibrary();
        String previousIntColumnFamily = "previousInt";
        byte[] previousIntColumnFamilyBA = previousIntColumnFamily.getBytes(StandardCharsets.UTF_8);

        String nextIntcolumnFamily = "nextInt";
        byte[] nextIntcolumnFamilyBA = nextIntcolumnFamily.getBytes(StandardCharsets.UTF_8);
         try (final ColumnFamilyOptions cfOpts = new ColumnFamilyOptions().optimizeUniversalStyleCompaction()) {

            // list of column family descriptors, first entry must always be default column family
            final List<ColumnFamilyDescriptor> cfDescriptors = Arrays.asList(
                    new ColumnFamilyDescriptor(RocksDB.DEFAULT_COLUMN_FAMILY, cfOpts),
                    new ColumnFamilyDescriptor(previousIntColumnFamilyBA, cfOpts),
                    new ColumnFamilyDescriptor(nextIntcolumnFamilyBA, cfOpts)
            );

            // a list which will hold the handles for the column families once the db is opened
            final List<ColumnFamilyHandle> columnFamilyHandleList = new ArrayList<>();

            String dbPath = "/Users/abc/job_127b2b84f80b368b8edfe02b2762d10d_op"+
            "_StreamFlatMap_11f49afc24b1cce91c7169b1e5140284__1_1__uuid_19b333d3-3278-4e51-93c8-ac6c3608507c/db/";
            try (final DBOptions options = new DBOptions()
                    .setCreateIfMissing(true)
                    .setCreateMissingColumnFamilies(true);

                 final RocksDB db = RocksDB.open(options, dbPath, cfDescriptors, columnFamilyHandleList)) {

                try {
                    for(ColumnFamilyHandle columnFamilyHandle : columnFamilyHandleList){
                    // 有些 rocksdb 版本去除了 getName 這個方法
                        byte[] name = columnFamilyHandle.getName();
                        System.out.write(name);
                    }
                }finally {
                    // NOTE frees the column family handles before freeing the db
                    for (final ColumnFamilyHandle columnFamilyHandle :
                            columnFamilyHandleList) {
                        columnFamilyHandle.close();
                    }
                }
            }
    } catch (Exception e) {
          e.printStackTrace();
    }
}

上面的程序將會輸出:

default
previousInt
nextInt

我們可以打印出每個 column family 中的鍵值對:

// RocksdbKVIterator.java
TypeInformation<Integer> resultType = TypeExtractor.createTypeInfo(Integer.class);
TypeSerializer<Integer> serializer = resultType.createSerializer(new ExecutionConfig());

RocksIterator iterator =  db.newIterator(columnFamilyHandle);
iterator.seekToFirst();
iterator.status();

while (iterator.isValid()) {
    byte[] key = iterator.key();
    System.out.write(key);
    System.out.println(serializer.deserialize(new TestInputView(iterator.value())));
    iterator.next();
}

上面的程序將會輸出鍵值對,如 (testing123, 1423), (testing456, 1212) …

第九部分:關於並行度的設置

一個Flink程序由多個Operator組成(source、transformation和 sink)。

一個Operator由多個並行的Task(線程)來執行, 一個Operator的並行Task(線程)數目就被稱爲該Operator(任務)的並行度(Parallel)

並行度可以有如下幾種指定方式

1.Operator Level(算子級別)(可以使用)

一個算子、數據源和sink的並行度可以通過調用 setParallelism()方法來指定

actions.filter(new FilterFunction<UserAction>() {
            @Override
            public boolean filter(UserAction value) throws Exception {
                return false;
            }
        }).setParallelism(4);

2.Execution Environment Level(Env級別)(可以使用)

執行環境(任務)的默認並行度可以通過調用setParallelism()方法指定。爲了以並行度3來執行所有的算子、數據源和data sink, 可以通過如下的方式設置執行環境的並行度:

執行環境的並行度可以通過顯式設置算子的並行度而被重寫

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(4);

3.Client Level(客戶端級別,推薦使用)(可以使用)

並行度可以在客戶端將job提交到Flink時設定。

對於CLI客戶端,可以通過-p參數指定並行度

./bin/flink run -p 10 WordCount-java.jar

4.System Level(系統默認級別,儘量不使用)

在系統級可以通過設置flink-conf.yaml文件中的parallelism.default屬性來指定所有執行環境的默認並行度

示例

image-20200921112124816

image-20200921112136826

Example1

在fink-conf.yaml中 taskmanager.numberOfTaskSlots 默認值爲1,即每個Task Manager上只有一個Slot ,此處是3

Example1中,WordCount程序設置了並行度爲1,意味着程序 Source、Reduce、Sink在一個Slot中,佔用一個Slot

Example2

通過設置並行度爲2後,將佔用2個Slot

Example3

通過設置並行度爲9,將佔用9個Slot

Example4

通過設置並行度爲9,並且設置sink的並行度爲1,則Source、Reduce將佔用9個Slot,但是Sink只佔用1個Slot

注意

1.並行度的優先級:算子級別 > env級別 > Client級別 > 系統默認級別 (越靠前具體的代碼並行度的優先級越高)

2.如果source不可以被並行執行,即使指定了並行度爲多個,也不會生效

3.儘可能的規避算子的並行度的設置,因爲並行度的改變會造成task的重新劃分,帶來shuffle問題,

4.推薦使用任務提交的時候動態的指定並行度

5.slot是靜態的概念,是指taskmanager具有的併發執行能力; parallelism是動態的概念,是指程序運行時實際使用的併發能力

第十部分:Flink-Connector (Kafka)

第一節:源碼理解

Funtion:UDF---處理數據的邏輯

RichFunction: open/close 管理函數的生命週期的方法 ...RunTimeContext函數的運行時上下文

SourceFunction: 提供了自定義數據源的功能,run方法是獲取數據的方法

ParallelSourceFunction:

image-20201012113629573

創建一個新的流數據源消費者

Flink Kafka Consumer是一個流數據源,它從Apache Kafka提取並行數據流。使用者可以在多個並行實例中運行,每個實例將從一個或多個Kafka分區提取數據。

Flink Kafka消費者參與檢查點並保證沒有數據丟失

當出現故障時,計算過程只處理一次元素。

(注:這些保證自然假設Kafka本身不會丟失任何數據。)

請注意,Flink在內部快照偏移量,將其作爲分佈式檢查點的一部分。提交到kafka上的offset只是爲了使外部的outside view of progress與Flink的view of progress同步。通過這種方式,監視和其他工作可以瞭解Flink Kafka消費者在某個主題上消費了多少數據。

FlinkKafkaConsumerBase:

所有Flink Kafka Consumer數據源的基類。這個類實現了所有Kafka版本的公共行爲

回顧自定義數據源---

open方法和run方法

image-20201012131740281

Flink-Kafka-Consumer:

package com.lagou.source;

import org.apache.flink.api.common.serialization.SimpleStringSchema;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer;

import java.util.Properties;

public class FromKafka {
    public static void main(String[] args) throws Exception {
            StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
            Properties properties = new Properties();
            properties.setProperty("bootstrap.servers", "teacher2:9092");

            FlinkKafkaConsumer<String> consumer = new FlinkKafkaConsumer<>("mytopic", new SimpleStringSchema(), properties);
            //從最早開始消費
            consumer.setStartFromEarliest();
            DataStream<String> stream = env.addSource(consumer);
            stream.print();
            //stream.map();
            env.execute();


    }
}

flink-kafka 是如何消費的?以及如何分區分配等

open方法源碼:

(1)指定offset提交模式

OffsetCommitMode:

OffsetCommitMode:表示偏移量如何從外部提交回Kafka brokers/ Zookeeper的行爲

它的確切值是在運行時在使用者子任務中確定的。

image-20201012132333714

  • DISABLED:完全禁用offset提交。
  • ON_CHECKPOINTS:只有當檢查點完成時,纔將偏移量提交回Kafka。
  • KAFKA_PERIODIC:使用內部Kafka客戶端的自動提交功能,定期將偏移量提交回Kafka。

image-20201012133036183

使用多個配置值確定偏移量提交模式

如果啓用了checkpoint,並且啓用了checkpoint完成時提交offset,返回ON_CHECKPOINTS。

如果未啓用checkpoint,但是啓用了自動提交,返回KAFKA_PERIODIC。

其他情況都返回DISABLED。

(2)

接下來創建和啓動分區發現工具

image-20201012133633643

創建用於爲此子任務查找新分區的分區發現程序。

參數1:topicsDescriptor : 描述我們是爲固定主題還是主題模式發現分區,也就是fixedTopics和topicPattern的封裝。其中fixedTopics明確指定了topic的名稱,稱爲固定topic。topicPattern爲匹配topic名稱的正則表達式,用於分區發現。

image-20201012133917211

參數2:indexOfThisSubtask :此consumer子任務的索引。

參數3:numParallelSubtasks : 並行consumer子任務的總數

方法返回一個分區發現器的實例

(3)

打開分區發現程序,初始化所有需要的Kafka連接。

image-20201012134652814

注意是線程不安全的

初始化所有需要的Kafka鏈接源碼:

image-20201012134925865

KafkaPartitionDiscoverer:

image-20201012134941777

創建出KafkaConsumer對象。

(4)

subscribedPartitionsToStartOffsets = new HashMap<>();

已訂閱的分區列表,這裏將它初始化

private Map<KafkaTopicPartition, Long> subscribedPartitionsToStartOffsets;

用來保存將讀取的一組主題分區,以及要開始讀取的初始偏移量。

(5)

用戶獲取所有fixedTopics和匹配topicPattern的Topic包含的所有分區信息

image-20201012141209322

(6)

如果consumer從檢查點恢復狀態,restoredState用來保存要恢復的偏移量

選擇TreeMap數據類型,目的是有序

image-20201012150718485

在initializeState實例化方法中填充:

image-20201012151532274

回顧:context.isRestored的機制:

當程序發生故障的時候值爲true

image-20201012151426149

if (restoredState != null) {
// 從快照恢復邏輯...
} else {
// 直接啓動邏輯...
}

如果restoredState沒有存儲某一分區的狀態, 需要重頭消費該分區

image-20201012141822620

過濾掉不歸該subtask負責的partition分區

image-20201012142445184

assign方法:

返回應該分配給特定Kafka分區的目標子任務的索引

image-20201012142846614

subscribedPartitionsToStartOffsets.put(restoredStateEntry.getKey(), restoredStateEntry.getValue());

將restoredState中保存的一組topic的partition和要開始讀取的起始偏移量保存到subscribedPartitionsToStartOffsets

其中restoredStateEntry.getKey爲某個Topic的摸個partition,restoredStateEntry.getValue爲該partition的要開始讀取的起始偏移量

過濾掉topic名稱不符合topicsDescriptor的topicPattern的分區

image-20201012150056860

(7) 直接啓動consumer

image-20201012152143992

該枚舉類型有5個值:

  • GROUP_OFFSETS:從保存在zookeeper或者是Kafka broker的對應消費者組提交的offset開始消費,這個是默認的配置
  • EARLIEST:儘可能從最早的offset開始消費
  • LATEST:從最近的offset開始消費
  • TIMESTAMP:從用戶提供的timestamp處開始消費
  • SPECIFIC_OFFSETS:從用戶提供的offset處開始消費

根據startup mode,獲取從哪個地方開始消費。然後,partition discoverer就會拉取初始分區的數據

image-20201012152514274

如果startup模式爲SPECIFIC_OFFSETS:

異常情況:如果沒有配置具體從哪個offset開始消費

正常情況:獲取每個分區指定的消費起始offset

Long specificOffset = specificStartupOffsets.get(seedPartition);

image-20201012153043847

image-20201012164509269

Run方法:

(1) 判斷保存分區和讀取起始偏移量的集合是否爲空:

image-20201012211020853

(2)

記錄Kafka offset成功提交和失敗提交的數量

image-20201012210848272

(3)

獲取當前自任務的索引

image-20201012211250222

image-20201012211151422

(4)

註冊一個提交時的回調函數,提交成功時,提交成功計數器加一;提交失敗時,提交失敗計數器加一

image-20201012211416696

(5)

接下來判斷subscribedPartitionsToStartOffsets集合是否爲空。如果爲空,標記數據源的狀態爲暫時空閒。

image-20201012211656663

(6)創建一個KafkaFetcher,藉助KafkaConsumer API從Kafka的broker拉取數據

image-20201012212051017

(7)

根據分區發現間隔時間,來確定是否啓動分區定時發現任務

如果沒有配置分區定時發現時間間隔,則直接啓動獲取數據任務;否則,啓動定期分區發現任務和數據獲取任務

image-20201012212354702

循環拉取數據源碼:

image-20201012212916899

createAndStartDiscoveryLoop:啓動分區發現任務的方法:

image-20201012213547827

嘗試發現新的分區:

image-20201012213627808

將發現的新分區添加到kafkaFetcher中

image-20201012213721209

啓動分區發現定時任務

image-20201012213919489

partitionDiscoverer.discoverPartitions()的調用,即發現分區的執行過程。

image-20201012214319780

image-20201012214636507

image-20201012214844411

kafkaFetcher的runFetchLoop方法

此方法爲FlinkKafkaConsumer獲取數據的主入口,通過一個循環來不斷獲取kafka broker的數據。

image-20201012215703725

KafkaConsumerThread線程的run方法實例化handover

image-20201012215942755

回到KafkaFecher類中的runFetchLoop方法

image-20201012220346826

image-20201012221406546

partitionConsumerRecordsHandler方法

image-20201012222153359

@Override
	public void open(Configuration configuration) throws Exception {
		// determine the offset commit mode 
		// 指定offset的提交模式:   DISABLED、 ON_CHECKPOINTS 、KAFKA_PERIODIC
		this.offsetCommitMode = OffsetCommitModes.fromConfiguration(
				getIsAutoCommitEnabled(),
				enableCommitOnCheckpoints,
				((StreamingRuntimeContext) getRuntimeContext()).isCheckpointingEnabled());

		// create the partition discoverer
		//  創建一個分區發現器
		this.partitionDiscoverer = createPartitionDiscoverer(
				topicsDescriptor,
				getRuntimeContext().getIndexOfThisSubtask(),
				getRuntimeContext().getNumberOfParallelSubtasks());
		// 實例化出 consumer對象
		this.partitionDiscoverer.open();

		// 已經訂閱的分區列表
		subscribedPartitionsToStartOffsets = new HashMap<>();
		// 獲取kafka中的所有分區
		final List<KafkaTopicPartition> allPartitions = partitionDiscoverer.discoverPartitions();
		if (restoredState != null) {
			//restoredState: 快照  consumer是從快照中恢復的方式創建
			for (KafkaTopicPartition partition : allPartitions) {
				if (!restoredState.containsKey(partition)) {
					restoredState.put(partition, KafkaTopicPartitionStateSentinel.EARLIEST_OFFSET);
				}
			}

			for (Map.Entry<KafkaTopicPartition, Long> restoredStateEntry : restoredState.entrySet()) {
				if (!restoredFromOldState) {
					// seed the partition discoverer with the union state while filtering out
					// restored partitions that should not be subscribed by this subtask
					// 過濾一下和當前的subTask沒有關係的分區數據
					if (KafkaTopicPartitionAssigner.assign(
						restoredStateEntry.getKey(), getRuntimeContext().getNumberOfParallelSubtasks())
						== getRuntimeContext().getIndexOfThisSubtask()){
						subscribedPartitionsToStartOffsets.put(restoredStateEntry.getKey(), restoredStateEntry.getValue());
					}
				} else {
					// when restoring from older 1.1 / 1.2 state, the restored state would not be the union state;
					// in this case, just use the restored state as the subscribed partitions
					subscribedPartitionsToStartOffsets.put(restoredStateEntry.getKey(), restoredStateEntry.getValue());
				}
			}

			LOG.info("Consumer subtask {} will start reading {} partitions with offsets in restored state: {}",
				getRuntimeContext().getIndexOfThisSubtask(), subscribedPartitionsToStartOffsets.size(), subscribedPartitionsToStartOffsets);
		} else {
			//重新創建一個新的consumer
			// use the partition discoverer to fetch the initial seed partitions,
			// and set their initial offsets depending on the startup mode.
			// for SPECIFIC_OFFSETS and TIMESTAMP modes, we set the specific offsets now;
			// for other modes (EARLIEST, LATEST, and GROUP_OFFSETS), the offset is lazily determined
			// when the partition is actually read.
			switch (startupMode) {
			//startupMode : consumer的消費策略
				case SPECIFIC_OFFSETS:
					if (specificStartupOffsets == null) {
						throw new IllegalStateException(
							"Startup mode for the consumer set to " + StartupMode.SPECIFIC_OFFSETS +
								", but no specific offsets were specified.");
					}

					for (KafkaTopicPartition seedPartition : allPartitions) {
						Long specificOffset = specificStartupOffsets.get(seedPartition);
						if (specificOffset != null) {
							// since the specified offsets represent the next record to read, we subtract
							// it by one so that the initial state of the consumer will be correct
							subscribedPartitionsToStartOffsets.put(seedPartition, specificOffset - 1);
						} else {
							// default to group offset behaviour if the user-provided specific offsets
							// do not contain a value for this partition
							subscribedPartitionsToStartOffsets.put(seedPartition, KafkaTopicPartitionStateSentinel.GROUP_OFFSET);
						}
					}

					break;
				case TIMESTAMP:
					if (startupOffsetsTimestamp == null) {
						throw new IllegalStateException(
							"Startup mode for the consumer set to " + StartupMode.TIMESTAMP +
								", but no startup timestamp was specified.");
					}

					for (Map.Entry<KafkaTopicPartition, Long> partitionToOffset
						: fetchOffsetsWithTimestamp(allPartitions, startupOffsetsTimestamp).entrySet()) {
						subscribedPartitionsToStartOffsets.put(
							partitionToOffset.getKey(),
							(partitionToOffset.getValue() == null)
								// if an offset cannot be retrieved for a partition with the given timestamp,
								// we default to using the latest offset for the partition
								? KafkaTopicPartitionStateSentinel.LATEST_OFFSET
								// since the specified offsets represent the next record to read, we subtract
								// it by one so that the initial state of the consumer will be correct
								: partitionToOffset.getValue() - 1);
					}

					break;
				default:
					for (KafkaTopicPartition seedPartition : allPartitions) {
						subscribedPartitionsToStartOffsets.put(seedPartition, startupMode.getStateSentinel());
					}
			}

			if (!subscribedPartitionsToStartOffsets.isEmpty()) {
				switch (startupMode) {
					case EARLIEST:
						LOG.info("Consumer subtask {} will start reading the following {} partitions from the earliest offsets: {}",
							getRuntimeContext().getIndexOfThisSubtask(),
							subscribedPartitionsToStartOffsets.size(),
							subscribedPartitionsToStartOffsets.keySet());
						break;
					case LATEST:
						LOG.info("Consumer subtask {} will start reading the following {} partitions from the latest offsets: {}",
							getRuntimeContext().getIndexOfThisSubtask(),
							subscribedPartitionsToStartOffsets.size(),
							subscribedPartitionsToStartOffsets.keySet());
						break;
					case TIMESTAMP:
						LOG.info("Consumer subtask {} will start reading the following {} partitions from timestamp {}: {}",
							getRuntimeContext().getIndexOfThisSubtask(),
							subscribedPartitionsToStartOffsets.size(),
							startupOffsetsTimestamp,
							subscribedPartitionsToStartOffsets.keySet());
						break;
					case SPECIFIC_OFFSETS:
						LOG.info("Consumer subtask {} will start reading the following {} partitions from the specified startup offsets {}: {}",
							getRuntimeContext().getIndexOfThisSubtask(),
							subscribedPartitionsToStartOffsets.size(),
							specificStartupOffsets,
							subscribedPartitionsToStartOffsets.keySet());

						List<KafkaTopicPartition> partitionsDefaultedToGroupOffsets = new ArrayList<>(subscribedPartitionsToStartOffsets.size());
						for (Map.Entry<KafkaTopicPartition, Long> subscribedPartition : subscribedPartitionsToStartOffsets.entrySet()) {
							if (subscribedPartition.getValue() == KafkaTopicPartitionStateSentinel.GROUP_OFFSET) {
								partitionsDefaultedToGroupOffsets.add(subscribedPartition.getKey());
							}
						}

						if (partitionsDefaultedToGroupOffsets.size() > 0) {
							LOG.warn("Consumer subtask {} cannot find offsets for the following {} partitions in the specified startup offsets: {}" +
									"; their startup offsets will be defaulted to their committed group offsets in Kafka.",
								getRuntimeContext().getIndexOfThisSubtask(),
								partitionsDefaultedToGroupOffsets.size(),
								partitionsDefaultedToGroupOffsets);
						}
						break;
					case GROUP_OFFSETS:
						LOG.info("Consumer subtask {} will start reading the following {} partitions from the committed group offsets in Kafka: {}",
							getRuntimeContext().getIndexOfThisSubtask(),
							subscribedPartitionsToStartOffsets.size(),
							subscribedPartitionsToStartOffsets.keySet());
				}
			} else {
				LOG.info("Consumer subtask {} initially has no partitions to read from.",
					getRuntimeContext().getIndexOfThisSubtask());
			}
		}
	}

該方法包含的內容爲FlinkKafkaConsumer的初始化邏輯。

首先設置提交offset的模式。

接下來創建和啓動分區發現工具。

subscribedPartitionsToStartOffsets 爲已訂閱的分區列表,這裏將它初始化。

run:

kafka-console-producer.sh --broker-list teacher2:9092 --topic mytopic

1.1 消費策略

  • setStartFromGroupOffsets()【默認消費策略】

    默認讀取上次保存的offset信息 如果是應用第一次啓動,讀取不到上次的offset信息,則會根據這個參數auto.offset.reset的值來進行消費數據

  • setStartFromEarliest() 從最早的數據開始進行消費,忽略存儲的offset信息

  • setStartFromLatest() 從最新的數據進行消費,忽略存儲的offset信息

  • setStartFromSpecificOffsets(Map<KafkaTopicPartition, Long>) 從指定位置進行消費

  • 當checkpoint機制開啓的時候,KafkaConsumer會定期把kafka的offset信息還有其他operator的狀態信息一塊保存起來。當job失敗重啓的時候,Flink會從最近一次的checkpoint中進行恢復數據,重新消費kafka中的數據。

  • 爲了能夠使用支持容錯的kafka Consumer,需要開啓checkpoint env.enableCheckpointing(5000); // 每5s checkpoint一次

1.2 Kafka consumer offset自動提交:

kafka consumer offset自動提交的配置需要根據job是否開啓checkpoint來區分

checkpoint關閉時:

checkpoint開啓時:

如果啓用了checkpoint,並且啓用了checkpoint完成時提交offset,返回ON_CHECKPOINTS。

如果未啓用checkpoint,但是啓用了自動提交,返回KAFKA_PERIODIC。

其他情況都返回DISABLED。

OffsetCommitMode是一個枚舉類型,具有如下三個值:

  • DISABLED:完全禁用offset提交。
  • ON_CHECKPOINTS:當checkpoint完成的時候再提交offset。
  • KAFKA_PERIODIC:週期性提交offset。

Flink kafka Producer

nc

代碼接受nc

把接收到的nc的數據,給到kafka flink kafka producer

代碼:

package com.lagou.sink;

import org.apache.flink.api.common.serialization.SimpleStringSchema;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaProducer;

import java.util.Properties;

public class SinkToKafka {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStreamSource<String> data = env.socketTextStream("teacher2", 7777);
        Properties properties = new Properties();
        properties.setProperty("bootstrap.servers","teacher2:9092");
        FlinkKafkaProducer producer = new FlinkKafkaProducer("teacher2:9092", "mytopic2", new SimpleStringSchema());
        data.addSink(producer);
        env.execute();
    }
}

十一部分 Flink CEP

CEP 即Complex Event Processing - 複雜事件處理,Flink CEP 是在 Flink 中實現的複雜時間處理(CEP)庫。處理事件的規則,被叫做“模式”(Pattern),Flink CEP 提供了 Pattern API,用於對輸入流數據進行復雜事件規則定義,用來提取符合規則的事件序列。  Pattern API 大致分爲三種:個體模式,組合模式,模式組。

Flink CEP 應用場景:

CEP 在互聯網各個行業都有應用,例如金融、物流、電商、智能交通、物聯網行業等行業:

實時監控:

在網站的訪問日誌中尋找那些使用腳本或者工具“爆破”登錄的用戶;

我們需要在大量的訂單交易中發現那些虛假交易(超時未支付)或發現交易活躍用戶;

或者在快遞運輸中發現那些滯留很久沒有簽收的包裹等。

風險控制:

比如金融行業可以用來進行風險控制和欺詐識別,從交易信息中尋找那些可能存在的危險交易和非法交易。

營銷廣告:

跟蹤用戶的實時行爲,指定對應的推廣策略進行推送,提高廣告的轉化率。

1、基礎

(1)定義 複合事件處理(Complex Event Processing,CEP)是一種基於動態環境中事件流的分析技術,事件在這裏通常是有意義的狀態變化,通過分析事件間的關係,利用過濾、關聯、聚合等技術,根據事件間的時序關係和聚合關係制定檢測規則,持續地從事件流中查詢出符合要求的事件序列,最終分析得到更復雜的複合事件。 (2)特徵 CEP的特徵如下: 目標:從有序的簡單事件流中發現一些高階特徵; 輸入:一個或多個簡單事件構成的事件流; 處理:識別簡單事件之間的內在聯繫,多個符合一定規則的簡單事件構成複雜事件; 輸出:滿足規則的複雜事件。 在這裏插入圖片描述

(3)功能 CEP用於分析低延遲、頻繁產生的不同來源的事件流。CEP可以幫助在複雜的、不相關的時間流中找出有意義的模式和複雜的關係,以接近實時或準實時的獲得通知或組織一些行爲。 CEP支持在流上進行模式匹配,根據模式的條件不同,分爲連續的條件或不連續的條件;模式的條件允許有時間的限制,當條件範圍內沒有達到滿足的條件時,會導致模式匹配超時。 看起來很簡單,但是它有很多不同的功能: ① 輸入的流數據,儘快產生結果; ② 在2個事件流上,基於時間進行聚合類的計算; ③ 提供實時/準實時的警告和通知; ④ 在多樣的數據源中產生關聯分析模式; ⑤ 高吞吐、低延遲的處理 市場上有多種CEP的解決方案,例如Spark、Samza、Beam等,但他們都沒有提供專門的庫支持。然而,Flink提供了專門的CEP庫。 (4)主要組件 Flink爲CEP提供了專門的Flink CEP library,它包含如下組件:Event Stream、Pattern定義、Pattern檢測和生成Alert。 首先,開發人員要在DataStream流上定義出模式條件,之後Flink CEP引擎進行模式檢測,必要時生成警告。 在這裏插入圖片描述

2、 Pattern API

處理事件的規則,被叫作模式(Pattern)。 Flink CEP提供了Pattern API用於對輸入流數據進行復雜事件規則定義,用來提取符合規則的事件序列。 模式大致分爲三類: ① 個體模式(Individual Patterns) 組成複雜規則的每一個單獨的模式定義,就是個體模式。

start.times(3).where(_.behavior.startsWith(‘fav’))

② 組合模式(Combining Patterns,也叫模式序列) 很多個體模式組合起來,就形成了整個的模式序列。 模式序列必須以一個初始模式開始:

val start = Pattern.begin(‘start’)

③ 模式組(Group of Pattern) 將一個模式序列作爲條件嵌套在個體模式裏,成爲一組模式。

2.1 個體模式

個體模式包括單例模式和循環模式。單例模式只接收一個事件,而循環模式可以接收多個事件。

(1)量詞 可以在一個個體模式後追加量詞,也就是指定循環次數。

// 匹配出現4次
start.time(4)
// 匹配出現0次或4次
start.time(4).optional
// 匹配出現2、3或4次
start.time(2,4)
// 匹配出現2、3或4次,並且儘可能多地重複匹配
start.time(2,4).greedy
// 匹配出現1次或多次
start.oneOrMore
// 匹配出現0、2或多次,並且儘可能多地重複匹配
start.timesOrMore(2).optional.greedy

(2)條件 每個模式都需要指定觸發條件,作爲模式是否接受事件進入的判斷依據。CEP中的個體模式主要通過調用.where()、.or()和.until()來指定條件。按不同的調用方式,可以分成以下幾類: ① 簡單條件 通過.where()方法對事件中的字段進行判斷篩選,決定是否接收該事件

start.where(event=>event.getName.startsWith(“foo”))

② 組合條件 將簡單的條件進行合併;or()方法表示或邏輯相連,where的直接組合就相當於與and。

Pattern.where(event => …/*some condition*/).or(event => /*or condition*/)

③ 終止條件 如果使用了oneOrMore或者oneOrMore.optional,建議使用.until()作爲終止條件,以便清理狀態。 ④ 迭代條件 能夠對模式之前所有接收的事件進行處理;調用.where((value,ctx) => {…}),可以調用ctx.getEventForPattern(“name”)

2.2 模式序列

不同的近鄰模式如下圖: 在這裏插入圖片描述

(1)嚴格近鄰 所有事件按照嚴格的順序出現,中間沒有任何不匹配的事件,由.next()指定。例如對於模式“a next b”,事件序列“a,c,b1,b2”沒有匹配。 (2)寬鬆近鄰 允許中間出現不匹配的事件,由.followedBy()指定。例如對於模式“a followedBy b”,事件序列“a,c,b1,b2”匹配爲{a,b1}。 (3)非確定性寬鬆近鄰 進一步放寬條件,之前已經匹配過的事件也可以再次使用,由.followedByAny()指定。例如對於模式“a followedByAny b”,事件序列“a,c,b1,b2”匹配爲{ab1},{a,b2}。 除了以上模式序列外,還可以定義“不希望出現某種近鄰關係”: .notNext():不想讓某個事件嚴格緊鄰前一個事件發生。 .notFollowedBy():不想讓某個事件在兩個事件之間發生。 需要注意

①所有模式序列必須以.begin()開始;

②模式序列不能以.notFollowedBy()結束;

③“not”類型的模式不能被optional所修飾;

④可以爲模式指定時間約束,用來要求在多長時間內匹配有效。

next.within(Time.seconds(10))

2.3 模式的檢測

指定要查找的模式序列後,就可以將其應用於輸入流以檢測潛在匹配。調用CEP.pattern(),給定輸入流和模式,就能得到一個PatternStream。

val input:DataStream[Event] = …
val pattern:Pattern[Event,_] = …
val patternStream:PatternStream[Event]=CEP.pattern(input,pattern)

2.4 匹配事件的提取

創建PatternStream之後,就可以應用select或者flatSelect方法,從檢測到的事件序列中提取事件了。 select()方法需要輸入一個select function作爲參數,每個成功匹配的事件序列都會調用它。 select()以一個Map[String,Iterable[IN]]來接收匹配到的事件序列,其中key就是每個模式的名稱,而value就是所有接收到的事件的Iterable類型。

def selectFn(pattern : Map[String,Iterable[IN]]):OUT={
  val startEvent = pattern.get(“start”).get.next
  val endEvent = pattern.get(“end”).get.next
  OUT(startEvent, endEvent)
}

flatSelect通過實現PatternFlatSelectFunction實現與select相似的功能。唯一的區別就是flatSelect方法可以返回多條記錄,它通過一個Collector[OUT]類型的參數來將要輸出的數據傳遞到下游。

process

select

2.5超時事件的提取

當一個模式通過within關鍵字定義了檢測窗口時間時,部分事件序列可能因爲超過窗口長度而被丟棄;爲了能夠處理這些超時的部分匹配,select和flatSelect API調用允許指定超時處理程序。

Flink CEP 開發流程:

  1. DataSource 中的數據轉換爲 DataStream;
  2. 定義 Pattern,並將 DataStream 和 Pattern 組合轉換爲 PatternStream;
  3. PatternStream 經過 select、process 等算子轉換爲 DataStraem;
  4. 再次轉換的 DataStream 經過處理後,sink 到目標庫。  

select方法:

SingleOutputStreamOperator<PayEvent> result = patternStream.select(orderTimeoutOutput, new PatternTimeoutFunction<PayEvent, PayEvent>() {
    @Override
    public PayEvent timeout(Map<String, List<PayEvent>> map, long l) throws Exception {
        return map.get("begin").get(0);
    }
}, new PatternSelectFunction<PayEvent, PayEvent>() {
    @Override
    public PayEvent select(Map<String, List<PayEvent>> map) throws Exception {
        return map.get("pay").get(0);
    }
});

對檢測到的模式序列應用選擇函數。對於每個模式序列,調用提供的{@link PatternSelectFunction}。模式選擇函數只能產生一個結果元素。

對超時的部分模式序列應用超時函數。對於每個部分模式序列,調用提供的{@link PatternTimeoutFunction}。模式超時函數只能產生一個結果元素。

您可以在使用相同的{@link OutputTag}進行select操作的{@link SingleOutputStreamOperator}上獲得由{@link SingleOutputStreamOperator}生成的{@link SingleOutputStreamOperator}生成的超時數據流。

@param timedOutPartialMatchesTag 標識端輸出超時模式的@link OutputTag}

@param patternTimeoutFunction 爲超時的每個部分模式序列調用的模式超時函數。

@param patternSelectFunction 爲每個檢測到的模式序列調用的模式選擇函數。

@param <L> 產生的超時元素的類型

@param <R>結果元素的類型

return {@link DataStream},其中包含產生的元素和在邊輸出中產生的超時元素。

DataStream<PayEvent> sideOutput = result.getSideOutput(orderTimeoutOutput);

獲取{@link DataStream},該{@link DataStream}包含由操作發出到指定{@link OutputTag}的邊輸出的元素。

3、NFA:非確定有限自動機

FlinkCEP在運行時會將用戶的邏輯轉化成這樣的一個NFA Graph (nfa對象)

所以有限狀態機的工作過程,就是從開始狀態,根據不同的輸入,自動進行狀態轉換的過程。

img

上圖中的狀態機的功能,是檢測二進制數是否含有偶數個 0。從圖上可以看出,輸入只有 1 和 0 兩種。從 S1 狀態開始,只有輸入 0 纔會轉換到 S2 狀態,同樣 S2 狀態下只有輸入 0 纔會轉換到 S1。所以,二進制數輸入完畢,如果滿足最終狀態,也就是最後停在 S1 狀態,那麼輸入的二進制數就含有偶數個 0。

4、案例

Flink CEP 開發流程:

  1. DataSource 中的數據轉換爲 DataStream;watermark、keyby
  2. 定義 Pattern,並將 DataStream 和 Pattern 組合轉換爲 PatternStream;
  3. PatternStream 經過 select、process 等算子轉換爲 DataStream;
  4. 再次轉換的 DataStream 經過處理後,sink 到目標庫。

案例1:惡意登錄檢測

需求:找出5秒內,連續登錄失敗的賬號

思路:

1、數據源

​ new CEPLoginBean(1L, "fail", 1597905234000L), ​ new CEPLoginBean(1L, "success", 1597905235000L), ​ new CEPLoginBean(2L, "fail", 1597905236000L), ​ new CEPLoginBean(2L, "fail", 1597905237000L), ​ new CEPLoginBean(2L, "fail", 1597905238000L), ​ new CEPLoginBean(3L, "fail", 1597905239000L), ​ new CEPLoginBean(3L, "success", 1597905240000L)

2、在數據源上做出watermark

3、在watermark上根據id分組keyby

4、做出模式pattern

 Pattern<CEPLoginBean, CEPLoginBean> pattern = Pattern.<CEPLoginBean>begin("start").where(new IterativeCondition<CEPLoginBean>() {
            @Override
            public boolean filter(CEPLoginBean value, Context<CEPLoginBean> ctx) throws Exception {
                return value.getLogresult().equals("fail");
            }
        })
                .next("next").where(new IterativeCondition<CEPLoginBean>() {
                    @Override
                    public boolean filter(CEPLoginBean value, Context<CEPLoginBean> ctx) throws Exception {
                        return value.getLogresult().equals("fail");
                    }
                })
                .within(Time.seconds(5));

5、在數據流上進行模式匹配

6、提取匹配成功的數據

代碼:

依賴:

 <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-cep_2.12</artifactId>
            <version>1.11.1</version>
        </dependency>

package com.lagou.bak;

import org.apache.flink.api.common.eventtime.*;
import org.apache.flink.cep.CEP;
import org.apache.flink.cep.PatternStream;
import org.apache.flink.cep.functions.PatternProcessFunction;
import org.apache.flink.cep.pattern.Pattern;
import org.apache.flink.cep.pattern.conditions.IterativeCondition;
import org.apache.flink.streaming.api.TimeCharacteristic;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
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.windowing.time.Time;
import org.apache.flink.util.Collector;

import java.util.List;
import java.util.Map;

public class CEPLoginTest {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
        env.setParallelism(1);
        DataStreamSource<CEPLoginBean> data = env.fromElements(
                new CEPLoginBean(1L, "fail", 1597905234000L),
                new CEPLoginBean(1L, "success", 1597905235000L),
                new CEPLoginBean(2L, "fail", 1597905236000L),
                new CEPLoginBean(2L, "fail", 1597905237000L),
                new CEPLoginBean(2L, "fail", 1597905238000L),
                new CEPLoginBean(3L, "fail", 1597905239000L),
                new CEPLoginBean(3L, "success", 1597905240000L)
        );

        SingleOutputStreamOperator<CEPLoginBean> watermarks = data.assignTimestampsAndWatermarks(new WatermarkStrategy<CEPLoginBean>() {
            @Override
            public WatermarkGenerator<CEPLoginBean> createWatermarkGenerator(WatermarkGeneratorSupplier.Context context) {
                return new WatermarkGenerator<CEPLoginBean>() {
                    long maxTimeStamp = Long.MIN_VALUE;

                    @Override
                    public void onEvent(CEPLoginBean event, long eventTimestamp, WatermarkOutput output) {
                        maxTimeStamp = Math.max(maxTimeStamp, event.getTs());
                    }

                    long maxOutOfOrderness = 500L;

                    @Override
                    public void onPeriodicEmit(WatermarkOutput output) {
                        output.emitWatermark(new Watermark(maxTimeStamp - maxOutOfOrderness));
                    }
                };
            }
        }.withTimestampAssigner(((element, recordTimestamp) -> element.getTs())));

        KeyedStream<CEPLoginBean, Long> keyed = watermarks.keyBy(value -> value.getId());

        Pattern<CEPLoginBean, CEPLoginBean> pattern = Pattern.<CEPLoginBean>begin("start").where(new IterativeCondition<CEPLoginBean>() {
            @Override
            public boolean filter(CEPLoginBean value, Context<CEPLoginBean> ctx) throws Exception {
                return value.getLogresult().equals("fail");
            }
        })
                .next("next").where(new IterativeCondition<CEPLoginBean>() {
                    @Override
                    public boolean filter(CEPLoginBean value, Context<CEPLoginBean> ctx) throws Exception {
                        return value.getLogresult().equals("fail");
                    }
                })
                .within(Time.seconds(5));

        PatternStream<CEPLoginBean> patternStream = CEP.pattern(keyed, pattern);
        SingleOutputStreamOperator<String> process = patternStream.process(new PatternProcessFunction<CEPLoginBean, String>() {
            @Override
            public void processMatch(Map<String, List<CEPLoginBean>> match, Context ctx, Collector<String> out) throws Exception {
                System.out.println(match);
                List<CEPLoginBean> start = match.get("start");
                List<CEPLoginBean> next = match.get("next");
                String res = "start:" + start + "...next:" + next;
                out.collect(res + start.get(0).getId());
            }
        });

        process.print();

        env.execute();
    }
}



案例2:檢測交易活躍用戶

需求:找出24小時內,至少5次有效交易的用戶:

思路:

1、數據源:

				new ActiveUserBean("100XX", 0.0D, 1597905234000L),
                new ActiveUserBean("100XX", 100.0D, 1597905235000L),
                new ActiveUserBean("100XX", 200.0D, 1597905236000L),
                new ActiveUserBean("100XX", 300.0D, 1597905237000L),
                new ActiveUserBean("100XX", 400.0D, 1597905238000L),
                new ActiveUserBean("100XX", 500.0D, 1597905239000L),
                new ActiveUserBean("101XX", 0.0D, 1597905240000L),
                new ActiveUserBean("101XX", 100.0D, 1597905241000L)

2、watermark轉化

3、keyby轉化

4、做出pattern

至少5次:timesOrMore(5)

24小時之內:within(Time.hours(24))

 Pattern<ActiveUserBean, ActiveUserBean> pattern = Pattern.<ActiveUserBean>begin("start").where(new SimpleCondition<ActiveUserBean>() {
            @Override
            public boolean filter(ActiveUserBean value) throws Exception {
                return value.getMoney() > 0;
            }
        }).timesOrMore(5).within(Time.hours(24));;

5、模式匹配

6、提取匹配成功數據

代碼:

package com.lagou.bak;

import org.apache.flink.api.common.eventtime.*;
import org.apache.flink.cep.CEP;
import org.apache.flink.cep.PatternStream;
import org.apache.flink.cep.functions.PatternProcessFunction;
import org.apache.flink.cep.pattern.Pattern;
import org.apache.flink.cep.pattern.conditions.IterativeCondition;
import org.apache.flink.cep.pattern.conditions.SimpleCondition;
import org.apache.flink.streaming.api.TimeCharacteristic;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
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.util.Collector;

import java.util.List;
import java.util.Map;

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

        DataStreamSource<ActiveUserBean> data = env.fromElements(
                new ActiveUserBean("100XX", 0.0D, 1597905234000L),
                new ActiveUserBean("100XX", 100.0D, 1597905235000L),
                new ActiveUserBean("100XX", 200.0D, 1597905236000L),
                new ActiveUserBean("100XX", 300.0D, 1597905237000L),
                new ActiveUserBean("100XX", 400.0D, 1597905238000L),
                new ActiveUserBean("100XX", 500.0D, 1597905239000L),
                new ActiveUserBean("101XX", 0.0D, 1597905240000L),
                new ActiveUserBean("101XX", 100.0D, 1597905241000L)
        );
        SingleOutputStreamOperator<ActiveUserBean> watermark = data.assignTimestampsAndWatermarks(new WatermarkStrategy<ActiveUserBean>() {
            @Override
            public WatermarkGenerator<ActiveUserBean> createWatermarkGenerator(WatermarkGeneratorSupplier.Context context) {
                return new WatermarkGenerator<ActiveUserBean>() {
                    long maxTimeStamp = Long.MIN_VALUE;

                    @Override
                    public void onEvent(ActiveUserBean event, long eventTimestamp, WatermarkOutput output) {
                        maxTimeStamp = Math.max(maxTimeStamp, event.getTs());
                    }

                    long maxOutOfOrderness = 500l;

                    @Override
                    public void onPeriodicEmit(WatermarkOutput output) {
                        output.emitWatermark(new Watermark(maxTimeStamp - maxOutOfOrderness));
                    }
                };
            }
        }.withTimestampAssigner(((element, recordTimestamp) -> element.getTs())));
        KeyedStream<ActiveUserBean, String> keyed = watermark.keyBy(value -> value.getUid());
        Pattern<ActiveUserBean, ActiveUserBean> pattern = Pattern.<ActiveUserBean>begin("start").where(new SimpleCondition<ActiveUserBean>() {
            @Override
            public boolean filter(ActiveUserBean value) throws Exception {
                return value.getMoney() > 0;
            }
        }).timesOrMore(5).within(Time.hours(24));

        PatternStream<ActiveUserBean> patternStream = CEP.pattern(keyed, pattern);
        SingleOutputStreamOperator<ActiveUserBean> process = patternStream.process(new PatternProcessFunction<ActiveUserBean, ActiveUserBean>() {
            @Override
            public void processMatch(Map<String, List<ActiveUserBean>> match, Context ctx, Collector<ActiveUserBean> out) throws Exception {
                System.out.println(match);
            }
        });
        process.print();
        env.execute();

    }
}

案例3:超時未支付

需求:找出下單後10分鐘沒有支付的訂單

思路:

1、數據源:

				new PayEvent(1L, "create", 1597905234000L),
                new PayEvent(1L, "pay", 1597905235000L),
                new PayEvent(2L, "create", 1597905236000L),
                new PayEvent(2L, "pay", 1597905237000L),
                new PayEvent(3L, "create", 1597905239000L)

2、轉化watermark

3、keyby轉化

4、做出Pattern(下單以後10分鐘內未支付)

注意:下單爲create 支付爲pay ,create和pay之間不需要是嚴格臨近,所以選擇followedBy

Pattern<PayEvent, PayEvent> pattern = Pattern.<PayEvent>
                begin("begin")
                .where(new IterativeCondition<PayEvent>() {
                    @Override
                    public boolean filter(PayEvent payEvent, Context context) throws Exception {
                        return payEvent.getName().equals("create");
                    }
                })
                .followedBy("pay")
                .where(new IterativeCondition<PayEvent>() {
                    @Override
                    public boolean filter(PayEvent payEvent, Context context) throws Exception {
                        return payEvent.getName().equals("pay");
                    }
                })
                .within(Time.seconds(600));

5、模式匹配

6、取出匹配成功的數據

(1)採用測輸出的方式

OutputTag<PayEvent> orderTimeoutOutput = new OutputTag<PayEvent>("orderTimeout") {};

(2)採用select方法

SingleOutputStreamOperator<PayEvent> result = patternStream.select(orderTimeoutOutput, new PatternTimeoutFunction<PayEvent, PayEvent>() {
            @Override
            public PayEvent timeout(Map<String, List<PayEvent>> map, long l) throws Exception {
                return map.get("begin").get(0);
            }
        }, new PatternSelectFunction<PayEvent, PayEvent>() {
            @Override
            public PayEvent select(Map<String, List<PayEvent>> map) throws Exception {
                return map.get("pay").get(0);
            }
        });

        //result.print();
        DataStream<PayEvent> sideOutput = result.getSideOutput(orderTimeoutOutput);
        sideOutput.print();

代碼:

package com.lagou.bak;

import org.apache.flink.api.common.eventtime.*;
import org.apache.flink.api.java.functions.KeySelector;
import org.apache.flink.cep.CEP;
import org.apache.flink.cep.PatternSelectFunction;
import org.apache.flink.cep.PatternStream;
import org.apache.flink.cep.PatternTimeoutFunction;
import org.apache.flink.cep.pattern.Pattern;
import org.apache.flink.cep.pattern.conditions.IterativeCondition;
import org.apache.flink.streaming.api.TimeCharacteristic;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.timestamps.BoundedOutOfOrdernessTimestampExtractor;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.util.OutputTag;

import java.util.List;
import java.util.Map;

public class TimeOutPayCEPMain {
    public static void main(String[] args) throws Exception {

        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);

        DataStream<PayEvent> source = env.fromElements(
                new PayEvent(1L, "create", 1597905234000L),
                new PayEvent(1L, "pay", 1597905235000L),
                new PayEvent(2L, "create", 1597905236000L),
                new PayEvent(2L, "pay", 1597905237000L),
                new PayEvent(3L, "create", 1597905239000L)

        )
//                .assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor<PayEvent>(Time.milliseconds(500L)) {
//            @Override
//            public long extractTimestamp(PayEvent payEvent) {
//                return payEvent.getTs();
//            }
//        })

                .assignTimestampsAndWatermarks(new WatermarkStrategy<PayEvent>() {
                    @Override
                    public WatermarkGenerator<PayEvent> createWatermarkGenerator(WatermarkGeneratorSupplier.Context context) {
                        return new WatermarkGenerator<PayEvent>() {
                            long maxTimestamp = Long.MIN_VALUE;

                            @Override
                            public void onEvent(PayEvent event, long eventTimestamp, WatermarkOutput output) {
                                maxTimestamp = Math.max(maxTimestamp, event.getTs());
                            }

                            @Override
                            public void onPeriodicEmit(WatermarkOutput output) {
                                long maxOutofOrderness = 500l;
                                output.emitWatermark(new Watermark(maxTimestamp - maxOutofOrderness));
                            }
                        };
                    }
                }.withTimestampAssigner(((element, recordTimestamp) -> element.getTs())))

                /*.keyBy(new KeySelector<PayEvent, Object>() {
                    @Override
                    public Object getKey(PayEvent value) throws Exception {
                        return value.getId();
                    }
                }*/
                .keyBy(value -> value.getId()
                );

        // 邏輯處理代碼
        OutputTag<PayEvent> orderTimeoutOutput = new OutputTag<PayEvent>("orderTimeout") {
        };
        Pattern<PayEvent, PayEvent> pattern = Pattern.<PayEvent>
                begin("begin")
                .where(new IterativeCondition<PayEvent>() {
                    @Override
                    public boolean filter(PayEvent payEvent, Context context) throws Exception {
                        return payEvent.getName().equals("create");
                    }
                })
                .followedBy("pay")
                .where(new IterativeCondition<PayEvent>() {
                    @Override
                    public boolean filter(PayEvent payEvent, Context context) throws Exception {
                        return payEvent.getName().equals("pay");
                    }
                })
                .within(Time.seconds(600));

        PatternStream<PayEvent> patternStream = CEP.pattern(source, pattern);
        SingleOutputStreamOperator<PayEvent> result = patternStream.select(orderTimeoutOutput, new PatternTimeoutFunction<PayEvent, PayEvent>() {
            @Override
            public PayEvent timeout(Map<String, List<PayEvent>> map, long l) throws Exception {
                return map.get("begin").get(0);
            }
        }, new PatternSelectFunction<PayEvent, PayEvent>() {
            @Override
            public PayEvent select(Map<String, List<PayEvent>> map) throws Exception {
                return map.get("pay").get(0);
            }
        });

        //result.print();
        DataStream<PayEvent> sideOutput = result.getSideOutput(orderTimeoutOutput);
        sideOutput.print();


        env.execute("execute cep");
    }

}

十二部分 FlinkSQL

1、什麼是 Table API 和 Flink SQL

Flink 本身是批流統一的處理框架,所以 Table API 和 SQL,就是批流統一的上層處理 API。

Table API 是一套內嵌在 Java 和 Scala 語言中的查詢 API,它允許我們以非常直觀的方式,

組合來自一些關係運算符的查詢(比如 select、filter 和 join)。而對於 Flink SQL,就是直接可

以在代碼中寫 SQL,來實現一些查詢(Query)操作。Flink 的 SQL 支持,基於實現了 SQL 標

準的 Apache Calcite(Apache 開源 SQL 解析工具)。

無論輸入是批輸入還是流式輸入,在這兩套 API 中,指定的查詢都具有相同的語義,得到相同的結果

2、入門代碼:

依賴:

 		<dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-table</artifactId>
            <version>1.11.1</version>
            <type>pom</type>
            <scope>provided</scope>
        </dependency>

        <!-- Either... -->
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-table-api-java-bridge_2.12</artifactId>
            <version>1.11.1</version>
            <scope>provided</scope>
        </dependency>
        <!-- or... -->
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-table-api-scala-bridge_2.12</artifactId>
            <version>1.11.1</version>
            <scope>provided</scope>
        </dependency>


        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-table-planner-blink_2.12</artifactId>
            <version>1.11.1</version>
            <scope>provided</scope>
        </dependency>

依賴說明:

flink-table-api-java-bridge_2.1:橋接器,主要負責 table API 和 DataStream/DataSet API

的連接支持,按照語言分 java 和 scala。

flink-table-planner-blink_2.12:計劃器,是 table API 最主要的部分,提供了運行時環境和生

成程序執行計劃的 planner;

如果是生產環境,lib 目錄下默認已 經有了 planner,就只需要有 bridge 就可以了

flink-table:flinktable的基礎依賴

代碼:

1、Flink執行環境env

2、用env,做出Table環境tEnv

StreamTableEnvironment tEnv = StreamTableEnvironment.create(env);

基於 blink 版本的流處理環境(Blink-Streaming-Query)或者,基於 blink 版本的批處理環境(Blink-Batch-Query):

		EnvironmentSettings settings = EnvironmentSettings.newInstance()
                .useBlinkPlanner()
//                .inBatchMode()
                .inStreamingMode()
                .build();

3、獲取流式數據源

DataStreamSource<Tuple2<String, Integer>> data = env.addSource(new SourceFunction<Tuple2<String, Integer>>() {
            @Override
            public void run(SourceContext<Tuple2<String, Integer>> ctx) throws Exception {
                while (true) {
                    ctx.collect(new Tuple2<>("name", 10));
                    Thread.sleep(1000);
                }
            }

            @Override
            public void cancel() {

            }
        });

4、將流式數據源做成Table

(1)table方式:

Table table = tEnv.fromDataStream(data, $("name"), $("age"));

(2)sql方式:

		tEnv.createTemporaryView("userss",data, $("name"), $("age"));
        String s = "select name from userss";
        Table table = tEnv.sqlQuery(s);

5、對Table中的數據做查詢

(1)table方式:

Table name = table.select($("name"));

(2)sql方式:

tEnv.createTemporaryView("userss",data, $("name"), $("age"));
        String s = "select name from userss";
        Table table = tEnv.sqlQuery(s);

6、將Table轉成數據流:

DataStream<Tuple2<Boolean, Row>> result = tEnv.toRetractStream(name, Row.class);
package com.lagou.table;

import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.source.SourceFunction;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.TableResult;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import org.apache.flink.types.Row;
import org.apache.flink.util.CloseableIterator;

import static org.apache.flink.table.api.Expressions.$;

public class TableApiDemo {
    public static void main(String[] args) throws Exception {
        //Flink執行環境env
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        //用env,做出Table環境tEnv
        StreamTableEnvironment tEnv = StreamTableEnvironment.create(env);
        //獲取流式數據源
        DataStreamSource<Tuple2<String, Integer>> data = env.addSource(new SourceFunction<Tuple2<String, Integer>>() {
            @Override
            public void run(SourceContext<Tuple2<String, Integer>> ctx) throws Exception {
                while (true) {
                    ctx.collect(new Tuple2<>("name", 10));
                    Thread.sleep(1000);
                }
            }

            @Override
            public void cancel() {

            }
        });
        //Table方式
        //將流式數據源做成Table
        Table table = tEnv.fromDataStream(data, $("name"), $("age"));
        
        //對Table中的數據做查詢
        Table name = table.select($("name"));
        //將處理結果輸出到控制檯
        DataStream<Tuple2<Boolean, Row>> result = tEnv.toRetractStream(name, Row.class);

        //SQL方式:
        /*tEnv.createTemporaryView("userss",data, $("name"), $("age"));
        String s = "select name from userss";
        Table table = tEnv.sqlQuery(s);
        DataStream<Tuple2<Boolean, Row>> result = tEnv.toRetractStream(table, Row.class);*/

        result.print();
        env.execute();

    }
}

3、外部鏈接

Connectors

Name Version Maven dependency SQL Client JAR
Filesystem Built-in Built-in
Elasticsearch 6 flink-connector-elasticsearch6 Download
Elasticsearch 7 flink-connector-elasticsearch7 Download
Apache Kafka 0.10 flink-connector-kafka-0.10 Download
Apache Kafka 0.11 flink-connector-kafka-0.11 Download
Apache Kafka 0.11+ (universal) flink-connector-kafka Download
Apache HBase 1.4.3 flink-connector-hbase Download
JDBC flink-connector-jdbc Download

Formats

Name Maven dependency SQL Client JAR
Old CSV (for files) Built-in Built-in
CSV (for Kafka) flink-csv Built-in
JSON flink-json Built-in
Apache Avro flink-avro Download

1. 數據查詢語言DQL 數據查詢語言DQL基本結構是由SELECT子句,FROM子句,WHERE 子句組成的查詢塊: SELECT <字段名錶> FROM <表或視圖名> WHERE <查詢條件>

2 .數據操縱語言DML 數據操縱語言DML主要有三種形式:

  1. 插入:INSERT
  2. 更新:UPDATE
  3. 刪除:DELETE

3. 數據定義語言DDL 數據定義語言DDL用來創建數據庫中的各種對象-----表、視圖、 索引、同義詞、聚簇等如: CREATE TABLE/VIEW/INDEX/SYN/CLUSTER 表 視圖 索引 同義詞 簇

DDL操作是隱性提交的!不能rollback

4. 數據控制語言DCL 數據控制語言DCL用來授予或回收訪問數據庫的某種特權,並控制 數據庫操縱事務發生的時間及效果,對數據庫實行監視等。如:

連接外部系統在 Catalog 中註冊表,直接調用 tableEnv.connect()就可以,裏面參數要傳

入一個 ConnectorDescriptor,也就是 connector 描述器。對於文件系統的 connector 而言,

flink 內部已經提供了,就叫做 FileSystem()。

<dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-csv</artifactId>
            <version>1.11.1</version>
        </dependency>
        tEnv.connect(new FileSystem().path("sensor.txt"))// 定義表數據來源,外部連接
                .withFormat(new Csv()) // 定義從外部系統讀取數據之後的格式化方法
                .withSchema(new Schema()
                        .field("id", DataTypes.STRING())
                        .field("timestamp", DataTypes.BIGINT())
                        .field("temperature", DataTypes.DOUBLE())) // 定義表結構
                .createTemporaryTable("inputTable"); // 創建臨時表

連接Kafka:

ConnectTableDescriptor descriptor = tEnv.connect(
                // declare the external system to connect to
                new Kafka()
                        .version("universal")
                        .topic("animal")
                        .startFromEarliest()
                        .property("bootstrap.servers", "hdp-2:9092")
        )

                // declare a format for this system
                .withFormat(
//                        new Json()
                        new Csv()
                )

                // declare the schema of the table
                .withSchema(
                        new Schema()
//                                .field("rowtime", DataTypes.TIMESTAMP(3))
//                                .rowtime(new Rowtime()
//                                        .timestampsFromField("timestamp")
//                                        .watermarksPeriodicBounded(60000)
//                                )
//                                .field("user", DataTypes.BIGINT())
                                .field("message", DataTypes.STRING())
                );
        // create a table with given name
        descriptor.createTemporaryTable("MyUserTable");

        Table table1 = tEnv.sqlQuery("select * from MyUserTable");
        DataStream<Tuple2<Boolean, Row>> tuple2DataStream = tEnv.toRetractStream(table1, Row.class);
        tuple2DataStream.print();

4、查詢數據

4.1 Table API

官網:https://ci.apache.org/projects/flink/flink-docs-release-1.11/dev/table/tableApi.html

select/filter/as

Table filtered = table.select($("name"), $("age")).filter($("age").mod(2).isEqual(0));
        //將處理結果輸出到控制檯
        DataStream<Tuple2<Boolean, Row>> result = tEnv.toRetractStream(filtered, Row.class);
 Table mingzi = table.select($("name").as("mingzi"));
        DataStream<Tuple2<Boolean, Row>> result = tEnv.toRetractStream(mingzi, Row.class);

4.2 SQL

 tEnv.createTemporaryView("userss",data, $("name"), $("age"));
        String s = "select name,age from userss where mod(age,2)=0";
        Table table = tEnv.sqlQuery(s);
        DataStream<Tuple2<Boolean, Row>> result = tEnv.toRetractStream(table, Row.class);

5、輸出表

5.1 輸出到文件:

代碼:

tEnv.connect(new FileSystem().path("D:\\data\\out.txt"))
                .withFormat(new Csv())
                .withSchema(new Schema().field("name", DataTypes.STRING()).field("age",DataTypes.INT()))
                .createTemporaryTable("outputTable");
        filtered.executeInsert("outputTable");

hive支持的輸出到orc

package com.lagou.bak;

import org.apache.flink.core.fs.Path;
import org.apache.flink.orc.OrcSplitReaderUtil;
import org.apache.flink.orc.vector.RowDataVectorizer;
import org.apache.flink.orc.writer.OrcBulkWriterFactory;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.sink.filesystem.StreamingFileSink;
import org.apache.flink.streaming.api.functions.source.SourceFunction;
import org.apache.flink.table.data.GenericRowData;
import org.apache.flink.table.data.RowData;
import org.apache.flink.table.types.logical.*;
import org.apache.hadoop.conf.Configuration;
import org.apache.orc.TypeDescription;

import java.util.Properties;

public class StreamingWriteFileOrc {
    public static void main(String[] args) throws Exception{
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.enableCheckpointing(10000);
        env.setParallelism(1);
        DataStream<RowData> dataStream = env.addSource(
                new MySource());

        //寫入orc格式的屬性
        final Properties writerProps = new Properties();
        writerProps.setProperty("orc.compress", "LZ4");

        //定義類型和字段名
        LogicalType[] orcTypes = new LogicalType[]{
                new IntType(), new DoubleType(), new VarCharType()};
        String[] fields = new String[]{"a1", "b2", "c3"};
        TypeDescription typeDescription = OrcSplitReaderUtil.logicalTypeToOrcType(RowType.of(
                orcTypes,
                fields));

        //構造工廠類OrcBulkWriterFactory
        final OrcBulkWriterFactory<RowData> factory = new OrcBulkWriterFactory<RowData>(
                new RowDataVectorizer(typeDescription.toString(), orcTypes),
                writerProps,
                new Configuration());

        StreamingFileSink orcSink = StreamingFileSink
                .forBulkFormat(new Path("d:\\data\\out"), factory)//  file:///tmp/aaaa
                .build();

        dataStream.addSink(orcSink);

        env.execute();
    }

    public static class MySource implements SourceFunction<RowData> {
        @Override
        public void run(SourceContext<RowData> sourceContext) throws Exception{
            while (true){
                GenericRowData rowData = new GenericRowData(3);
                rowData.setField(0, (int) (Math.random() * 100));
                rowData.setField(1, Math.random() * 100);
                rowData.setField(2, org.apache.flink.table.data.StringData.fromString(String.valueOf(Math.random() * 100)));
                sourceContext.collect(rowData);
                Thread.sleep(10);
            }
        }

        @Override
        public void cancel(){

        }
    }
}

5.2 輸出到Kafka

定義

//往kafka上輸出表
        DataStreamSource<String> data = env.addSource(new SourceFunction<String>() {
            @Override
            public void run(SourceContext<String> ctx) throws Exception {
                int num = 0;
                while (true) {
                    num++;
                    ctx.collect("name"+num);
                    Thread.sleep(1000);
                }
            }

            @Override
            public void cancel() {

            }
        });

        Table name = tEnv.fromDataStream(data, $("name"));

        ConnectTableDescriptor descriptor = tEnv.connect(
                // declare the external system to connect to
                new Kafka()
                        .version("universal")
                        .topic("animal")
                        .startFromEarliest()
                        .property("bootstrap.servers", "hdp-2:9092")
        )

                // declare a format for this system
                .withFormat(
//                        new Json()
                        new Csv()
                )

                // declare the schema of the table
                .withSchema(
                        new Schema()
//                                .field("rowtime", DataTypes.TIMESTAMP(3))
//                                .rowtime(new Rowtime()
//                                        .timestampsFromField("timestamp")
//                                        .watermarksPeriodicBounded(60000)
//                                )
//                                .field("user", DataTypes.BIGINT())
                                .field("message", DataTypes.STRING())
                );
        // create a table with given name
        descriptor.createTemporaryTable("MyUserTable");

        name.executeInsert("MyUserTable");

5.3 輸出到mysql (瞭解)

CREATE TABLE MyUserTable (
  ...
) WITH (
  'connector.type' = 'jdbc', -- required: specify this table type is jdbc
  
  'connector.url' = 'jdbc:mysql://localhost:3306/flink-test', -- required: JDBC DB url
  
  'connector.table' = 'jdbc_table_name',  -- required: jdbc table name

  -- optional: the class name of the JDBC driver to use to connect to this URL.
  -- If not set, it will automatically be derived from the URL.
  'connector.driver' = 'com.mysql.jdbc.Driver',

  -- optional: jdbc user name and password
  'connector.username' = 'name',
  'connector.password' = 'password',
  
  -- **followings are scan options, optional, used when reading from a table**

  -- optional: SQL query / prepared statement.
  -- If set, this will take precedence over the 'connector.table' setting
  'connector.read.query' = 'SELECT * FROM sometable',

  -- These options must all be specified if any of them is specified. In addition,
  -- partition.num must be specified. They describe how to partition the table when
  -- reading in parallel from multiple tasks. partition.column must be a numeric,
  -- date, or timestamp column from the table in question. Notice that lowerBound and
  -- upperBound are just used to decide the partition stride, not for filtering the
  -- rows in table. So all rows in the table will be partitioned and returned.

  'connector.read.partition.column' = 'column_name', -- optional: the column name used for partitioning the input.
  'connector.read.partition.num' = '50', -- optional: the number of partitions.
  'connector.read.partition.lower-bound' = '500', -- optional: the smallest value of the first partition.
  'connector.read.partition.upper-bound' = '1000', -- optional: the largest value of the last partition.

  -- optional, Gives the reader a hint as to the number of rows that should be fetched
  -- from the database when reading per round trip. If the value specified is zero, then
  -- the hint is ignored. The default value is zero.
  'connector.read.fetch-size' = '100',

  -- **followings are lookup options, optional, used in temporary join**

  -- optional, max number of rows of lookup cache, over this value, the oldest rows will
  -- be eliminated. "cache.max-rows" and "cache.ttl" options must all be specified if any
  -- of them is specified. Cache is not enabled as default.
  'connector.lookup.cache.max-rows' = '5000',

  -- optional, the max time to live for each rows in lookup cache, over this time, the oldest rows
  -- will be expired. "cache.max-rows" and "cache.ttl" options must all be specified if any of
  -- them is specified. Cache is not enabled as default.
  'connector.lookup.cache.ttl' = '10s',

  'connector.lookup.max-retries' = '3', -- optional, max retry times if lookup database failed

  -- **followings are sink options, optional, used when writing into table**

  -- optional, flush max size (includes all append, upsert and delete records),
  -- over this number of records, will flush data. The default value is "5000".
  'connector.write.flush.max-rows' = '5000',

  -- optional, flush interval mills, over this time, asynchronous threads will flush data.
  -- The default value is "0s", which means no asynchronous flush thread will be scheduled.
  'connector.write.flush.interval' = '2s',

  -- optional, max retry times if writing records to database failed
  'connector.write.max-retries' = '3'
)

代碼:


第十三分 作業提交

Flink的jar文件並不是Flink集羣的可執行文件,需要經過轉換之後提交給集羣

轉換過程:

1、在Flink Client中,通過反射啓動jar中的main函數,生成Flink StreamGraph和JobGraph。將JobGraph提交給Flink集羣。

2、Flink集羣收到JobGraph後,將JobGraph翻譯成ExecutionGraph,然後開始調度執行,啓動成功之後開始消費數據

總結:

Flink的核心執行流程就是,把用戶的一系列API調用,轉化爲StreamGraph -- JobGraph -- ExecutionGraph -- 物理執行拓撲(Task DAG)

image-20201017174411777

Flink提交作業的核心過程圖

PipelineExecutor:流水線執行器:

是Flink Client生成JobGraph之後,將作業提交給集羣運行的重要環節

image-20201017174842456

Session模式:AbstractSessionClusterExecutor

Per-Job模式:AbstractJobClusterExecutor

IDE調試:LocalExecutor

Session模式:

作業提交通過: yarn-session.sh腳本

在啓動腳本的時候檢查是否已經存在已經啓動好的Flink-Session模式的集羣,

然後在PipelineExecutor中,通過Dispatcher提供的Rest接口提交Flink JobGraph

Dispatcher爲每一個作業提供一個JobMaser,進入到作業執行階段

Per-Job模式:一個作業一個集羣,作業之間相互隔離。

在PipelineExecutor執行作業提交的時候,可以創建集羣並將JobGraph以及所有需要的文件一起提交給Yarn集羣,在Yarn集羣的容器中啓動Flink Master(JobManager進程),進行初始化後,從文件系統中獲取JobGraph,交給Dispatcher,之後和Session流程相同。

流圖:

image-20201017191746914

img

<img src="Flink大數據講義.assets/1275415-20201012210627328-264459047.png" alt="img" style="zoom:150%;" />

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