手把手開發Flink程序-DataStream

目的:

  1. 學習Flink的基本使用方法

  2. 掌握在一般使用中需要注意的事項

 

手把手的過程中會講解各種問題的定位方法,相對囉嗦,內容類似結對編程。

大家遇到什麼問題可以在評論中說一下,我來完善文檔

 

Flink專輯的各篇文章鏈接: 手把手開發Flink程序-基礎

手把手開發Flink程序-DataSet

手把手開發Flink程序-DataStream

 

現在我們繼續解決手把手開發Flink程序-DataSet中統計數字的問題,但是不再使用DataSet,而是使用DataStream。原來的需求是

  1. 生成若干隨機數字

  2. 統計奇數和偶數的個數

  3. 統計質數格式

  4. 統計每個數字出現的次數

步驟:

使用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

鏈接:https://pan.baidu.com/s/19PQzxWQsQsf8E7v-a-sRUA 密碼:9uuq

發佈了12 篇原創文章 · 獲贊 11 · 訪問量 449
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章