手把手开发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_typecount(1)
count240
mod1001
rate1001

哇,结果也出来了。而且比以前的结果多很多,怎么结果数量多了呢?是否本地执行和集群执行不一致?让我们按照手把手开发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_typecount(1)
count1
mod2
rate100

不错我们成功了。

从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

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值