目的:
-
学习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