记录
上一篇我们主要通过一个简单的demo程序来展示了Flink是如何去实现数据流处理的。现在我们将跟着代码一起梳理一下FLink的工作流程。
直奔主题
{
//获取运行环境
StreamExecutionEnvironment streamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment();
//设置使用eventTime,默认使用processTime
streamExecutionEnvironment.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
//设置并行度
streamExecutionEnvironment.setParallelism(1);
//设置socket端口号并读取输入数据
//task1
DataStream<String> dataStream = streamExecutionEnvironment.socketTextStream("localhost", 1234);
// Task 2
dataStream
.flatMap(new FlatMapFunction<String, IpAndCount>() {
@Override
public void flatMap(String value, Collector<IpAndCount> collector) throws Exception {
String[] wordArray = value.split(REGEX);
collector.collect(new IpAndCount(new SimpleDateFormat("HH:mm:ss").parse(wordArray[0]), wordArray[1], 1L));
}
})
.assignTimestampsAndWatermarks(
WatermarkStrategy.<IpAndCount>forBoundedOutOfOrderness(Duration.ofSeconds(3)).withTimestampAssigner(new SerializableTimestampAssigner<IpAndCount>() {
@Override
public long extractTimestamp(IpAndCount ipAndCount, long l) {
return ipAndCount.getDate().getTime();
}
}))
.keyBy(new KeySelector<IpAndCount,String>() {
@Override
public String getKey(IpAndCount o) throws Exception {
return o.getIp();
}
})
// Task 3
.timeWindow(Time.seconds(5))
//4
.reduce(new MyReduceFunction(), new MyProcessWindows())
.print();
try {
streamExecutionEnvironment.execute("Socket Window IpCount");
} catch (Exception e) {
e.printStackTrace();
}
}
static class MyReduceFunction implements ReduceFunction<IpAndCount> {
@Override
public IpAndCount reduce(IpAndCount ipAndCount, IpAndCount t1) throws Exception {
ipAndCount.setCount(ipAndCount.getCount() + t1.getCount());
return ipAndCount;
}
}
static class MyProcessWindows extends ProcessWindowFunction<IpAndCount, String, String, TimeWindow> {
@Override
public void process(String s, Context context, Iterable<IpAndCount> elements, Collector<String> out) throws Exception {
IpAndCount next = elements.iterator().next();
String buffer = "窗口范围是:" + new SimpleDateFormat("HH:mm:ss").format(context.window().getStart()) + "----" + new SimpleDateFormat("HH:mm:ss").format(context.window().getEnd()) + "\n" +
"时间:" + new SimpleDateFormat("HH:mm:ss").format(next.getDate()) + "\t" +
"ip:" + next.getIp() + "\t" +
"次数" + next.getCount() + "\t";
out.collect(buffer);
}
}
这是完整的一个处理数据任务的代码,那么任务究竟是在flink中是如何被一步步执行的呢?
这段用于计算任务的代码首先会被Flink解析,生成一个如上图所示的Dataflow Graph 简称DAG(一种有向无环图)然后通过client会把这个DAG提交到jobManager(传送门)
对于图中的三个Task,每个Task对应执行了什么计算,完全可以和我们定义的任务的源码对应上,我也在源代码的注释中,用"//Task n"的形式给出了标注。
- 第一个 Task 执行的计算很简单,就是连接nc服务模拟接收日志数据,然后将日志数据发往下一个 Task。
- 第二个 Task 执行了两个 map 变换,把文本数据转换成了结构化(java中定义的entity)的数据,并添加 Watermark(水印)主要是用于触发按时间汇总的操作。
- 第三个 Task 执行了剩余的计算任务,按时间汇总日志,并按照特定的格式输出打印到控制台上。
这个 DAG 仍然是一个逻辑图,它到底是怎么在 Flink 集群中执行的呢?图中每个 Task 都标注了一个 Parallelism(并行度)的数字吗?这个并行度的意思就是,这个 Task 可以被多少个线程并行执行。比如图中的第二个任务,它的并行度是 4,就代表 Task 在 Flink 集群中运行的时候,会有 4 个线程都在执行这个 Task,每个线程就是一个 SubTask(子任务)。注意,如果 Flink 集群的节点数够多,这 4 个 SubTask 可能会运行在不同的 TaskManager 节点上。
建立了 SubTask 的概念之后,我们再重新回过头来看一下这个图中的两个箭头。第一个箭头连接前两个 Task,这个箭头标注了 REBALANCE(重新分配),因为第一个 Task 并行度是 1,而第二个 Task 并行度是 4,意味着从第一个 Task 流出的数据将被重新分配给第二个 Task 的 4 个线程,也就是 4 个 SubTask(子任务)中,这样就实现了并行处理。这和消息队列中每个主题分成多个分区进行并行收发的设计思想是一样的。
再来看连接第二、第三这两个 Task 的箭头,这个箭头上标注的是 HASH,为什么呢?可以看到,第二个 Task 中最后一步业务逻辑是:keyBy()该匿名内部类返回的是IpAndCount的ip属性,也就是按照 IP 这个字段做一个 HASH 分流。这里可以想一下,第三个 Task,它的并行度是 4,也就是有 4 个线程在并行执行汇总。如果要统计每个 IP 的日志条数,那必须得把相同 IP 的数据发送到同一个 SubTask(子任务)中去,这样在每个 SubTask(子任务)中,对于每一条数据,只要在对应 IP 汇总记录上进行累加就可以了。
反之,要是相同 IP 的数据被分到多个 SubTask(子任务)上,这些 SubTask 又可能分布在多个物理节点上,那就没办法统计了。所以,第二个 Task 会把数据按照 IP 地址做一个 HASH 分流,保证 IP 相同的数据都发送到第三个 Task 中相同的 SubTask(子任务)中。这个 HASH 分流的的设计和消息队列中严格顺序消息的实现方法:通过 HASH 算法,让 key 相同的数据总是发送到相同的分区上来保证严格顺序和 Flink 这里的设计就是一样的。
最后在第三个 Task 中,4 个 SubTask 并行进行数据汇总,每个 SubTask 负责汇总一部分 IP 地址的数据。最终打印到控制台上的时候,也是 4 个线程并行打印。输出的计算结果,每一行数据前面的数字,就是第三个 Task 中 SubTask 的编号。
在上述标记4中reduce表示将数据合并成一个新的数据,返回单个的结果值,并且 reduce 操作每处理一个元素总是创建一个新值。reduce需要针对分组或者一个window(窗口)来执行,也就是分别对应于keyBy、window/timeWindow 处理后的数据,根据ReduceFunction将元素与上一个reduce后的结果合并,产出合并之后的结果。参见上一篇最后的执行结果。)
以上就是demo中计算任务在Flink集群中完整的执行过程,下一篇准备记录-深入理解Watermark。