目的:
-
學習Flink的基本使用方法
-
掌握在一般使用中需要注意的事項
手把手的過程中會講解各種問題的定位方法,相對囉嗦,內容類似結對編程。
大家遇到什麼問題可以在評論中說一下,我來完善文檔
Flink專輯的各篇文章鏈接: 手把手開發Flink程序-基礎
現在我們繼續解決手把手開發Flink程序-DataSet中統計數字的問題,但是不再使用DataSet,而是使用DataStream。原來的需求是
-
生成若干隨機數字
-
統計奇數和偶數的個數
-
統計質數格式
-
統計每個數字出現的次數
步驟:
使用DataStream實現原有邏輯
初始化一個新的Job
讓我們在原來的NumStat的基礎上修改,所以直接複製NumStat爲NumStatStream。
原來我們拿到的數字集合env.generateSequence(0, size)返回類型爲DataSource<Long>,DataSource<Long>其實是DataSet。現在我們要使用DataStream了,所以需要拿到一個DataStream<Long>。其實只需要這樣修改
// 將ExecutionEnvironment替換爲StreamExecutionEnvironment
// final ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
之後修改所有出錯的位置
// numbers的類型變
// MapOperator<Long, Integer> numbers = ...;
SingleOutputStreamOperator<Integer> numbers = ...;
//
// 統計奇數偶數的比例
numbers.map(num -> num % 2)
.name("Calculate Mod value")
.map(mod -> new Tuple2<Integer, Integer>(mod, 1))
.name("Create mod result item")
.returns(TypeInformation.of(new TypeHint<Tuple2<Integer, Integer>>() {}))
//.groupBy(0) //被keyBy(0)替換了
.keyBy(0)
//.aggregate(Aggregations.SUM, 1) // 被sum(1)替換了
.sum(1)
.name("Sum Mod result")
.map(result -> NumStatResult.builder()
.group(group)
.type("mod")
.key(result.f0)
.value(result.f1)
.build())
.name("Convert to db result")
//.output(mysqlSink) // 被writeUsingOutputFormat替換了
.writeUsingOutputFormat(mysqlSink)
.name("Save data to mysql");
簡直不能在順利了,分分鐘就搞定了。修改後完整代碼
package org.myorg.quickstart;
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.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.myorg.quickstart.component.MysqlSink;
import org.myorg.quickstart.model.NumStatResult;
import java.util.Date;
public class NumStatStream {
public static void main(String[] args) throws Exception {
int size = 1000;
if (args != null && args.length >= 1) {
size = Integer.parseInt(args[0]);
}
statisticsNums(size);
}
private static void statisticsNums(int size) throws Exception {
// set up the execution environment
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
Date group = new Date();
MysqlSink mysqlSink = new MysqlSink();
SingleOutputStreamOperator<Integer> numbers = env.generateSequence(0, size)
.name("Generate index")
.map(num -> (int) (Math.random() * 100))
.name("Generate Numbers");
// 統計奇數偶數的比例
numbers.map(num -> num % 2)
.name("Calculate Mod value")
.map(mod -> new Tuple2<Integer, Integer>(mod, 1))
.name("Create mod result item")
.returns(TypeInformation.of(new TypeHint<Tuple2<Integer, Integer>>() {}))
.keyBy(0)
.sum(1)
.name("Sum Mod result")
.map(result -> NumStatResult.builder()
.group(group)
.type("mod")
.key(result.f0)
.value(result.f1)
.build())
.name("Convert to db result")
.writeUsingOutputFormat(mysqlSink)
.name("Save data to mysql");
// 統計每個數字出現的頻率
numbers.map(num -> new Tuple2<Integer, Integer>(num, 1))
.name("Create rate item")
.returns(TypeInformation.of(new TypeHint<Tuple2<Integer, Integer>>() {}))
.keyBy(0)
.sum(1)
.name("Sum rate result")
.map(result -> NumStatResult.builder()
.group(group)
.type("rate")
.key(result.f0)
.value(result.f1)
.build())
.name("Convert to db model")
.writeUsingOutputFormat(mysqlSink)
.name("Save to mysql");
// 統計質數個數
numbers.filter(num -> isPrime(num))
.map(num -> new Tuple2<Integer, Integer>(0, 1))
.name("Wrap to count item")
.returns(TypeInformation.of(new TypeHint<Tuple2<Integer, Integer>>() {}))
.keyBy(0)
.sum(1)
.name("Sum Prime count result")
.map(result -> NumStatResult.builder()
.group(group)
.type("count")
.key(0)
.value(result.f1)
.build())
.name("Convert to db item")
.writeUsingOutputFormat(mysqlSink)
.name("Save data to mysql");
env.execute();
}
private static boolean isPrime(int src) {
if (src < 2) {
return false;
}
if (src == 2 || src == 3) {
return true;
}
if ((src & 1) == 0) {// 先判斷是否爲偶數,若偶數就直接結束程序
return false;
}
double sqrt = Math.sqrt(src);
for (int i = 3; i <= sqrt; i += 2) {
if (src % i == 0) {
return false;
}
}
return true;
}
}
執行看效果
讓我們執行一下看看。執行也異常順利,沒有報任何錯。
讓我們再看看結果
select result_type, count(1) from result group by result_type
result_type | count(1) |
---|---|
count | 240 |
mod | 1001 |
rate | 1001 |
哇,結果也出來了。而且比以前的結果多很多,怎麼結果數量多了呢?是否本地執行和集羣執行不一致?讓我們按照手把手開發Flink程序-DataSet中打包發佈的方法發佈一次。
-
修改build.gradle中mainClass。mainClassName = 'org.myorg.quickstart.NumStatStream'
-
使用命令製作FatJar。./gradlew clean fatJar
-
發佈到Flink集羣
這Plan比以前簡略了很多
運行結果和本地執行結果一樣,有很多數據。看來確實哪裏出問題了。
解決結果數據過多問題
Flink中的DataStream和java自帶的Stream不太一樣,java的Stream其實一定會結束的,但是DataStream是一個沒有結尾的流,因此當對數據進行統計的時候應該統計多少呢?默認實現中就當做每個元素統計一遍,所以上面的程序一下子就收穫了很多統計結構,你算算是不是一條記錄一個結果呢?
無限的數據其實沒法統計,flink統計DataStream數據需要指定一個Window,這樣無限的數據就變成了多個有限的Window數據了。我們測試使用的數據只有1000條,很快就能跑完,我們給一個2s的窗口就夠了。我們在所有的keyBy邏輯後增加.timeWindow(Time.milliseconds(2000))。
修改後代碼
// 統計奇數偶數的比例
numbers.map(num -> num % 2)
.name("Calculate Mod value")
.map(mod -> new Tuple2<Integer, Integer>(mod, 1))
.name("Create mod result item")
.returns(TypeInformation.of(new TypeHint<Tuple2<Integer, Integer>>() {}))
.keyBy(0)
.timeWindow(Time.milliseconds(2000)) // 這個是新增加的時間窗口
.sum(1)
.name("Sum Mod result")
編譯後執行一下看結果,數據庫中是空的。部署到Flink上,執行看結果,還是空的,但是從執行的日誌上看各個階段都是在幹活的
解決沒有結果數據問題
到底發生了什麼呢?當前執行計劃顯示的太簡略了,看不出來數據走到哪裏了,爲了能看清楚數據,我們增加配置,執行計劃展示更加詳細的內容
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.disableOperatorChaining(); // 新增加的代碼
Date group = new Date();
重新編譯、打包、部署、執行後看執行計劃
現在的結果看起來清晰多了,從這裏可以看出來Convert to db result 等紅框框出來的幾個階段接收到的數據爲0,前一個階段沒有把數據發送到後面。我們找到代碼看看前一段代碼是什麼。
// 統計奇數偶數的比例
numbers.map(num -> num % 2)
.name("Calculate Mod value")
.map(mod -> new Tuple2<Integer, Integer>(mod, 1))
.name("Create mod result item")
.returns(TypeInformation.of(new TypeHint<Tuple2<Integer, Integer>>() {}))
.keyBy(0)
.timeWindow(Time.milliseconds(2000))
.sum(1)
.name("Sum Mod result")
// 這裏是分界線,上面的邏輯執行了,後面沒有接收到數據,沒有執行
.map(result -> NumStatResult.builder()
.group(group)
.type("mod")
.key(result.f0)
.value(result.f1)
.build())
.name("Convert to db result")
.writeUsingOutputFormat(mysqlSink)
.name("Save data to mysql");
查閱官網的Event Time可以知道,當我們使用時間類型的窗口時需要指定窗口的Time Characteristic。OK,那我們就給增加一個
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setStreamTimeCharacteristic(TimeCharacteristic.IngestionTime); // 新增加的代碼
env.disableOperatorChaining();
Date group = new Date();
重新執行看效果
select result_type, count(1) from result group by result_type
result_type | count(1) |
---|---|
count | 1 |
mod | 2 |
rate | 100 |
不錯我們成功了。
從Kafka中讀取數據
一般應用中數據應該來自一個Source,比如kafka。flink-playground中就內置了一個kafka,我們就用這個kafka,修改如下內容:
-
數據讀取指向kafka
-
做一個小程序,向kafka中放隨機數據
完成這些內容後,我們的Job會一直在運行,將接收到的數據分成每兩秒一包,將統計結果保存到mysql,看起來很是那麼回事。
鏈接kafka的方法,參考:https://ci.apache.org/projects/flink/flink-docs-release-1.9/dev/connectors/kafka.html
修改邏輯,支持kafka
修改內容
build.gradle
// 增加了kafka的引用
compile 'org.apache.flink:flink-connector-kafka_2.11:1.9.2'
NumStatStream.java
// 新增加從Kafka讀取數據邏輯
private static DataStream<Integer> readDataFromKafka(StreamExecutionEnvironment env) {
Properties properties = new Properties();
properties.setProperty(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka:9092"); // 在flink playground中kafka實例的名稱就是kafka,這裏配置了這個名字,這個名字只能在docker-compose啓動的一些實例中訪問。所以這個程序完成後,不能在IDE中調試。
properties.setProperty("group.id", "num_stat"); // 隨便給一個名字
return env.addSource(new FlinkKafkaConsumer("num_input", // 隨便給一個Topic
new SimpleStringSchema(), // 指定編解碼規則
properties))
.map(str -> Integer.valueOf(String.valueOf(str)))
.name("Received numbers");
}
private static void statisticsNums(int size) throws Exception {
...
// 創建數字流的邏輯改爲從kafka讀取
DataStream<Integer> numbers = readDataFromKafka(env);
新增加一個SendNumbers.java,負責向kafka發送1000條隨機數據
package org.myorg.quickstart;
import org.apache.flink.api.common.serialization.SimpleStringSchema;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.serialization.ByteArraySerializer;
import java.util.Properties;
public class SendNumbers {
public static void main(String[] args) {
Properties kafkaProps = createKafkaProperties();
KafkaProducer<byte[], byte[]> producer = new KafkaProducer<>(kafkaProps);
SimpleStringSchema serializationSchema = new SimpleStringSchema();
for (int i = 0; i < 1000; i++) {
String value = String.valueOf((int) (Math.random() * 100));
ProducerRecord<byte[], byte[]> record = new ProducerRecord<>(
"num_input", // 這個名字要和接收的一樣
serializationSchema.serialize(value));
producer.send(record);
}
}
private static Properties createKafkaProperties() {
Properties kafkaProps = new Properties();
kafkaProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka:9092");
kafkaProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, ByteArraySerializer.class.getCanonicalName());
kafkaProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, ByteArraySerializer.class.getCanonicalName());
return kafkaProps;
}
}
部署到flink
按照之前的方式編譯、打包、部署、執行,結果如圖
不是一般的順利呀,從這裏可以看見,我們的Job一直在等待數據。
發送數據
發送數據的代碼運行的時候也只能在docker中運行,因爲我們鏈接kafka使用了kafka這個名字。我們不需要給他單獨起一個容器。在flink Playground中,容器啓動的時候,有些容器將本地的一個目錄映射到了容器裏面,我們隨便選擇一個容器,比如jobmanager。
從文件docker-compose.yaml可以知道這個目錄/flink/flink-playgrounds/operations-playground/conf映射到了容器內的/opt/flink/conf。我們將打好的包quickstart-0.1-SNAPSHOT.jar放到conf目錄,然後進入容器
docker exec -it operations-playground_jobmanager_1 bash
root@0170a9080991:/opt/flink# cd conf
root@0170a9080991:/opt/flink/conf# ls
flink-conf.yaml log4j-cli.properties log4j-console.properties quickstart-0.1-SNAPSHOT.jar
root@0170a9080991:/opt/flink/conf# java -cp quickstart-0.1-SNAPSHOT.jar org.myorg.quickstart.SendNumbers
12:46:34,063 INFO org.apache.kafka.clients.producer.ProducerConfig - ProducerConfig values:
acks = 1
...
value.serializer = class org.apache.kafka.common.serialization.ByteArraySerializer
12:46:34,484 INFO org.apache.kafka.common.utils.AppInfoParser - Kafka version: 2.2.0
12:46:34,484 INFO org.apache.kafka.common.utils.AppInfoParser - Kafka commitId: 05fcfde8f69b0349
12:46:34,817 INFO org.apache.kafka.clients.Metadata - Cluster ID: ku7ks1XoQkq4DVGiWHLP_A
root@0170a9080991:/opt/flink/conf#
回到flink查看結果
檢查數據庫,也得到了了正確的結果。
在容器中不斷重複執行java -cp quickstart-0.1-SNAPSHOT.jar org.myorg.quickstart.SendNumbers,不斷髮送數據,可以看到數據不斷增加。
我們鏈接kafka也成功了。
大家遇到什麼問題可以在評論中說一下,我來完善文檔
大家既然看到了這裏,那就順手給個贊吧👍
本期的最終代碼參見文件num-stat-datastream.zip