1.Flink简介
Apache Flink是一个框架和分布式处理引擎,用于对无界和有界数据流进行状态计算
应用行业:市场营销报表,电商,业务流程
物联网,电信业,金融业
Flink的主要特点:事件驱动(Event-driven)
Flink的世界观中一切都是流组成的,离线数据是有界的流,实时数据是没有界限的流
分层API
High-level Analytics API : SQL/Table API(dynamic tables)
Stream- & Batch Data Porcessing :DataStream API(streams,windows)
Stateful Event-Driven Application : ProcessFuntion(events,state,time)
其他特点:
支持事件时间(event-time)和处理时间(processing-time)语义
精准一次(exactly-once)的状态一致性保证
低延迟,每秒处理数百万个事件,毫秒级延迟
与众多常用存储系统的连接
高可用,动态扩展,实现7*24全天候运行
Flink vs Spark Streaming
流(Stream)和微批(micro-batching)
2.快速上手
WorkCount案例:博客中查看
DataStreamSource<String> source.flatMap()
.keyBy("word")//key分组统计
.filter()
.map()
.timeWindow(Time.seconds(2),Time.seconds(2))//设置一个窗口函数,模拟数据流动
.sum("count")//计算时间窗口内的词语个数
3.Flink部署
1.Standalone模式单机
安装版本:Flink-1.10.1 Flink-1.10.1-bin-scala_2.12.tgz
控制台提交job
Overview
jobs
Task managers
job manager
submit new job
命令行提交job
2.Yarn模式
Flink on Yarn
Session Cluster
Per Job Cluster
3.Kubernetes部署
4.Flink运行架构
1、Flink运行时的组件
JobManger(作业管理器):全局管理,接收提交的.jar分析流程,生成执行计划图 执行的task,分发给taskManager
控制一个应用程序执行的主进程,也就是说,每个应用程序都会被一个不同的JobManager所控制执行。
JobManager会先接收到要执行的应用程序,这个应用程序包括:作业图(JobGraph)逻辑数据流图和打包了所有的类库和其他资源的Jar包
JobManager会把JobGraph转换成一个物理层面的数据流图,这个图叫做执行图,包含了所有可以并发执行的任务
JobManager会向资源管理器(ResourceManager)
请求执行任务必要的资源,也就是TaskManager上的插槽Slot,
一旦它获取到了足够的资源,就会将执行图分发到真正运行他们的TaskManager上,
而在运行过程中,JobManager会负责所有需要中央协调的操作,比如检查点(checkpoints)的协调
TaskManager(任务管理器):干活的
Flink中的工作进程,通常在Flink中会有多个TaskManager运行,每一个TaksManger都包含了一定数量的插槽(slots:cpu资源),插槽的数量限制了TaskManager能够执行的任务数量
启动后,TaskManger会向资源管理器注册他的插槽,收到资源管理器的指令后,TaskManager就会将插槽给JobManager调用
JobManager可以向插槽分配任务(tasks)来执行
在执行过程中,一个taskManger可以跟其他运行同一应用程序的taskManager交换数据
ResourceManger(资源管理器):为Job分配taks计算资源,
管理TaskManager的插槽slot,
为不同环境提供了不同资源管理器,如YARN/Mesos/K8s/stndalone部署
当JobManager申请插槽资源时,
ResourceManager会将有空闲插槽的TaskManager分配给JobManager,
如果ResourceManager没有足够的插槽来满足JobManager的请求,
它还可以向资源提供平台发起会话,以提供启动TaskManager进程的容器
Dispacher(分发器):提供resultf接口,方便应用的提交
可以跨作用运行,它为应用程序提供了Rest接口
当一个应用被提交执行时,分发器就会启动并将应用移交给一个JobManager.
Dispatcher也会启动一个WebUI,用来方便地展示和监控作业执行的信息。
Dispatcher在架构中可能并不是必需的,这取决于应用提交的运行方式
2.任务提交流程
app提交应用->Dispatcher启动并提交应用->JobManager请求Slots
->ResoureceManager(查看TaskManager可用slots,注册Slots)
->TaskManager(JobManager提交slots中执行的任务)
(YARN)提交
FlinkClient->
1.上传flink的jar包和配置
2.提交Job->ResourceManager(YARN)->NodeManager/JobManager去请求资源
->ResourceManager(YARN)
->启动NodeManager/TaskManager
3.任务调度原理
FlinkClien提交应用
JobManager生成可执行图、发送任务、取消任务、检查点保存存盘
↑↓
TaskManager给状态信息、心跳信息、统计信息
怎么实现并行计算?
利用分布式进行并且计算,
每一个设置并行度-分配到不同的slot上多线程
并行的任务,需要占用多少?
一个流处理程序,到底包含多少个任务?
代码中算子调用对应的几个任务呢,什么时候能合并,什么时候不能合并
并行度(Parallelism)?
代码里设置并行度
env.setParallelism(4);//
提交job设置并行度 -p
集群配置中设置默认并行度
TaskManager和Slots
一个TaskManager都是一个JVM进程,会在独立的线程上执行一个或多个子任务
TaskManager通过task slot(任务)来控制一个TaskManager能接收多少个task(子任务)
默认:flink允许子任务共享slot,哪怕不是同一个任务的子任务,一个slot可以保存作业的整个管道piplin
流程:数据->TaskManager->task slot->Source、map->keyBy、window开窗计算、apply->Sink print输出
启动 nc *lk -7777socket文本流连接 ``
先分组,每个组的最大并行度,各组的最大并行度叠加
env.socketTexStream占用一个solt,flatMap占用一个,sum和pring可以共享占2个 因为代码中分组了slot如:.slotSharingGroup("red");
并行子任务的分配
程序与数据流(DataFlow)
DataStream<String> lines = env.addSource(new FlinkKafkaConsumer<>()); Source
DataStream<Event> events = lines.map((line) -> parse(line));
DataStream<Statistics> stats = events.keyBy("id") Transformation转换运算
.timeWindow()
.apply(new MyWindowAggregationFunction());
stats.addSink(new RollingSink(path)); Sink
所有Flink程序由三部分组成:Source、Transformation、Sink
Source负责读取数据源、
Transformation利用各种算子进行处理加工
Sink负责输出
每一个dataFlow以一个或多个sources开始或一个或多个finks结束
StreamGraph(代码) ->JobGraph(Client) ->ExecutionGraph是JobGraph的并行化版本,是调度层最核心的数据结构(jobManager生成) ->物理执行图(task上生成)
数据传输和任务链
算子之间传输数据的形式可以是:
one-to-one(forwarding)的模式
redistributing的模式,具体是哪一种形式,取决于算子的种类
One-to-one:Stream维护者分区以及元素的顺序(比如Source和map之间)。
这意味着map算子的子任务看到的元素个数以及顺序跟source算子的子任务生产的元素的个数、顺序相同。
map、fliter、flatMap等算子都是one-to-one的对应关系。
Redistributing:stream的分区会发生变化。每一个算子的子任务依据所选择的tramsformation发送数据到不同的目标任务。
例如,keyBy基于hashCode重分区、broadcast和rebalance会随机重新分区,这些算子都会引起redistribute过程。
而redistribute过程就类似与Spark中的shuffle过程
FORWARD(forward):one-to-one
HASH(hash):hashCode重分区
REBALANCE(rebalance):轮询的方式,轮询选择下一个分区
shuffle:完全随机分机
任务链(Operator Chain)
Flink采用了一种称为任务链的优化技术,可以在特点条件下减少本地通信的开销。
任务链的要求: 必须将两个或多个算子设为相同的并行度,并通过本地转发(loacl forward)的方式连接
”相同并行度“的one-to-one操作,Flink这样相连的算子连接在一起形成一个task
原来的算子成为里面的subtask
并行度相同、并且是one-to-one操作,两个条件缺一不可
.disableChaining()不管数据传输方式,不参与任务链合并
env.disableOperatorChaining()所有任务不合并 任务链
5.Flink 流处理 DataStream API
SingleOutputSteamOperator继承了DataStream
StreamExecutionEnvironment.createLocalEncironment(1) 本地执行环境
StreamExecutionEnvironment.createRomoteEnvironment("ip",6123,"YOURPATH/WordCount.jar")远程生产执行环境
创建执行环境 封装了以上↑
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment()
//socket文本流
DataStream<String> inputStream = env.socketTextStream("localhost",7777);
1.从集合读取数据 fromElements(...)//直接模拟数据
DataStream<String> dataStreams = env.fromCollection(Array.asList(new ddInfo("测试数据","测试","测试")));
2.从文件读取数据
DataStream<String> dataStreams = env.readTextFile("C:\\RuanJian\\jeecg-boot\\resource\\sensor.txt");
3.kafka中读取数据
引入依赖:flink-conector-kafka-0.11_2.12
Properties properties =new Properties();
properties.setProperty("bootstrap.servers","localhost:9092");
DataStream<String> dataStreams = env.addSource(new FlinkKafkaConsumer011(String)("sensor",new info(),properties));
创建kafka生产者主题:./bin/kafka-console-producer.sh --broker-list localhost:9092 --topic sensor
4.自定义数据源:如生产随机数据
Transform
map简单算子
flatMap简单算子
Filter 简单算子
//特点 相同的key都在一个分区中
KeyBy 分组数据流,流拆分成不同的分区-》KeyedStream
滚动聚合算子:sum()min()max()minBy()包含最小时间戳 maxBy()
Reduce:一个分组数据流的聚合操作,KeyedStream.Reduce
//多条流转换算子
Split和Select
DataStream -> SplitStream:根据某些特征把一个DataStream拆分成两个或多个DataStream
Split()分流
Select()
SplitStream<T> splitStream = dataStream.split(new OutputSelector<T>(){
@Override
public Iterable<String> select(T value){
return (value.getTemperature() > 30) ? Collections.singletonList("high") : Collections.singletonList("low");
}
})
DataStream<SensorReading> highTempStream = splitStream.select("high");
DataStream<SensorReading> lowTempStream = splitStream.select("low");
highTempStream.pring("high");
highTempStream.pring("low");
//多条流合流
Connect和CoMap CoFlatMap
DataStream -> ConnectedDStreams 连接两个类型一至的数据流
Connect()合流
CoMap()
DataStream<Tuple2<String,Double>> waringStream = highTempStream.map(new MapFunction<SensorReading,Tuple2<String,Double>>(){
@Override
public Tuple2<String,Double> map<SensorReading value> throws Exception{
return new Tuple2<>(value.getId(),value.getTemperature());
}
});
ConnectedStreams<Tuple2<String,Double>,SensorReading> connectedStreams = warningStream.connect(lowTempStream);
DataStream<Object> resultStream = connectedStreams.map(new CoMapFunction<Tuple2<String,Double>,SensorReading,Object>(){
@Override
public Object map1(Tuple2<String,Double> value) throws Exception{
return new Tuple3<>(value.f0,value.f1,"high temp warning");
}
@Override
public Object map2(SensorReading value) throws Exception{
return new Tuple2<>(value.getId(),"normal");
}
})
env.execute();
//union联合:联合多个DataStream
Union
highTempStream.union(lowTempStream,allTempStream);
支持的数据类型
基础数据类型
Flink流应用程序处理的是以数据对象表示的事件流,
所以在Flink的内部,我们需要能够处理这些对象,他们需要被序列化和反序列化以便通过网络传送他们。
或者从状态后端、检查点和保存点读取他们,Flink需要明确知道所处理的数据类型。
Flink使用类型信息的概念表示数据类型,并为每个数据类型生成特定的序列化器、反序列化器和比较器。
Flink还具有类型提取系统,该系统分析函数的输入和返回类型,
并启动获取类型信息从而获取序列化器和反序列化器。
DataStream<Integer/String/Double/flat...> numberStream = env.fromFlements(1,2,3,4);
numberStream.map(data -> data * 2);
Java和Scala元组(Tuples:Tuple/Tuple0/Tuple1...)
DataStream<Tuple2<String,Integer>> personStream = env.fromElements(
new Tuple2("adam",27);
new Tuple2("Sarah",23);
);
personStream.filter(p -> p.f1 > 18);
Scala样例类(case classes)
case calss Person(name:String,age:Int)
val persons:DataStream[Person] = env.fromElements(Person("Adam",27),Person("Sarah",23));
persons.filter(p -> p.age > 18)
Java简单对象(POJOs)
public class Person{
public String name;
public int age;
public Person() {}
public Person(String name,int age){
this.name = name;
this.age = age;
}
}
DataStream<Person> persons = env.fromElements(
new Person("Alex",21
new Person("Wendy",23);
);
其他(Arrays,Lists,Map,Enum)
Flink对Java和Scala中的一些特性目的的类型也都支持,
比如Java的:ArrayList,HashMap,Enum等
实现UDF函数-更细粒度的控制流
函数类(Function Class)
Flink可以调用所有udf函数的接口(实现方式为接口或抽象类)
如:
MapFunction,(接口)
FilterFunction,
ProcessFunction(抽象类,最底层)
DataStream<String> flinkTweets = tweetStreams.filter(new FlinkFilter("flink"));
public static class FlinkFilter implements FilterFunction(String){
private String KeyWord;
FlinkFilter(String KeyWord){
this.KeyWord = KeyWord;
}
@Override
public boolean filter(String value) throws Exception{
return value.contains(this.KeyWord);
}
}
//匿名实现
DataStream<String> flinkTweets = tweetsStream.filter(new FlinkFunction<String>(){
@Override
public boolean filter(String value) throws Exception{
return value.contains("flink");
}
});
匿名函数(Lamdba Functions)
DataSteam<String> tweetStream = env.readTextFile(".../File.txt");
DataStream<String> flinkStream = tweetStream.fliter(tweet -> tweet.contains("flink"));
富函数-功能加强版(Rich Functions)
与常规函数不同在于,可以获取运行环境的上下文,并拥有一些生命周期方法,可以实现更复杂的功能。
RichMapFunction,
RichFlatMapFunction,
RichFilterFunction
Rich Function有一个生命周期的方法有:
open()方法是rich function初始化方法,当算子map或filter被调用前open被调用。
close()方式是生命周期最后调用的方法。
getRuntimeContext()方法提供了函数的RuntimeContext的信息上下文,
例如函数执行的并行度,任务的名字,以及state状态。
//创建执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment()
env.setParallelism(4);设置并行度
//从文件读取数据
DataStream<String> dataStreams = env.readTextFile("C:\\RuanJian\\jeecg-boot\\resource\\sensor.txt");
//装换成SensorReading类型
DataSteam<SensorReading> dataStream = inputStream.map(line -> {
String[] fields = line.split(",");
return new SensorReading(fields[0],new Long(fields[1]),new Doule(fields[2]));
});
DataStream<Tuple2<String,Integer>> resultlStream = dataStream.map(new MyMapper());
resultStream.print();
env.execute();
//实现自定义普通函数类
public static class myMapper implements MapFunction<SensorReading,Tuple2<String,Integer>>{
@Override
public Tuple2<String,Integer> map(SensorReading value) throws Exception{
return new Tuple2<>(value.getId,value.getId.length));
}
}
//实现自定义富函数类
public static class myMapper extend RichMapFunction<SensorReading,Tuple2<String,Integer>>{
@Override
public Tuple2<String,Integer> map(SensorReading value) throws Exception{
return new Tuple2<>(value.getId,getRuntimeContext().getIndexOfThisSubtask());
}
@Override
public void open(Configuration parameters) throws Exception{
//初始化工作,一般是定义状态,或建立数据库连接
}
}
数据重分区操作//作用:数据在任务之间传输的方式定义
其他分区方式:dataStream.broadcast()广播分区,与Keyby()相似
rebalarce()均匀轮训分区,
rescal() 给rebalarce分组
global() 全部数据给第一个分区
partitionCustom用户自定义重分区器
Sink
Flink-kafka实现ETL:
1.取kfaka某主题的数据,2.计算转换数据,3.(Sink输出)存KafKa某主题
引入依赖:flink-conector-kafka-0.11_2.12
Properties properties =new Properties();
properties.setProperty("bootstrap.servers","localhost:9092");
DataStream<String> dataStreams = env.addSource(new FlinkKafkaConsumer011(String)("sensor",new info(),properties));
创建kafka producer topic:./bin/kafka-console-producer.sh --broker-list localhost:9092 --topic sensor
//装换成SensorReading类型
DataSteam<SensorReading> dataStream = inputStream.map(line -> {
String[] fields = line.split(",");
return new SensorReading(fields[0],new Long(fields[1]),new Doule(fields[2]));
});
dataStream.addSink(new FlinkKafKaProducer011<String>("localhost:9092","sinktest",new SimpleStringScheam()));
env.execute();
./bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic sinktest 查看kafka consumer topic
Flink没有类似于Spark中的foreach方法,让用户进行迭代操作,
对外的输出操作都用Slink完成。
官方提供了一部分框架的sink,以外需要自定义sink
stream.addSink(new MySink(xxx));
kafka(source/sink)
rabitmq(source/sink)
nifi (source/sink)
es (sink)
redis(sink)->需引入依赖支持:Apache Bahir
......
Flink Sink Redis
org.apache.bahir
flink-connector-redis2.11
//装换成SensorReading类型
DataSteam<SensorReading> dataStream = inputStream.map(line -> {
String[] fields = line.split(",");
return new SensorReading(fields[0],new Long(fields[1]),new Doule(fields[2]));
});
FlinkJedisPoolConfig config = new FlinkJedisPoolConfig.Builder()
.setHost("localhost")
.setPost(6379)
.build()
dataStream.addSink(new RedisSink<>(config,new MyRedisMapper()));;
env.execute();
public static class MyRedisMapper implements RedisMapper<SensorReading>{
//定义保存数据到redis命令,存成Hash表 hset sensor_temp id temp
@Override
public RedisCommandDescription getCommandDescription(){
return new RedisCommandDescription(RedisCommand.HSET,"sensor_temp");
}
@Override
public String getKeyFromData(SensorReading data){
data.getId();
}
@Override
public String getValueFromData(SensorReading data){
data.getTemp().toString();
}
}
Flink Sink ES
flink-connector-elastiesearch6_2.12
//装换成SensorReading类型
DataSteam<SensorReading> dataStream = inputStream.map(line -> {
String[] fields = line.split(",");
return new SensorReading(fields[0],new Long(fields[1]),new Doule(fields[2]));
});
List<HttpHost> httpHosts = new ArrayList<HttpHost>();
httpHosts.add(new HttpHost("localhost",9200));
dataStream.addSink(new ElasticSearchSink.Builder<SensorReading>(httpHosts,new MyEsSinkFunction()).build());
env.execute();
public static class MyEsSinkFunction implements ElasticsearchSinkFunction<SensorReading>{
public void process(SensorReading element,RuntimeContext cxt,RequestIndexer indexer){
//定义写入数据
HashMap<String,String> dataSource = new HashMap<>();
dataSource.put("id",element.getId());
dataSource.put("id",element.getTemperature().toString());
dataSource.put("ts",element.getTimestamp().toString());
//创建请求,作为向es发起的写入命令
IndexRequest indexRequest = Requests.indexRequest()
.index("sensor")
.type("readingdata")
.source(dataSource);
//用indexer发送请求
indexer.add(indexRequest);
}
}
curl "localhost:9200/_cat/indices?v"
curl "localhost:9200/sensor/_search?pretty" 查看数据
Flink Sink Mysql
mysql-connector-java
//装换成SensorReading类型
DataSteam<SensorReading> dataStream = inputStream.map(line -> {
String[] fields = line.split(",");
return new SensorReading(fields[0],new Long(fields[1]),new Doule(fields[2]));
});
dataStream.addSink(new MyJdbcSink());
env.execute();
public static class MyJdbcSink extends RichSinkFunction<SensorReading>{
Connection conn = null;
PreparedStatement insertStmt = null;
PreparedStatement updateStmt = null;
//创建连接
@Override
public void open(Configuration parameters){
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test","root","123456");
insertStmt = conn.propareStatement("insert into sensor_temp (id,temp) values (?,?)");
updateStmt = conn.propareStatement("update sensor_temp set temp = ? where id = ?");
}
//每来一条数据,调用连接执行sql
@Override
public void invoke(SensorReading value,Context cxt){
//直接执行更新语句,如果没有更新那么就插入
updateStmt.setDouble(1,value.getTemperature());
updateStmt.setString(2,value.getId());
updateStmt.execute();
if(updateStmt.getUpdateCount() == 0){
insertStmt.setString(1,value.getId());
insertStms.setDouble(2,value,getTemperature());
updateStmt.execute();
}
}
@Override
public void close() throws Exception{
insertStmt.close();
updateStmt.close();
conn.close();
}
}
6.Flink中的Window
window概念
一般真实的流都是无界的,怎样处理无界的数据?
可以把无限的数据流进行切分,得到有限的数据集进行处理---也就是得到有界流
窗口(window)就是把无界流切割(某个时间段的)为有界流,
它会将流数据分发到有限大小的桶(bucket)中进行分析。
window类型
时间窗口(Time Window)
滚动时间窗口(Tumling Windows)window size
将数据根据固定的窗口长度对数据切分
时间对齐,窗口长度固定,没有重叠
dataStream.keyBy("id")//必须先Keyby
.timeWindow(Time.seconds(15)//十五秒);
滑动时间窗口(Sliding Windows)window size/window slide
滑动窗口是固定窗口的更广义的一种形式,滑动窗口由固定的窗口长度和滑动间隔组成。
窗口长度固定,可以有重叠
.timeWindow(Time.seconds(15),//参数二);
会话窗口(Session Windows)session gap
由一系列事件组合一个指定时间长度的timeout间隙组成,
一段时间没有收到新数据就会生成新的窗口。
特点:时间无对齐
.window(EventTimeSessionWindows.withGap(Time.minutes(1)//间隔1分钟));
计数窗口(Count Window)
滚动计数窗口
.countWindow(parms1,)
滑动计数窗口
.countWindow(parms1,parms2)
窗口分配器4类
window()方法接收的输入参数是一个WindowAssigner
WindowAssigner负责将每条输入的数据分发到正确的window中
GlobalWindows
Tumling Windows
Sliding Windows
Session Windows
窗口函数(window Funciton)(无界变有界流分桶后的聚合)
window function定义了要对窗口中收集的数据做的计算操作
增量聚合函数-统计(incrementa aggregation function)
每条数据到来就进行计算,保持一个简单状态
ReduceFunction,
AggregateFunction
全窗口函数(full window function)
先把窗口所有数据收集起来,等到计算的时候会遍历所有数据
ProcessWindowFunction,
WindowFunction
代码:
//开窗测试
//socket文本流
DataStream<String> inputStream = env.socketTextStream("localhost",7777);
//增量聚合函数
DataStream<Integer> resultStream= dataStream.keyBy("id")
.timeWindow(Time.seconds(15))
.aggregate(new AggregateFunction<SensorReading,Integer,Integer>(){
@Override
创建累加器
@Override
累加
@Override
结果
@Override
merge session窗口一般操作
})
resultStream.pring();
env.execute();
//全窗口聚合函数
DataStream<Tuple3<String,Long,Integer>> resultStream2= dataStream.keyBy("id")
.timeWindow(Time.seconds(15))
.apply(new WindowFunction<SensorReading,Tuple3<String,Long,Integer>,Tuple,TimeWindow>(){
@Override
public void apply(Tuple tuple,TimeWindow window,Iterable<SensorReading> input,Collector<Tuple3<String,Long,Integer>> out){
String id = tuple.getField(0);
Long windowEnd = window.getEnd();
Integer count = IteratorUtils.toList(input.iterator()).size();
out.collect(new Tuple<>(id,windowEnd,count));
}
})
或
.pracess(new ProcessWindowFunction<SendorReading,Object,Tuple,TimeWindow>(){
})
resultStream2.pring();
env.execute();
其他API
.trigger()--触发器
.evictor()--移除器
.allowedLateness(Time.minutes(1))--允许处理迟到的数据
.sideOutputLateData()--将迟到的数据放入侧输出流
.getSideOutput()--获取侧输出流
分为:keyby后开窗,和不keyby开窗如有:windowAll()...
7.Flink的时间语义与Wartermark
时间语义概念
Flin中的时间语义
EventTime事件创建时间->KafKaQueue->IngestionTime数据进入Flink的时间->ProcessingTime开窗后处理算子的本地系统时间如:.timeWindow(Time.seconds(5)),与机器相关
设置Event Time
事件时间
//对执行环境调用setStreamTimeCharacteristic方法,设置流的时间特性
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//具体时间需要从数据中提取时间戳,不设置默认是IngestionTime数据进入Flink的时间
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
水位线(Watermark)
EventTime处理由网络原因发生的乱序数据(水位标记)
Flink以EventTime模式处理数据流时,会根据数据里的时间戳处理基于时间的算子
把EventTime进展变慢了,比如把表时间调慢了2分钟时间还是10点发车,
如设置5秒开窗的时间窗口.timeWindow(Time.seconds(5)//五秒),处理乱序时间数据
并表时间调慢3秒:1 4 5/秒(不关闭) 2 3 6 7 8窗口关闭
遇到一个时间戳达到啦窗口关闭时间,不应该立即出发窗口计算,而是等待一段时间,等迟到的数据来了在关闭窗口。
重点:watermark可以设置延迟触发时间窗口关闭时间
数据流中的watermark表示data timestamp小于watermark timestamp的数据都已经到达了因此Watermark触发window的执行
1 2(watemark) 3 5 5(watemark) 6 7 8
watermark是一条特殊的数据记录,必须单调递增,与数据的时间戳相关
watermark的传递、引入和设定
watermark在任务间的传递:
每一个任务都可能有上游多个并行任务再给他发watermark,并同时有并行向下游任务广播watermark,
从上游向下游传递是把watermark广播出去,通过watermark传递可以一层一层的推进eventTime,每一个任务eventTime不一样是对的因为流处理又先后顺序:有的数据是souce任务/Trform/sink任务当然eventTime不一样
以最小的watermark作为(当前任务)事件时钟向下游广播
watermark在代码中的设置
//AssignerWithPunctuatedWatermarks//断电性指定数据和时间戳生成watermark
//AssignerWithPeriodicWatermarks //周期性生成watermark
//new AscendingTimestampExtractor<SendorReading>(){}//有序数据设置事件时间和watermark
//new BoundedOutOfOrderness有界乱序数据情况下的时间戳提取器,1.提取时间戳2.生成设置watermark
assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor<SensorReading>(TIme.second(2)//延迟时间,最大的乱序程度){
@Override
public long extractTimestamp(SensorReading element){
return element.getTimestamp() * 1000L;
}
});
OutputTag<SensorReaqding> outputTag = new OutputTag<SensorReading>("late"//控制台输出时的标识){};
//迟到数据处理
DataStream minTempStream = minTempStream.keyBy("id")
.timeWindow(Time.minutes(15));
.allowedLateness(1) //设置迟到1分钟关窗
.sideOutputLateData(outputTag);//放入侧输出流
.minBy("temperature");//获取字段最小值
minTempStream.pring()打印流//.pring("minTemp")表示minTemp是打印输出时的标识
minTempStream.getSideOutput(outputTag).pring("late");
env.execute();
总结:watermark延时时间 对所有时间无论是time window,还是allowedLateeness(1)迟到时间,都影响生效
时间语义的应用
事件时间语义的设置
Wartermark概念和原理
Flink状态管理
状态的作用为了保证:扩容并行度调整,机器挂了,内存不够,机器挂了
1.Flink会进行状态管理,状态一致性的保证,2.故障后的恢复以及高效存储和访问,以便开发人员专注于应用程序的逻辑。
2.可以认为状态就是一个本地变量(在内存中),可以被当前任务的所有业务逻辑访问到,不会跨任务访问状态。
无状态的(任务/算子):map/flamap/比如:timewindow().状态()
有状态的(任务/算子) :window/redcue/...
状态和有状态的算子相关联,为了运行时了解算子状态需要先注册其状态
算子状态Operatior state
作用范围:当前的(算子)访问,当前任务输入来的数据都能访问到当前状态
同一个分区访问同一个状态
状态对于同一子任务是共享的
算子状态的数据结构
List state、列表状态:一直数据的列表
Union List state、联合列表状态:发生故障时,或者从保存点(savepoint)启动应用程序如何恢复
Broadcast state 广播状态:如果一个算子有多个任务,每个任务状态又相同,这种情况适合广播状态
算子状态代码使用
public static class MyCountMapper implements MapFunction<SensorReading,Integer>,ListCheckpointed<Integer>{
//作为算子状态
private Integer count = 0;
@Override
public Integer map(SensorReading value) throws Exception{
count++;
return count;
}
@Override //操作状态到List
public List<Integer> snapshotState(long checkpointId,long timestamp) throws Exception{
return Collections.singletonList(count);
}
@Override //读取状态
public void restoreState(List<Integer> state) throws Exception{
for(Integer num : state)
count += num;
}
}
键控状态Keyed state//分组/分区KeyBy的状态KeyBy的状态
1.根据输入数据流中的定义的键(key),来访问和维护
2.Flink为每个key维护一个状态实例,并将具有相同键的所有数据,都分区到同一个算子任务中,
这个任务会维护和处理这个key对应的状态。
3.当任务处理一条数据时,它会自动将状态的访问范围限定为当前数据的key。
键控状态数据结构
Value state值状态:将状态表示为单个值
List state:状态表示为一组数据列表
Map state:状态表示为一组Key-value
Reducing state & Aggregating state聚合状态:表示为一个用于操作的列表
键控状态代码使用
//声明
Private ValueState<Integer> keyCountState= getRuntimeContext().getState(new ValueStateDescriptor<Integer>("my-value",Integer.class));
getRuntimeContext().getListState(...);
......
//读取状态
Integer myValue = myValueState.value();
//对状态赋值
myValueState.update(10);
状态后端state Backends
每传入一条数据,(有状态的)算子任务都会读取和更新状态
所以每个并行任务都会在本地维护其状态,以确保快速的状态访问
状态的存储,访问以及维护,由一个可插入的组件决定,这个组件就是状态后端(state backends)
状态后端主要负责两件事:本地状态管理,以及检查点(checkpoint)状态写入远程存储
MemoryStateBackend
内存级的状态后端,会将键控状态作为内存中的对象管理,将它们存储在TaskManager的JVM堆上,
而将checkpoint存储在JobManager的内存中,特点:快速,低延迟,但不稳定
FsStateBackend
将checkpoint存到远程的持久化文件系统(flieSystem)上,而对于本地状态,跟MemoryStateBackend一样,
也会存在TaskManager的JVM堆上,特点:有内存及本地访问速度,容错保证。
RocksDBStateBackend
将所有状态序列后,存入本地RocksDB中,特点:读写速度慢,
flink-conf.yml中配置状态后端
env.setStateBackend(new MemoryStateBackend());
env.setStateBackend(new FsStateBackend("远程IP"));
env.setStateBackend(new RocksDBStateBackend("远程IP"));需引入maven依赖
8.ProcessFunction API
3层API分别为:顶层SQL/TableApi->DataStream/DataSetAPI->Stateful Stream Processing
我们之前学习的转换算子是无法访问事件的时间戳信息和Watermark信息,如:MapFunction这样的map转换算子无法访问
这在一些场景下极为重要,
DataStream API提供了Low-Level转换算算子,可以访问时间戳,watermark以及注册定时事件,还可以特定事件如:超时事件
ProcessFunction用来构建事件驱动的应用 及 自定义业务逻辑,Flink SQL就是使用ProcessFunction实现的
Flink提供了8个Process Function
ProcessFunction
KeyedProcessFunction 分组后调用KeyedProcessFunction :常见
CoProcessFunction 链接流后调用
ProcessJoinFunction 两条流join后
BroadcastProcessFunction 广播流后调
KeyedBroadcastProcessFunction 分组广播后
ProcessWindowFunction 全窗窗口
ProcessAllWindowFunction 不keyby直接基于DataStream 开窗时时调
dataStream.keyBy("id");
.process(new MyProcess());
.pring();
//KeyedProcessFunction 测试
public static class MyProcess extends KeyedProcessFunction<Tuple,SensorReading,Integer>{
ValueState<Long> tsTimerState;
@Override
public void processElement(SensorReading value,Context ctx,Collector<Integer> out )throws Exception{
out.collect(value.getId().length());
ctx.timestamp();//获取时间戳
ctx.getCurrentKey()//当前key
ctx.output();
ctx.timerService().currentProcessingTime();处理时间
ctx.timerService().currentWatermark();//事件时间
ctx.timerService().registerProcessingTimeTimer(ctx.timerService().currentProcessingTime() + 1000L);//处理时间的定时器
tsTimerState.update(ctx.timerService().currentProcessingTime() + 1000L);//状态保存时间
ctx.timerService().registerEventTimeTimer((value.getTimestamp() + 10) * 1000L);//事件时间的定时器
ctx.timerService().deleteProcessingTimeTimer(10000L)//删除定时器
}
@Override
public void onTimer(long timestamp,OnTimerContext ctx,Collector<Integer> out)throws Exception{
system.out.print(timestamp + "定时器触发");
}
}
//需求:监控温度传感器的温度值,如果温度值在10秒内连续上升,则报警
dataStream.keyBy(SendorReading::getId);
.process(new TempIncreaseWarning(10));
.pring();
public static class TempIncreaseWarning extends KeyedProcessFunction<String,SendorReading,String>{
//当前统计的时间间隔
private Integer interval;
public TempIncreaseWarning(Integer interval){
this.interval = interval;
}
//定义状态,保存上一次温度值,定时器时间戳
private ValueState<Double> lastTempState;
private ValueState<Long> timerTsState;
@Override
public void open(Configuration parameters)throws Exception{
lastTempState = getRuntimeContext().getState(new ValueStateDescriptor<Double>("last-temp",Double.class,Double.Min_value));
timerTsState = getRuntimeContext().getState(new ValueStateDescriptor<Double>("last-temp",Long.class));
}
@Override
public void processElement(SensorReading value,Context ctx,Collector<String> out )throws Exception{
//取出状态
Double lastTemp = lastTempState.value();
Long timerTs = timerTsState.value();
//如果温度上升并且没有定时器,注册10秒的定时器
if(value.getTemperature() > lastTemp && timerTs == null){
//计算出定时器时间戳
Long ts = ctx.timerService.currentProcessingTime() + interval * 1000L;
ctx.timerService.registerProcessingTimeTimer(ts);
timerTsState.update(ts);
}else if{
//如果温度下降,并有定时器,删除定时器
ctx.timerService.deleteProcessingTimeTimer(timeTs);
timeTsState.clear();
}
//更新温度状态
lastTempState.update(value.getTemperature());
}
@Override
public void onTimer(long timestamp,OnTimerContext ctx,Collector<String> out)throws Exception{
//定时器触发,输出报警信息
out.collect("传感器"+ctx.getCurrentKey().getField(0)+"温度值连续"+interval+"s上升");
lastTempStat.clear();
}
@Override
public void close()throws Exception{
lastTempStat.clear();
}
}
侧输出流(SideOutput)
除了split算子可以将一条流分成多条流,这些流的数据类型也都相同。
processFunction的side output功能也能产生多条流,这些流的数据类型可以不一样
一个 side output可以定义为OutputTag[X]对象,X是输出流的数据类型,
processfuntion通过Context对象发射一个事件到一个或多个side output
9.状态编程和容错机制(暂缓,实操时学习)
1.有状态的算子和应用程序
算子状态(operator state)
键控状态(keyed state)
2.状态一致性
一致性级别
端到端(end-to-end)状态一致性
3.检查点(checkpoint)
flink的检查点算法
flink+kafka如何实现端到端的exa...
4.选择一个状态后端(state backend)
10.Table API和SQL
Flinke对批处理和流处理,提供了统一的上层API
TableAPI是一套内嵌在Java和Scala语言中的查询API,
代码案例:
引入依赖
flink-table-planner_2.12
//创建执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment()
env.setParallelism(4);设置并行度
//从文件读取数据
DataStream<String> dataStreams = env.readTextFile("C:\\RuanJian\\jeecg-boot\\resource\\sensor.txt");
//装换成SensorReading类型
DataSteam<SensorReading> dataStream = inputStream.map(line -> {
String[] fields = line.split(",");
return new SensorReading(fields[0],new Long(fields[1]),new Doule(fields[2]));
});
//创建表环境
StreamTableEnvironment tableEvn = StreamTableEnvironment.create(env);
//基于流创建一张表
Table dataTable = tableEvn.fromDateStream(dataStream);
//调用table Api进行转换 方式一
Table resultTable = dataTable.select("id,temperature")
.where("id = ‘sendor_1’");
//执行SQL 方式二
tableEnv.createTemporarView("sensorView",dataTable);//dataTable注册为sensor,自定义的
String sql = "select id,temperature from sensorView where id = 'sensor_1'";
Table resultSqlTable = tableEvn.sqlQuery(sql);
//输出数据
tableEnv.toAppendStream(resultTable,Row.class).pring("result");
tableEnv.toAppendStream(resultSqlTable,Row.class).pring("sql");
env.execute();
基本程序结构
TableAPI和SQL的程序结构,与流式处理的程序结构十分类似。
StreamTableEnvironment tableEnv = ...
//创建一张表,用于读取数据,表名为:inputTable
tableEnv.connect().createTemporarTable("inputTable");
//创建一张表,用于把计算结果输出
tableEnv.connect().createTemporaryTable("outputTable");
//通过Table Api查询算子,得到一张结果表
Table result = tableEnv.from("inputTable").select();
//通过SQL查询语句,得到一张结果表
String Sql = "select * from input Table ..."
Table sqlResult = tableEnv.sqlQuery(sql);
//输出结果表到输出表中
result.insertInto("outputTable");
表环境配置StreamTableEnvironment->继承TableEnvironment
//1.1老版本planner的流处理
EnvironmentSettings oldStreamSettings = EnvironmentSettings.newInstance()
.useOldPlanner() //设置处理的版本
.inStreamingMode()//设置流处理或批处理模式
.build()
StreamTableEnvironment oldStreamTableEnv = StreamTableEnvironment.create(env,oldStreamSettings);
//1.2老版本Flink planner的批处理
ExecutionEvnvironment batchEnv = ExecutionEnvironment.getExecutionEnvironment();
BatchTableEnvironment oldBatchTableEnv = StreamTableEnvironment.create(batchEnv);
//2.1基于Blink的流处理
EnvironmentSettings blinkStreamSettings = EnvironmentSettings.newInstance()
.useBlinkPlanner()//设置处理的版本
.inStreamingMode()//设置流处理或批处理模式
.build()
StreamTableEnvironment blinkStreanTableEnv = StreamTableEnvironment.create(env,blinkStreamSettings);
//2.2基于Blink的批处理
EnvironmentSettings blinkBatchSettings = EnvironmentSettings.newInstance()
.useBlinkPlanner()//设置处理的版本
.inBatchingMode()//设置流处理或批处理模式
.build()
TableEnvironment blinkBatchTableEnv = TableEnvironment.create(env,blinkBatchSettings);
创建表-从文件读取数据
TableEnvironment可以注册目录Catalog,并可以基于Cataog注册表
表(Table)是标识符(identifier)来指定的,3部分组成:Catalog名、数据库database名,对象名
表可以是常规也可以是视图View
常规表(Table)一般用来描述外部数据,如文件,数据库或从消息队列的数据,或从DataStream转换而来的数据
视图(View)可以从现有表中创建、通常是table API或SQL查询的一个结果集。
TableEnvironment可以调用.connect()方法,连接外部系统
并调用.createTemporaryTable()方法,在Catalog中注册表
tableEnv.connect()//定义表的数据来源,和外部系统建立连接
.withFormat()//定义数据格式化方法
.withSchema()//定义表结构
.createTemporaryTable("MyTable")//创建临时表
读取文件数据
String filePath="D:\\RuanJian\\java03\\jeecg-boot\\src\\main\\resources\\sensor.txt";
tableEnv.connect(new FileSystem().path(filePath))//外部文件系统连接
.withFormat(new Csv())//以csv格式进行数据格式化
.withSchema(new Schema()
.field("id",DataTypes.STRING())
.field("timestamp",DataTypes.BIGINT())
.field("temp",DataTypes.DOUBLE())
)
.createTemporaryTable("inputTable");
Table inputTable = tableEnv.from("inputTable");基于临时表(外部数据)创建一张表
inputTabel.printSchema()//打印表结构
tableEnv.toAppendStream(inputTable,Row.class).pring();//表转换为流打印输出
env.execute();
表的查询
Table API是基于’表‘的Table类,提供一套操作方法,这些方法会返回一个新的Table对象。
//有些关系型转换操作,可以由多个方法调用组成,构成链式调用结构。
Table sensorTable = tableEnv.from("inputTable");
1.0简单转换
Table resultTable = sensorTable.select("id,temperature").filter("id === 'sensor_1'");
1.1查询装换Table API:聚合装换
Table aggTable = sensorTable.groupBy().select("id,id.count as count,temperature.avg as avgTemp");
1.2SQL
tableEnv.sqlQuery("select id,temperature from inputTable where id = 'sensor_6'");
Table sqlAggTable = tableEnv.sqlQuery("select id,count(id)as cnt,avn(temperature) as avgTemp from inputTable group by id");
tableEnv.toAppendStream(resultTable.Row.class).pring("result");
tableEnv.toRetractStream(aggTable.Row.class).pring("agg");
tableEnv.toRetractStream(sqlAggTable.Row.class).pring("sqlagg");
env.execute();
表的输出-输出到文件
String outputPath="D:\\RuanJian\\java03\\jeecg-boot\\src\\main\\resources\\out.txt";
tableEnv.connect(new FileSystem().path(outputPath))//外部文件系统连接
.withFormat(new Csv())//以csv格式进行数据格式化
.withSchema(new Schema()
.field("id",DataTypes.STRING())
.field("temperature",DataTypes.DOUBLE())
)
.createTemporaryTable("outputTable");
resultTable.insertInto("outputTable");//将转化后的数据写到outputTable对应的文件中
env.execute();
KafKa数据连接,读写kafka
tableEnv.connect(new KafKa()
.version("0.11")
.topic("sensor")
.property("zookeeper.connect","localhost:2181")
.property("bootstrap.servers","localhost:9092")
)
.withFormat(new Csv())//序列化和反序列化Json或Csn
.withScheam(new Schema()
.field("id",DataTypes.STRING())
.field("timestamp",DataTypes.BIGINT())
.field("temp",DataTypes.DOUBLE())
)
.createTemporaryTable("inputTable");
Table sensorTable = tableEnv.from("inputTable");
1.0简单转换
Table resultTable = sensorTable.select("id,temperature").filter("id === 'sensor_1'");
1.1查询装换Table API:聚合装换
Table aggTable = sensorTable.groupBy().select("id,id.count as count,temperature.avg as avgTemp");
tableEnv.connect(new KafKa()
.version("0.11")
.topic("sinkTest")
.property("zookeeper.connect","localhost:2181")
.property("bootstrap.servers","localhost:9092")
)
.withFormat(new Csv())//序列化和反序列化Json或Csn
.withScheam(new Schema()
.field("id",DataTypes.STRING())
//.field("timestamp",DataTypes.BIGINT())
.field("temp",DataTypes.DOUBLE())
)
.createTemporaryTable("outputTable");
resultTable.insertInto("outputTable");
env.execute();
更新模式
对于流式查询,需要声明如何在表和外部连接器之间转换。
与外部系统交换的消息类型,由更新模式(Update Mode)指定
Append追加模式
表和外部连接器只交换插入(insert)消息
Retract撤回模式
表和外部链接器交换添加add 和撤回retract 消息
插入操作 编码为add消息,
删除操作 编码为retract消息
更新操作 编码为上一条的retract和下一条的add消息
Upsert更新插入模式
更新和插入都被编码为Upsert消息;
删除编码为delete消息
输出到外部系统
输出到ES
tableEnv.connect(new Elasticsearch()
.version("6")
.host("localhost",9200,"http")
.index("sensor")
.documentType("temp")
)
.inUpsertMode()//默认是Append模式,设置为Upsert模式
.withFormat(new Json())
.withSchema(new Schema()
.field("id",DataTypes.STRING())
.field("count",DataTypes.BIGGINT)
)
.createTemporarTable("esoutputTable");
aggresultTable.insertInto("esoutputTable");
输出到Mysql
flink-jdbc_2.12
String sinkDDL=
"create table jdbcOutputTable("+
" id varchar(20) not null, "+
" cnt bigint not null"+
") with ("+
" 'connector.type' = 'jdbc', "+
" 'connector.url' = 'jdbc:mysql://localhost:3306/test', "+
" 'connector.table' = 'sensor_count' "+
" 'connector.driver' = 'com.mysql.jdbc.Driver' "+
" 'connector.usernaem' = 'root' "+
" 'connector.password' = '123456' )";
tableEnv.sqlUpdate(sinkDDL) //执行DDL创建表
aggResultSqlTable.insertInto("jdbcOutputTable");
表和流的装换
Table装换为DataStream
1.表可以转换为DataStream或DataSet,
这样自定义流处理或批处理程序就可以继续在Table API或SQL查询的结果上运行了。
2.将Table 转换为 DataStream或DataSet时,需要指定生成的数据类型
即要将表的每一行转换成的数据类型
3.表作为流式查询的结果,是动态更新的
4.有两种转换模式:追加append模式 和 撤回retract模式
/*流转表*/
默认转换后的Table Schema和DataStream中的字段定义一一对应,
也可以单独制定出来。
Table dataTable = tableEvn.fromDateStream(dataStream,"id,timestamp as ts, temperature");
//基于流创建一张表
Table dataTable = tableEvn.fromDateStream(dataStream);
//基于临时表(外部数据)创建一张表
Table inputTable = tableEnv.from("inputTable");
//基于SQL创建表 方式二
tableEnv.createTemporarView("sensorView",dataTable);//dataTable注册为sensor,自定义的视图名
String sql = "select id,temperature from sensorView where id = 'sensor_1'";
Table resultSqlTable = tableEvn.sqlQuery(sql);
/*表转流*/
tableEnv.toAppendStream(resultTable.Row.class).pring("result");
tableEnv.toRetractStream(aggTable.Row.class).pring("agg");
append Mode模式:
用于表只会被插入操作更改的场景
DataStream<Row> resultStream = tableEnv.toAppendStream(resultTable.Row.class).pring("result");
retract Mode模式:
用于任何场景
DataStream<Tuple2<Boolean,Row>> aggResultStream = tableEnv.toRetractStream(aggResultTable,Row.class);
基于DataStream 创建临时视图
tableEnv.createTemporarView("sensorView",dataStream,"id,temperature,timestamp as ts");//dataTable注册为sensor,自定义的视图名
基于Table 创建临时视图
tableEnv.createTemporarView("sensorView",dataTable);//dataTable注册为sensor,自定义的视图名
查看执行计划:通过TableEnvironment.explain(Table)方法或。explain()完成,返回字符串
String explaination = tableEnv.explain(resultTable);
System.out.print(explaination);
动态表和持续查询
动态表(Dynamic Tables)
动态表是Flink对流数据的Table API和SQL支持的核心概念
与表示批处理数据的静态表不同,动态表是随时间变化的。
持续(连续)查询(Continuous Query)
动态表可以像静态的批处理表一样进行查询,查询一个动态表会产生持续查询(Continuous Query)
连续查询永远不会终止,并会生成另一个动态表
查询会不断更新其动态结果表,以反映其动态输入表上的更改
过程:Stream ->Dynamic Table->Continuous Query ->Dynamic Table ->Stream
1.流被转换为动态表
2.对动态表计算连续查询,生成新的动态表
3.生成的动态表被转换回流
将流转换成动态表
为了处理带有关系查询的流,必须先转换为表
从概念上讲,流的每个数据记录,都被解释为对结果表的插入insert修改update操作
持续查询会在动态表上做计算处理,并作为结果生成最新的动态表
user url select uers,count(url) as cnt user cnt
mary ./home from clicks mary 2 //Upsert by KEY /- delete by KEY
Bob ./cart group by user Bob 1
myary ./parod
将动态表转换成DataStream
与常规的数据库表一样,动态表可以insert update delete更改,进行持续的修改。
将动态表装换为流或将其写入外部系统时,需要对这些更改进行编码
Append-only仅追加流
仅通过insert更改来修改的动态表,可以直接转换为仅追加流
retract撤回流
撤回流是包含两类信息的流:添加信息和撤回信息
Upsert(更新插入)流
Upsert流也包含两类信息的流:Upsert信息和删除信息
处理时间特性(Time Attributes)
1.基于时间的操作如:TableAPI和SQL中的窗口操作,需要定义相关的时间语义和时间数据来源的信息
2.Table可以提供一个逻辑上的时间字段,用于在表处理程序中,指示时间和访问相应的时间戳。
3.时间属性,可以是每个表schema的一部分,一旦定义了时间属性,
他就可以作为一个字段引用,并且可以在基于时间的操作中使用。
4.时间属性的行为类似于常规时间戳,可以访问,并且进行计算
定义处理时间(Processing Time)
1.处理时间语义下,允许表处理程序根据机器的本地时间生成结果,他是时间的最简单概念,
即不需要提取时间戳,也不需要生成watermark
由DataStream转换成表时指定
在定义Schema期间,可以使用.proctime,指定字段名定义处理时间字段
这个proctime属性只能通过附加逻辑字段来扩展物理schema,因此只能在schema定义的末尾定义它
Table sensorTable = tableEnv.fromDataStream(dataStream,"id,temp,timestamp,pt.proctime");pt.proctime系统时间
定义Table Schema时指定
.withScheam(new Schema()
.field("id",DataTypes.STRING())
.field("timestamp",DataTypes.BIGINT())
.field("temp",DataTypes.DOUBLE())
.field("pt",DataTypes.TIMESTAMP(3)).proctiom()
)
在创建表的DDL中定义
String sinkDDL=
"create table jdbcOutputTable("+
" id varchar(20) not null, "+
" ts bigint, "+
" temperature double, "+
" pt as PROCTIME(), "+
") with ("+
" 'connector.type' = 'filesystem' "+
" 'connector.path' = '/sensor.txt' "+
" 'format.type' = 'csv' )";
tableEnv.sqlUpdate(sinkDDL) //执行DDL创建表
事件时间特性
和上述事件时间作用一样↑
事件时间定义有三种方法:
由DataStream转换成表时指定
定义Table Schems时指定
在创建表的DDL中定义
由DataStream转换成表时指定:使用.rowtime可以定义事件时间属性
Table sensorTable = tableEnv.fromDataStream(dataStream,"id,temperature,timestamp.rowtime,rt.rowtime");
.withScheam(new Schema()
.field("id",DataTypes.STRING())
.field("timestamp",DataTypes.BIGINT())
.rowtime(new Rowtime()
.timestampsFromField("timestamp")//从字段中提取时间戳
.watermarksPeriodicBound(1000)//watermark延迟一秒
)
.field("temp",DataTypes.DOUBLE())
)
String sinkDDL=
"create table jdbcOutputTable("+
" id varchar(20) not null, "+
" ts bigint, "+
" temperature double, "+
" rt as TO_TIMESTAMP(FROM_UNIXTIME(ts)), "+
" watermark for rt as rt - interval '1' second "+
") with ("+
" 'connector.type' = 'filesystem' "+
" 'connector.path' = '/sensor.txt' "+
" 'format.type' = 'csv' )";
tableEnv.sqlUpdate(sinkDDL) //执行DDL创建表
分组窗口
时间语义,要配合窗口操作才能发挥作用
TableAPI和SQL中有两种窗口:
Group Windows(分组窗口)
按窗口对表进行分组,窗口的别名必须在group by 子句中,像常规分组字段一样引用
Table table = in put.window([w:GroupWindow] as "w") //定义窗口别名w
.groupBy("w,a") //按字段a 和窗口w分组
.select("a,b.sum");//聚合
滚动窗口(Tumbling window)
.window(Tumble.over("10.minutes").on("rowtime").as("w"))
.window(Tumble.over("10.minutes").on("proctime").as("w"))
.window(Tumble.over("10.rows").on("proctime").as("w"))
滑动窗口(Sliding window)
.window(Slide.over("10.minutes").every("5.minutes").on("rowtime").as("w"))
.window(Slide.over("10.minutes").every("5.minutes").on("proctime").as("w"))
.window(Slide.over("10.rows").every("5.rows").on("proctime").as("w"))
会话窗口(session window)
.window(Session.withGap("10.minutes").on("rowtime").as("w"))
.window(Session.withGap("10.minutes").on("proctime").as("w"))
SQL中的Group window
TUMBLE(time_attr,interval)参数1时间字段、窗口长度
滚动窗口
HOP(time_attr,interval,interval)时间字段、窗口滑动步长、窗口长度
滑动窗口
SESSION(time_attr,interval)时间字段、窗口间隔
会话窗口
代码操作:
Table dataTable = tableEvn.fromDateStream(dataStream,"id,timestamp as ts, temperature");
1.1
Table resultTable = dataTable.window(Tumble.over("10.seconds").on("rt").as("tw"))
.groupBy("id,tw")
.select("id,id.count,temp.avg,tw.end")
1.2
String Sql = "select id,count(id) as cnt,avg(temp) as avgTemp,tumble_end(rt,interval '10' second) "+
"from sensor group by id,tumble(rt,interval '10' second)";
Table SqlresultTable = tableEnv.SqlQuery(Sql);
tableEnv.toAppendStream(resultTable.Row.class).pring("result");
tableEnv.toRetractStream(SqlresultTable.Row.class).pring("sql");
env.execute();
开窗函数
Over Windows
使用window(w:overwindows*),并在select()方法中通过别名来引用
Table table = input.window([w:OverWindow] as "w") //定义窗口别名w
.select("a,b.sum over w,c.min over w");//聚合
无界Over window
可以在事件时间或处理时间,以及指定为时间间隔、或行计数的范围内,定义Over window
无界Over window使用常亮指定的
//无界的事件时间 over window
.window(Over.partitionBy("a").orderBy("rowtime").preceding(UNBOUNDED_RANGE).as("w")) ///partitionBy分区/preceding前面多少
//无界的处理时间 over window
.window(Over.partitionBy("a").orderBy("proctime").preceding(UNBOUNDED_RANGE).as("w")) //preceding前面多少
//无界的事件时间Row-count over window
.window(Over.partitionBy("a").orderBy("rowtime").preceding(UNBOUNDED_ROW).as("w")) //preceding前面多少
//无界的处理时间Row-count over window
.window(Over.partitionBy("a").orderBy("proctime").preceding(UNBOUNDED_ROW).as("w")) //preceding前面多少
有界Over window
可以在事件时间或处理时间,以及指定为时间间隔、或行计数的范围内,定义Over window
无界Over window使用常亮指定的
//无界的事件时间 over window
.window(Over.partitionBy("a").orderBy("rowtime").preceding("1.minutes").as("w")) ///partitionBy分区/preceding前面多少
//无界的处理时间 over window
.window(Over.partitionBy("a").orderBy("proctime").preceding("1.minutes").as("w")) //preceding前面多少
//无界的事件时间Row-count over window
.window(Over.partitionBy("a").orderBy("rowtime").preceding("10.rows").as("w")) //preceding前面多少
//无界的处理时间Row-count over window
.window(Over.partitionBy("a").orderBy("proctime").preceding("10.rows").as("w")) //preceding前面多少
SQL中的Over window
所有聚合必须在同一窗口上定义,也就是说必须是相同的分区、排序和范围
Order By必须在单一的时间属性上指定
Select count(amount) OVER(
PARTITION BY user
Order by proctime
rows between 2 preceding and current row
)
from Orders
代码操作:
Table overresultTable = dataTable.window(Over.partitionBy("id").orderBy("rt").preceding("2.rows").as("ow"))
.select("id,rt,id.count over ow,temp.avg over ow")
String Sql = "select id,rt,count(id) over ow,avg(temp) over ow "+
"from sensor "+
"window ow as (partition by id order by rt rows between 2 preceding and current row)";
Table overSqlresultTable = tableEnv.SqlQuery(Sql);
tableEnv.toAppendStream(overresultTable.Row.class).pring("result");
tableEnv.toRetractStream(overSqlresultTable.Row.class).pring("sql");
env.execute();
系统内置函数
比较函数
SQL:
value1 = value2
value1 > value2
TableAPI:
ANY1 === ANY2
ANY1 > ANY2
逻辑函数
SQL:
boolean1 or boolean2
boolean is false
not boolean
TableAPI:
boolean1 || boolean2
boolean.isFalse
!boolean
算数函数
SQL:
numeric1 +numeric2
power(numeric1,numeric2)
TableAPI:
numeric1 + numeric2
numeric1.power(numeric2)
字符串函数
SQL:
string1 || string2 字符串拼接
upper(string) 转大写
char_length(string) 字符串长度
TableAPI:
String1 + String2
String.upperCase()
String.charLength()
时间函数
SQL:
Date string 日期YYYY-MM-DD HH:mm:ss
timestamp string 时间戳
current_time 当前时间
interval string range 时间间隔
TableAPI:
string.toDate
string.toTimestamp
currentTime()
numeric.days
numeric.minutes
聚合函数
SQL:
count(*) 计数
sum(expression) 求和
rank() 排序取行号
row_number()
TableAPI:
field.count
field.sum()
标量函数(ScalarFnction)
自定义函数UDF
用户定义的函数必须先注册,然后才能在查询中使用
函数通过调用registerFunction()方法在TableEnvironment中注册,当用户定义的函数被注册时,
他被插入到TableEnvironment的函数目录中,这样TableApi或SQL解析器就可以识别并正确的解释它
标量函数Scalar functions
定义的标量函数,可以将0 1或多个标量值,映射到新的标量值
必须在org.apache.flink.table.functions中扩展基类ScalarFuntion 并实现(一个或多个)求值(eval)方法
求值方法必须公开声明命名为eval
public static class HashCode extends ScalarFnction{
private int factor = 13;
public HashCode(int factor){
this.factor = factor;
}
public int eval(String s){
return s.hashCode() * factor;
}
}
//求id的hash值
HashCode hashCode = new HashCode(23);
//需要在环境中注册UDF
tableEnv.registerFunction("HashCode" hashCode);
Table resultTable = sensorTable.select("id,ts,hashCode(id)");
//SQL
tableEnv.createTemporaryView("sensor",sensorTable);
String Sql = "select id,ts,hashCode(id) from second ";
Table SqlresultTable = tableEnv.SqlQuery(Sql);
tableEnv.toAppendStream(resultTable.Row.class).pring("result");
tableEnv.toAppendStream(SqlresultTable.Row.class).pring("sql");
env.execute();
表函数(TableFunction)
表函数的行为由求值方法决定,求值方法必须是public并命名为eval
public static class Split extends TableFunction<Tuple2<String,Integer>>{
private String separator = ",";
public Split(String,separator){
this.separator = separator;
}
public void eval(String str){
for(String s : str.split(separator)){
collect(new Tuple2<String,Integer>(s,s.length()));
}
}
}
Split split = new Split("_");
//需要在环境中注册UDF
tableEnv.registerFunction("split" split);
Table resultTable = sensorTable.joinLateral("split(id) as (word,length)")
.select("id,ts,word,length");
//SQL
tableEnv.createTemporaryView("sensor",sensorTable);
String Sql = "select id,ts,word,length from second,lateral table(split(id)) as splitid(word,length) ";
Table SqlresultTable = tableEnv.SqlQuery(Sql);
tableEnv.toAppendStream(resultTable.Row.class).pring("result");
tableEnv.toAppendStream(SqlresultTable.Row.class).pring("sql");
env.execute();
聚合函数(Aggregate Function)
可以把一个表中的数据,聚合成一个标量值
用户定义的聚合函数通过继承AggregateFunction实现必须实现的方法:
createAccumulator()
accumulate()
getValue()
AggregateFunctiond的工作原理
首先需要一个累加器Accumulator,用来保存聚合中间结果的数据结构,通过createAccumulator()来创建
随后,对每个输入行调用函数的accumulate()方法来更新累加器
处理完所有行后,将调用函数的getValue()方法来计算并返回结果
public static class AvgTemp extends AggregateFunction<Double,Tuple2<Double,Integer>>{
@Override
public Double getValue(){
return accumulator.f0 / accumulator.f1;
}
@Override
public Tuple2<Double,Integer> createAccumulator(){
return new Tuple2<>(0.0,0);
}
//必须实现一个accumulate方法,来数据之后更新状态
public void accumulate(Tuple2<Double,Integer> accumulator,Double temp){
accumulator.f0 += temp;
accumulator.f1 += 1;
}
}
AvgTemp avgTemp = new AvgTemp();
//需要在环境中注册UDF
tableEnv.registerFunction("avgTemp" avgTemp);
Table resultTable = sensorTable.groupBy("id")
.aggregate("avgTemp(temp) as avgtemp")
.select("id,avgtemp");
//SQL
tableEnv.createTemporaryView("sensor",sensorTable);
String Sql = "select id,avgTemp(temp) from second group by id ";
Table SqlresultTable = tableEnv.SqlQuery(Sql);
tableEnv.toRetractStream(resultTable.Row.class).pring("result");
tableEnv.toRetractStream(SqlresultTable.Row.class).pring("sql");
env.execute();
表聚合函数(Tabl Aggregate Function)
可以输出多个结果,可以把一个表中的数据聚合为具有多行多列的结果表
通过继承TableAggregateFunction来实现
必须实现的方法:
createAccumulator()
accumulate()
emitValue()
Table resultTable = sensorTable.groupBy("id")
.flatAggregate("avgTemp(temp) as avgtemp")
.select("id,avgtemp");
11.Flink CEP简介 用户行为分析 用户画像
Flink CEP高级API是复杂事件处理(Complex Event Processing)的库,理解为Flink SQL一个层级
CEP允许在无休止的事件流中检测事件模式,让我们有机会掌握数据的重要部分。
一个或多个简单事件构成的事件流通过一定的规则匹配,输出用户想得到的结果 ---复杂事件处理