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 官網 中把流分為兩類:有界流和無界流。
簡單來說,無界流有流的起點但是沒有終點,資料來源源不斷地到達,因此我們也需要持續地處理資料。而有界流是有流的結束點的,且資料可以在全部到達後進行處理,因此我們可以把有界流理解為批處理(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 中輸出的日誌:
可以看到,單詞的統計會隨著流資料的輸入而不斷增長。
每次更新囉裡囉嗦的話
我提供了本實驗的原始碼,請在我的 Github 獲取:ousheobin/flink-word-count 。
歡迎大家在上面折騰各種玩法,比如實現無界的資料來源,或者把你的 word count 結果輸出到 Kafka MQ。