Flink 入門篇之 寫個WordCount

SteveOu發表於2020-04-05

Word Count 是資料處理框架、平臺的Hello World。程式作用很簡單,就是數有多少英文的單詞。我們今天要使用 Flink 1.10 製作一個流版本的 Word Count。在這個流版本的 Word Count 中呢,字串會以流的方式從輸入到Flink中,並且我們會觀察在處理過程中,字元統計的情況。

狀態!狀態!

在 Flink 的官網中,我們可以看到 Flink 社群對於 Flink 的定義是 Apache Flink® — Stateful Computations over Data Streams, 一個在資料流之上的有狀態計算引擎。在正式開工之前,我們需要先對於狀態有一個很初步很初步的瞭解。 Flink 官網上對於 State 有非常多專業的解釋,我今天先初步在本文裡面先談談簡單的一些理解,感興趣的朋友呢可以去讀讀 Flink 官方的解釋。

在 Flink 中,任務是使用有向無環圖進行描述的,每個圖的節點實際上是一個操作函式或運算元(Operator),而這些函式和運算元在 Flink 處理單一的元素和事件時會儲存資料(官網使用的是 remember 記住),因此我們可以說 Flink 的運算元和函式是有狀態的(Stateful)。狀態在Flink中非常重要,我會在後面單獨談談 Flink 的狀態。

何為流

我們在學習程式設計的過程中,會接觸到各種各樣的流,檔案流,網路流,Java 的流 API 等等等等。在 Flink 的世界裡,資料可以形成事件流,日誌記錄,感測器資料,使用者的行為等等都可以形成資料流。Flink 官網 中把流分為兩類:有界流和無界流。

Flink 官網的解釋圖

簡單來說,無界流有流的起點但是沒有終點,資料來源源不斷地到達,因此我們也需要持續地處理資料。而有界流是有流的結束點的,且資料可以在全部到達後進行處理,因此我們可以把有界流理解為批處理(Batch)。與真正的批處理有所不同的是,Flink是有狀態的,每個狀態中的資料可能會隨著資料的不斷到達而發生變化。值得注意的是,由於無界流會源源不斷地到達,一般我們會使用時間的特徵作為無界流的處理順序。

搭建 Maven 專案

首先建立一個 Maven 專案,並新增以下關鍵的依賴:

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <flink.version>1.10.0</flink.version>
    <java.version>1.8</java.version>
    <scala.binary.version>2.11</scala.binary.version>
    <maven.compiler.source>${java.version}</maven.compiler.source>
    <maven.compiler.target>${java.version}</maven.compiler.target>
</properties>

<dependencies>
    <dependency>
        <groupId>org.apache.flink</groupId>
        <artifactId>flink-java</artifactId>
        <version>${flink.version}</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>org.apache.flink</groupId>
        <artifactId>flink-streaming-java_${scala.binary.version}</artifactId>
        <version>${flink.version}</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>1.7.15</version>
        <scope>compile</scope>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>1.7.7</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
        <scope>runtime</scope>
    </dependency>
</dependencies>
複製程式碼

編寫 Word Count Job

完成 Maven 後我們需要編寫 Word Count 的 任務程式碼。和普通的 Java 程式一樣,Flink 會找到並執行 main 方法中所定義的處理邏輯。首先我們需要在 main 方法的第一句就使用 getExecutionEnvironment() 方法獲得程式執行所需要的流環境

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
複製程式碼

然後我們需要向流處理的環境註冊一個資料來源,以源源不斷地讀入字串:

DataStream<String> source = env.addSource(new WordCountSource()).name("SteveJobs-Word");
複製程式碼

在獲取到資料來源後,我們需要對資料進行處理。首先是需要使用 flatMap 配合分詞工具對輸入的字串進行切分,得到多個二元組。這些二元組的第一個元素是所得的單詞,而第二個元素是 1 。為什麼是 1 呢,因為我們會根據單詞進行分組,每個單詞都會擁有一個獨立的 狀態 ,每個狀態都會儲存當前處理過的資料資訊。在這個樣例中,flink 會對每個單詞的狀態中所有元組的第二個元素進行求和操作。由於第二個元素都是1,可知其求和的結果等於單詞的個數。

