Flink
一、概念
Apache Flink 是一个实时计算框架和分布式处理引擎,用于在无边界和有边界数据流上进行有状态的计算。Flink 能在所有常见集群环境中运行,并能以内存速度和任意规模进行计算。
- 特点:
- 支持高吞吐、低延迟、高性能的流处理
- 支持带有事件时间的窗口(Window)操作
- 支持有状态计算的Exactly-once语义<Exactly-once:当任意条数据流转到某分布式系统中,如果系统在整个处理过程中对该任意条数据都仅精确处理一次,且处理结果正确,则被认为该系统满足Exactly-Once一致性>
- 支持高度灵活的窗口(Window)操作,支持基于time、count、session,以及data-driven的窗口操作
- 支持具有反压功能的持续流模型
- 支持基于轻量级分布式快照(Snapshot)实现的容错
- 一个运行时同时支持Batch on Streaming处理和Streaming处理
- Flink在JVM内部实现了自己的内存管理,避免了出现oom
- 支持迭代计算
- 支持程序自动优化:避免特定情况下Shuffle、排序等昂贵操作,中间结果有必要进行缓存
二、Spark Streaming 与 Flink的区别
(主要区别)
-
Flink:实时处理模型,基于事件处理
-
Spark Streaming:微批模型
(次要区别)
- 架构模型:Spark Streaming 在运行时的主要角色包括:Master、Worker、Driver、Executor, Flink 在运行时主要包:Jobmanager、Taskmanager 和 Slot。
- 任务调度:Spark Streaming 连续不断的生成微小的数据批次,构建有向无环图 DAG, Spark Streaming 会依次创DStreamGraph、JobGenerator、JobScheduler。Flink 根据用户 提交的代码生成 StreamGraph,经过优化生成 JobGraph,然后提交给JobManager 进行处理, JobManager 会根据 JobGraph 生成 ExecutionGraph,ExecutionGraph 是 Flink 调度最核心的数据结构,JobManager 根据 ExecutionGraph 对 Job 进行调度。
- 时间机制:Spark Streaming 支持的时间机制有限,只支持处理时间。 Flink 支持了流 处理程序在时间上的三个定义:处理时间、事件时间、注入时间。同时也支持 watermark 机制来处理滞后数据。
- 容错机制:对于 Spark Streaming 任务,我们可以设置 checkpoint,然后假如发生故障 并重启,我们可以从上次 checkpoint 之处恢复,但是这个行为只能使得数据不丢失,可能会重复处理,不能做到恰好一次处理语义。Flink 则使用两阶段提交协议来解决这个问题。
三、实时计算框架
-
storm
- 优势:
框架简单,学习成本低
实时性很好,可以提供毫秒级延迟
稳定性很好,框架比较成熟 - 劣势:
编程成本较高
框架处理逻辑和批处理完全不一样,无法共用代码
框架Debug较为复杂
- 优势:
-
SparkStreaming(微批处理)
- 优势:
编程原语丰富,编程简单
框架封装层级较高,封装性好
可以共用批处理处理逻辑,兼容性好
基于Spark,可以无缝内嵌Spark其他子项目,如Spark Sql,MLlib等 - 劣势:
微批处理,时间延迟大
稳定性相对较差
机器性能消耗较大
- 优势:
-
Flink(流式处理)
- 优势
Flink流处理为先的方法可提供低延迟,高吞吐率,近乎逐项处理的能力
Flink的很多组件是自行管理的
通过多种方式对工作进行分析进而优化任务
提供了基于Web的调度视图
- 优势
四、常用算子
-
处理方式core(流处理、批处理、流批和一)
package org.zz.shujia.flink.core; import org.apache.flink.api.common.RuntimeExecutionMode; import org.apache.flink.api.common.functions.FlatMapFunction; import org.apache.flink.api.common.typeinfo.Types; import org.apache.flink.api.java.tuple.Tuple2; import org.apache.flink.streaming.api.datastream.DataStream; import org.apache.flink.streaming.api.datastream.KeyedStream; import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment; import org.apache.flink.util.Collector; public class Demo02WordCountBatch { public static void main(String[] args) throws Exception { /** * BATCH:批处理 * 1、底层基于MR模型 * 2、只能用于处理有界的数据 * STREAM:流处理 * 1、底层基于持续流模型 * 2、既能处理无界流也可以用于有界流 * * 流批合一:在Flink中同一套DataStream的API既可以用作流处理也可以用作批处理 */ StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); DataStream<String> ds = env.socketTextStream("master", 8888); // env.setRuntimeMode(RuntimeExecutionMode.BATCH); //批处理 env.setRuntimeMode(RuntimeExecutionMode.STREAMING); //流处理 //方式一: DataStream<String> map = ds.flatMap(new FlatMapFunction<String, String>() { @Override public void flatMap(String line, Collector<String> collector) throws Exception { String[] strings = line.split(","); for (String string : strings) { collector.collect(string); } } }); //方式二: DataStream<String> flatMap = ds.flatMap((line, collector) -> { String[] split = line.split(","); for (String s : split) { collector.collect(s); } }, Types.STRING); DataStream<Tuple2<String, Integer>> map1 = flatMap.map(word -> Tuple2.of(word, 1), Types.TUPLE(Types.STRING, Types.INT)); KeyedStream<Tuple2<String, Integer>, String> keyBy = map1.keyBy(word -> word.f0); DataStream<Tuple2<String, Integer>> sum = keyBy.sum(1); sum.print(); env.execute(); } }
-
读操作(Source)
- FileSource
/** * 新版读取文件的方式 * 1、指定一个格式的方式 * 2、指定一个路径 * 3、通过monitorContinuously来实现对目录的实时监控,将读文件转换无界流进行处理 */ StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); FileSource<String> fileSource = FileSource.forRecordStreamFormat(new TextLineInputFormat() , new Path("")) .monitorContinuously(Duration.ofMillis(5))// 指定时间间隔监控目录的变化 .build(); DataStreamSource<String> source = env.fromSource(fileSource, WatermarkStrategy.noWatermarks(), "fileSource"); source.print(); env.execute();
-
SocketSource
/** * Socket Source: * 一般用于代码调试及开发 */ DataStreamSource<String> socketDS = env.socketTextStream("master", 8888);
-
CollectionSource
ArrayList<String> arr = new ArrayList<>(); arr.add("java,scala,python"); arr.add("java,scala,python"); arr.add("java,scala,python"); arr.add("java,scala,python"); arr.add("java,scala,python"); DataStreamSource<String> arrDS = env.fromCollection(arr
-
写操作(sink)
-
FileSink
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); env.setRuntimeMode(RuntimeExecutionMode.STREAMING); DataStreamSource<String> file = env.readTextFile("spark/flink/data"); DataStream<Tuple2<String, Integer>> stuNum = file .map(line -> Tuple2.of(line.split(",")[4], 1), Types.TUPLE(Types.STRING,Types.INT)) .keyBy(kv -> kv.f0) .sum(1); DataStream<String> nuwValue = stuNum.map(kv -> kv.f0 + "\t" + kv.f1); //存储 FileSink<String> fileSink = FileSink.forRowFormat(new Path("flink/data/clazz_cnt"), new SimpleStringEncoder<String>("utf-8")).build(); nuwValue.sinkTo(fileSink); env.execute();
-
-
常用算子
- Map
/* * map操作:传入一条数据返回一条数据 */ studentDS.map(new MapFunction<String, String>() { @Override public String map(String s) throws Exception { return s.split(",")[1]; } }).print();
-
FlatMap
/* * flatmap 操作:传入一条数据返回一系列数据 */ source.flatMap(new FlatMapFunction<String, String>() { /** * 传入一条数据flatMap方法就会执行一次 */ @Override public void flatMap(String value, Collector<String> out) throws Exception { for (String word : value.split(",")) { out.collect(word); } } }).print();
-
Filter
/* * FLink中的算子不是懒执行的,不需要action算子触发 * 统一由env.execute()触发Flink任务的执行 */ studentDS.filter(new FilterFunction<String>() { @Override public boolean filter(String s) throws Exception { String[] strings = s.split(","); String clazz = strings[4]; String gender = strings[3]; return clazz.startsWith("文科") && "女".equals(gender); } });
-
KeyBy
/* * keyBy:进行流上的分组 * 让相同key的数据能够进入同一个线程对应的Task中进行处理 * 同一个Task中也会有不同的key */ DataStream<Tuple2<String, Integer>> kvDS = lineDS.flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() { @Override public void flatMap(String s, Collector<Tuple2<String, Integer>> collector) throws Exception { for (String word : s.split(",")) { collector.collect(Tuple2.of(word, 1)); } } }); KeyedStream<Tuple2<String, Integer>, String> keyBy = kvDS.keyBy(kv -> kv.f0); keyBy.sum(1).print();
-
Reduce
DataStream<Tuple2<String, Integer>> kvDS = lineDS.flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() { @Override public void flatMap(String s, Collector<Tuple2<String, Integer>> collector) throws Exception { for (String word : s.split(",")) { collector.collect(Tuple2.of(word, 1)); } } }); KeyedStream<Tuple2<String, Integer>, String> keyBy = kvDS.keyBy(kv -> kv.f0); SingleOutputStreamOperator<Tuple2<String, Integer>> reduce = keyBy.reduce(new ReduceFunction<Tuple2<String, Integer>>() { @Override public Tuple2<String, Integer> reduce(Tuple2<String, Integer> t1, Tuple2<String, Integer> t2) throws Exception { return Tuple2.of(t1.f0, t1.f1 + t2.f1); } });
五、核心组件
-
Client(作业客户端)
-
JobManager(作业管理器)
-
TaskManager(任务管理器)
工作流程:
- 当 Flink 集群启动后,首先会启动一个 JobManager 和一个或多个的 TaskManager。
- JobManager负责作业调度,收集TaskManager的Heartbeat和统计信息,
- TaskManager 之间以流的形式进行数据的传输。
六、Flink事件时间
- Processing Time : 处理时间,当前机器处理该事件的时间(即进入某个算子时的系统时间),有着最好的性能和最低的延迟
- Ingestion Time : 摄入时间,数据进入Flink框架的时间,在Source Operator中设置,每个事件拿到当前时间作为时间戳,后续的时间窗口基于该时间;相比ProcessingTime可以提供更可预测的结果
- Event Time : 事件时间是每条事件在它产生的时候记录的时间,该时间记录在事件中,在处理的时候可以被提取出来;事件事件对于乱序、延时、或者数据重放等情况,都能给出正确都结果,事件时间依赖于事件本身,而跟物理时钟没有关系,利用事件时间编程必须如何制定如何生成事件时间的watermark;
- 事件时间存在一定的延时,因此自然的需要延时和无序事件等待一段时间;因此,使用事件时间编程通常需要与处理时间相结合;
水位线Watermark
Watermark是Flink上为了处理EventTime时间类型的窗口计算提出的一种机制,本质上也是一种时间戳;WaterMark是用于处理乱序事件的,而正确的处理乱序事件,通常用watermark机制结合window来实现;
-
当operator通过Event Time的时间窗口来处理数据时,它必须在确定所有属于该时间窗口的消息全部流入此操作符后,才能开始处理数据,但是由于消息可能是乱序的,所以operator无法直接确认任何所有属于该时间窗口的消息全部流入此操作符;
-
WaterMark包含一个时间戳,Flink使用WaterMark保证所有小于该时间戳的消息都已流入,Flink的数据源在确认所有小于该时间戳的消息都已流入,Flink的数据源在确认所有小于某个时间戳的消息都已输出到Flink流处理器后,会生成一个包含该时间戳的WaterMark,插入到消息流中输出到Flink流处理系统中;
-
Flink operator算子按照时间窗口缓存所有流入的消息,当操作符处理到WaterMark时,它对所有小于该WaterMark时间戳的时间窗口的数据进行处理并发送到下一个操作符节点,然后也将WaterMark发送到下一个操作符节点;
-
一旦一个watermark到达了operator,operator可以将内部事件时间提前到watermark的时间戳
两种水位线策略
方式1、使用数据中最大的时间作为水位线
方式2、将水位线前移5s,解决数据延时到达的问题(前移的太多就会导致整体任务延时较大)DataStream<Tuple2<String, Long>> assDS = wordTimeDS .assignTimestampsAndWatermarks( //(分配时间戳与水位线) WatermarkStrategy //(水位线策略) // 方式1、使用数据中最大的时间作为水位线 // .<Tuple2<String, Long>>forMonotonousTimestamps() // 方式2、将水位线前移5s,解决数据延时到达的问题 // 前移的太多就会导致整体任务延时较大 .<Tuple2<String, Long>>forBoundedOutOfOrderness(Duration.ofSeconds(5)) // 告诉flink,数据中哪部分作为时间戳,即使用事件时间 .withTimestampAssigner(TimestampAssignerSupplier.of((kv, ts) -> kv.f1) ));
七、Flink窗口
-
Time Window
1、基于时间的窗口 基于事件时间: 滑动:SlidingEventTimeWindows 滚动:TumblingEventTimeWindows 基于处理时间: 滚动:TumblingProcessingTimeWindows
案例: StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); env.setParallelism(1); DataStream<String> wordDS = env.socketTextStream("master", 8888); // 如果后续需要基于事件时间进行统计 则需要设置事件时间以及水位线 DataStream<Tuple2<String, Long>> assignDS = wordDS.map(line -> { String[] split = line.split(","); String word = split[0]; long ts = Long.parseLong(split[1]); return Tuple2.of(word, ts); }, Types.TUPLE(Types.STRING, Types.LONG)) .assignTimestampsAndWatermarks(WatermarkStrategy .<Tuple2<String, Long>>forMonotonousTimestamps() .withTimestampAssigner((kv, ts) -> kv.f1) ); // 每个5s统计最近10s内的单词数量 assignDS.map(kv -> Tuple2.of(kv.f0, 1),Types.TUPLE(Types.STRING,Types.INT)) .keyBy(kv -> kv.f0) .window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5))) .sum(1) .print(); env.execute();
-
Session Window
2、基于会话的窗口 基于事件时间:EventTimeSessionWindows 基于处理时间:ProcessingTimeSessionWindows
DataStream<String> wordDS = env.socketTextStream("master", 8888); // 基于处理时间:ProcessingTimeSessionWindows // wordDS.map(word -> Tuple2.of(word, 1), Types.TUPLE(Types.STRING, Types.INT)) // .keyBy(kv->kv.f0) // .window(ProcessingTimeSessionWindows.withGap(Time.seconds(5))) // .sum(1) // .print(); // 基于事件时间:EventTimeSessionWindows // 指定事件时间以及水位线 DataStream<Tuple2<String, Long>> assignDS = wordDS.map(line -> { String[] split = line.split(","); String word = split[0]; long ts = Long.parseLong(split[1]); return Tuple2.of(word, ts); }, Types.TUPLE(Types.STRING, Types.LONG)) .assignTimestampsAndWatermarks(WatermarkStrategy .<Tuple2<String, Long>>forMonotonousTimestamps() .withTimestampAssigner((kv, ts) -> kv.f1) ); assignDS.map(word -> Tuple2.of(word.f0, 1), Types.TUPLE(Types.STRING, Types.INT)) .keyBy(kv->kv.f0) .window(EventTimeSessionWindows.withGap(Time.seconds(5))) .sum(1) .print();
-
Count Window
/* 3、基于计数的窗口:可以直接在KeyBy之后直接.出来
* 滑动:
* 滚动:
*/
wordDS.map(word -> Tuple2.of(word, 1), Types.TUPLE(Types.STRING, Types.INT))
.keyBy(kv -> kv.f0)
// .countWindow(5) // 每个key每5条数据统计一次
.countWindow(10, 5) // 每个key每5条数据统计最近的10条数据
.sum(1)
.print();
八、卡口流量案例
public static void main(String[] args) throws Exception {
/*
* 需求:基于卡口过车数据统计道路的拥堵情况
* 拥堵情况:通过车速以及车流量进行判断
* 思路:通过对道路以及卡口进行分组,统计车流量以及平均车速
* 写代码的思路:
* 1、接入数据,通过Socket模拟实时过车数据
* 2、提取数据中的时间,并设置事件时间及水位线
* 3、按照道路及卡口分组,使用滑动窗口每隔1分钟统计最近10分钟内车流量信息
* 4、打印数据
*/
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
DataStreamSource<String> lineDS = env.socketTextStream("master", 8888);
// 对每条数据进行切分,并将其转换成Car的对象
SingleOutputStreamOperator<Car> carDS = lineDS.map(line -> {
String[] splits = line.split(",");
return new Car(splits[0], Integer.parseInt(splits[1]), Integer.parseInt(splits[2])
, splits[3], splits[4], splits[5]
, splits[6], Long.parseLong(splits[7]) * 1000, Double.parseDouble(splits[8])
);
});
// 设置事件时间以及水位线
SingleOutputStreamOperator<Car> assignDS = carDS.assignTimestampsAndWatermarks(
WatermarkStrategy
.<Car>forMonotonousTimestamps()
.withTimestampAssigner((car, ts) -> car.getTime())
);
// 按照卡口以及道路id进行分组
assignDS.keyBy(car -> car.getRoadId() + "|" + car.getkId())
// 使用滑动窗口,每隔1分钟统计最近10分钟内的卡口数据
.window(SlidingEventTimeWindows.of(Time.minutes(10), Time.minutes(1)))
// 统计车流量又需要统计平均车速
// 没有直接的API可以使用,需要使用底层的API自定义窗口的计算逻辑
.process(new ProcessWindowFunction<Car, Result, String, TimeWindow>() {
/**
*
* @param s 分组的Key,由卡口编号及道路编号拼接而成
* @param context Flink任务运行时的上下文环境
* @param elements 每个分组每个窗口接收到的数据,即最近10分钟内的数据
* @param out 用于将结果输出到下游
*/
// 每个窗口会执行一次,即1分钟执行一次
@Override
public void process(String s, ProcessWindowFunction<Car, Result, String, TimeWindow>.Context context, Iterable<Car> elements, Collector<Result> out) throws Exception {
// 提取道路id以卡口id
String[] roadAndKId = s.split("\\|");
int cnt = 0;
double sumSpeed = 0;
for (Car car : elements) {
sumSpeed += car.getSpeed();
// 统计车流量
cnt++;
}
// 计算平均车速
double avgSpeed = sumSpeed / cnt;
// 构建Result对象,通过out进行输出
out.collect(new Result(roadAndKId[0], roadAndKId[1], cnt, avgSpeed));
}
}).print();
env.execute();
}
}
class Result {
private String roadId;
private String kId;
private Integer carCnt;
private Double avgSpeed;
public Result(String roadId, String kId, Integer carCnt, Double avgSpeed) {
this.roadId = roadId;
this.kId = kId;
this.carCnt = carCnt;
this.avgSpeed = avgSpeed;
}
@Override
public String toString() {
return "Result{" +
"roadId='" + roadId + '\'' +
", kId='" + kId + '\'' +
", carCnt=" + carCnt +
", avgSpeed=" + avgSpeed +
'}';
}
}
class Car {
private String car;
private Integer cityId;
private Integer countyId;
private String kId;
private String cameraId;
private String direction;
private String roadId;
private Long time;
private Double speed;
public Car(String car, Integer cityId, Integer countyId, String kId, String cameraId, String direction, String roadId, Long time, Double speed) {
this.car = car;
this.cityId = cityId;
this.countyId = countyId;
this.kId = kId;
this.cameraId = cameraId;
this.direction = direction;
this.roadId = roadId;
this.time = time;
this.speed = speed;
}
public String getCar() {
return car;
}
public Integer getCityId() {
return cityId;
}
public Integer getCountyId() {
return countyId;
}
public String getkId() {
return kId;
}
public String getCameraId() {
return cameraId;
}
public String getDirection() {
return direction;
}
public String getRoadId() {
return roadId;
}
public Long getTime() {
return time;
}
public Double getSpeed() {
return speed;
}
}