1. flink介绍
基于事件驱动,在flink中,一切都是由流组成,离线数据是有界限的流,实时数据是没有界限的流,这就是有界流和无界流
3. Flink部署
3.3 Standalone模式
3.3.1 Standalone模式运行无界流WorkCount
1. 启动standalone集群
bin/start-cluster.sh
2. 命令行提交Flink应用
bin/flink run -d -m hadoop162:8081 -c com.atguigu.flink.java.chapter_2.Flink03_WC_UnBoundedStream ./flink-prepare-1.0-SNAPSHOT.jar
3.3.2 Standalone高可用(HA)
任何时候都有一个 主 JobManager 和多个备用 JobManagers,以便在主节点失败时有备用 JobManagers 来接管集群。这保证了没有单点故障,一旦备 JobManager 接管集群,作业就可以正常运行。主备 JobManager 实例之间没有明显的区别。每个 JobManager 都可以充当主备节点。
3.4 Yarn模式
3.4.1 Flink on Yarn的3种部署模式
3.4.1.1 Session-Cluster
1. Session-Cluster模式需要先启动Flink集群,向Yarn申请资源, 资源申请到以后,永远保持不变。以后提交任务都向这里提交。这个Flink集群会常驻在yarn集群中,除非手工停止。
2. 在向Flink集群提交Job的时候, 如果资源被用完了,则新的Job不能正常提交.
3. 缺点: 如果提交的作业中有长时间执行的大作业, 占用了该Flink集群的所有资源, 则后续无法提交新的job.
所以, Session-Cluster适合那些需要频繁提交的多个小Job, 并且执行时间都不长的Job.
提交任务
--启动一个Flink-Session
bin/yarn-session.sh -d
--在Session上运行Job
bin/flink run -c com.atguigu.flink.java.chapter_2.Flink03_WC_UnBoundedStream ./flink-prepare-1.0-SNAPSHOT.jar
--关闭
yarn application -kill application_1599402747874_0001
3.4.1.2 Per-Job-Cluster
一个Job会对应一个集群,每提交一个作业会根据自身的情况,都会单独向yarn申请资源,直到作业执行完成,一个作业的失败与否并不会影响下一个作业的正常提交和运行。独享Dispatcher和ResourceManager,按需接受资源申请;适合规模大长时间运行的作业。
每次提交都会创建一个新的flink集群,任务之间互相独立,互不影响,方便管理。任务执行完成之后创建的集群也会消失。
提交任务
bin/flink run -t yarn-per-job -c com.atguigu.flink.java.chapter_2.Flink03_WC_UnBoundedStream ./flink-prepare-1.0-SNAPSHOT.jar
3.4.1.3 Application Mode
Application Mode会在Yarn上启动集群, 应用jar包的main函数(用户类的main函数)将会在JobManager上执行. 只要应用程序执行结束, Flink集群会马上被关闭. 也可以手动停止集群.
与Per-Job-Cluster的区别: 就是Application Mode下, 用户的main函数式在集群中执行的
提交任务
bin/flink run-application -t yarn-application -c com.atguigu.flink.java.chapter_2.Flink03_WC_UnBoundedStream ./flink-prepare-1.0-SNAPSHOT.jar
3.4.2 Yarn模式高可用
Yarn模式的高可用和Standalone模式的高可用原理不一样.
Standalone模式中, 同时启动多个Jobmanager, 一个为leader其他为standby的, 当leader挂了, 其他的才会有一个成为leader.
yarn的高可用是同时只启动一个Jobmanager, 当这个Jobmanager挂了之后, yarn会再次启动一个, 其实是利用的yarn的重试次数来实现的高可用.
4. Flink运行架构
4.1 运行架构
--Dispatcher
负责接收用户提供的作业,并且负责为这个新提交的作业启动一个新的 JobManager 组件
--ResourceManager
负责资源的管理,在整个 Flink 集群中只有一个 ResourceManager
--JobManager
负责管理作业的执行,在一个 Flink 集群中可能有多个作业同时执行,每个作业 都有自己的 JobManager 组件
4.2
核心概念
4.2.1 TaskManager与Slots
--slots
1. 可以控制一个worker能接收多少个task
2. 可以共享
3. 对内存进行隔离和划分,不对cup进行隔离,spark的core是对CPU进行了划分,不能共享
4.2.2 Parallelism (并行度)
一个特定算子的子任务(subtask)的个数被称之为这个算子的并行度(parallelism)(分区数)
,一般情况下,
一个流程序的并行度
,可以认为就
是其所有算子中最大的并行度
。一个程序中,不同的算子可能具有不同的并行度。
- Spark的并行度设置后需要调用特殊的算子(repartition)或特殊的操作(shuffle)才能进行改变,比如调用flatMap算子后再调用repartition改变分区
- Flink的并行度设置可以在任何算子后使用,并且为了方便,也可以设置全局并行度
- 如果Flink的一个算子的并行度为2,那么这个算子在执行时,这个算子对应的task就会拆分成2个subtask,发到不同的Slot中执行
--slot是静态的
一共有多少个slot
--并行度是动态的
有多少个slot在使用
代码实现改变并行度
// 算子并行度>环境整体>参数>配置文件
env.setParallelism(1); //环境的并行度
map(...)..setParallelism(2) //算子的并行度
Stream在算子之间传输数据的形式可以是one-to-one(forwarding)的模式也可以是redistributing的模式,具体是哪一种形式,取决于算子的种类。
One-to-one
:
stream(比如在source和map operator之间)维护着分区以及元素的顺序。那意味着flatmap 算子的子任务看到的元素的个数以及顺序跟source 算子的子任务生产的元素的个数、顺序相同,map、fliter、flatMap等算子都是one-to-one的对应关系。
类似于
spark
中的窄依赖
Redistributing
:
stream(map()跟keyBy/window之间或者keyBy/window跟sink之间)的分区会发生改变。每一个算子的子任务依据所选择的transformation发送数据到不同的目标任务。例如,keyBy()基于hashCode重分区、broadcast和rebalance会随机重新分区,这些算子都会引起redistribute过程,而redistribute过程就类似于Spark中的shuffle过程。
类似于
spark
中的宽依赖
4.2.3 Task与SubTask
一个算子就是一个Task. 一个算子的并行度是几, 这个Task就有几个SubTask
4.2.4 Operator Chains(任务链)
相同并行度的one to one操作,Flink将这样相连的算子链接在一起形成一个task,原来的算子成为里面的一部分。 每个task被一个线程执行,在一个slot里面
将算子链接成task是非常有效的优化:它能减少线程之间的切换和基于缓存区的数据交换,在减少时延的同时提升吞吐量。链接的行为可以在编程API中进行指定。
图中的Flat Map -> Map 就在一个任务链中,并行度为16
4.2.5 ExecutionGraph(执行图)
由Flink程序直接映射成的数据流图是StreamGraph,也被称为
逻辑流图
,因为它们表示的是计算逻辑的高级视图。为了执行一个流处理程序,Flink需要将逻辑流图转换为物理数据流图(也叫执行图),详细说明程序的执行方式。
Flink 中的执行图可以分成四层:
StreamGraph
-> JobGraph -> ExecutionGraph -> Physical Graph
。
--StreamGraph:
是根据用户通过 Stream API 编写的代码生成的最初的图。用来表示程序的拓扑结构
--JobGraph
StreamGraph经过优化后生成了 JobGraph,是提交给 JobManager 的数据结构。主要的优化为: 将多个符合条件的节点(算子) chain 在一起作为一个节点(slot),这样可以减少数据在节点之间流动所需要的序列化/反序列化/传输消耗
--ExecutionGraph
JobManager 根据 JobGraph 生成ExecutionGraph。ExecutionGraph是JobGraph的并行化版本,就是划分子任务,分区个数,是调度层最核心的数据结构。
--Physical Graph
PhysicalGraph 根据 ExecutionGraph 对Job进行调度后,在各个TaskManager 上部署 Task 后形成的“图”,并不是一个具体的数据结构。
4.3 提交流程
当一个应用提交执行时,Flink的各个组件是如何交互协作的:
如果将Flink集群部署到YARN上,那么就会有如下的提交流程:
1. Flink任务提交后,Client向HDFS上传Flink的jar包和配置
2. 向Yarn RM提交任务,RM分配Container资源
3. RM通知对应的NodeManager开启Container然后启动Application,启动后加载Flink的Jar包和配置构建环境,然后启动JobManager
4. ApplicationMaster向ResouceManager申请资源启动TaskManager
5. ResourceManager分配NM开启Container资源后,由ApplicationMaster通知资源所在节点的NodeManager启动TaskManager
6. NodeManager加载Flink的Jar包和配置构建环境并启动TaskManager
7. TaskManager启动后向RM注册slots,RM返回slots信息给JobManager
8. TaskManager启动后向JobManager发送心跳包,并等待JobManager向其分配任务
5 Flink核心编程
5.1 Environment
Flink Job在提交执行计算时,需要首先建立和Flink框架之间的联系,也就指的是当前的flink运行环境,只有获取了环境信息,才能将task调度到不同的taskManager执行
// 批处理环境
ExecutionEnvironment benv = ExecutionEnvironment.getExecutionEnvironment();
// 流式数据处理环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
5.1 Source
Flink框架可以从不同的来源获取数据,将数据提交给框架进行处理, 我们将获取数据的来源称之为数据源(Source)
5.2.1 从Java的集合中读取数据
env.fromCollection(Arrays.asList(1, 10, 100, 100));
env.fromElements(10, 100, 100, 1000);
5.2.2 从文件读取数据
env.readTextFile("input")
5.2.3 从Socke读取数据
env.socketTextStream("hadoop162", 9999);
5.2.4 从Kafka读取数据
Properties props = new Properties();
props.setProperty("bootstrap.servers","hadoop162:9092,hadoop163:9092,hadoop164:9092");
props.setProperty("group.id", "Flink03_Source_Kafka");
props.setProperty("auto.offset.reset", "latest");
//SimpleStringSchema反序列化器
DataStreamSource<String> ds = env.addSource(new FlinkKafkaConsumer<>("sensor", new SimpleStringSchema(), props));
5.2.5 自定义Source
public class Flink04_Source_Custom {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.addSource(new MySocketSource("hadoop100", 9999))
.print();
env.execute();
}
public static class MySocketSource implements SourceFunction<WaterSensor> {
private String host;
private int port;
private boolean cancel = false;
private Socket socket;
private BufferedReader reader;
public MySocketSource(String host, int port) {
this.host = host;
this.port = port;
}
@Override
public void run(SourceContext<WaterSensor> sourceContext) throws Exception {
socket = new Socket(host, port);
reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
String line = reader.readLine();
while (!cancel && line != null) {
String[] split = line.split(",");
sourceContext.collect(new WaterSensor(split[0], Long.valueOf(split[1]), Integer.valueOf(split[2])));
line = reader.readLine();
}
}
@Override
public void cancel() {
cancel = true;
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
5.2 Transform
5.3.1 map
作用:将数据流中的数据进行转换, 形成新的数据流,消费一个元素并产出一个元素
参数:map(MapFunction<T, R> mapper)
例子:
source.map(x -> x * x)
.print();
5.3.1 Rich…Function类
所有Flink函数类都有其Rich版本。它与常规函数的不同在于,可以获取运行环境的上下文,并拥有一些生命周期方法,所以可以实现更复杂的功能。也有意味着提供了更多的,更丰富的功能
可以用来批处理,如连接数据库等
1. 默认生命周期方法, 初始化方法, 在每个并行度上只会被调用一次, 而且先被调用
2. 默认生命周期方法, 最后一个方法, 做一些清理工作, 在每个并行度上只调用一次, 而且是最后被调用
3. getRuntimeContext()方法提供了函数的RuntimeContext的一些信息,例如函数执行的并行度,任务的名字,以及state状态. 开发人员在需要的时候自行调用获取运行时上下文对象.
env.fromElements(1,2,3,4,5)
.map(new RichMapFunction<Integer, Integer>() {
@Override
public void open(Configuration parameters) throws Exception {
System.out.println("每个子任务只在创建时执行一次");
}
@Override
public void close() throws Exception {
System.out.println("每个子任务只在结束时执行一次");
}
@Override
public Integer map(Integer value) throws Exception {
return value * value;
}
})
.print();
5.3.2 flatMap
作用:传入一个元素并产生零个或多个元素
定义:public SingleOutputStreamOperator flatMap(FlatMapFunction<T, R> flatMapper)
使用:
source
.flatMap((Integer value, Collector<Integer> out) -> {
out.collect(value * value);
out.collect(value * value * value);
})
.returns(Types.INT)
//作用和filter类似
source
.flatMap((Integer value, Collector<Integer> out) -> {
if (value % 2 == 0) out.collect(value * value);
})
.returns(Types.INT)
//在使用Lambda表达式表达式的时候, 由于泛型擦除的存在, 在运行的时候无法获取泛型的具体类型, 全部当做Object来处理, 及其低效, 所以Flink要求当参数中有泛型的时候, 必须明确指定泛型的类型.
5.3.4 keyBy
作用:把流中的数据分到不同的分区中.具有
相同key的元素
会分到同一个分区中.一个分区中可以有多重不同的key,在内部是使用的
hash分区
来实现的
定义:public KeyedStream<T, K> keyBy(KeySelector<T, K> key)
说明:
--什么值不可以作为KeySelector的Key:
1. 没有覆写hashCode方法的POJO, 而是依赖Object的hashCode.因为这样分组没有任何的意义,每个元素都会得到一个独立无二的组,可以运行, 但是分的组没有意义
2. 任何类型的数组
使用:
env
.fromElements(new WaterSensor("sensor_1", 1000L, 10), new WaterSensor("sensor_2", 2000L, 20), new WaterSensor("sensor_1", 3000L, 30), new WaterSensor("sensor_1", 4000L, 40))
.keyBy(WaterSensor::getId)
.sum("vc")
.print();
env
.fromElements(new WaterSensor("sensor_1", 1000L, 10), new WaterSensor("sensor_2", 2000L, 20), new WaterSensor("sensor_1", 3000L, 30), new WaterSensor("sensor_1", 4000L, 40))
.keyBy(WaterSensor::getId)
.sum("vc")
.print();
//使用的是分组内第一条数据变量,由于数据是一条一条的进
怎么分区:
new KeyGroupStreamPartitioner<>(keySelector, StreamGraphGenerator.DEFAULT_LOWER_BOUND_MAX_PARALLELISM))
DEFAULT_LOWER_BOUND_MAX_PARALLELISM = 128 maxParallelism
return KeyGroupRangeAssignment.assignKeyToParallelOperator(key, maxParallelism, numberOfChannels);
key 0 1
maxParallelism = 128
numberOfChannels = 2
return computeOperatorIndexForKeyGroup(maxParallelism, parallelism, assignToKeyGroup(key, maxParallelism));
assignToKeyGroup(key, maxParallelism) = MathUtils.murmurHash(keyHash) % maxParallelism [0,128)
[0,127] * 2 / 128
keyGroupId * parallelism / maxParallelism;
5.3.5 shuffle
作用:把流中的元素随机打乱,进入不同分区中,对于可能产生数据倾斜的地方可以使用
示例:
env
.fromElements(10, 3, 5, 9, 20, 8)
.shuffle()
.print();
env.execute();
5.3.6 split和select
已经过时, 在1.12中已经被移除
作用:
在某些情况下,我们需要将数据流根据某些特征拆分成两个或者多个数据流,给不同数据流增加标记以便于从流中取出.
split用于给流中的每个元素添加标记. select用于根据标记取出对应的元素, 组成新的流.
// 奇数一个流, 偶数一个流
SplitStream<Integer> splitStream = env
.fromElements(10, 3, 5, 9, 20, 8)
.split(value -> value % 2 == 0
? Collections.singletonList("偶数")
: Collections.singletonList("奇数"));
splitStream
.select("偶数")
.print("偶数");
splitStream
.select("奇数")
.print("奇数");
env.execute();
5.3.7 connect
作用:Flink中的connect算子可以连接两个保持他们类型的数据流,两个数据流被connect之后,只是被放在了一个同一个流中,内部依然保持各自的数据和形式不发生任何变化,两个流相互独立。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HnJkZmsu-1632745444877)(C:\Users\guanh\AppData\Roaming\Typora\typora-user-images\1608549329941.png)]
示例:
DataStreamSource<Integer> intStream = env.fromElements(1, 2, 3, 4, 5);
DataStreamSource<String> stringStream = env.fromElements("a", "b", "c");
// 把两个流连接在一起: 貌合神离
ConnectedStreams<Integer, String> cs = intStream.connect(stringStream);
cs.getFirstInput().print("first");
cs.getSecondInput().print("second");
env.execute();
注意:
1. 两个流中存储的数据类型可以不同
2. 只是机械的合并在一起, 内部仍然是分离的2个流
3. 只能2个流进行connect, 不能有第3个参与
5.3.8 union
作用:对
两个或者两个以上
的DataStream进行union操作,产生一个包含所有DataStream元素的新DataStream
示例:
DataStreamSource<Integer> stream1 = env.fromElements(1, 2, 3, 4, 5);
DataStreamSource<Integer> stream2 = env.fromElements(10, 20, 30, 40, 50);
DataStreamSource<Integer> stream3 = env.fromElements(100, 200, 300, 400, 500);
// 把多个流union在一起成为一个流, 这些流中存储的数据类型必须一样: 水乳交融
stream1
.union(stream2)
.union(stream3)
.print();
connect与 union 区别:
1. union之前两个流的类型必须是一样,connect可以不一样
2. connect只能操作两个流,union可以操作多个
5.3.9 简单滚动聚合算子
常见的滚动聚合算子:sum,min,max,minBy,maxBy
作用:KeyedStream的每一个支流做聚合。执行完成后,会将聚合的结果合成一个流返回,所以结果都是DataStream
参数:如果流中存储的是POJO或者scala的样例类, 参数使用字段名,
如果流中存储的是元组, 参数就是位置(基于0…)
DataStreamSource<Integer> stream = env.fromElements(1, 2, 3, 4, 5);
KeyedStream<Integer, String> kbStream = stream.keyBy(ele -> ele % 2 == 0 ? "奇数" : "偶数");
kbStream.sum(0).print("sum");
kbStream.max(0).print("max");
kbStream.min(0).print("min");
env
.fromCollection(waterSensors)
.keyBy(WaterSensor::getId);
.sum("vc")
.print("...");
env
.fromCollection(waterSensors)
.keyBy(WaterSensor::getId);
.maxBy("vc", false)
.print("max...");
//maxBy和minBy可以指定当出现相同值的时候,其他字段是否取第一个. true表示取第一个, false表示取最后一个.
5.3.10 reduce
作用:一个分组数据流的聚合操作,合并当前的元素和上次聚合的结果,产生一个新的值,返回的流中包含每一次聚合的结果,而不是只返回最后一次聚合的最终结果。为什么还要把中间值也保存下来? 考虑流式数据的特点: 没有终点, 也就没有最终的概念了. 任何一个中间的聚合结果都是值!
示例:
env
.fromCollection(waterSensors)
.keyBy(WaterSensor::getId)
.reduce((value1, value2) -> {
System.out.println("reducer function ...");
return new WaterSensor(value1.getId(), value1.getTs(), value1.getVc() + value2.getVc());
})
.print("reduce...");
//聚合后结果的类型, 必须和原来流中元素的类型保持一致!
5.3.11 process
作用:process算子在Flink算是一个比较底层的算子, 很多类型的流上都可以调用, 可以从流中获取更多的信息(不仅仅数据本身)
聚合用:
env.socketTextStream("hadoop100", 9999)
.map(line -> {
String[] split = line.split(",");
return new WaterSensor(split[0], Long.valueOf(split[1]), Integer.valueOf(split[2]));
})
.keyBy(WaterSensor::getId)
.process(new KeyedProcessFunction<String, WaterSensor, Integer>() {
private Map<String, Integer> map = new HashMap<>();
@Override
public void processElement(WaterSensor value, Context ctx, Collector<Integer> out) throws Exception {
String key = ctx.getCurrentKey();
Integer sum = map.get(key);
map.put(key, sum == null ? value.getVc() : sum + value.getVc());
out.collect(map.get(key));
}
})
.print();
5.3.12 对流重新分区的几个算子
--shuffle
对流中的元素随机分区
--reblance
对流中的元素平均分布到每个区.当处理倾斜数据的时候, 进行性能优化
--rescale
同 rebalance一样, 也是平均循环的分布数据,但是是range的形式. 但是要比rebalance更高效, 因为rescale不需要通过网络, 完全走的"管道"
5.4 Sink
5.4.1 KafkaSink
添加Kafka Connector依赖
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-kafka_2.11</artifactId>
<version>1.11.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.75</version>
</dependency>
Sink到Kafka的示例代码
env.fromCollection(waterSensors)
.map(JSON::toJSONString)
.addSink(new FlinkKafkaProducer<String>("hadoop162:9092,hadoop163:9092", "sensor", new SimpleStringSchema()));
env.execute();
5.4.2 RedisSink
添加Redis Connector依赖
<!-- https://mvnrepository.com/artifact/org.apache.flink/flink-connector-redis -->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-redis_2.11</artifactId>
<version>1.1.5</version>
</dependency>
Sink到Redis的示例代码
class Flink_Sink_Redis {
public static void main(String[] args) throws Exception {
ArrayList<WaterSensor> waterSensors = new ArrayList<>();
waterSensors.add(new WaterSensor("sensor_1", 1607527992000L, 20));
waterSensors.add(new WaterSensor("sensor_1", 1607527994000L, 50));
waterSensors.add(new WaterSensor("sensor_1", 1607527996000L, 50));
waterSensors.add(new WaterSensor("sensor_2", 1607527993000L, 10));
waterSensors.add(new WaterSensor("sensor_2", 1607527995000L, 30));
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(2);
FlinkJedisPoolConfig conf = new FlinkJedisPoolConfig.Builder()
.setHost("hadoop162")
.setPort(6379)
.setMaxTotal(100)
.setTimeout(10000)
.setMaxIdle(10)
.build();
env.fromCollection(waterSensors)
.keyBy(WaterSensor::getId)
.sum("vc")
.addSink(new RedisSink<>(conf, new RedisMapper<WaterSensor>() {
@Override
public RedisCommandDescription getCommandDescription() {
return new RedisCommandDescription(RedisCommand.HSET, "sensor");
}
@Override
public String getKeyFromData(WaterSensor waterSensor) {
// 指的的是Hash的那个field
return waterSensor.getId();
}
@Override
public String getValueFromData(WaterSensor waterSensor) {
//保存的value
return JSON.toJSONString(waterSensor);
}
}));
env.execute();
}
}
5.4.3 ElasticsearchSink
添加Elasticsearch
Connector依赖
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-elasticsearch6_2.12</artifactId>
<version>1.11.2</version>
</dependency>
Sink到Elasticsearch的示例代码
class Flink_Sink_ES {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(2);
List<HttpHost> esHosts = Arrays.asList(new HttpHost("hadoop162", 9200), new HttpHost("hadoop163", 9200));
ElasticsearchSink.Builder<WaterSensor> esSinkBuilder = new ElasticsearchSink.Builder<>(esHosts, new ElasticsearchSinkFunction<WaterSensor>() {
@Override
public void process(WaterSensor waterSensor, RuntimeContext runtimeContext, RequestIndexer requestIndexer) {
IndexRequest index = Requests.indexRequest("sensor")
.type("_doc")
.source(JSON.toJSONString(waterSensor), XContentType.JSON);
requestIndexer.add(index);
}
});
//esSinkBuilder.setBulkFlushInterval(3000);
esSinkBuilder.setBulkFlushMaxActions(1);
env.socketTextStream("hadoop100", 9999)
.map(line -> {
String[] split = line.split(",");
return new WaterSensor(split[0], Long.valueOf(split[1]), Integer.valueOf(split[2]));
})
.keyBy(WaterSensor::getId)
.sum("vc")
.addSink(esSinkBuilder.build())
.setParallelism(1);
env.execute();
}
}
5.4.4 自定义Sink
Ø 导入Mysql驱动
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.49</version>
</dependency>
public static void main(String[] args) throws Exception {
ArrayList<WaterSensor> waterSensors = new ArrayList<>();
waterSensors.add(new WaterSensor("sensor_1", 1607527992000L, 20));
waterSensors.add(new WaterSensor("sensor_1", 1607527994000L, 50));
waterSensors.add(new WaterSensor("sensor_1", 1607527996000L, 50));
waterSensors.add(new WaterSensor("sensor_2", 1607527993000L, 10));
waterSensors.add(new WaterSensor("sensor_2", 1607527995000L, 30));
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(2);
env.fromCollection(waterSensors)
.addSink(new RichSinkFunction<WaterSensor>() {
private Connection conn;
private PreparedStatement ps;
@Override
public void open(Configuration parameters) throws Exception {
String url = "jdbc:mysql://hadoop162:3306/test?useSSL=false";
String sql = "REPLACE into sensor values(?,?,?)";
conn = DriverManager.getConnection(url, "root", "aaaaaa");
ps = conn.prepareStatement(sql);
}
@Override
public void close() throws Exception {
if (ps != null) ps.close();
if (conn != null) conn.close();
}
@Override
public void invoke(WaterSensor value, Context context) throws Exception {
ps.setString(1, value.getId());
ps.setLong(2, value.getTs());
ps.setInt(3, value.getVc());
ps.execute();
}
});
env.execute();
}
6. Flink高阶编程
7.1 Flink的window机制
7.1.1 窗口概述
流式计算是一种被设计用于**处理无限数据集**的数据处理引擎,而无限数据集是指一种不断增长的本质上无限的数据集,而Window窗口是一种切割无限数据为有限块进行处理的手段。
在Flink中, 窗口(window)是处理无界流的核心. 窗口把流切割成有限大小的多个"存储桶"(bucket), 我们在这些桶上进行计算.
7.1.2 窗口的分类
窗口分为3类:
1. 基于时间的窗口(时间驱动)
2. 基于元素个数的(数据驱动)
3. 全局窗口
在每一个子任务中都会创建一个窗口,窗口结束时才会输出最后的结果
7.1.2.1 基于时间的窗口
时间窗口包含一个开始时间戳(包括)和结束时间戳(不包括), 这两个时间戳一起限制了窗口的尺寸.
滚动窗口(Tumbling Windows)
滚动窗口有固定的大小, 窗口与窗口之间不会重叠也没有缝隙.比如,如果指定一个长度为5分钟的滚动窗口, 当前窗口开始计算, 每5分钟启动一个新的窗口.
示例代码:
env
.socketTextStream("hadoop100", 9999)
.flatMap(new FlatMapFunction<String, Tuple2<String, Long>>() {
@Override
public void flatMap(String value, Collector<Tuple2<String, Long>> out) throws Exception {
Arrays.stream(value.split("\\W+")).forEach(word -> out.collect(Tuple2.of(word, 1L)));
}
})
.keyBy(t -> t.f0)
.window(TumblingProcessingTimeWindows.of(Time.seconds(8))) // 添加滚动窗口
.sum(1)
.print();
1. 时间间隔可以通过: Time.milliseconds(x), Time.seconds(x), Time.minutes(x),等等来指定.
2. 我们传递给window函数的对象叫窗口分配器.
滑动窗口(Sliding Windows)
滑动窗口也是有固定的长度. 另外一个参数我们叫滑动步长, 用来控制滑动窗口启动的频率.
所以, 如果滑动步长小于窗口长度, 滑动窗口会重叠. 这种情况下, 一个元素可能会被分配到多个窗口中
例如, 滑动窗口长度10分钟, 滑动步长5分钟, 则, 每5分钟会得到一个包含最近10分钟的数据.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u8gh09ZH-1632745444878)(C:\Users\guanh\AppData\Roaming\Typora\typora-user-images\1608620522198.png)]
env
.socketTextStream("hadoop102", 9999)
.flatMap(new FlatMapFunction<String, Tuple2<String, Long>>() {
@Override
public void flatMap(String value, Collector<Tuple2<String, Long>> out) throws Exception {
Arrays.stream(value.split("\\W+")).forEach(word -> out.collect(Tuple2.of(word, 1L)));
}
})
.keyBy(t -> t.f0)
.window(SlidingProcessingTimeWindows.of(Time.seconds(10), Time.seconds(5))) // 添加滚动窗口
.sum(1)
.print();
会话窗口(Session Windows)
会话窗口分配器会根据活动的元素进行分组,一组一个窗口,. 会话窗口不会有重叠, 与滚动窗口和滑动窗口相比, 会话窗口也没有固定的开启和关闭时间.
如果会话窗口有一段时间没有收到数据, 会话窗口会自动关闭, 这段没有收到数据的时间就是会话窗口的gap(间隔)
我们可以配置静态的gap, 也可以通过一个gap extractor 函数来定义gap的长度. 当时间超过了这个gap, 当前的会话窗口就会关闭, 后序的元素会被分配到一个新的会话窗口
因为会话窗口没有固定的开启和关闭时间, 所以会话窗口的创建和关闭与滚动,滑动窗口不同. 在Flink内部, 每到达一个新的元素都会创建一个新的会话窗口, 如果这些窗口彼此相距比较定义的gap小, 则会对他们进行合并. 为了能够合并, 会话窗口算子需要合并触发器和合并窗口函数: ReduceFunction, AggregateFunction, or ProcessWindowFunction
//1.静态gap
.window(ProcessingTimeSessionWindows.withGap(Time.seconds(10)))
//2.动态gap
.window(ProcessingTimeSessionWindows.withDynamicGap(new SessionWindowTimeGapExtractor<Tuple2<String, Long>>() {
@Override
public long extract(Tuple2<String, Long> element) { // 返回 gap值, 单位毫秒
return element.f0.length() * 1000;
}
}))
7.1.2.2 基于元素个数的窗口
按照指定的数据条数生成一个Window,与时间无关
滚动窗口
//默认的CountWindow是一个滚动窗口,只需要指定窗口大小即可,当元素数量达到窗口大小时,就会触发窗口的执行,分组后,一个组一个窗口。
.countWindow(3)
滑动窗口
//滑动窗口和滚动窗口的函数名是完全一致的,只是在传参数时需要传入两个参数,一个是window_size,一个是sliding_size。下面代码中的sliding_size设置为了2,也就是说,每收到两个相同key的数据就计算一次,每一次计算的window范围是3个元素。
.countWindow(3, 2)
7.1.3 Window Function
指定了窗口的分配器后,接着可以指定如何计算,由*window function*来负责. 一旦窗口关闭, *window function* 去计算处理窗口中的每个元素,*window function* 可以是ReduceFunction,AggregateFunction,ProcessWindowFunction中的任意一种
ReduceFunction,AggregateFunction更加高效, 原因就是Flink可以对到来的元素进行增量聚合(在前面聚合结果的基础上聚合), ProcessWindowFunction 可以得到一个包含这个窗口中所有元素的迭代器, 以及这些元素所属窗口的一些元数据信息.
env
.socketTextStream("hadoop162", 9999)
.flatMap(new FlatMapFunction<String, Tuple2<String, Long>>() {
@Override
public void flatMap(String value, Collector<Tuple2<String, Long>> out) throws Exception {
String[] s = value.split(" ");
for (String word : s) {
out.collect(Tuple2.of(word, 1L));
}
}
})
.keyBy(t -> t.f0)
.window(TumblingProcessingTimeWindows.of(Time.seconds(10)))
/*.reduce(new ReduceFunction<Tuple2<String, Long>>() {
@Override
public Tuple2<String, Long> reduce(Tuple2<String, Long> value1, Tuple2<String, Long> value2) throws Exception {
System.out.println("reduce...");
return Tuple2.of(value1.f0, value1.f1 + value2.f1);
}
})*/
/*.aggregate(new AggregateFunction<Tuple2<String, Long>, Long, String>() {
// 创建累加器
@Override
public Long createAccumulator() {
System.out.println("createAccumulator...");
return 0L;
}
// 聚合
@Override
public Long add(Tuple2<String, Long> value, Long accumulator) {
System.out.println("add...");
return accumulator + value.f1;
}
// 返回窗口关闭之后的聚合结果
@Override
public String getResult(Long accumulator) {
System.out.println("getResult...");
return "最终结果: " + accumulator;
}
// 合并累计器 只有在 SessionWindow才会使用
@Override
public Long merge(Long a, Long b) {
System.out.println("merge...");
return a + b;
}
})*/
.process(new ProcessWindowFunction<Tuple2<String, Long>, Long, String, TimeWindow>() {
@Override
public void process(String key,
Context context,
Iterable<Tuple2<String, Long>> elements,
Collector<Long> out) throws Exception {
System.out.println("process...");
}
})
.print();
7.1.4 窗口的生命周期(重点)
-
窗口的开始时间、结束时间怎么确定
TumblingEventTimeWindows.java 的 74行 timestamp:当前数据的度量时间 windowSize:窗口大小 窗口开始时间:start = timestamp - (timestamp - offset + windowSize) % windowSize; offset是用来调整时差的 +windowsize 防止 (timestamp - offset + windowSize)为负,这样会导致窗口的开始时间提前,导致有的数据进不来 算法,取窗口长度的整数倍(以1970年为开始) 1549044122 - (1549044122 + 5)% 5 = 1549044120 end = start + windowSize 1549044120 + 5 = 1549044125 => 窗口的划分 [start,end)
-
窗口什么时候创建?
TumblingEventTimeWindows.java 的 75 行 Collections.singletonList(new TimeWindow(start, start + size)) 属于本窗口的 第一条数据 来的时候, new 一个 Window 单例的,后面来的数据,不会再重复创建窗口
-
窗口什么时候触发计算?
EventTimeTrigger.java 的 38行 当 window.maxTimestamp() <= ctx.getCurrentWatermark() 的时候 =》 watermark >= windowEnd - 1ms 触发计算
-
窗口什么时候销毁?
EventTimeTrigger.java 的 38行 当 window.maxTimestamp() + 允许迟到时间戳 <= ctx.getCurrentWatermark() 的时候 =》 watermark >= windowEnd - 1ms 触发计算
-
窗口为什么是左闭右开?
TimeWindow.java 的 85行 maxTimestamp = end - 1ms
怎么知道当前来的数据,属于哪个窗口?也就是,窗口是怎么划分的?
窗口划分:TumblingEventTimeWindows类 assignWindows方法
=》 窗口开始时间:timestamp - (timestamp + windowSize) % windowSize
=》 窗口结束时间:new TimeWindow(start, start + size) =》 start + size
=》 窗口是左闭右开: 属于窗口的最大时间戳为 maxTimestamp = end - 1
窗口触发条件:window.maxTimestamp() <= ctx.getCurrentWatermark()
Watermark>=maxTimes
=》 由watermark触发窗口的计算,当 watermark >=窗口最大时间戳时(窗口结束时间),触发窗口计算,
watermark>窗口最大时间戳(窗口结束时间)+允许迟到时间,关闭窗口(乱序,且设置了允许迟到时间)
7.2 Keyed vs Non-Keyed Windows
其实, 在用window前首先需要确认应该是在keyBy后的流上用, 还是在没有keyBy的流上使用.
在keyed streams上使用窗口, 窗口计算被并行的运用在多个slotTask上, 可以认为每个task都有自己单独窗口. 正如前面的代码所示.
在非non-keyed stream上使用窗口, 流的并行度只能是1, 所有的窗口逻辑只能在一个单独的task上执行.
.windowAll**(**TumblingProcessingTimeWindows.*of***(**Time.*seconds***(**10**)))**
需要注意的是: 非key分区的流, 即使把并行度设置为大于1 的数, 窗口也只能在某个分区上使用
7.3 Flik中的时间语义与WaterMark
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qXHZdHU6-1632745444879)(C:\Users\guanh\AppData\Roaming\Typora\typora-user-images\1608638711152.png)]
Flink是一个事件驱动的计算框架,kafka也是事件驱动的,也就是以事件发生的时间为准
--处理时间(process time)
是每一个执行基于时间操作的算子的本地系统时间,与机器相关
--事件时间(event time)
事件时间是指的这个事件发生的时间,它通常由事件中的时间戳描述,例如采集的日志数据中,每一条日志都会记录自己的生成时间,Flink通过时间戳分配器访问事件时间戳。
--Ingestion Time:是数据进入Flink的时间。
7.3.1 Flink中的WaterMark(重点)
Flink中去测量事件时间的进度的机制就是 watermark(水印). watermark作为数据流的一部分在流动, 并且携带一个时间戳t.
一个Watermark(t)表示在这个流里面事件时间已经到了时间t, 意味着此时, 流中不应该存在这样的数据: 他的时间戳t2<=t (时间比较旧或者等于时间戳)
Watermark的理解:
1. 用来衡量事件时间的进展
2. 本身是一个特殊的时间戳
3. 触发窗口的计算、输出和关闭
4. 单调递增的
5. 解决乱序的问题
6. 触发定时器
有序流中的水印
事件是有序的(按照他们自己的时间戳来看),
watermark是流中一个简单的周期性的标记
乱序流中的水印
从时间戳来看, 这些事件是乱序的,来到flink的时间和事件时间不一致, 则watermark对于这些乱序的流来说至关重要。通常情况下, 水印是一种标记, 是流中的一个点,如果他大于窗口结束时间-1ms(window.maxTimestamp() <= ctx.getCurrentWatermark()),那么会引发窗口的计算
7.3.4 Flink中如何产生水印
- flink默认的在onPeriodicEmit中
7.3.5 EventTime和WaterMark的使用
Flink内置了两个WaterMark生成器:
1. Monotonously Increasing Timestamps(时间戳单调增长:其实就是允许的延迟为0)
WatermarkStrategy.forMonotonousTimestamps();
2. Fixed Amount of Lateness(允许固定时间的延迟)
WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(10));
7.3.6 WaterMark 知识点(重点)
/**
*dsf fdsf
* TODO 1.watermark的概念和理解
* 1、表示 事件时间的 进展
* 2、解决 乱序 的问题
* 3、是一个特殊的 时间戳,插入到流里面
* 4、单调递增的
* 5、触发 窗口的 计算、关窗
* 6、Flink认为,在 wm之前的数据都处理完了
* 7、触发定时器
*
* TODO 2.watermark的使用方式
* 1.WatermarkStrategy
* .<T>forMonotonousTimestamps()
* .withTimestampAssigner(指定 事件时间 的提取)
* 2.WatermarkStrategy
* .<T>forBoundedOutOfOrderness(Duration乱序程度)
* .withTimestampAssigner(指定 事件时间 的提取)
*
* TODO 3.watermark的生成方式
* 1、周期性(onPeriodicEmit): 官方提供的都是 周期性,默认周期 200ms
* 比如读文件的时候,会发现所有窗口的对应的wm,都是long的最大值
* 2、间歇性(onEvent): 来一条数据 就 生成一次 watermark
*
* TODO 4.watermark在多并行度下的确定
* 以最小的为准,类比 木桶原理
*
* TODO 5.watermark的传递
* 一 对 多 =》 广播
* 多 对 一 =》 取最小
* 多 对 多 =》 前面两者的结合
*
* TODO 6.迟到数据的处理 - 窗口允许迟到
* 1.当 wm 达到 windowEnd的时候,会正常的计算输出,但是不会关窗
* 2.当 windowEnd <= wm <= windowEnd+允许迟到的时间,每来一条 迟到数据 ,都会触发一次计算,更新之前的结果
* 3.当 wm >= windowEnd + 允许迟到的时间,真正的关窗,再有迟到的数据,不会计算
*
* TODO 7.迟到数据的处理 - 侧输出流(关窗之后的迟到数据)
* 放入到侧输出流里
* 注意 OutputTag要用 匿名内部类, Tag主要区分在于 名字
*
* TODO 8.Watermark版本区别
* 在 1.10(包含)之前,没有 WatermarkStrategy,按照如下使用:
* new BoundedOutOfOrdernessTimestampExtractor
* new AscendingTimestampExtractor
*
* TODO 9.对 乱序、迟到 数据的处理总结
* 1.watermark
* 2.窗口允许迟到
* 3.侧输出流
*
*/
读文件的问题:
1、为了保证所有的数据都被计算、所有的窗口都被触发,Flink会在文件结尾时,将watermark设置为 Long的最大值
2、为什么所有窗口是一起触发的?
watermark更新周期是200ms,还没更新就要结束了,watermark从 初始值 一下子变成 Long的最大值
会话窗口:
waterMark和上一条数据间隔时间 >= 指定间隔时间时触发窗口计算和关闭
定时器(Timer)
使用:
--定时器注册
1. 每调用一次,都会 new一个Timer对象,添加到一个队列里
2. 这个队列是 去重的 =》 如果 重复注册相同时间的定时器, 只会注册一个
3. 去重只会在同一个分组内去重, 因为不同分组是隔离的,是分开算的
--定时器触发
timer.getTimestamp() <= time
=> 注册的时间 <= watermark ( 注意watermark的 1ms问题)
watermark是在一个并行度内,而不是一个组
//基于事件时间,ctx.timestamp()只能获取事件时间,如果不是或没有就为null
ctx.timerService().registerEventTimeTimer(ctx.timestamp() + 5000L);
//基于处理时间
ctx.timerService().registerProcessingTimeTimer(ctx.timestamp() + 5000L);
7. 状态编程和容错机制**
7.1 状态
7.1.1 要点
1.什么是状态
保存起来的数据,可以是历史的计算结果,也可以是数据本身
2.状态分类
1.算子状态
作用范围:算子
一个并行任务 维护 一个状态 =》 同一个算子的多个并行任务之间,状态不共享
实现方式: 继承 一个接口(CheckpointedFunction),重写两个方法(快照、初始 化)
主要用在: source
数据结构: List
2.键控状态
作用范围: 同一分组
每个分组 维护 一个状态 =》 即使 多个分组在同一个 并行任务中, 每个分组还是各 自维护一个状态
数据结构: value、list、map、reducing、aggregating
使用步骤: 定义 -> open里 创建 -> 使用
3.状态后端干什么?
1. 本地状态的管理
2. 完成 checkpoint的远程存储
4.状态后端的分类
1. MemoryStateBackend
本地状态:
存在TaskManager内存,
state maxStateSize默认5M,maxStateSize<=akka。framesize 默认 10M
用于本地测试,不推介生产环境使用
checkpoint:JobManager内存
2. FsStateBackend
本地状态: TaskManager内存
checkpoint: 外部文件系统(HDFS)
适用场景:可以用于生产环境,主要是 分钟级窗口, 又大又长的状态
3. RocksDBStateBackend
本地状态:TaskManager所在节点的RocksDB中(内存+磁盘)
checkpoint: 外部文件系统(HDFS),增量同步
适用场景:用于生产环境,可以支持 天级窗口,超大超长的状态,影响一点本地状态的 读写效率
5. 状态后端的指定
1. 方式一: 配置文件 指定 默认的状态后端
2. 方式二: 代码里指定, 执行环境 set ,如果是RocksDB,要先导入依赖
3. 结合 开启 checkpoint,checkpoint默认是不打开的
6. 一致性级别
1. at-most-once: 可能丢,不会重
2. at-least-once:不会丢,可能重
3. exactly-once: 不会丢,不会重
7.端到端一致性
1.source端: 可重置,可以重新获取数据
2.flink内部: checkpoint算法保证
3.sink端:幂等写入、事务性写入(WAL、2PC)
事务性写入 都提供了 模板类
7.1.2 sink一致性
1. 幂等会出现暂时不一致:
是指一批数据回滚后,在发生故障前这批数据已经有写入sink的了,回滚会重新重播这部分数 据,但是它是幂等操作,所以还是保证了Exactly-once。
2. 预写日志(Write-Ahead-Log)
① 把结果数据先当成状态保存,然后收到checkpoint完成的通知时,一次性写入sink系统。
② 由于数据提前在状态后端(state backend)中做了缓存,所以无论什么 sink 系统,都能 用这种方式一批搞定
③ DataStream API提供了一个模板类:GenericWriteAheadSink来实现这种事务性sink
3. 两阶段提交two-parse commit
① 对于每个checkpoint,sink任务会启动一个事务,并将接下来所有接收的数据添加到事务里。
② 将这些数据写入外部sink系统,但是不提交它们,只是“预提交”。
比如写到kafka,先写到kafka预提交的事务(还不能被消费)
③ 当它收到checkpoint完成的通知时,才正式提交事务,实现结果的真正写入。
④ 这种方式真正实现了exactly-once。
⑤ 它需要一个提供事务支持的外部sink系统,Flink提供了TwoPhaseCommitSinkFunction 接口。
4. TwoPhaseCommitSink对外部sink系统的要求
① 外部系统必须提供事务支持,或者 sink 任务必须能够模拟外部系统上的事务
② 在 checkpoint 的间隔期间里,必须能够开启一个事务并接受数据写入
③ 在收到 checkpoint 完成的通知之前,事务必须是“等待提交”的状态。
在故障恢复的情况下,这可能需要一些时间。如果这个时候sink系统关闭事务(例如超 时了),那么未提交的数据就会丢失
④ sink 任务必须能够在进程失败后恢复事务
⑤ 提交事务必须是幂等操作
7.1.3 CheckPoint算法
Flink检查点算法的正式名称是异步分界线快照(asynchronous barrier
snapshotting)。该算法大致基于Chandy-Lamport分布式快照算法,它使Flink可以保证exactly-once,
异步:
-
Flink的检查点算法
1. JM中的 Checkpoint Coordinator 向所有的source节点 trigger Checkpoint,JM会生 成一种特殊的数据 barrier,每个task收到它后,就会触发自己的Checkpoint 2. source节点向下游广播barrier,barrier 就是实现 Chandy-Lamport 分布式快照算法 的核心,下游的 task 只有收到所有 input 的 barrier 才会执行相应的Checkpoint, 将当前算子的状态保存到对应的地方(HDFS),完成以后会通知JM,state保存在哪个地 方 3. 当sink收到barrier时,sink会触发下一轮事务,Checkpoint也完成后,JM收到所有的通 知以后,会认为本次全局的Checkpoint已经完成,JM会将这些checkpoint元数据信息存 储到状态后端,然后通知所有的task本次完成,sink收到消息后,才会进行第二阶 段 的提交,或者是触发预写日志的提交,
-
barrier
1. barrier对齐 每个task从input收到CheckPointbarrier n后,它就不能处理来自该流的任何数据, 只会将数据保存在该Task的buffer中,直到收到所有的barrier,才会触发当前的 Check-point,然后处理缓冲区的数据,这就是能保证exactly-once的原因,如果迟迟 不来,就可能造成背压 这种方式会有一点点影响性能 2. barrier不对齐 每个task只要没有收到所有的barrier就会继续计算,当所有的barrier来齐以后,就会 触发checkpoint,有可能造成数据的重复。
7.1.4 Flink+Kafka实现端到端的Exactly-Once语义
端到端的状态一致性的实现,需要每一个组件都实现
1. source:
kafka consumer作为source,可以将偏移量保存下来,如果后续任务出现了故障,恢复的 时候可以由连接器重置偏移量,重新消费数据,保证一致性
对于source而言,它会把当前的offset作为状态保存起来,下次从checkpoint恢复 时,source任务可以重新提交偏移量,从上次保存的位置开始重新消费数据
2. flink内部:
利用checkpoint机制,把状态存盘,发生故障的时候可以恢复,保证内部的状态一致性
3. sink:
kafka producer作为sink,采用两阶段提交 sink,需要实现一个 TwoPhaseCommitSink-
Function
1) 第一条数据来了之后,开启一个 kafka 的事务(transaction),正常写入 kafka 分 区日志但标记为未提交(对消费者不可见),这就是“预提交”
2) jobmanager 触发 checkpoint 操作,barrier 从 source 开始向下传递,遇到 barrier 的算子将状态存入状态后端,并通知 jobmanager
3) sink 连接器收到 barrier,保存当前状态,存入 checkpoint,通知 jobmanager, 并开启下一阶段的事务,用于提交下个检查点的数据
4) jobmanager 收到所有任务的通知,发出确认信息,表示 checkpoint 完成
5) sink 任务收到 jobmanager 的确认信息,正式提交这段时间的数据
6) 外部kafka关闭事务,提交的数据可以正常消费了
7.2 广播状态
1. 通过流广播:ds.broadcast(new MapStateDescriptor)
2. 与另外一个流连接:ds1.connect(bs2)
3. 在BroadcastProcessFunction中去处理
适用场景:动态配置更新、 规则匹配、 控制执行逻辑(类似开关)
广播的流bs: 数据量小、 更新慢
7.2 保存点
1、Flink 还提供了可以自定义的镜像保存功能,就是保存点(savepoints)
2、原则上,创建保存点使用的算法与检查点完全相同,因此保存点可以认为就是具有一些额外元数据的检查点
3、Flink不会自动创建保存点,因此用户(或外部调度程序)必须明确地触发创建操作
4、保存点是一个强大的功能。除了故障恢复外,保存点可以用于:有计划的手动备份,更新应用程序,版本迁移,暂停和重启应用,等等
7.3 参数配置
// Checkpoint常用配置(ck默认是禁用)
// 传入5000表示生成barrier的周期,第二个参数表示CheckpointingMode为对齐,默认就是EXACTLY_ONCE
env.enableCheckpointing(5000, EXACTLY_ONCE); //生产环境中百万级数据 10min
// env.getCheckpointConfig().setCheckpointingMode();
// 设置ck超时时间,如果barrier迟迟不来,任务就会失败
env.getCheckpointConfig().setCheckpointTimeout(60000L);
// 异步ck,同时最多有多少个checkpoint在执行,如果过多的话,可能导致barrier迟迟不能对齐,造成阻塞
env.getCheckpointConfig().setMaxConcurrentCheckpoints(2);
// 默认为 false,表示从 ck恢复;true,从savepoint恢复
env.getCheckpointConfig().setPreferCheckpointForRecovery(false);
// 上一个ck结束后,到下一个ck开启,最小间隔多久,百万级数据配3-5分钟
env.getCheckpointConfig().setMinPauseBetweenCheckpoints(500L);
8.CEP
8.1 什么是 CEP
1. 复杂事件处理(Complex Event Processing,CEP)
2. Flink CEP是在 Flink 中实现的复杂事件处理(CEP)库
3. CEP 允许在无休止的事件流中检测事件模式,让我们有机会掌握数据中重要的部分
4. 一个或多个由简单事件构成的事件流通过一定的规则匹配,然后输出用户想得到的数据 —— 满足规则的复杂事件
8.2 Pattern API
1、个体模式(Individual Patterns)
组成复杂规则的每一个单独的模式定义,就是“个体模式”
2、组合模式(Combining Patterns,也叫模式序列)
很多个体模式组合起来,就形成了整个的模式序列
模式序列必须以一个“初始模式”开始:
3、模式组(Groups of patterns)
将一个模式序列作为条件嵌套在个体模式里,成为一组模式
8.3 个体模式(Individual Patterns)
1、个体模式可以包括“单例(singleton)模式”和“循环(looping)模式”
2、单例模式只接收一个事件,而循环模式可以接收多个
量词(Quantifier):可以在一个个体模式后追加量词,也就是指定循环次数
start.times(4)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-grAQi8Vt-1632745444880)(C:\Users\guanh\AppData\Roaming\Typora\typora-user-images\1627469636917.png)]
1、条件(Condition)
每个模式都需要指定触发条件,作为模式是否接受事件进入的判断依据
CEP 中的个体模式主要通过调用 .where() .or() 和 .until() 来指定条件
按不同的调用方式,可以分成以下几类:
2、简单条件(Simple Condition)
通过 .where() 方法对事件中的字段进行判断筛选,决定是否接受该事件
3、组合条件(Combining Condition)
将简单条件进行合并;.or() 方法表示或逻辑相连,where 的直接组合就是
pattern.where(new Condition).or()
4、终止条件(Stop Condition)
如果使用了 oneOrMore 或者 oneOrMore.optional,建议使用 .until() 作为终止条件,以便清理状态
5、迭代条件(Iterative Condition)
能够对模式之前所有接收的事件进行处理
.where(new IterativeCondition<Event>(){...})
8.3 模式序列
1、严格近邻(Strict Contiguity)
所有事件按照严格的顺序出现,中间没有任何不匹配的事件,由 .next() 指定
例如对于模式”a next b”,事件序列 [a, c, b1, b2] 没有匹配
2、宽松近邻( Relaxed Contiguity )
允许中间出现不匹配的事件,由 .followedBy() 指定
例如对于模式”a followedBy b”,事件序列 [a, c, b1, b2] 匹配为 {a, b1}
3、非确定性宽松近邻( Non-Deterministic Relaxed Contiguity )
进一步放宽条件,之前已经匹配过的事件也可以再次使用,由 .followedByAny() 指定
例如对于模式”a followedByAny b”,事件序列 [a, c, b1, b2] 匹配为 {a, b1},{a, b2}
除以上模式序列外,还可以定义“不希望出现某种近邻关系”:
.notNext() —— 不想让某个事件严格紧邻前一个事件发生
.notFollowedBy() —— 不想让某个事件在两个事件之间发生
需要注意:
所有模式序列必须以 .begin() 开始
模式序列不能以 .notFollowedBy() 结束
“not” 类型的模式不能被 optional 所修饰
此外,还可以为模式指定时间约束,用来要求在多长时间内匹配有效
next.within(Time.seconds(10))
8.4 模式的检查
1、指定要查找的模式序列后,就可以将其应用于输入流以检测潜在匹配
调用 CEP.pattern(),给定输入流和模式,就能得到一个 PatternStream
8.5 匹配事件的提取
创建 PatternStream 之后,就可以应用 select 或者 flatselect 方法,从检测到的事件序列中提取事件了
select() 方法需要输入一个 select function 作为参数,每个成功匹配的事件序列都会调用它
select() 以一个 Map<String,List <IN>> 来接收匹配到的事件序列,其中 key 就是每个模式的名称,而 value 就是所有接收到的事件的 List 类型
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ujZcrE1q-1632745444880)(C:\Users\guanh\AppData\Roaming\Typora\typora-user-images\1627470270277.png)]
8.6 超时事件的提取
1、当一个模式通过 within 关键字定义了检测窗口时间时,部分事件序列可能因为超过窗口长度而被丢弃;为了能够处理这些超时的部分匹配,select 和 flatSelect API 调用允许指定超时处理程序
2、超时处理程序会接收到目前为止由模式匹配到的所有事件,由一个 OutputTag 定义接收到的超时事件序列
9. 案例
9.1 实时窗口TopN思路
1.Environment
2.Source
3.Transform
=> 3.1 转换数据类型、指定watermark生成、事件时间提取
=> 3.2 能过滤就先过滤 => 只需要pv行为,过滤出pv行为
=> 3.3 考虑 统计维度 => 统计维度是 商品,按照商品分组
=> 3.4 每隔5分钟输出最近一小时 => 滑动窗口,长度1小时,步长5分钟
=> 3.5 统计求和
=> 3.5.1 aggregate传两个参数 : 因为 聚合之后,就没有 窗口的 概念了
=> 3.5.1.1 第一个参数: AggregateFunction 进行增量聚合
=> 3.5.1.2 第二个参数: ProcessWindowFunction 对聚合后的结果,打上 窗口结束时间 的标签
=> 3.6 按照 窗口结束时间 分组 : 让 同一个窗口的 统计结果 到一起,进行 TopN的计算
=> 3.7 使用 process 进行排序
=> processElement() 是一条一条处理数据的,所以要先把 同一个时间的窗口的 统计结果 存起来
=> 同一小时的窗口,考虑用 状态 来保存数据,因为按照 窗口结束时间 分组,那就等于按照 窗口 隔离
=> 存到什么时候? => 注册一个定时器,注册时间 = 窗口结束时间 + 小延迟
=> OnTimer() 进行排序
=> 从状态里取出数据,放入到一个 List里
=> 调用 list的 sort方法, 实现一个 Comparator接口,定义为 降序: 后减前
=> 取前 N 个 : 如果要传参的方式,定义一个构造器
=> 注意 传参 与 实际list 的大小,进行比较,取小的, 避免 数组下标越界问题
4.Sink
9.2 黑名单加TopN
需求:用户点击广告TopN,如果用户在一天内点击了100次,就将用户拉入黑名单
1. 在上面3.3后,使用process实现黑名单过滤
2. 设置一个累加器状态用来累加用户点击次数,设置一个注册时间状态用来判断是否已经注册定时器,设置一个开关状态,来判断是否发生黑名单到侧输出流
3. processElement()中,注册一个零点的定时器,用来清空前一天的状态
4. 判断是否达到阈值
达到阈值,是恶意行为,就要过滤掉,不往后传递 => 不用 采集器 传递 = 丢弃
达到阈值,做一个告警 => 这边是实现过滤,那么就用 侧输出流实现告警
没到达阈值,往后传递,继续累加计数
5. 在onTimer中清空状态
6. 获取侧输出流 getSideOutput()
7. 要再做一次分组,接上面3.3
问题
-
资源规模
bin/yarn-session.sh -d -tm -jm(4-8g) -tm (6-8G)-s -qu(队列)
bin/flink -t 指定提交模式 -p 并行度 -s slot 的个数
最大就是yarn能提供的大小(NM的台数),TM的个数是不固定的,是提交job的时候,去RM动态获取的,不管是Session-Cluster 还是pro-job 都是可以动态的申请资源,使用完了会动态的释放到,不过session-cluster会开启一个flink集群而且任务完成后不会关,可以同时运行多个job,开启session的时候新版本中是指定不了TM的个数,会根据提交的job去集群获取
Flink通信架构
Flink 的网络协议栈是组成 flink-runtime 模块的核心组件之一,是每个 Flink 作业的核心。它连接所有 TaskManager 的各个子任务(Subtask),因此,对于 Flink 作业的性能包括吞吐与延迟都至关重要
-
TaskManager 和 JobManager 之间通过基于 Akka 的 RPC 通信的控制通道
-
TaskManager 之间的网络协议栈依赖于更加底层的 Netty API
数据传输:
高吞吐:Flink 不是一个一个地发送每条记录,而是将若干记录缓冲到其网络缓冲区中并一次性发送它们。这降低了每条记录的发送成本因此提高了吞吐量。
低延迟:当网络缓冲区超过一定的时间未被填满时会触发超时发送,通过减小超时时间,可以通过牺牲一定的吞吐来获取更低的延迟
合后的结果,打上 窗口结束时间 的标签
=> 3.6 按照 窗口结束时间 分组 : 让 同一个窗口的 统计结果 到一起,进行 TopN的计算
=> 3.7 使用 process 进行排序
=> processElement() 是一条一条处理数据的,所以要先把 同一个时间的窗口的 统计结果 存起来
=> 同一小时的窗口,考虑用 状态 来保存数据,因为按照 窗口结束时间 分组,那就等于按照 窗口 隔离
=> 存到什么时候? => 注册一个定时器,注册时间 = 窗口结束时间 + 小延迟
=> OnTimer() 进行排序
=> 从状态里取出数据,放入到一个 List里
=> 调用 list的 sort方法, 实现一个 Comparator接口,定义为 降序: 后减前
=> 取前 N 个 : 如果要传参的方式,定义一个构造器
=> 注意 传参 与 实际list 的大小,进行比较,取小的, 避免 数组下标越界问题
4.Sink
## 9.2 黑名单加TopN
需求:用户点击广告TopN,如果用户在一天内点击了100次,就将用户拉入黑名单
- 在上面3.3后,使用process实现黑名单过滤
- 设置一个累加器状态用来累加用户点击次数,设置一个注册时间状态用来判断是否已经注册定时器,设置一个开关状态,来判断是否发生黑名单到侧输出流
- processElement()中,注册一个零点的定时器,用来清空前一天的状态
-
判断是否达到阈值
达到阈值,是恶意行为,就要过滤掉,不往后传递 => 不用 采集器 传递 = 丢弃
达到阈值,做一个告警 => 这边是实现过滤,那么就用 侧输出流实现告警
没到达阈值,往后传递,继续累加计数 - 在onTimer中清空状态
- 获取侧输出流 getSideOutput()
- 要再做一次分组,接上面3.3
# 问题
1. 资源规模
bin/yarn-session.sh -d -tm -jm(4-8g) -tm (6-8G)-s -qu(队列)
bin/flink -t 指定提交模式 -p 并行度 -s slot 的个数
最大就是yarn能提供的大小(NM的台数),TM的个数是不固定的,是提交job的时候,去RM动态获取的,不管是Session-Cluster 还是pro-job 都是可以动态的申请资源,使用完了会动态的释放到,不过session-cluster会开启一个flink集群而且任务完成后不会关,可以同时运行多个job,开启session的时候新版本中是指定不了TM的个数,会根据提交的job去集群获取
# Flink通信架构
Flink 的网络协议栈是组成 flink-runtime 模块的核心组件之一,是每个 Flink 作业的核心。它连接所有 TaskManager 的各个子任务(Subtask),因此,对于 Flink 作业的性能包括吞吐与延迟都至关重要
1. TaskManager 和 JobManager 之间通过基于 Akka 的 RPC 通信的控制通道
2. TaskManager 之间的网络协议栈依赖于更加底层的 Netty API
数据传输:
高吞吐:Flink 不是一个一个地发送每条记录,而是将若干记录缓冲到其网络缓冲区中并一次性发送它们。这降低了每条记录的发送成本因此提高了吞吐量。
低延迟:当网络缓冲区超过一定的时间未被填满时会触发超时发送,通过减小超时时间,可以通过牺牲一定的吞吐来获取更低的延迟