DataStream<Tuple2<String,Integer>> proc = source.flatMap(new TextTokenizer())
                .keyBy(0)
                .sum(1)
                .name("Process-Word-Count");
複製程式碼

在 Flink 中,一切的資料都需要輸出到 Sink 中。Sink 可以輸出資料到資料庫,到 Elasticsearch 中,到 HDFS 中,或是到 Kafka MQ 中。因此,我們需要指定 Flink 的計算結果輸出位置。如在本樣例中,我們會把資料輸出到

proc.addSink(new WordCountSink())
    .name("Word-Count-Sink");
複製程式碼

Flink 的任務需要在本地打包,並上傳到伺服器載入後才可以在 Flink 叢集上執行。因此我們需要在 main 方法的末尾,指定任務的名稱,並呼叫環境的 execute 方法啟動任務。

env.execute("Word Count");
複製程式碼

最終,我們可以得到 WordCountJob 的寫法:

public class WordCountJob  {

    public static void main(String[] args) throws Exception {

        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        DataStream<String> source = env.addSource(new WordCountSource())
                .name("SteveJobs-Word");

        DataStream<Tuple2<String,Integer>> proc = source.flatMap(new TextTokenizer())
                .keyBy(0)
                .sum(1)
                .name("Process-Word-Count");

        proc.addSink(new WordCountSink())
                .name("Word-Count-Sink");

        env.execute("Word Count");
    }

}
複製程式碼

資料的 Source 和 Sink

在這個專案中,我們先做一個有界流的試驗。在 Flink 中,Source 和 Sink 是資料輸入和資料輸出的運算元,即資料通過 Source 運算元輸入, 通過 Sink 運算元輸出。根據剛剛所介紹的,有界流實質上是指批處理,所處理的資料是有限的。因此我們的輸入資料來源部分我們實現了簡單的遍歷器資料來源 (JavaDoc),直接上程式碼:

public class WordCountSource extends FromIteratorFunction<String> implements Serializable {

    private static final long serialVersionUID = 0L;

    public WordCountSource() {
        super(new WordIterator());
    }

    private static class WordIterator implements Iterator<String>,Serializable{

        private static final long serialVersionUID = 4L;

        private int index = 0;
        private int length = -1;

        private WordIterator(){
            length = StaticWordData.STEVE_JOBS_WORD.length;
        }

        @Override
        public boolean hasNext() {
            return (index < length);
        }

        @Override
        public String next() {
            return StaticWordData.STEVE_JOBS_WORD[index ++];
        }
    }

}
複製程式碼

Sink 作為輸出,我們參照了 Flink 官方 Code Walkthroughs 的寫法,直接輸出到日誌中。 具體寫法可以參考 Sink 的 JavaDoc ,以下是我的寫法:

public class WordCountSink implements SinkFunction<Tuple2<String,Integer>> {

    private static final long serialVersionUID = 1L;
    private static final Logger logger = LoggerFactory.getLogger(WordCountSink.class);

    @Override
    public void invoke(Tuple2<String, Integer> value, Context context) throws Exception {
        logger.info("{ Word: \""+ value.f0 + "\", Cnt:" + value.f1 +"}");
    }
}

複製程式碼

測試

在編寫好程式碼以後,使用 Maven 的指令進行打包:

$ mvn clean package
複製程式碼

我在之前的更新中介紹瞭如何搭建單機版 Flink , 傳送門 。 我們把單機版 Flink 啟動起來,然後選擇 Submit New Job ,點選 Add new 通過網頁版上傳剛剛打包好的包。

上傳好的效果圖

我們可以點選 Show Plan 檢視 我們在程式中定義的執行圖。

執行圖

點選 Submit,即可提交任務到 Flink 並執行。我們可以在 Task Manager 中,選擇並檢視當前執行任務的 Task Manager 的 Log。即可看到我們在 Sink 中輸出的日誌:

Log

可以看到,單詞的統計會隨著流資料的輸入而不斷增長。

每次更新囉裡囉嗦的話

我提供了本實驗的原始碼,請在我的 Github 獲取:ousheobin/flink-word-count

歡迎大家在上面折騰各種玩法,比如實現無界的資料來源,或者把你的 word count 結果輸出到 Kafka MQ。

相關文章