概念
flink是开源大数据流处理框架,分布式处理引擎,内存执行(为了保存读取状态的时候很快[在本地读取])。
批处理spark+流处理storm—》流批一体lambda(合并两套)----》流处理flink
不同级别API:
最高级最简单(好用):flinksql
声明式sql(老bug):table Api
核心api(不会):DataStream、DataSet API
底层(最难):有状态的流
spark底层是RDD sparkStream是DataStream,一组组小批数据RDD,有阶段的进行
flink底层是DataFlow和Event事件序列
第一道面试题,使用flink的原因:
⚫ Flink 的延迟是毫秒级别,而 Spark Streaming 的延迟是秒级延迟。
⚫ Flink 提供了严格的精确一次性语义保证。
⚫ Flink 的窗口 API 更加灵活、语义更丰富。
⚫ Flink 提供事件时间语义,可以正确处理延迟数据。
⚫ Flink 提供了更加灵活的可对状态编程的 API。
(延迟低 精准一次写入 api灵活 提供事件时间 可对状态变成)
flink配置
第二道面试题:flink的部署模式:
在一些应用场景中,对于集群资源分配和占用的方式,可能会有特定的需求。Flink 为各
种场景提供了不同的部署模式,主要有以下三种:
⚫ 会话模式(Session Mode) 启动集群保持会话并在客户端提交作业,适合规模小执行时间短的大量作业,集群一直在,作业抢集群的资源
⚫ 单作业模式(Per-Job Mode)yarn和k8s用但作业模式隔离资源,为每一个提交的作业启动一个集群,客户端执行交给jobmanager,提交任务才创建集群,客户端提交,一个任务一个集群
⚫ 应用模式(Application Mode)不像前两种需要客户端,我们不浪费贷款和下载依赖,直接给jobmanager,就是创建一个集群,任务结束jm关闭,提交任务创建集群,直接jobmanager执行,多个作业也只创建一个集群
它们的区别主要在于:集群的生命周期以及资源的分配方式;以及应用的 main 方法到底
在哪里执行——客户端(Client)还是 JobManager。
单机+cluster
解压之后直接start-cluster.sh,8081端口访问
集群+cluster
vi flink-conf.yaml
jobmanager.rpc.address: master
vi workders
slave1
slave2
分发flink后start-cluster.sh,8081端口访问
提交任务使用flink run -m master:8081 -c 主类 jar包
不借助yarn的standalone实现三个部署
1会话模式:
设置flink-conf.yaml的jobmanager然后start-cluster.sh
flink run的时候指定-m 指定jobmanager即可
2单作业模式
standalone不支持哦,因为需要借助外部资源yarn或者k8s
3应用模式
- 提前把jar包放到lib目录下
cp my.jar $FLINK_HOME/lib - 开启一个任务来启动jobmanager
standalone-job.sh start --job-classname 主类
standalone-job.sh会自动搜索 - 启动taskmanager
taskmanager.sh start - 关闭集群
standalone-job.sh stop
taskmanager.sh stop
借助yarn的yarn模式实现三个部署
客户端把flink提交给yarn的resourcemanager,yarn的resourcemanager向yarn的nodemanager申请容器,flink部署jobmanager和taskmanager来启动集群,根据jobmanager上的作业所需要的slots数量动态分配taskmanager资源
#配置yarn环境变量
HADOOP_HOME=/opt/module/hadoop-2.7.5
export PATH=$PATH:$HADOOP_HOME/bin:$HADOOP_HOME/sbin
export HADOOP_CONF_DIR=${HADOOP_HOME}/etc/hadoop
#必须要配置HADOOP_CLASSPATH,而且要在HADOOP_HOME放到PATH里面之后
export HADOOP_CLASSPATH=`hadoop classpath
#配置jobmanager和taskmanager资源大小
vi flink-conf.yaml
jobmanager.memory.process.size: 1600m
taskmanager.memory.process.size: 1728m
taskmanager.numberOfTaskSlots: 8
parallelism.default: 1
启动hadoop
1会话模式
首先启动hadoop,然后yarn-session向yarn申请资源,开启会话,启动flink
yarn-session.sh -nm test
-d后台
-jmjobmanager所需内存单位MB
-nm name yarn ui的人物名
-ququeen yarn队列名
-tm每个taskmanager 所需内存
运行使用flink run -c 主类 jar包
-m / -jobmanager确定jonmanager地址,地址在yarn8088端口可以看到
2单作业模式
有了yarn环境,直接提交一个单独作业不提前开启yarn -session就是单作业模式
flink run -d -t yarn-per-job -c 主类 jar包
早期:
flink run -m yarn-cluster -c 主类 jar包
flink list -t yarn-per-job -Dyarn.application.id=application_xx_yy查看
flink cancel -t yarn-per-job -Dyarn.application.id=application.xxx_yy jobid取消当前应用的作业
取消作业,整个flink集群也会停掉
3应用模式
flink run-application -t yarn-application -c 主类 jar包运行
flink lit -t yarn-application -Dyarn.application.id=application_xxx_yy查看
flink cancel -t yarn-application -Dyarn.application.id=application_xx_yy取消
框架的结构
jobmanager:master管理者,负责管理调度,只有一个
taskmanager:worker工作者,执行任务处理数据,一个或多个
可以看到flink任务到jobmanager,jobmanager把任务交给taskmanager做。
第三道面试题:jm和tm的启动方式
JobManager 和 TaskManagers 可以以不同的方式启动:
⚫ 作为独立(Standalone)集群的进程,直接在机器上启动
⚫ 在容器中启动
⚫ 由资源管理平台调度启动,比如 YARN、K8S
第四道面试题:作业提交流程
(1)一般情况下,由客户端(App)通过分发器提供的REST接口,将作业提交给JobManager。
(2)由分发器启动 JobMaster,并将作业(包含 JobGraph)提交给 JobMaster。
(3)JobMaster 将 JobGraph 解析为可执行的 ExecutionGraph,得到所需的资源数量,然后
向资源管理器请求任务槽资源(slots)。
(4)资源管理器判断当前是否由足够的可用资源;如果没有,启动新的 TaskManager。
(5)TaskManager 启动之后,向 ResourceManager 注册自己的可用任务槽(slots)。
(6)资源管理器通知 TaskManager 为新的作业提供 slots。
(7)TaskManager 连接到对应的 JobMaster,提供 slots。
(8)JobMaster 将需要执行的任务分发给 TaskManager。
(9)TaskManager 执行任务,互相之间可以交换数据。
如果部署模式不同,或者集群环境不同(例如 Standalone、YARN、K8S 等),其中一些步骤可能会不同或被省略,也可能有些组件会运行在同一个 JVM 进程中。比如我们在上一章践过的独立集群环境的会话模式,就是需要先启动集群,如果资源不够,只能等待资源释放,而不会直接启动新的TaskManager。
逻辑图-作业图-流程图-物理图
客户端完成-交给jobmanager的-jobmanger的优化的-jobmanager完成-taskmanager完成
任务和任务槽
任务槽是task slots
taskmanager.numberOfTaskSlots: 8设置任务槽数量,一个任务槽表示一个线程,多设几个增加并行度
默认flink子任务共享slot
.map().slogSharingGroup("1")设置为一个组的才会被共享
并行度parallelism是动态的,是实际的并行度
parallelism.default设置,会在多个taskmanager上平均分配
依赖
pom.xml (比赛提供pom.xml文件)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>work</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<flink.version>1.14.0</flink.version>
<scala.version>2.12</scala.version>
<hive.version>3.1.2</hive.version>
<mysqlconnect.version>5.1.47</mysqlconnect.version>
<clickhouse.version>0.3.2</clickhouse.version>
<hdfs.version>3.1.3</hdfs.version>
<spark.version>3.1.1</spark.version>
<hbase.version>2.2.3</hbase.version>
<kafka.version>2.4.1</kafka.version>
<lang3.version>3.9</lang3.version>
<flink-connector-redis.verion>1.1.5</flink-connector-redis.verion>
</properties>
<dependencies>
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-reflect</artifactId>
<version>${scala.version}.16</version>
</dependency>
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-compiler</artifactId>
<version>${scala.version}.16</version>
</dependency>
<dependency>
<groupId>ru.yandex.clickhouse</groupId>
<artifactId>clickhouse-jdbc</artifactId>
<version>0.3.2</version>
</dependency>
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
<version>${scala.version}.16</version>
</dependency>
<!--kafka-->
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka_${scala.version}</artifactId>
<version>${kafka.version}</version>
</dependency>
<!--flink 实时处理-->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-runtime-web_${scala.version}</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-clients_${scala.version}</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-streaming-scala_${scala.version}</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-kafka_${scala.version}</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.json4s</groupId>
<artifactId>json4s-core_2.12</artifactId>
<version>3.6.6</version>
</dependency>
<dependency>
<groupId>org.json4s</groupId>
<artifactId>json4s-jackson_2.12</artifactId>
<version>3.6.6</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-table-planner_${scala.version}</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>com.clickhouse</groupId>
<artifactId>clickhouse-http-client</artifactId>
<version>0.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-json</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-table-api-scala-bridge_${scala.version}</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-redis_2.11</artifactId>
<exclusions>
<exclusion>
<groupId>org.apache.flink</groupId>
<artifactId>flink-shaded-hadoop2</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</exclusion>
</exclusions>
<version>${flink-connector-redis.verion}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${lang3.version}</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-hive_${scala.version}
</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-hbase-2.2_${scala.version}</artifactId>
<version>${flink.version}</version>
</dependency>
<!--mysql连接器-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysqlconnect.version}</version>
</dependency>
<!--spark处理离线-->
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-core_${scala.version}</artifactId>
<exclusions>
<exclusion>
<groupId>org.apache.hive</groupId>
<artifactId>hive-exec</artifactId>
</exclusion>
</exclusions>
<version>${spark.version}</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-sql_${scala.version}</artifactId>
<exclusions>
<exclusion>
<groupId>org.apache.hive</groupId>
<artifactId>hive-exec</artifactId>
</exclusion>
</exclusions>
<version>${spark.version}</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-hive_${scala.version}</artifactId>
<version>${spark.version}</version>
</dependency>
<!-- hadoop相关-->
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>${hdfs.version}</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-auth</artifactId>
<version>${hdfs.version}</version>
</dependency>
<!--hbase 相关-->
<dependency>
<groupId>org.apache.hbase</groupId>
<artifactId>hbase-mapreduce</artifactId>
<version>${hbase.version}</version>
</dependency>
<dependency>
<groupId>org.apache.hbase</groupId>
<artifactId>hbase-client</artifactId>
<version>${hbase.version}</version>
</dependency>
<!--jdbc-->
<!-- 连接mysql需要驱动包-->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-jdbc_${scala.binary.version}</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<!--clickhouse-->
<!-- 连接ClickHouse需要驱动包-->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.6</version>
</dependency>
<dependency>
<groupId>ru.yandex.clickhouse</groupId>
<artifactId>clickhouse-jdbc</artifactId>
<version>${clickhouse.version}</version>
<!-- 去除与Spark 冲突的包 -->
<exclusions>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</exclusion>
<exclusion>
<groupId>net.jpountz.lz4</groupId>
<artifactId>lz4</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<sourceDirectory>src/main/scala</sourceDirectory>
<testSourceDirectory>src/test/java</testSourceDirectory>
<resources>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
<plugins>
<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
<version>3.2.2</version>
<configuration>
<recompileMode>incremental</recompileMode>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
<version>3.1.6</version>
<configuration>
<scalaCompatVersion>2.11</scalaCompatVersion>
<scalaVersion>2.11.12</scalaVersion>
<encoding>UTF-8</encoding>
</configuration>
<executions>
<execution>
<id>compile-scala</id>
<phase>compile</phase>
<goals>
<goal>add-source</goal>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile-scala</id>
<phase>test-compile</phase>
<goals>
<goal>add-source</goal>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
初始化
//批处理,读文件
val env=ExecutionEnvironment.getExecutionEnvironment
//本地执行环境,不给参数的话并行度默认cpu核心数
val localEnvironment=StreamExecutionEnvironment.createLocalEnvironment()
//流处理,kafka,socket,
val env = StreamExecutionEnvironment.getExecutionEnvironment
//流处理需要execute,不然不会运行
//streaming转batch
env.setRuntimeMode(RuntimeExecutionMode.BATCH)
//触发执行
env.execute()
flink run -Dexecution.runtime-mode=BATCH默认就是STREAMING模式
事件时间和处理时间
处理时间是机器的系统时间
事件时间是每个设备在对应的设备上发生的时间,就是数据生成的时间,就是Timestamp时间戳
获取数据
#批处理
val data=env.readTextFile("input/works.txt")
//流处理
env.readTextFile("")
env.addSource(new FlinkKafkaConsumer[String](topic, new MykafkaDeserializationSchemaConsumer, names.getProp())())
env.socketTextStream()
env.socketTextStream("master",7777)

| 方法 | 意思 | 案例 |
|---|---|---|
| socketTextStream | 获取端口流数据 | env.socketTextStream(“host”,port) |
| fromCollection | 获取列表数据 | env.fromCollection(List(Event(‘’),…)) |
| fromElements | 获取元素数据 | env.fromElements(Event()…) |
| readTestFile | 获取文件数据 | env.readTextFile(“path”) |
| env.addSource | 从类库获取数据 | env.addSource(new FlinkKafkaConsumer[](‘topic’,new SimpleStringSchema(),prop)) |
kafka源
val prop=new Properties()
val BOOTSTRAP_SERVERS="master:9092,slave1:9092,slave2:9092"
prop.setProperty("bootstrap.servers",BOOTSTRAP_SERVERS)
prop.setProperty("key.deserializer","org.apache.kakfa.common.serialization.StringDeserializer")
prop.setProperty("value.deserializer","org.apache.kafka.common.serialization.StringDeserializer")
prop.setProperty("auto.offset.reset","latest")
//topic可以是一个也可以是多个,还可以匹配正则
//DeserializationSchema或KeyedDeserializationSchema,进行反序列化,可以自定义
//prop是kafka客户端属性bootstrap.servers key/value.deserializer...
val data=env.addSource(new FlinkKafkaConsumer[String]("topic",new SimpleStringSchema(),prop))
data.print("from kafka:")
env.execute()
自定义源
class Clicksource extends SourceFunction[Event]{
var running=true
def run=={
while(running){
val random=new Random
random.nextInt() //随机数
//Calender.getInstance.getTimeInMillis 获取当前时间戳
collect.collect(data)
Thread.sleep(1000)
}
}
def cancel=={
running=false
}
}
//在主类里面
env.addSource(new ClickSource).print()
env.execute()
=-------------
.addSource(new RichParallelSourceFunction[Int] {
override def run(sourceContext: SourceFunction.SourceContext[Int]):Unit= {
for (i <- 0 to 7) {
// 将偶数发送到下游索引为 0 的并行子任务中去
// 将奇数发送到下游索引为 1 的并行子任务中去
if ((i + 1) % 2 == getRuntimeContext.getIndexOfThisSubtask) {
sourceContext.collect(i + 1)
}
}
}
// 这里???是 Scala 中的占位符
override def cancel(): Unit = ???
})
处理数据
并行度设置:
setParallelism(2)设置并行度flink run -p 2 -c 主类 jar包设置并行度parallelism.default : 2flink-conf.yaml设置并行度
一对一:map filter flatMap
重分区:keyBy window apply
// 禁用算子链 disableChaining
.map((_,1)).disableChaining()
// 从当前算子开始新链 startNewChain
.map((_,1)).startNewChain()
//算子
data.flatMap(_.split(" ")).map((_,1)).groupBy(0).sum(1)
//批处理groupBy 流处理keyBy
//流处理没有groupBy
| 算子 | 意思 | 案例 |
|---|---|---|
| flatMap | 平坦化 | data.flatMap(_.split(" ")) |
| map | foreach对每个数据处理 | .map((_,1)) |
| keyBy | 分组 | .keyBy(_._1)批groupBy同理,flink没有直接聚合的API,必须要先分区。为了提高效率,先分区才能聚合keyBy,tuple用一个多个位置组合,pojo用字段,lambda表达式或keySelector提取 .keyBy(_=>true)放到一个分区 |
| sum | 累加 | .sum(1)指定按照元祖第2个加 |
| filter | 过滤 | .filter(_.user.equals(“Mary”)) |
| sum | 求和 | keyBy后才能聚合 |
| min | 最小这些聚合操作只要指定字段就行 | |
| max | 最大 | 元祖是_1,_2 |
| minBy | 最小值所在记录 | 可以指定位置或指定名称 |
| maxBy | 最大值所在记录 | |
| reduce | 聚合,将keyedStream转为DataStream,不改变类型 | .redis(+) |
| udf | 自定义 | 有filterFunction MapFunction ReduceFunction |
| rich udf | 富函数 | RichMapFunction RichFilterFunction RichReduceFunction 有上下文对象getRuntimeContext.getState,自带open和close方法 |
自定义map
class UserExtractor extends MapFunction[Event,String]{
map=={
value.user}
}
自定义filter
class UserFilter extends FilterFunction[Event]{
filter==>{value.user.equals("Mary")}
}
自定义flatMap
//自定义map很强大,可以过滤,因为有一个out输出,很灵活!
class MyFlatMap extends FlatMapFunction[Event,String]{
flatMap==>{
if(value.user.equals("Mary")){
out.collect(value.user)
}else if(value.user.equals("Bob")){
out.collect(value.user)
}
}
}
自定义RichMap
richFlatMap有out参数,
.map(new RichMapFunction[Event,Long](){
open==>{
//数据库链接,配置文件读取,IO流创建
getRuntime.Context.getIndexOfThisSubtask //获取任务索引
//程序执行的并行度,任务名称,以及状态
(state)。
}
//任务结束执行,关闭数据库连接
close==>{}
//任务的真正执行,来一条处理一条
map=>{}
})
自定义RichFlatMap
class MyFlatMap extends RichFlatMapFunction[IN,OUT]{
override def open(parameters: Configuration): Unit = {
// 做一些初始化工作
// 例如建立一个和 MySQL 的连接
}
override def flatMap(value: IN, out: Collector[OUT]): Unit = {
// 对数据库进行读写
}
override def close(): Unit = {
// 清理工作,关闭和 MySQL 数据库的连接。
} }
第五道面试题:物理分区策略有哪些
常见的物理分区策略有随机分区、轮询分区、重缩放和广播,还有一种特殊的分区策略— —全局分区,并且 Flink 还支持用户自定义分区策略.
随机分区:数据随机地分配到下游算子的并行任务与中去。随机分布服从均匀分布,data.shuffle.print("shuffle").setParallelism(4)
轮询分区:按照先后顺序将数据一次分发data.rebalance.print("rebalance").setParallelism(4)
重缩放分区:和轮询类似。底层是轮询,但rebalance是所有人依次发牌,recale是分成小团体依次轮流发牌,接收方是发送方整倍数时效率更高data.setParallelism(2).rescale.setParallelism(4).print("rescale")
广播:将输入数据复制并发送到下游算子的所有并行任务中去data.broadcast.print("broadcast").setPraallelism(4)
全局分区:将所有的输入流都发送到下游算子的第一个并行子任务中区,相当于强行让下游任务的并行度变为1data.global().print("global")
自定义分区:data.partitionCustom(new Partitioner[Int]{partition==>{key%2},data=>data}).print()参数是一个partitioner对象,第二个是分区器的字段
保存数据

//到kafka
data.addSink(new FlinkKafkaProducer[String]("dim_customer_login_log", new SimpleStringSchema(), one.getProp()))
//打印
data.print

| 算子 | 意思 | 案例 |
|---|---|---|
| addSInk | 保存到指定类型 |
sink到文件系统
val stream = env.fromElements(
Event("Mary", "./home", 1000L),
Event("Bob", "./cart", 2000L),
Event("Alice", "./prod?id=100", 3000L),
Event("Alice", "./prod?id=200", 3500L),
Event("Bob", "./prod?id=2", 2500L),
Event("Alice", "./prod?id=300", 3600L),
Event("Bob", "./home", 3000L),
Event("Bob", "./prod?id=1", 2300L),
Event("Bob", "./prod?id=3", 3300L)
)
val fileSink = StreamingFileSink
.forRowFormat(
new Path("./output"),
new SimpleStringEncoder[String]("UTF-8")
)
//通过.withRollingPolicy()方法指定“滚动策略”
.withRollingPolicy(
DefaultRollingPolicy.builder()
//15分钟数据
.withRolloverInterval(TimeUnit.MINUTES.toMillis(15))
//最近5分钟没有收到新的数据
.withInactivityInterval(TimeUnit.MINUTES.toMillis(5))
// 文件大小达到1G
.withMaxPartSize(1024 * 1024 * 1024)
//达到这三个就写
.build()
)
.build
stream.map(_.toString).addSink(fileSink)
env.execute()
sink到kafka
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
val properties = new Properties()
properties.put("bootstrap.servers", "hadoop102:9092")
val stream = env.readTextFile("input/clicks.csv")
stream
.addSink(new FlinkKafkaProducer[String](
"clicks",
new SimpleStringSchema(),
properties
))
env.execute()
///kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic clicks查看
sink到redis
使用FlinkJedisPoolConfig.Builder().build和new RedisSink
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
val conf = new FlinkJedisPoolConfig.Builder()
.setHost("redis")
.setPassword("123456")
.build()
env.addSource(new ClickSource)
.addSink(new RedisSink[Event](conf, new MyRedisMapper()))
env.execute()
//redisMapper是redis映射类接口,说明怎样将数据转换成可以写入redis的类型
//有三个类,getKeyFrom获取redis的key getValue获取写入redis的value getCommand获取写入redis的命令
class MyRedisMapper extends RedisMapper[Event] {
override def getKeyFromData(t: Event): String = t.user
override def getValueFromData(t: Event): String = t.url
override def getCommandDescription: RedisCommandDescription = new RedisCommandDescription(RedisCommand.HSET, "clicks")
}
sink到ElasticsearchSink
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
val stream = env.fromElements(
Event("Mary", "./home", 1000L),
Event("Bob", "./cart", 2000L),
Event("Alice", "./prod?id=100", 3000L),
Event("Alice", "./prod?id=200", 3500L),
Event("Bob", "./prod?id=2", 2500L),
Event("Alice", "./prod?id=300", 3600L),
Event("Bob", "./home", 3000L),
Event("Bob", "./prod?id=1", 2300L),
Event("Bob", "./prod?id=3", 3300L)
)
val httpHosts = new util.ArrayList[HttpHost]()
httpHosts.add(new HttpHost("hadoop102", 9200, "http"))
val esBuilder = new ElasticsearchSink.Builder[Event](
httpHosts,
new ElasticsearchSinkFunction[Event] {
override def process(t: Event, runtimeContext: RuntimeContext,
requestIndexer: RequestIndexer): Unit = {
val data = new java.util.HashMap[String, String]()
data.put(t.user, t.url)
val indexRequest = Requests
.indexRequest()
.index("clicks")
.type("type")
.source(data)
requestIndexer.add(indexRequest)
}
}
)
stream.addSink(esBuilder.build())
env.execute()
sink到mysql
pom依赖
stream.addSink(
JdbcSink.sink(
"INSERT INTO clicks (user, url) VALUES (?, ?)",
new JdbcStatementBuilder[Event] {
override def accept(t: PreparedStatement, u: Event): Unit = {
t.setString(1, u.user)
t.setString(2, u.url)
}
},
new JdbcConnectionOptions.JdbcConnectionOptionsBuilder()
.withUrl("jdbc:mysql://localhost:3306/test")
.withDriverName("com.mysql.jdbc.Driver")
.withUsername("username")
.withPassword("password")
.build()
)
)
env.execute ()
自定义sink
集成sinkFunction和对应的RichSinkFunction抽象类,被addSink调用即可
//hbase
class MyHbaseSink() extends RichSinkFunction[work3Result]{
var conn:client.Connection=_
override def open(parameters: Configuration): Unit = {
val conf = HBaseConfiguration.create()
conf.set("hbase.zookeeper.quorum","bigdata1,bigdata2,bigdata3")
conf.set("hbase.zookeeper.property.clientPort","2181")
conf.set("hbase.master","bigdata1")
conf.set("hbase.rootdir","/hbase")
conn = ConnectionFactory.createConnection(conf)
}
override def invoke(value: work3Result, context: SinkFunction.Context): Unit = {
//Exists? no useful
// val admin = conn.getAdmin
// val bool: Boolean = admin.tableExists(TableName.valueOf("threemin_warning_state_agg"))
// if(!bool){
// val threemin_warning_state_agg = new HTableDescriptor(TableName.valueOf("threemin_warning_state_agg"))
// threemin_warning_state_agg.addFamily(new HColumnDescriptor("info"))
// admin.createTable(threemin_warning_state_agg)
// }
// admin.close()
val table = conn.getTable(TableName.valueOf("threemin_warning_state_agg"))
val put=new Put(Bytes.toBytes(value.change_machine_id))
put.addColumn("info".getBytes(),"change_machine_id".getBytes(),(value.change_machine_id).toString.getBytes())
put.addColumn("info".getBytes(),"totalwarning".getBytes(),(value.totalwarning).toString.getBytes())
put.addColumn("info".getBytes(),"windoow_end_time".getBytes(),(value.window_end_time).getBytes())
table.put(put)
}
override def close(): Unit = {
if(conn!=null){
conn.close()
}
}
}
================================================
//mysql
class MyMysqlSink extends RichSinkFunction[work3Result]{
var conn:Connection=_
var statement:PreparedStatement=_
override def open(parameters: Configuration): Unit = {
Class.forName("com.mysql.jdbc.Driver")
conn = DriverManager.getConnection("jdbc:mysql://bigdata1:3306/shtd_industry", "root", "123456")
}
override def invoke(value: work3Result, context: SinkFunction.Context): Unit = {
println(value.totalwarning)
statement = conn.prepareStatement("insert into threemin_warning_state_agg values(?,?,?)")
statement.setInt(1,value.change_machine_id)
statement.setInt(2,value.totalwarning)
statement.setString(3,value.window_end_time)
statement.executeUpdate()
}
override def close(): Unit = {
if(conn!=null) {
conn.close()
}
if(statement!=null) {
statement.close()
}
}
}
水位线
WatermarkStrategy.forMonotonousTimestamps()有序
WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(0))乱序
在事件时间下,基于数据自带的时间戳定义时钟,只有数据的时间戳比当前时钟大,才推动时钟前进(插入时间戳)
对于迟到数据,等2秒,就是当前已有数据的最大时间戳减去2秒。就是要插入的水位线的时间戳。
面试题:水位线的特性:
水位线就代表了当前的事件时间时钟,而且可以在数据的时间戳基础
上加一些延迟来保证不丢数据,这一点对于乱序流的正确处理非常重要。
我们可以总结一下水位线的特性:
⚫ 水位线是插入到数据流中的一个标记,可以认为是一个特殊的数据
⚫ 水位线主要的内容是一个时间戳,用来表示当前事件时间的进展
⚫ 水位线是基于数据的时间戳生成的
⚫ 水位线的时间戳必须单调递增,以确保任务的事件时间时钟一直向前推进
⚫ 水位线可以通过设置延迟,来保证正确处理乱序数据
⚫ 一个水位线 Watermark(t),表示在当前流中事件时间已经达到了时间戳 t, 这代表 t 之前的所有数据都到齐了,之后流中不会出现时间戳 t’ ≤ t 的数据水位线是 Flink 流处理中保证结果正确性的核心机制,它往往会跟窗口一起配合,完成对乱序数据的正确处理
水位线延迟越高,数据结果越准确,但是实时性降低。相反同理。可以设置处理迟到处理,让实时性高一点。
因为难以选择的特性,flink把水位线控制权交给程序员
//1.自定义水位线
//flink内置水位线能满足应用需求,不需要自定义
//timestampAssigner是提取时间戳元素
//WatermarkGenerator是按照既定的方法生成水位线
//既定有onEvent每个时间到来调用
//obPeriodicEmit周期性调用,默认200ms
//env.getConfig.setAutoWatermarkInterval(60*1000L)
//2.flink内置水位线生成器:有序流forMontonousTimestamps
//只要当前最大的时间戳作为水位线就可
//withTimestampAssigner将timestamp字段提取出来
stream.assignTimestampsAndWatermarks(
WatermarkStrategy.forMonotonousTimestamps[Event]()
.withTimestampAssigner(
new SerializableTimestampAssigner[Event] {
override def extractTimestamp(element: Event,recordTimestamp: Long):
Long = element.timestamp
}
)
)
//3.乱序流 forBoundOutOfOrderness
//设置一个固定量的延迟时间,最大的时间戳减去延迟的结果。
//需要一个maxOutOfOrdemess参数表示最大乱序程序
.assignTimestampsAndWatermarks(WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(10))
.withTimestampAssigner(
new SerializableTimestampAssigner[MyOrderMaster] {
override def extractTimestamp(element: MyOrderMaster, recordTimestamp: Long): Long = {
val format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
val timeStr = element.modified_time
format.parse(timeStr.toString).getTime
}
}
)
)
//在自定义的数据源中抽取事件时间,然后发送水位线后,就不能再在程序中使用assignTimestampsAndWatermarks了。只能二选一
env.fromSource(
KafkaSource.builder()
.setTopics("ChangeRecord")
.setBootstrapServers(name.bootstrapServer)
.setValueOnlyDeserializer(new SimpleStringSchema())
.setProperty("auto.offset.reset","latest").build()
,WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(2))
.withTimestampAssigner(new SerializableTimestampAssigner[String] {
override def extractTimestamp(element: String, recordTimestamp: Long): Long = {
val format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
format.parse(element.split(",")(5)).getTime
}
}),"work2"
)
水位线的传递在重分区(多个task分区)下是按照最慢的那个时钟,也就是最小的那个水位线为准,水位线在上下游的传递避免了分布式系统没有统一时钟的问题,每次都以处理完之前的所有数据为标准确定自己的时钟,保证窗口处理的结果总是正确的。
窗口
窗口把无界数据切割成有限的数据块。
窗口分为时间窗口和计数窗口,或者滑动窗口,滚动窗口,会话窗口和全局窗口
全局窗口全局有效,会把相同key的所有数据分配到同一个窗口,因为无界流数据永无止尽,所以窗口没有结束的时候,也不会触发计算,需要添加自定义触发器;
flink计数窗口底层就是全局窗口。
在一定窗口之前,要先确定是否基于按键分区(window之前有没有keyBy),不keyBy就是windowAll并行度为1,手动调大并行度无效哦,不推荐
1.时间窗口
//1.滚动+处理时间窗口
stream.keyBy()
.window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
.aggregate()
//2.滑动+处理时间窗口
stream.keyBy(...)
.window(SlidingProcessingTimeWindows.of(Time.seconds(10),Time.seconds(5)))
.aggregate(...)
//3.会话+处理时间
stream.keyBy(...)
.window(ProcessingTimeSessionWindows.withGap(Time.seconds(10)))
.aggregate(...)
//动态间隔
.window(ProcessingTimeSessionWindows.withDynamicGap(new
SessionWindowTimeGapExtractor[(String, Long)] {
override def extract(element: (String, Long)) {
// 提取 session gap 值返回, 单位毫秒
element._1.length * 1000
}
}))
//4.滚动+事件时间窗口
stream.keyBy(...)
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.aggregate(...)
//5.滑动+事件时间
stream.keyBy(...)
.window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))
.aggregate(...)
//6.会话+事件时间窗口
stream.keyBy(...)
.window(EventTimeSessionWindows.withGap(Time.seconds(10)))
.aggregate(...)
flink的窗口分配器:
Time参数的类库为:
2.计数窗口
//1.滚动计数窗口
stream.keyBy()
.countWindow(10)
//2.滑动计数窗口
stream.keyBy()
.countWindow(10,3)
全局窗口
//3.全局窗口
stream.keyBy()
.window(GlobalWindows.create())
//必须自定义触发器才能实现窗口计算,否则起不到任何作用
窗口函数

由图上可知,
- 按键流的窗口或全局窗口之后,需要用窗口函数
reduce,aggreagte,apply,process - 窗口window/windowAll后才能用窗口函数,窗口函数运行完后会变成dataStream
- dataStream进行keyBy后才能用
sum,max,min,reduce的聚合函数
由上图可知,按键和非按键窗口函数的api由触发器,移除器,延迟数据,聚合函数,窗口函数,侧输出流。
.window和.windowAll的区别在于有没有.keyBy
.reduce/aggregate/apply/process给出了窗口函数reduceFunction/aggregateFunction/ProcessWindowFunction结果为dataStream
.trigger自定义触发器
.evictor定义移除器
.allowedLateness指定延迟时间
.sideOutputLateData定义侧输出流
.getSideOutput获取侧输出流
Flink 处理迟到数据,对于结果的正确性有三重保障:水位线的延迟,口允许迟到数据,以及将迟到数据放入窗口侧输出流。
增量聚合函数
reduceFunction和aggregateFunction
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
env
.addSource(new ClickSource)
// 数据源中的时间戳是单调递增的,所以使用下面的方法,只需要抽取时间戳就好了
// 等同于最大延迟时间是 0 毫秒
.assignAscendingTimestamps(_.timestamp)
.map(r => (r.user, 1L))
// 使用用户名对数据流进行分组
.keyBy(_._1)
// 设置 5 秒钟的滚动事件时间窗口
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
// 保留第一个字段,针对第二个字段进行聚合
.reduce((r1, r2) => (r1._1, r1._2 + r2._2))
//把reduce换成.aggregate(new Myaggr)
//.aggregate(new Myaggr)
.print()
env.execute()
//自定义窗口处理函数
class AvgPv extends AggregateFunction[Event, (Set[String], Double), Double] {
// 创建空累加器,类型是元组,元组的第一个元素类型为 Set 数据结构,用来对用户名进行去重
// 第二个元素用来累加 pv 操作,也就是每来一条数据就加一
override def createAccumulator(): (Set[String], Double) = (Set[String](), 0L)
// 累加规则,每条数据都调用。
override def add(value: Event, accumulator: (Set[String], Double)):
(Set[String], Double) = (accumulator._1 + value.user, accumulator._2 + 1L)
// 获取窗口关闭时向下游发送的结果
override def getResult(accumulator: (Set[String], Double)): Double =
accumulator._2 / accumulator._1.size
//两个窗口聚合
// merge 方法只有在事件时间的会话窗口时,才需要实现,这里无需实现。
override def merge(a: (Set[String], Double), b: (Set[String], Double)):
(Set[String], Double) = ???
}
class UvCountByWindow extends ProcessWindowFunction[Event, String, String,
TimeWindow] {
override def process(key: String, context: Context, elements: Iterable[Event],
out: Collector[String]): Unit = {
// 初始化一个 Set 数据结构,用来对用户名进行去重
var userSet = Set[String]()
// 将所有用户名进行去重
elements.foreach(userSet += _.user)
// 结合窗口信息,包装输出内容
val windowStart = context.window.getStart
val windowEnd = context.window.getEnd
out.collect(" 窗 口 : " + new Timestamp(windowStart) + "~" + new
Timestamp(windowEnd) + "的独立访客数量是:" + userSet.size)
}
}
当窗口到达结束时间需要触发计算时,就会调用这里的 apply 方法。我们可以从 input 集合中取出窗口收集的数据,结合 key 和 window 信息,通过收集器(Collector)输出结果。这里 Collector 的用法,与 FlatMapFunction 中相同。
不过我们也看到了,WindowFunction 能提供的上下文信息较少,也没有更高级的功能。事实上,它的作用可以被 ProcessWindowFunction 全覆盖,所以之后可能会逐渐弃用。一般在实际应用,直接使用 ProcessWindowFunction就可以了。
ProcessWindowFunction 是 Window API 中最底层的通用窗口函数接口。之所以说它“最底层”,是因为除了可以拿到窗口中的所有数据之外,ProcessWindowFunction 还可以获取到一个“上下文对象”(Context)。这个上下文对象非常强大,不仅能够获取窗口信息,还可以访问当前的时间和状态信息。这里的时间就包括了处理时间(processing time)和事件时间水位线(event time watermark)。这就使得 ProcessWindowFunction 更加灵活、功能更加丰富,可以认为是一个增强版的 WindowFunction。
增量聚合和全窗口函数的结合使用
基于第一个参数(增量聚合函数)来处理窗口数据,每来一个数
据就做一次聚合;等到窗口需要触发计算时,则调用第二个参数(全窗口函数)的处理逻辑输出结果。需要注意的是,这里的全窗口函数就不再缓存所有数据了,而是直接将增量聚合函数
的结果拿来当作了 Iterable 类型的输入。一般情况下,这时的可迭代集合中就只有一个元素了。
//全窗口函数
//先收集数据到内部缓存起来。等到窗口要输出结果的时候再取出数据进行计算。
env
.addSource(new ClickSource)
.assignAscendingTimestamps(_.timestamp)
// 为所有数据都指定同一个 key,可以将所有数据都发送到同一个分区
.keyBy(_ => "key")
.window(TumblingEventTimeWindows.of(Time.seconds(10)))
.process(new UvCountByWindow)
.print()
env.execute()
}
// 自定义窗口处理函数
class UvCountByWindow extends ProcessWindowFunction[Event, String, String, TimeWindow] {
override def process(key: String, context: Context, elements: Iterable[Event],
out: Collector[String]): Unit = {
// 初始化一个 Set 数据结构,用来对用户名进行去重
var userSet = Set[String]()
// 将所有用户名进行去重
elements.foreach(userSet += _.user)
// 结合窗口信息,包装输出内容
val windowStart = context.window.getStart
val windowEnd = context.window.getEnd
out.collect(" 窗 口 : " + new Timestamp(windowStart) + "~" + new Timestamp(windowEnd) + "的独立访客数量是:" + userSet.size)
}
//滑动
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
env
.addSource(new ClickSource)
.assignAscendingTimestamps(_.timestamp)
// 使用 url 作为 key 对数据进行分区
.keyBy(_.url)
.window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))
// 注意这里调用的是 aggregate 方法
// 增量聚合函数和全窗口聚合函数结合使用
.aggregate(new UrlViewCountAgg, new UrlViewCountResult)
.print()
env.execute()
}
class UrlViewCountAgg extends AggregateFunction[Event, Long, Long] {
override def createAccumulator(): Long = 0L
// 每来一个事件就加一
override def add(value: Event, accumulator: Long): Long = accumulator + 1L
// 窗口闭合时发送的计算结果
override def getResult(accumulator: Long): Long = accumulator
override def merge(a: Long, b: Long): Long = ???
}
class UrlViewCountResult extends ProcessWindowFunction[Long, UrlViewCount, String, TimeWindow] {
// 迭代器中只有一个元素,是增量聚合函数在窗口闭合时发送过来的计算结果
override def process(key: String, context: Context, elements: Iterable[Long],out: Collector[UrlViewCount]): Unit = {
out.collect(UrlViewCount(
key,
elements.iterator.next(),
context.window.getStart,
context.window.getEnd
))
}
}
case class UrlViewCount(url: String, count: Long, windowStart: Long, windowEnd: Long)
}
触发器
触发器用来控制窗口什么时候触发执行窗口函数
onElement表示每一个元素到来都调用
onEventTime表示触发事件定时器时才调用
onProcessingTime表示触发处理时间定时器才调用
clear表示窗口关闭销毁调用
窗口进行操作的类型:CONTINUE啥也不干 FIRE触发 PURGE清除 FIRE_AND_PURGE触发并清除窗口
stream.keyBy(...)
.window(...)
.trigger(new MyTrigger())
移除器
移除器用来定义移除某些数据的逻辑
evictBefore表示执行窗口函数之前的移除数据操作,默认都是窗口函数之前移除
evictAfter表示执行窗口函数之后的数据操作
stream.keyBy(...)
.window(...)
.evictor(new MyEvictor())
允许延迟
解决迟到问题设置的允许的最大延迟。水位线会变成窗口结束时间+延迟时间后才真正窗口内容清空和正式关闭窗口
stream.keyBy(...)
.window(TumblingEventTimeWindows.of(Time.hours(1)))
.allowedLateness(Time.minutes(1))
侧输出流
处理迟到数据,将迟到数据放入侧输出流进行另外处理
//把迟到数据放到输出标签outputTag里面
//sideOutputLateData(标签名)
val stream = env.addSource(new ClickSource)
val outputTag = new OutputTag[Event]("late")
stream.keyBy("user")
.window(TumblingEventTimeWindows.of(Time.hours(1)))
.sideOutputLateData(outputTag)
//把迟到数据拿出来 getSideOutput
val winAggStream = stream.keyBy(...)
.window(TumblingEventTimeWindows.of(Time.hours(1)))
.sideOutputLateData(outputTag)
.aggregate(new MyAggregateFunction)
val lateStream = winAggStream.getSideOutput(outputTag)
总结
乱序数据定义水位线延迟时间—允许迟到数据–迟到了放到侧输出流里面
// 为了方便测试,读取 socket 文本流进行处理
val stream = env
.socketTextStream("localhost", 7777)
.map(data => {
val fields = data.split(",")
Event(fields(0).trim, fields(1).trim, fields(2).trim.toLong)
})
// 方式一:设置 watermark 延迟时间,2 秒钟
.assignTimestampsAndWatermarks(WatermarkStrategy
// 最大延迟时间设置为 5 秒钟,forBoundedOutputOrderness处理乱序数据
.forBoundedOutOfOrderness[Event](Duration.ofSeconds(2))
.withTimestampAssigner(new SerializableTimestampAssigner[Event] {
// 指定时间戳是哪个字段
override def extractTimestamp(element: Event, recordTimestamp: Long): Long = element.timestamp
})
)
// 定义侧输出流标签
val outputTag = OutputTag[Event]("late")
val result = stream
.keyBy(_.url)
.window(TumblingEventTimeWindows.of(Time.seconds(10)))
// 方式二:允许窗口处理迟到数据,设置 1 分钟的等待时间
.allowedLateness(Time.minutes(1))
// 方式三:将最后的迟到数据输出到侧输出流
.sideOutputLateData(outputTag)
.aggregate(new UrlViewCountAgg, new UrlViewCountResult)
// 打印输出
result.print("result")
result.getSideOutput(outputTag).print("late")
// 为方便观察,可以将原始数据也输出
stream.print("input")
env.execute()
}
class UrlViewCountAgg extends AggregateFunction[Event, Long, Long] {
override def createAccumulator(): Long = 0L
// 每来一个事件就加一
override def add(value: Event, accumulator: Long): Long = accumulator + 1L
// 窗口闭合时发送的计算结果
override def getResult(accumulator: Long): Long = accumulator
override def merge(a: Long, b: Long): Long = ???
}
class UrlViewCountResult extends ProcessWindowFunction[Long,UrlViewCount, String, TimeWindow] {
// 迭代器中只有一个元素,是增量聚合函数在窗口闭合时发送过来的计算结果
override def process(key: String, context: Context, elements: Iterable[Long],
out: Collector[UrlViewCount]): Unit = {
out.collect(UrlViewCount(
key,
elements.iterator.next(),
context.window.getStart,
context.window.getEnd
))
}
}
ProcessWinowFunction<IN,OUT,KEY,W> 、ProcessAllWindowFunction类似
W表示窗口类型比如TImeWindow或ProcessWindws
process(key,context,elemetn,out):process函数所需参数
processWindowFunction只有prcess和clear方法,没有onTImer
topN
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
val eventStream = env
.addSource(new ClickSource)
.assignAscendingTimestamps(_.timestamp)
// 只需要 url 就可以统计数量,所以抽取 url 转换成 String,直接开窗统计
eventStream.map(_.url)
// 开窗口
.windowAll(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))
.process(new ProcessAllWindowFunction[String, String, TimeWindow] {
override def process(context: Context, elements: Iterable[String], out:
Collector[String]): Unit = {
126
// 初始化一个 Map,key 为 url,value 为 url 的 pv 数据
val urlCountMap = Map[String, Long]()
// 将 url 和 pv 数据写入 Map 中
elements.foreach(
r => urlCountMap.get(r) match {
case Some(count) => urlCountMap.put(r, count + 1L)
case None => urlCountMap.put(r, 1L)
}
)
// 将 Map 中的 KV 键值对转换成列表数据结构
// 列表中的元素是(K,V)元组
var mapList = new ListBuffer[(String, Long)]()
urlCountMap.keys.foreach(
k => urlCountMap.get(k) match {
case Some(count) => mapList += ((k, count))
case None => mapList
}
)
// 按照浏览量数据进行降序排列
mapList.sortBy(-_._2)
// 拼接字符串并输出
val result = new StringBuilder
result.append("==================================\n")
for (i <- 0 to 1) {
val temp = mapList(i)
result
.append("浏览量 No." + (i + 1) + " ")
.append("url: " + temp._1 + " ")
.append("浏览量是:" + temp._2 + " ")
.append("窗口结束时间是:" + new Timestamp(context.window.getEnd) +
"\n")
}
result.append("===================================\n")
out.collect(result.toString())
}
})
.print()
env.execute()
KeyedProcessFunction
们没有进行按键分区,直接将所有数据放在一个分区上进行
了开窗操作。这相当于将并行度强行设置为 1,在实际应用中是要尽量避免的,所以 Flink 官方也并不推荐使用 AllWindowedStream 进行处理。如果我们可以利用增量聚合函数的特性,每来一条数据就更新一次对应 url 的浏览量,那么到窗口触发计算时只需要做排序输出就可以了。
基于这样的想法,我们可以从两个方面去做优化:一是对数据进行按键分区,分别统计浏览量;二是进行增量聚合,得到结果最后再做排序输出。所以,我们可以使用增量聚合函数AggregateFunction 进行浏览量的统计,然后结合 ProcessWindowFunction 排序输出来实现 Top N
的需求。
env.setParallelism(1)
val eventStream = env
.addSource(new ClickSource)
.assignAscendingTimestamps(_.timestamp)
// 需要按照 url 分组,求出每个 url 的访问量
val urlCountStream = eventStream
.keyBy(_.url)
// 开窗口
.window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))
// 增量聚合函数和全窗口聚合函数结合使用
// 计算结果是每个窗口中每个 url 的浏览次数
.aggregate(new UrlViewCountAgg, new UrlViewCountResult)
// 对结果中同一个窗口的统计数据,进行排序处理
val result = urlCountStream
.keyBy(_.windowEnd)
.process(new TopN(2))
result.print()
env.execute()
}
class TopN(n: Int) extends KeyedProcessFunction[Long, UrlViewCount, String] {
// 定义列表状态,存储 UrlViewCount 数据
var urlViewCountListState: ListState[UrlViewCount] = _
override def open(parameters: Configuration): Unit = {
urlViewCountListState = getRuntimeContext.getListState(
new ListStateDescriptor[UrlViewCount]("list-state",
classOf[UrlViewCount]))
}
override def processElement(i: UrlViewCount, context:
KeyedProcessFunction[Long, UrlViewCount, String]#Context, collector:
collector[String]): Unit = {
// 每来一条数据就添加到列表状态变量中
urlViewCountListState.add(i)
// 注册一个定时器,由于来的数据的 windowEnd 是相同的,所以只会注册一个定时器
context.timerService.registerEventTimeTimer(i.windowEnd + 1)
}
override def onTimer(timestamp: Long, ctx: KeyedProcessFunction[Long,
UrlViewCount, String]#OnTimerContext, out: Collector[String]): Unit = {
// 导入隐式类型转换
import scala.collection.JavaConversions._
// 下面的代码将列表状态变量里的元素取出,然后放入 List 中,方便排序
val urlViewCountList = urlViewCountListState.get().toList
// 由于数据已经放入 List 中,所以可以将状态变量手动清空了
urlViewCountListState.clear()
// 按照浏览次数降序排列
urlViewCountList.sortBy(-_.count)
// 拼接要输出的字符串
val result = new StringBuilder
result.append("=========================\n")
for (i <- 0 until n) {
val urlViewCount = urlViewCountList(i)
result
.append("浏览量 No." + (i + 1) + " ")
.append("url: " + urlViewCount.url + " ")
.append("浏览量:" + urlViewCount.count + " ")
.append("窗口结束时间:" + new Timestamp(timestamp - 1L) + "\n")
}
result.append("=========================\n")
out.collect(result.toString())
}
}
class UrlViewCountAgg extends AggregateFunction[Event, Long, Long] {
override def createAccumulator(): Long = 0L
override def add(value: Event, accumulator: Long): Long = accumulator + 1L
override def getResult(accumulator: Long): Long = accumulator
override def merge(a: Long, b: Long): Long = ???
}
class UrlViewCountResult extends ProcessWindowFunction[Long, UrlViewCount,
String, TimeWindow] {
override def process(key: String, context: Context, elements: Iterable[Long],
out: Collector[UrlViewCount]): Unit = {
130
// 迭代器中只有一条元素,就是增量聚合函数发送过来的聚合结果
out.collect(UrlViewCount(
key, elements.iterator.next(), context.window.getStart,
context.window.getEnd
))
}
}
case class UrlViewCount(url: String, count: Long, windowStart: Long, windowEnd:
Long)
侧输出流
val stream = env.addSource(new ClickSource)
val outputTag: OutputTag[String] = OutputTag[String]("user")
val longStream = stream.process(new ProcessFunction[Event, Long] {
override def processElement(value: Event, ctx: ProcessFunction[Event,
Long]#Context, out: Collector[Long]) = {
//将时间戳输出到主流中
out.collect(value.timestamp)
//将用户名输出到侧输出流中
ctx.output(outputTag, "side-output: " + value.user)
}
val stringStream = longStream.getSideOutput(outputTag)
多流转换
分流
侧输出流/filter/
1.filter
val stream = env.addSource(new ClickSource)
val maryStream = stream.filter(_.user == "Mary")
val bobStream = stream.filter(_.user == "Bob")
val elseStream = stream.filter(r => !(r.user == "Mary") && !(r.user == "Bob"))
maryStream.print("Mary pv")
bobStream.print("Bob pv")
elseStream.print("else pv")
2.侧输出流
val maryTag = OutputTag[(String, String, Long)]("Mary-pv")
val bobTag = OutputTag[(String, String, Long)]("Bob-pv")
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
val stream = env.addSource(new ClickSource)
val processedStream = stream
.process(new ProcessFunction[Event, Event] {
override def processElement(value: Event, ctx: ProcessFunction[Event,
Event]#Context, out: Collector[Event]): Unit = {
// 将不同的数据发送到不同的侧输出流
if (value.user == "Mary") {
ctx.output(maryTag, (value.user, value.url, value.timestamp))
} else if (value.user == "Bob") {
ctx.output(bobTag, (value.user, value.url, value.timestamp))
} else {
out.collect(value)
}
}
})
// 打印各输出流中的数据
processedStream.getSideOutput(maryTag).print("Mary pv")
processedStream.getSideOutput(bobTag).print("Bob pv")
processedStream.print("else pv")
合流
union、connect、broadcast
stream1.union(stream2,stream3...)
stream1.connect(stream2),只能两条流==>需要map/flatMap/process
stream1.broadcast(new MapStateDescriptor)
connect
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
val stream1 = env.fromElements(1,2,3)
val stream2 = env.fromElements(1L,2L,3L)
val connectedStreams = stream1.connect(stream2)
val result = connectedStreams
.map(new CoMapFunction[Int, Long, String] {
// 处理来自第一条流的事件
override def map1(in1: Int): String = "Int: " + in1
// 处理来自第二条流的事件
override def map2(in2: Long): String = "Long: " + in2
})
result.print()
env.execute()
}
//coMapFunction \CoProcesFunction \CoFlatMapFunction
// 自定义实现 CoProcessFunction
class OrderMatchResult extends CoProcessFunction[(String, String, Long),
(String, String, String, Long), String] {
// 定义状态变量,用来保存已经到达的事件;使用 lazy 定义是一种简洁的写法
lazy val appEvent = getRuntimeContext.getState(
new ValueStateDescriptor[(String, String, Long)]("app", classOf[(String,
String, Long)])
)
lazy val thirdPartyEvent = getRuntimeContext.getState(
new ValueStateDescriptor[(String, String, String, Long)]("third-party",
classOf[(String, String, String, Long)])
)
override def processElement1(value: (String, String, Long), ctx:
CoProcessFunction[(String, String, Long), (String, String, String, Long),
String]#Context, out: Collector[String]): Unit = {
if (thirdPartyEvent.value() != null) {
// 如果对应的第三方支付事件的状态变量不为空,则说明第三方支付事件先到达,对账成功
out.collect(value._1 + " 对账成功")
// 清空保存第三方支付事件的状态变量
thirdPartyEvent.clear()
} else {
// 如果是 app 支付事件先到达,就把它保存在状态中
appEvent.update(value)
// 注册 5 秒之后的定时器,也就是等待第三方支付事件 5 秒钟
ctx.timerService.registerEventTimeTimer(value._3 + 5000L)
}
}
// 和上面的逻辑是对称的关系
override def processElement2(value: (String, String, String, Long), ctx:
CoProcessFunction[(String, String, Long), (String, String, String, Long),
String]#Context, out: Collector[String]): Unit = {
if (appEvent.value() != null) {
out.collect(value._1 + " 对账成功")
appEvent.clear()
} else {
thirdPartyEvent.update(value)
ctx.timerService.registerEventTimeTimer(value._4 + 5000L)
}
}
override def onTimer(timestamp: Long, ctx: CoProcessFunction[(String, String,
Long), (String, String, String, Long), String]#OnTimerContext, out:
Collector[String]): Unit = {
// 如果 app 事件的状态变量不为空,说明等待了 5 秒钟,第三方支付事件没有到达
if (appEvent.value() != null) {
out.collect(appEvent.value()._1 + " 对账失败,订单的第三方支付信息未到")
appEvent.clear()
}
// 如果第三方支付事件没有到达,说明等待了 5 秒钟,app 事件没有到达
if (thirdPartyEvent.value() != null) {
out.collect(thirdPartyEvent.value()._1 + " 对账失败,订单的 app 支付信息未到")
thirdPartyEvent.clear()
}
}
}
broadcast
DataStream 调用.connect()方法时,传入的参数也可以不是一个 DataStream,而是一个“广播流”(BroadcastStream),这时合并两条流得到的就变成了一个“广播连接流”.
这种连接方式往往用在需要动态定义某些规则或配置的场景。因为规则是实时变动的,所以我们可以用一个单独的流来获取规则数据;而这些规则或配置是对整个应用全局有效的,所以不能只把这数据传递给一个下游并行子任务处理,而是要“广播”(broadcast)给所有的并行子任务。而下游子任务收到广播出来的规则,会把它保存成一个状态,这就是所谓的“广播状态”(broadcast state)。
广播状态底层是用一个“映射”(map)结构来保存的。在代码实现上,可以直接调用DataStream 的 broadcast()方法,传入一个“映射状态描述器”(MapStateDescriptor)说明状态的名称和类型,就可以得到规则数据的“广播流”(BroadcastStream):(BroadcastConnectedStream)
val ruleStateDescriptor = new MapStateDescriptor[](...);
val ruleBroadcastStream = ruleStream
.broadcast(ruleStateDescriptor)
接下来我们就可以将要处理的数据流,与这条广播流进行连接(connect),得到的就是所谓的“广播连接流"(BroadcastConnectedStream)。基于BroadcastConnectedStream 调用 process()方法,就可以同时获取规则和数据,进行动态处理了。
这里既然调用了 process()方法,当然传入的参数也应该是处理函数大家族中一员——如果对数据流调用过 keyBy()进行了按键分区,那么要传入的就是 KeyedBroadcastProcessFunction;
如果没有按键分区,就传入 BroadcastProcessFunction。
val output = stream
.connect(ruleBroadcastStream)
.process( new BroadcastProcessFunction[]() {...} )
public abstract class BroadcastProcessFunction<IN1, IN2, OUT> extends
BaseBroadcastProcessFunction {
...
public abstract void processElement(IN1 value, ReadOnlyContext ctx,
Collector<OUT> out) throws Exception;
public abstract void processBroadcastElement(IN2 value, Context ctx,
Collector<OUT> out) throws Exception;
...
}
双流联结Join
connect为联结 join为联结
join是将两条流根据某个字段的值进行配对处理,connect还需要设置定时器和自定义触发逻辑来实现
//相当于sql的内连接,只输出共有的部分
stream1.join(stream2)
.where(<KeySelector>) //第一个流的key
.equalTo(<KeySelector>) //第二个流的key
.window(<WindowAssigner>) //窗口分配器:滚动tumbling,滑动sliding,会话session
.apply(<JoinFunction>) //只能是apply,没有其他替代的
//JoinFunction
public interface JoinFunction<IN1, IN2, OUT> extends Function, Serializable {
OUT join(IN1 first, IN2 second) throws Exception;
}
===============================
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
val stream1 = env.fromElements(
("a", 1000L),
("b", 1000L),
("a", 2000L),
("b", 2000L)
).assignAscendingTimestamps(_._2)
val stream2 = env.fromElements(
("a", 3000L),
("b", 3000L),
("a", 4000L),
143
("b", 4000L)
).assignAscendingTimestamps(_._2)
stream1
.join(stream2)
.where(_._1) // 指定第一条流中元素的 key
.equalTo(_._1) // 指定第二条流中元素的 key
// 开窗口
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.apply(new JoinFunction[(String, Long), (String, Long), String] {
// 处理来自两条流的相同 key 的事件
override def join(first: (String, Long), second: (String, Long)) = {
first + "=>" + second
}
}).print()
env.execute()
间隔联结
一种基于KeyedStream的合流操作,就是针对一条流的每个数据,开辟出其时间戳前后的一段时间间隔,看这期间是否有来自另一条流的数据匹配。(就是加了个上届和下界,加了时间范围(间隔)的联结)
a.timestamp + lowerBound <= b.timestamp <= a.timestamp + upperBound
因为这种给流中数据划定范围的方式导致会有重复数据。
上图所示,下面的流的每个数据都在主动范围联结上面的流,2这个数据有1,0,3这条数据有1,重复了1。
stream1
.keyBy(_._1)
.intervalJoin(stream2.keyBy(_._1))
.between(Time.milliseconds(-2),Time.milliseconds(1))
.process(new ProcessJoinFunction[(String, Long), (String, Long), String] {
override def processElement(left: (String, Long), right: (String, Long), ctx:
ProcessJoinFunction[(String, Long), (String, Long), String]#Context, out:
Collector[String]) = {
//每当匹配一条数据就会调用processElement方法。
out.collect(left + "," + right)
}
});
========================
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
// 订单事件流
val orderStream: DataStream[(String, String, Long)] = env
.fromElements(
("Mary", "order-1", 5000L),
("Alice", "order-2", 5000L),
("Bob", "order-3", 20000L),
("Alice", "order-4", 20000L),
("Cary", "order-5", 51000L)
).assignAscendingTimestamps(_._3)
// 点击事件流
val pvStream: DataStream[Event] = env
.fromElements(
Event("Bob", "./cart", 2000L),
Event("Alice", "./prod?id=100", 3000L),
Event("Alice", "./prod?id=200", 3500L),
Event("Bob", "./prod?id=2", 2500L),
Event("Alice", "./prod?id=300", 36000L),
Event("Bob", "./home", 30000L),
Event("Bob", "./prod?id=1", 23000L),
Event("Bob", "./prod?id=3", 33000L)
).assignAscendingTimestamps(_.timestamp)
// 两条流进行间隔联结,输出匹配结果
orderStream
.keyBy(_._1)
.intervalJoin(pvStream.keyBy(_.user))
// 指定间隔
.between(Time.minutes(-5), Time.minutes(10))
.process(
new ProcessJoinFunction[(String, String, Long), Event, String] {
override def processElement(left: (String, String, Long), right: Event,
ctx: ProcessJoinFunction[(String, String, Long), Event, String]#Context, out:
Collector[String]): Unit = {
out.collect(left + "=>" + right)
}
})
.print()
env.execute()
状态编程
用来输出所有数据的结果。
无状态的算子,根据输入直接转换输出。map,filter,flatMap,不依赖其他数据
有状态的算子,需要一些其他数据(比如上一条数据的结果)来得到计算结果。窗口算子会保存已经到达的所有数据。
但是因为大数据场景,我们必须是在分布式框架下,在低延迟,高吞吐还要保证容错:(检查点checkpoint,保存点savepoint,状态一致性端到端精准一次),所以需要设置这些来管理状态,避免程序的正确性降低。
如果用不上状态也就不需要容错,因为来一条处理一条。
状态分为托管和原始两种:
托管就是flink统一管理,配置容错机制后,会自动持久化保存并在故障时自动回复。
原始状态是用字节保存,需要花费大量精力处理状态的管理和维护,不建议原始状态。直接flink的一整套容错机制就行。
- 值状态 列表状态 map状态
状态的类型是string/int 还是List 或者Map。
| name | function |
|---|---|
| valueState | value获取 update更新 |
| listState | get获取 update更新 add添加 addAll添加多个 |
| MapState | get(key)获取 put(k,v)添加 putAll(Map(k,v))添加多个 remove(k)删除 contains(k)是否存在 entries获取所有 keys所有键 values所有值 |
| reducingState | add不是添加而是和旧状态归约,得到新状态 |
| aggregatingState | |
| RuntimeContext | getState获取valueState状态 getMapState获取Map状态 getListState获取List状态 getReducingState获取 getAggregatingState |
| ttl | newBuilder构造器方法 setUpdateType失效时间(OnCreateAndWrite只有创建和更改更新时效,OnReadAndWrite读写操作都会更新时效时间,就是一直活跃,寿命一直延长) setStateVisiblity状态可见性(NeverReturnExpired默认过期就不能读,ReturnExpireDefNotCleanUp过期但存在就能读) build生成StateTtlConfig |
-
按键分区状态和算子状态
作用范围不同,算子状态是当前的算子任务实例。状态对于同一个任务共享。 按键分区是key来维护和管理,只有keyBy后才可使用。
按键分区使用很广泛。为什么sum/max/min/reduce只能keyBy才能使用,因为结果使用KeyedState来存储的,没有keyedState状态没法存前后结果。
另外RichFunction来自定义KeyedState,只要提供了富函数接口算子,也可以使用keyedState。比如map,filter.或者实现CheckpointedFunction接口定义OperatorState。
所以flink的所有算子都可以有状态。
无论是 Keyed State 还是 Operator State,它们都是在本地实例上维护的,也就是说每个并
行子任务维护着对应的状态,算子的子任务之间状态不共享。 -
托管状态和原始状态
托管就是flink的容错机制,原始需要自定义,不建议使用
//valueState
val descriptor = new ValueStateDescriptor[Long](
"my state", // 状态名称
classOf[Long] // 状态类型
)
==================================================
//自定义RichFunction
class MyFlatMapFunction extends RichFlatMapFunction[Long, String] {
// 声明状态,不用lazy懒加载的话会在生命周期开始就初始化状态变量。
lazy val state = getRuntimeContext.getState(new
ValueStateDescriptor[Long]("my state", classOf[Long]))
override defflatMap(input: Long, out: Collector[String] ): Unit = {
// 访问状态
var currentState = state.value()
currentState += 1 // 状态数值加 1
// 更新状态
state.update(currentState)
if (currentState >= 100) {
out.collect("state: " + currentState)
state.clear() // 清空状态
}
} }
========valueState=======
env
.addSource(new ClickSource)
.assignAscendingTimestamps(_.timestamp)
.keyBy(_.user) // 按照用户分组
.process(new PeriodicPvResult) // 自定义 KeyedProcessFunction 进行处理
.print()
env.execute()
}
// 注册定时器,周期性输出 pv
class PeriodicPvResult extends KeyedProcessFunction[String, Event, String] {
// 懒加载值状态变量,用来储存当前 pv 数据
lazy val countState = getRuntimeContext.getState(
new ValueStateDescriptor[Long]("count", classOf[Long])
)
// 懒加载状态变量,用来储存发送 pv 数据的定时器的时间戳
lazy val timerTsState = getRuntimeContext.getState(
new ValueStateDescriptor[Long]("timer-ts", classOf[Long])
)
override def processElement(value: Event, ctx: KeyedProcessFunction[String,
Event, String]#Context, out: Collector[String]): Unit = {
// 更新 count 值
val count = countState.value()
countState.update(count + 1)
// 如果保存发送 pv 数据的定时器的时间戳的状态变量为 0L,则注册一个 10 秒后的定时器
if (timerTsState.value() == 0L) {
// 注册定时器
ctx.timerService.registerEventTimeTimer(value.timestamp + 10 * 1000L)
// 将定时器的时间戳保存在状态变量中
timerTsState.update(value.timestamp + 10 * 1000L)
}
}
override def onTimer(timestamp: Long, ctx: KeyedProcessFunction[String, Event,
String]#OnTimerContext, out: Collector[String]): Unit = {
// 定时器触发,向下游输出当前统计结果
out.collect("用户 " + ctx.getCurrentKey + " 的 pv 是:" + countState.value())
// 清空保存定时器时间戳的状态变量,这样新数据到来时又可以注册定时器了
timerTsState.clear()
}
} }
====ListState=======
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
val stream1 = env
.fromElements(
("a", "stream-1", 1000L),
("b", "stream-1", 2000L)
)
.assignAscendingTimestamps(_._3)
val stream2 = env
.fromElements(
("a", "stream-2", 3000L),
("b", "stream-2", 4000L)
)
.assignAscendingTimestamps(_._3)
stream1.keyBy(_._1)
// 连接两条流
.connect(stream2.keyBy(_._1))
.process(new CoProcessFunction[(String, String, Long), (String, String,
Long), String] {
// 用来保存来自第一条流的事件的列表状态变量
lazy val stream1ListState = getRuntimeContext.getListState(
new ListStateDescriptor[(String, String, Long)]("stream1-list",
classOf[(String, String, Long)])
)
// 用来保存来自第二条流的事件的列表状态变量
lazy val stream2ListState = getRuntimeContext.getListState(
new ListStateDescriptor[(String, String, Long)]("stream2-list",
classOf[(String, String, Long)])
)
// 处理来自第一条流的事件
override def processElement1(left: (String, String, Long), context:
CoProcessFunction[(String, String, Long), (String, String, Long), String]#Context,
collector: Collector[String]): Unit = {
// 将事件添加到列表状态变量
stream1ListState.add(left)
// 导入隐式类型转换
import scala.collection.JavaConversions._
// 当前事件和第二条流的已经到达的事件做联结
for (right <- stream2ListState.get) {
collector.collect(left + " => " + right)
}
}
// 处理来自第二条流的事件
override def processElement2(right: (String, String, Long), context:
CoProcessFunction[(String, String, Long), (String, String, Long), String]#Context,
collector: Collector[String]): Unit = {
// 将事件添加到列表状态变量中
stream2ListState.add(right)
import scala.collection.JavaConversions._
// 当前事件和第一条流的已经到达的事件做联结
for (left <- stream1ListState.get) {
collector.collect(left + " => " + right)
}
}
})
.print()
env.execute()
============MapState============
env
.addSource(new ClickSource)
.assignAscendingTimestamps(_.timestamp)
.keyBy(_.url)
.process(new FakeWindowResult(10000L))
.print()
env.execute()
}
// 自定义 KeyedProcessFunction 实现滚动窗口功能
class FakeWindowResult(windowSize: Long) extends KeyedProcessFunction[String,
Event, String] {
// 初始化一个 MapState 状态变量,key 为窗口的开始时间,value 为窗口对应的 pv 数据
lazy val windowPvMapState = getRuntimeContext.getMapState(
new MapStateDescriptor[Long, Long]("window-pv", classOf[Long],
classOf[Long])
)
override def processElement(value: Event, ctx: KeyedProcessFunction[String,
Event, String]#Context, out: Collector[String]): Unit = {
// 根据事件的时间戳,计算当前事件所属的窗口开始和结束时间
val windowStart = value.timestamp / windowSize * windowSize
val windowEnd = windowStart + windowSize
// 注册一个 windowEnd - 1ms 的定时器,用来触发窗口计算
ctx.timerService.registerEventTimeTimer(windowEnd - 1)
// 更新状态中的 pv 值
if (windowPvMapState.contains(windowStart)) {
val pv = windowPvMapState.get(windowStart)
windowPvMapState.put(windowStart, pv + 1L)
} else {
// 如果 key 不存在,说明当前窗口的第一个事件到达
windowPvMapState.put(windowStart, 1L)
}
}
// 定时器触发,直接输出统计的 pv 结果
override def onTimer(timestamp: Long, ctx: KeyedProcessFunction[String, Event,
String]#OnTimerContext, out: Collector[String]): Unit = {
// 计算窗口的结束时间和开始时间
val windowEnd = timestamp + 1L
val windowStart = windowEnd - windowSize
// 发送窗口计算的结果
out.collect( "url: " + ctx.getCurrentKey()
+ " 访问量: " + windowPvMapState.get(windowStart)
+ " 窗口:" + new Timestamp(windowStart) + " ~ " + new Timestamp(windowEnd))
// 模拟窗口的销毁,清除 map 中的 key
windowPvMapState.remove(windowStart)
}
}
========aggregatingState=============
env
.addSource(new ClickSource)
.assignAscendingTimestamps(_.timestamp)
.keyBy(_.user)
.flatMap(new AvgTsResult)
.print()
env.execute()
}
// 自定义 RichFlatMapFunction
class AvgTsResult extends RichFlatMapFunction[Event, String]{
// 定义聚合状态,用来计算平均时间戳,中间累加器保存一个(sum, count)二元组
lazy val avgTsAggState = getRuntimeContext.getAggregatingState(
new AggregatingStateDescriptor[Event, (Long, Long), Long](
"avg-ts", // 状态变量的名字
new AggregateFunction[Event, (Long, Long), Long] {
override def add(value: Event, accumulator: (Long, Long)): (Long, Long)
=
(accumulator._1 + value.timestamp, accumulator._2 + 1)
override def createAccumulator(): (Long, Long) = (0L, 0L)
override def getResult(accumulator: (Long, Long)): Long = accumulator._1
/ accumulator._2
override def merge(a: (Long, Long), b: (Long, Long)): (Long, Long) = ???
}, // 增量聚合函数的定义,定义了聚合的逻辑
classOf[(Long, Long)] // 累加器的类型
)
)
// 定义一个值状态,用来保存当前用户访问频次
lazy val countState = getRuntimeContext.getState(
new ValueStateDescriptor[Long]("count", classOf[Long])
)
override def flatMap(value: Event, out: Collector[String]): Unit = {
// 更新 count 值
val count = countState.value()
countState.update(count + 1)
// 增量聚合
avgTsAggState.add(value)
// 达到 5 次就输出结果,并清空状态
if (count == 5){
out.collect(value.user + " 平均时间戳: " + new
Timestamp(avgTsAggState.get()))
countState.clear()
}
}
=======TTL=======
val ttlConfig = StateTtlConfig
.newBuilder(Time.seconds(10))
.setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite)
.setStateVisibility(StateTtlConfig.StateVisibility.NeverReturnExpired)
.build()
val stateDescriptor = new ValueStateDescriptor[String](
"my-state",
classOf[String]
)
//开启TTL
stateDescriptor.enableTimeToLive(ttlConfig)
============checkpointedFunction============
val descriptor = new ListStateDescriptor[String](
"buffer-elements",
classOf[String]
)
val checkpointedState = context.getOperatorStateStore().getListState(descriptor)
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
env.addSource(new ClickSource)
.assignAscendingTimestamps(_.timestamp)
.addSink(new BufferingSink(10))
env.execute()
}
//实现 SinkFunction 和 CheckpointedFunction 这两个接口
class BufferingSink(threshold: Int) extends SinkFunction[Event]
with CheckpointedFunction {
private var checkpointedState: ListState[Event] = _
private val bufferedElements = ListBuffer[Event]()
override def invoke(value: Event): Unit = super.invoke(value)
// 每来一条数据调用一次 invode()方法
override def invoke(value: Event, context: Context): Unit = {
// 将数据先缓存起来
bufferedElements += value
// 当缓存中的数据量到达了阈值,执行 sink 逻辑
if (bufferedElements.size == threshold) {
for (element <- bufferedElements) {
// 输出到外部系统,这里用控制台打印模拟
println(element)
}
println("==========输出完毕=========")
// 清空缓存
bufferedElements.clear()
}
}
// 对状态做快照
override def snapshotState(context: FunctionSnapshotContext): Unit = {
checkpointedState.clear() // 清空状态变量
// 把当前局部变量中的所有元素写入到检查点中
for (element <- bufferedElements) {
// 将缓存中的数据写入状态变量
checkpointedState.add(element)
}
}
// 初始化算子状态变量
override def initializeState(context: FunctionInitializationContext): Unit =
{
val descriptor = new ListStateDescriptor[Event](
"buffered-elements",
classOf[Event]
)
// 初始化状态变量
checkpointedState = context.getOperatorStateStore.getListState(descriptor)
// 如果是从故障中恢复,就将 ListState 中的所有元素添加到局部变量中
if (context.isRestored) {
import scala.collection.JavaConversions._
for (element <- checkpointedState.get()) {
bufferedElements += element
}
}
}
} }
算子状态也支持不同的结构类型,主要有三种:ListState、UnionListState 和 BroadcastState。
在 Flink 中,对状态进行持久化保存的快照机制叫作“检查点"(Checkpoint)。于是使用算子状态时,就需要对检查点的相关操作进行定义,实现一个 CheckpointedFunction 接口。
每次应用保存检查点做快照时,都会调用 snapshotState()方法,将状态进行外部持久化。而在算子任务进行初始化时,会调initializeState()方法。这又有两种情况:一种是整个应用第一次运行,这时状态会被初始化为一个默认值(default value);另一种是应用重启时,从检查点(checkpoint)或者保存点(savepoint)中读取之前状态的快照,并赋给本地状态。这里需要注意,CheckpointedFunction 接口中的两个方法,分别传入了一个上下文(context)作为参数。不同的是,snapshotState()方法拿到的是快照的上下文FunctionSnapshotContext,它可以提供检查点的相关信息,不过无法获取状态句柄;而 initializeState()方法拿到的是FunctionInitializationContext,这是函数类进行初始化时的上下文,是真正的 “运行时上下文”。FunctionInitializationContext 中提供了“算子状态存储”(OperatorStateStore)和“按键分区状态存储”(KeyedStateStore),在这两个存储对象中可以非常方便地获取当前任务实例中的Operator State 和 Keyed State。
当初始化好状态对象后,我们可以通过调用 isRestored()方法判断当前程序是从故障中恢复的还是第一次启动。在代码中初始化 BufferingSink 时,恢复出的 ListState 的所有元素会添加到一个局部变量 bufferedElements 中,以后进行检查点快照时就可以直接使用了。在调用snapshotState()时,直接清空 ListState,然后把当前局部变量中的所有元素写入到检查点中。
检查点:
val env = StreamExecutionEnvironment.getEnvironment
env.enableCheckpointing(1000)
除了检查点之外,Flink 还提供了“保存点”(savepoint)的功能。保存点在原理和形式上跟检查点完全一样,也是状态持久化保存的一个快照;区别在于,保存点是自定义的镜像保存,所以不会由 Flink 自动创建,而需要用户手动触发。这在有计划地停止、重启应用时非常有用
状态后端:
val env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setStateBackend(new HashMapStateBackend())
env.setStateBackend(new EmbeddedRocksDBStateBackend())
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-statebackend-rocksdb_2.11</artifactId>
<version>1.13.0</version>
</dependency>
容错机制
- 检查点
========启用检查点
val env=StreamExecutionEnvironment.getExecutionEnvironment
// 每隔 1 秒启动一次检查点保存
env.enableCheckpointing(1000)
========检查点存储
// 配置存储检查点到 JobManager 堆内存
env.getCheckpointConfig.setCheckpointStorage(new JobManagerCheckpointStorage)
// 配置存储检查点到文件系统
env.getCheckpointConfig.setCheckpointStorage(new
FileSystemCheckpointStorage("hdfs://namenode:40010/flink/checkpoints"))
========检查点高级配置
val checkpointConfig = env.getCheckpointConfig
// 设置精确一次模式
//at-least-once至少一次 exactly-once精准一次,默认精准一次
//对于低延迟流处理程序,at-least-once够用且处理效率更高。
checkpointConfig.setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE)
// 最小间隔时间 500 毫秒,上一个检查点完成之后,等多久保存下一个检查点。
//只要设了这个参数,setMaxConcurrentCheckpoints强制为1
checkpointConfig.setMinPauseBetweenCheckpoints(500)
// 超时时间 1 分钟,超过保存时间就丢掉。
checkpointConfig.setCheckpointTimeout(60000)
// 同时只能有一个检查点,设置并发检查点数量。
checkpointConfig.setMaxConcurrentCheckpoints(1)
// 开启检查点的外部持久化保存,作业取消后依然保留
checkpointConfig.enableExternalizedCheckpoints(ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION)
//DELETE_ON_ CANCELLATION表示任务取消的时候自动删除外部检查点。如果任务失败退出,自动保留检查点
//DETAIN_ON_CANCELLATION:表示取消的时候也保留外部检查点
// 启用不对齐的检查点保存方式,要求检查点模式必须为exctly-once,并且并发数为1,可以大大减少检查点保存时间
checkpointConfig.enableUnalignedCheckpoints
// 设置检查点存储,可以直接传入一个 String,指定文件系统的路径
checkpointConfig.setCheckpointStorage("hdfs://my/checkpoint/dir")
- 保存点
检查点是有flink自动管理,定期创建,发生故障自动读取进行回复。
保存点手动触发保存操作。更加灵活,手动备份。
使用uid(ID)保存,没有指定ID的话flink会自动设置,但重启可能会因为ID不同无法兼容,所以强烈建议为每个算子手动指定ID
val stream = env
.addSource(new StatefulSource)
.uid("source-id")
.map(new StatefulMapper)
.uid("mapper-id")
.print()
- 使用保存点
bin/flink savepoint :jobId [:targetDirectory]为运行的作业创建一个保存点镜像
state.savepoints.dir: hdfs:///flink/savepoints设置flink-conf.yaml的保存点默认路径
env.setDefaultSavepointDir("hdfs:///flink/savepoints")单独的作业,在程序中设置路径
bin/flink stop --savepointPath [:targetDIrectory] :jobId停掉一个作业直接创建保存点
bin/flink run -s :savepointPath [:runArgs]从保存点重启应用
状态一致性
一致性检查点是flink容错机制的核心。
事务实现一致性,表示结果正确性
分布式系统强调的一致性是相同数据的副本应该总是一致的,保证计算结果正确+不漏掉任何一个数据+不会重复处理同一个数据。
流式数据正常来讲肯定是正确的,但是发生故障且需要恢复状态进行回滚的时候就有可能出错。
通过检查点的保存状态回复后结果的正确来讨论状态的一致性。
| name | fun |
|---|---|
| 最多一次at-most-once | 故障直接重启 ,直接丢掉 |
| 至少一次at-least-once | 不能保证重复处理问题,适合去重那种 |
| 精准一次exactly-once | 正好一次,数据不丢,跟没故障一样。 |
我觉得是通过检查点保证至少一次(数据不丢),然后检查点数据判断达到正好一次。而这个判断就需要从输入和输出进行判断有没有处理过,重放的数据引起的状态改变有没有包含在里面。
输入端保证就用故障恢复,就是需要数据源有重放数据的能力,比如kafka的偏移量保存为状态。保证数据至少一次
输出端需要sink端有幂等写入(重复次数不影响结果)或事务写入(保证数据不重复。
事务写入:可以根据检查点来提交和回滚,当sink遇到阻碍就开始保存状态并开启事务,保存点保存完毕,再提交事务,故障的话会回退到上一个检查点,事务也被回滚撤销)
事务写入包括预写和两阶段提交
预写WAL:结果数据作为日志保存-检查点保存时结果也持久化–检查点完成后数据一次写入外部系统并将–提交操作写入日志。 可以看出性能有问题,但适合单机数据库。而且当已经写入了外部系统但在最终保存确认的时候出故障会导致flink重复写入。
两阶段提交2PC:先准备然后再提交。当数据到来或收到检查点的分界线,sink会开启事务,然后接收数据,因为事务开启所以现在是预提交—sink收到jm发送的检查点完成的通知时就提交事务,才是真正提交。而如果中间有任何一台发生故障都可以事务回滚。充分利用检查点机制,没有写日志的批处理的性能问题,只需要一个确认信息。但是缺点就是要求比预写高。
面试题:二阶段提交的外部系统要求
两阶段提交虽然精巧,却对外部系统有很高的要求。这里将 2PC 对外部系统的要求
列举如下:
⚫ 外部系统必须提供事务支持,或者 Sink 任务必须能够模拟外部系统上的事务。
⚫ 在检查点的间隔期间里,必须能够开启一个事务并接受数据写入。
⚫ 在收到检查点完成的通知之前,事务必须是“等待提交”的状态。在故障恢复的情况
下,这可能需要一些时间。如果这个时候外部系统关闭事务(例如超时了),那么未提交的数据就会丢失。
⚫ Sink 任务必须能够在进程失败后恢复事务。
⚫ 提交事务必须是幂等操作。也就是说,事务的重复提交应该是无效的。
可见,2PC 在实际应用同样会受到比较大的限制。具体在项目中的选型,最终还应该是一致性级别和处理性能的权衡考量。
flink和kafka的精准一次
kafka的可重置偏移量是很优秀的保证精准一次的输入源,最后只要保证输出端的精准一次,分布式当然用2PC了,当然也可以用和flink天生一对kafka当输出端
配置:
- 启动检查点
- kafka的producer设为精准一次
- 设置消费者隔离级别,默认是read_uncommited可以读取未提交的数据,应该设为read_commited遇到未提交数据会停止从分区消费数据
- 事务超时配置:Flink 的 Kafka连接器中配置的事务超时时间 transaction.timeout.ms 默认是 1小时,而Kafka
集群配置的事务最大超时时间 transaction.max.timeout.ms 默认是 15 分钟。这两个超时时间,前者应该小于等于后者。
tableApi
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
// 读取数据源
val eventStream = env
.fromElements(
Event("Alice", "./home", 1000L),
Event("Bob", "./cart", 1000L),
Event("Alice", "./prod?id=1", 5 * 1000L),
Event("Cary", "./home", 60 * 1000L),
Event("Bob", "./prod?id=3", 90 * 1000L),
Event("Alice", "./prod?id=7", 105 * 1000L)
)
// 获取表环境
val tableEnv = StreamTableEnvironment.create(env)
// 将数据流转换成表
val eventTable = tableEnv.fromDataStream(eventStream)
// 用执行 SQL 的方式提取数据
val visitTable = tableEnv.sqlQuery("select url, user from " + eventTable)
// 将表转换成数据流,打印输出
tableEnv.toDataStream(visitTable).print()
// 执行程序
import org.apache.flink.table.api.Expressions.$
val clickTable2 = eventTable.select($("url"), $("user"))
env.execute()
// 创建输入表,连接外部系统读取数据
tableEnv.executeSql("CREATE TEMPORARY TABLE inputTable ... WITH ( 'connector'
= ... )")
// 注册一个表,连接到外部系统,用于输出
tableEnv.executeSql("CREATE TEMPORARY TABLE outputTable ... WITH ( 'connector'
= ... )")
// 执行 SQL 对表进行查询转换,得到一个新的表
val table1 = tableEnv.sqlQuery("SELECT ... FROM inputTable... ")
// 使用 Table API 对表进行查询转换,得到一个新的表
val table2 = tableEnv.from("inputTable").select(...)
// 将得到的结果写入输出表
val tableResult = table1.executeInsert("outputTable")
val settings = EnvironmentSettings
.newInstance()
.inStreamingMode() // 使用流处理模式
.build()
val tableEnv = TableEnvironment.create(settings)
tEnv.useCatalog("custom_catalog")
tEnv.useDatabase("custom_database")
tableAPI创建环境
//1.
val env = StreamExecutionEnvironment.getExecutionEnvironment
val tableEnv = StreamTableEnvironment.create(env)
//2,
val settings = EnvironmentSettings
.newInstance()
.inStreamingMode() // 使用流处理模式
.build()
val tableEnv = TableEnvironment.create(settings)
//设置库
tEnv.useCatalog("custom_catalog")
tEnv.useDatabase("custom_database")
tableAPI建表
//从本地数据 case class
val eventStream = env
.fromElements(
Event("Alice", "./home", 1000L),
Event("Bob", "./cart", 1000L),
Event("Alice", "./prod?id=1", 5 * 1000L),
Event("Cary", "./home", 60 * 1000L),
Event("Bob", "./prod?id=3", 90 * 1000L),
Event("Alice", "./prod?id=7", 105 * 1000L)
)
val eventTable = tableEnv.fromDataStream(eventStream)
val visitTable = tableEnv.sqlQuery("select url, user from " + eventTable)
tableEnv.toDataStream(visitTable).print()
// 创建输入表,连接外部系统读取数据
tableEnv.executeSql("CREATE TEMPORARY TABLE inputTable ... WITH ( 'connector'
= ... )")
tableEnv.createTemporaryView("NewTable", newTable)
val eventTable = tableEnv.from("EventTable")
// 注册一个表,连接到外部系统,用于输出
tableEnv.executeSql("CREATE TEMPORARY TABLE outputTable ... WITH ( 'connector'
= ... )")
//从addSource
val eventStream = env.addSource(...)
tableEnv.fromDataStream(eventStream,[$("").as(""),$("")])
tableEnv.createTemporaryView("EventTable", eventStream,
$("timestamp").as("ts"),$("url"));
//tuple
val table = tableEnv.fromDataStream(stream, $("_2").as("myInt"),
$("_1").as("myLong"))
//case class
val table = tableEnv.fromDataStream(stream, $("user").as("myUser"),
$("url").as("myUrl"))
// row
env.fromElements(
Row.ofKind(RowKind.INSERT, "Alice", 12),
Row.ofKind(RowKind.INSERT, "Bob", 5),
Row.ofKind(RowKind.UPDATE_BEFORE, "Alice", 12),
Row.ofKind(RowKind.UPDATE_AFTER, "Alice", 100))
// 将更新日志流转换为表
val table = tableEnv.fromChangelogStream(dataStream)
tableAPI处理数据
// 执行 SQL 对表进行查询转换,得到一个新的表
//使用sql
tableEnv.createTemporaryView("NewTable", newTable)
val table1 = tableEnv.sqlQuery("SELECT ... FROM inputTable... ")
201
// 使用 Table API 对表进行查询转换,得到一个新的表
//不用sql
import org.apache.flink.table.api.Expressions.$
val table2 = tableEnv.from("inputTable").select(...)
val eventTable = tableEnv.from("EventTable")
val maryClickTable = eventTable
.where($("user").isEqual("Alice"))
.select($("url"), $("user"))
val clickTable2 = eventTable.select($("url"), $("user"))
tableAPI保存数据
//打印出来
//只有简单变换,没有统计
tableEnv.toDataStream(aliceVisitTable).print()
//有统计,count,sum,gourpby
//输出十更新日志
tableEnv.toChangelogStream(tableEnv.sqlQuery()).print()
// 将得到的结果写入输出表
val tableResult = table1.executeInsert("outputTable")
例子
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
// 读取数据源,并分配时间戳、生成水位线
val eventStream = env
.fromElements(
Event("Alice", "./home", 1000L),
Event("Bob", "./cart", 1000L),
Event("Alice", "./prod?id=1", 25 * 60 * 1000L),
Event("Alice", "./prod?id=4", 55 * 60 * 1000L),
Event("Bob", "./prod?id=5", 3600 * 1000L + 60 * 1000L),
Event("Cary", "./home", 3600 * 1000L + 30 * 60 * 1000L),
Event("Cary", "./prod?id=7", 3600 * 1000L + 59 * 60 * 1000L)
)
.assignAscendingTimestamps(_.timestamp)
// 创建表环境
val tableEnv = StreamTableEnvironment.create(env)
// 将数据流转换成表,并指定时间属性
val eventTable = tableEnv.fromDataStream(
eventStream,
$("user"),
$("url"),
$("timestamp").rowtime().as("ts")
// 将 timestamp 指定为事件时间,并命名为 ts
)
// 为方便在 SQL 中引用,在环境中注册表 EventTable
tableEnv.createTemporaryView("EventTable", eventTable);
// 设置 1 小时滚动窗口,执行 SQL 统计查询
val result = tableEnv
.sqlQuery(
"SELECT " +
"user, " +
"window_end AS endT, " + // 窗口结束时间
"COUNT(url) AS cnt " + // 统计 url 访问次数
"FROM TABLE( " +
"TUMBLE( TABLE EventTable, " + // 1 小时滚动窗口
"DESCRIPTOR(ts), " +
"INTERVAL '1' HOUR)) " +
"GROUP BY user, window_start, window_end "
)
tableEnv.toDataStream(result).print()
env.execute()
}
tableAPI水位线
Flink 中支持的事件时间属性数据类型必须为 TIMESTAMP 或者 TIMESTAMP_LTZ。这里TIMESTAMP_LTZ 是指带有本地时区信息的时间戳(TIMESTAMP WITH LOCAL TIME ZONE);一般情况下如果数据中的时间戳是“年-月-日-时-分-秒”的形式,那就是不带时区信息的,可以将事件时间属性定义为 TIMESTAMP 类型。
//创建表的时候就定义水位线
CREATE TABLE events (
user STRING,
url STRING,
ts BIGINT,
ts_ltz AS TO_TIMESTAMP_LTZ(ts, 3),
WATERMARK FOR ts_ltz AS time_ltz - INTERVAL '5' SECOND
) WITH (
...
);
// 方法一:
// 流中数据类型为二元组 Tuple2,包含两个字段;需要自定义提取时间戳并生成水位线
val stream = inputStream.assignTimestampsAndWatermarks(...)
// 声明一个额外的逻辑字段作为事件时间属性
val table = tEnv.fromDataStream(stream, $("user"), $("url"), $("ts").rowtime())
// 方法二:
// 流中数据类型为三元组 Tuple3,最后一个字段就是事件时间戳
val stream = inputStream.assignTimestampsAndWatermarks(...)
// 不再声明额外字段,直接用最后一个字段作为事件时间属性
val table = tEnv.fromDataStream(stream, $("user"), $("url"), $("ts").rowtime())
处理时间就比较简单,直接就是系统时间,不需要提取时间戳和生成水位线,所以必须额外声明一个字段,专门保存当前的处理时间。
CREATE TABLE EventTable(
user STRING,
url STRING,
ts AS PROCTIME()
) WITH (
...
);
val table = tEnv.fromDataStream(stream, $("user"), $("url"), $("ts").proctime())
tableAPI窗口
val result = tableEnv.sqlQuery(
"SELECT " +
"user, " +
"TUMBLE_END(ts, INTERVAL '1' HOUR) as endT, " +
"COUNT(url) AS cnt " +
"FROM EventTable " +
"GROUP BY " + // 使用窗口和用户名进行分组
"user, " +
"TUMBLE(ts, INTERVAL '1' HOUR)" // 定义 1 小时滚动窗口
)
| name | dun |
|---|---|
| 滚动tumble | TUMBLE(TABLE EventTable, DESCRIPTOR(ts), INTERVAL '1' HOUR) |
| 滑动hop | HOP(TABLE EventTable, DESCRIPTOR(ts), INTERVAL '5' MINUTES, INTERVAL '1' HOURS)); |
| 累积cumulate | CUMULATE(TABLE EventTable, DESCRIPTOR(ts), INTERVAL '1' HOURS, INTERVAL '1' DAYS)) |
| 会话 | 暂不支持 |
tableAPI分组聚合
sum/max/min/avg/count
//根据时间的持续增长,导致key不断增加,需要设置ttl防止耗尽资源
//结果准确性会低,但为了资源消耗,这是值得的
//1.
val tableConfig = tableEnv.getConfig();
// 配置状态保持时间
tableConfig.setIdleStateRetention(Duration.ofMinutes(60))
//2.
val tableEnv = ...
val configuration = tableEnv.getConfig().getConfiguration()
configuration.setString("table.exec.state.ttl", "60 min")
//分组聚合
val result = tableEnv.sqlQuery(
"SELECT " +
"user, " +
"window_end AS endT, " +
"COUNT(url) AS cnt " +
"FROM TABLE( " +
"TUMBLE( TABLE EventTable, " +
"DESCRIPTOR(ts), " +
"INTERVAL '1' HOUR)) " +
"GROUP BY user, window_start, window_end "
)
//例子
.fromElements(
Event("Alice", "./home", 1000L),
Event("Bob", "./cart", 1000L),
Event("Alice", "./prod?id=1", 25 * 60 * 1000L),
Event("Alice", "./prod?id=4", 55 * 60 * 1000L),
Event("Bob", "./prod?id=5", 3600 * 1000L + 60 * 1000L),
Event("Cary", "./home", 3600 * 1000L + 30 * 60 * 1000L),
Event("Cary", "./prod?id=7", 3600 * 1000L + 59 * 60 * 1000L)
)
.assignAscendingTimestamps(_.timestamp)
// 创建表环境
val tableEnv = StreamTableEnvironment.create(env)
// 将数据流转换成表,并指定时间属性
val eventTable = tableEnv.fromDataStream(
eventStream,
$("user"),
$("url"),
$("timestamp").rowtime().as("ts")
)
// 为方便在 SQL 中引用,在环境中注册表 EventTable
tableEnv.createTemporaryView("EventTable", eventTable);
// 设置累积窗口,执行 SQL 统计查询
val result = tableEnv
.sqlQuery(
"SELECT " +
"user, " +
"window_end AS endT, " +
"COUNT(url) AS cnt " +
"FROM TABLE( " +
"CUMULATE( TABLE EventTable, " + // 定义累积窗口
"DESCRIPTOR(ts), " +
"INTERVAL '30' MINUTE, " +
"INTERVAL '1' HOUR)) " +
"GROUP BY user, window_start, window_end "
)
tableEnv.toDataStream(result).print()
env.execute()
tableAPI开窗函数
RANGE BETWEEN INTERVAL '1' HOUR PRECEDING AND CURRENT ROW时间开窗范围
ROWS BETWEEN 5 PRECEDING AND CURRENT ROW行开窗
SELECT user, ts,
COUNT(url) OVER (
PARTITION BY user
ORDER BY ts
RANGE BETWEEN INTERVAL '1' HOUR PRECEDING AND CURRENT ROW
) AS cnt
FROM EventTable
========
SELECT user, ts,
COUNT(url) OVER w AS cnt,
MAX(CHAR_LENGTH(url)) OVER w AS max_url
FROM EventTable
WINDOW w AS (
PARTITION BY user
ORDER BY ts
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)
//例子
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
// 读取数据源,并分配时间戳、生成水位线
val eventStream = env
.fromElements(
Event("Alice", "./home", 1000L),
Event("Bob", "./cart", 1000L),
Event("Alice", "./prod?id=1", 25 * 60 * 1000L),
Event("Alice", "./prod?id=4", 55 * 60 * 1000L),
Event("Bob", "./prod?id=5", 3600 * 1000L + 60 * 1000L),
Event("Cary", "./home", 3600 * 1000L + 30 * 60 * 1000L),
Event("Cary", "./prod?id=7", 3600 * 1000L + 59 * 60 * 1000L)
)
.assignAscendingTimestamps(_.timestamp)
// 创建表环境
val tableEnv = StreamTableEnvironment.create(env)
// 将数据流转换成表,并指定时间属性
val eventTable = tableEnv.fromDataStream(
eventStream,
$("user"),
$("url"),
$("timestamp").rowtime().as("ts")
// 将 timestamp 指定为事件时间,并命名为 ts
)
// 为方便在 SQL 中引用,在环境中注册表 EventTable
tableEnv.createTemporaryView("EventTable", eventTable)
// 定义子查询,进行窗口聚合,得到包含窗口信息、用户以及访问次数的结果表
val subQuery =
"SELECT window_start, window_end, user, COUNT(url) as cnt " +
"FROM TABLE ( " +
"TUMBLE( TABLE EventTable, DESCRIPTOR(ts), INTERVAL '1' HOUR )) " +
"GROUP BY window_start, window_end, user "
// 定义 Top N 的外层查询
val topNQuery =
"SELECT * " +
"FROM (" +
"SELECT *, " +
"ROW_NUMBER() OVER ( " +
"PARTITION BY window_start, window_end " +
"ORDER BY cnt desc " +
") AS row_num " +
"FROM (" + subQuery + ")) " +
"WHERE row_num <= 2"
// 执行 SQL 得到结果表
val result = tableEnv.sqlQuery(topNQuery)
tableEnv.toDataStream(result).print()
env.execute()
}
tableAPI联合
两表连接笛卡尔积直接from a,b就行
内连/左连/右连/全连接和sql一样inner join ,left join,right join,full outer join =on
时间间隔限制,可以用between and也可以>=这样的
- ltime=rtime
- ltime>=rtime and ltime<rtime + INTERVAL ‘10’ MINUTE
- ltime BETWEEN rtime -INTERVAL ‘10’ SECOND AND rtime + INTERVAL ‘5’ SECOND
SELECT *
FROM Order o, Shipment s
WHERE o.id = s.order_id
AND o.order_time BETWEEN s.ship_time - INTERVAL '4' HOUR AND s.ship_time
时间了解是一种更新版本的方式,涉及版本表的定义。感兴趣的可以去官网资料。
tableAPI函数
| name | fun |
|---|---|
| 比较函数 | = <> is not null |
| 逻辑函数 | and or not is false |
| 算数函数 | + power rand() |
| 字符串函数 | `string1 |
| 时间函数 | date timestamp current_time interval string range mibute year to month interval '2-10' year to month |
| 聚合函数 | count sum rank row_number |
自定义函数:
// 注册函数
tableEnv.createTemporarySystemFunction("MyFunction", classOf[MyFunction])
//使用
tableEnv.from("MyTable").select(call("MyFunction", $("myField")))
tableEnv.from("MyTable").select(call(classOf[SubstringFunction],$("myField"))
tableEnv.sqlQuery("SELECT MyFunction(myField) FROM MyTable")
=============标量函数==========
class HashFunction extends ScalarFunction {
// 接受任意类型输入,返回 INT 型输出
def eval(@DataTypeHint(inputGroup = InputGroup.ANY) o: AnyRef) : Int ={
o.hashCode()
} }
// 注册函数
tableEnv.createTemporarySystemFunction("HashFunction",classOf[HashFunction])
// 在 SQL 里调用注册好的函数
tableEnv.sqlQuery("SELECT HashFunction(myField) FROM MyTable")
==============表函数===============
@FunctionHint(output = new DataTypeHint("ROW<word STRING, length INT>"))
class SplitFunction extends TableFunction[Row] {
def eval(str: String) {
str.split(" ").foreach(s => collect(Row.of(s, Int.box(s.length))))
} }
// 注册函数
tableEnv.createTemporarySystemFunction("SplitFunction",classOf[SplitFunction]
)
// 在 SQL 里调用注册好的函数
// 1. 交叉联结
tableEnv.sqlQuery(
"SELECT myField, word, length " +
"FROM MyTable, LATERAL TABLE(SplitFunction(myField))")
// 2. 带 ON TRUE 条件的左联结
tableEnv.sqlQuery(
"SELECT myField, word, length " +
"FROM MyTable " +
"LEFT JOIN LATERAL TABLE(SplitFunction(myField)) ON TRUE")
// 重命名侧向表中的字段
tableEnv.sqlQuery(
"SELECT myField, newWord, newLength " +
"FROM MyTable " +
"LEFT JOIN LATERAL TABLE(SplitFunction(myField)) AS T(newWord, newLength) ON
TRUE")
==================聚合函数=======================================
// 累加器类型定义
case class WeightedAvgAccumulator(var sum: Long = 0L, var count: Int = 0)
// 自定义聚合函数,输出为长整型的平均值,累加器类型为WeightedAvgAccumulator
class WeightedAvg extends AggregateFunction[java.lang.Long,
WeightedAvgAccumulator> {
override def createAccumulator(): WeightedAvgAccumulator = {
WeightedAvgAccumulator() // 创建累加器
}
override def getValue(acc: WeightedAvgAccumulator): java.lang.Long = {
if (acc.count == 0) {
null // 防止除数为 0
} else {
acc.sum / acc.count // 计算平均值并返回
}
}
// 累加计算方法,每来一行数据都会调用
def accumulate(acc: WeightedAvgAccumulator, iValue: java.lang.Long, iWeight:
Int) {
acc.sum += iValue * iWeight
acc.count += iWeight
} }
// 注册自定义聚合函数
tableEnv.createTemporarySystemFunction("WeightedAvg", classOf[WeightedAvg])
// 调用函数计算加权平均值
val result = tableEnv.sqlQuery(
"SELECT student, WeightedAvg(score, weight) FROM ScoreTable GROUP BY
student"
)
==============表聚合函数=====
// 聚合累加器的类型定义,包含最大的第一和第二两个数据
case class Top2Accumulator(
var first: Integer,
var second: Integer
)
// 自定义表聚合函数,查询一组数中最大的两个,返回值为(数值,排名)的二元组
class Top2 extends TableAggregateFunction[Tuple2[Integer, Integer],
Top2Accumulator] {
override def createAccumulator(): Top2Accumulator = {
Top2Accumulator(
Integer.MIN_VALUE,
Integer.MIN_VALUE
)
}
// 每来一个数据调用一次,判断是否更新累加器
def accumulate(acc: Top2Accumulator, value: Integer): Unit = {
if (value > acc.first) {
acc.second = acc.first
acc.first = value
} else if (value > acc.second) {
acc.second = value
}
}
// 输出(数值,排名)的二元组,输出两行数据
def emitValue(acc: Top2Accumulator, out: Collector[Tuple2[Integer, Integer]]):
Unit = {
if (acc.first != Integer.MIN_VALUE) {
out.collect(Tuple2.of(acc.first, 1))
}
if (acc.second != Integer.MIN_VALUE) {
out.collect(Tuple2.of(acc.second, 2))
}
} }
// 注册表聚合函数函数
tableEnv.createTemporarySystemFunction("Top2", classOf[Top2])
// 在 Table API 中调用函数
tableEnv.from("MyTable")
.groupBy($("myField"))
.flatAggregate(call("Top2", $("value")).as("value", "rank"))
.select($("myField"), $("value"), $("rank"))
sql客户端
start-cluster.sh sql-client.sh启动集群然后启动客户端
set 'execution.runtime-mode'='streaming'设置流处理/batch批
set 'sql-client.execution.result-mode'='table'设置执行结果为table、changelog、tableau
set 'table.exec.stat.ttl'='1000'设置ttl
执行sql查询:
建表:
CREATE TABLE EventTable(
> user STRING,
> url STRING,
> `timestamp` BIGINT
> ) WITH (
> 'connector' = 'filesystem',
> 'path' = 'events.csv',
> 'format' = 'csv'
> );
Flink SQL> CREATE TABLE ResultTable (
> user STRING,
> cnt BIGINT
> ) WITH (
> 'connector' = 'print'
> )
插入:
INSERT INTO ResultTable SELECT user, COUNT(url) as cnt FROM EventTable
GROUP BY user;
tableAPI联结外部系统
create table printtable(user string,cnt bigint) with( 'connector'='print')
kafka
不加上user的话会造成名字和系统冲突导致建表失败
CREATE TABLE KafkaTable (
`user` STRING,
`url` STRING,
`ts` TIMESTAMP(3) METADATA FROM 'timestamp'
) WITH (
'connector' = 'kafka',
'topic' = 'events',
'properties.bootstrap.servers' = 'localhost:9092',
'properties.group.id' = 'testGroup',
'scan.startup.mode' = 'earliest-offset',
'format' = 'csv'
)
upsert-kafka
CREATE TABLE pageviews_per_region (
user_region STRING,
pv BIGINT,
uv BIGINT,
PRIMARY KEY (user_region) NOT ENFORCED
) WITH (
'connector' = 'upsert-kafka',
'topic' = 'pageviews_per_region',
'properties.bootstrap.servers' = '...',
'key.format' = 'avro',
'value.format' = 'avro'
);
kafka+watermark
CREATE TABLE pageviews (
user_id BIGINT,
page_id BIGINT,
viewtime TIMESTAMP,
user_region STRING,
WATERMARK FOR viewtime AS viewtime - INTERVAL '2' SECOND
) WITH (
'connector' = 'kafka',
'topic' = 'pageviews',
'properties.bootstrap.servers' = '...',
'format' = 'json'
);
kafka插入upsertkafka
INSERT INTO pageviews_per_region
SELECT
user_region,
COUNT(*),
COUNT(DISTINCT user_id)
FROM pageviews
GROUP BY user_region;
filesystem
CREATE TABLE MyTable (
column_name1 INT,
column_name2 STRING,
...
part_name1 INT,
part_name2 STRING
) PARTITIONED BY (part_name1, part_name2) WITH (
'connector' = 'filesystem', -- 连接器类型
'path' = '...', -- 文件路径
'format' = '...' -- 文件格式
)
jdbc
-- 创建一张连接到 MySQL 的 表
CREATE TABLE MyTable (
id BIGINT,
name STRING,
age INT,
status BOOLEAN,
PRIMARY KEY (id) NOT ENFORCED
) WITH (
'connector' = 'jdbc',
'url' = 'jdbc:mysql://localhost:3306/mydatabase',
'table-name' = 'users'
);
-- 将另一张表 T 的数据写入到 MyTable 表中
INSERT INTO MyTable
SELECT id, name, age, status FROM T;
elasticsearch
-- 创建一张连接到 Elasticsearch 的 表
CREATE TABLE MyTable (
user_id STRING,
user_name STRING
uv BIGINT,
pv BIGINT,
PRIMARY KEY (user_id) NOT ENFORCED
) WITH (
'connector' = 'elasticsearch-7',
'hosts' = 'http://localhost:9200',
'index' = 'users'
);
hive
val env=StreamExecutionEnvironment.getExecutionEnvironment
val conf=new Configuration()
conf.setString("rest.bind-port","8081-8089")
env.enableCheckpointing(5000,CheckpointMode.EXACTLY_ONCE)
env.getCheckpointConfig.setCheckpointStorage("hdfs://master:9000/tmp/checkpoint")
env.getCheckpointConfig.setMaxConcurrentCheckpoints(2)
if((arg.length>0 && args(0).equals("local")) || args.length==0){
tableEnv=StreamTableEnvironment.create(env)
}
val myhive=new HiveCatalog("myhive","flinkt","/opt/hive-conf.xml")
tableEnv.registerCatalog("myhive",myhive)
tableEnv.useCatalog("myhive")
tableEnv.getConfig().setConfiguration.setString("table.exec.source.idle-timeout","20minutes")
tableEnv.executeSql(s"""drop table if exists kafka_order_master""")
tableEnv.executeSql(s"""create table kakfa_order_master(
|字段列表
|)
|with(
|'connector'='kafka',
|'topic'='order_master',
|'properties.bootstrap.servers'='master:9092,slave1:9092,slave2:9092',
|'format'='json',
|'scan.startup.mode'='latest-offset'
|)
|""".stripMargin)
tableEnv.executeSql(s"""CREATE TABLE hive_sink (
|user_id STRING,
|item_id STRING,
|behavior STRING,
|ts TIMESTAMP(3)
|) WITH (
|'connector' = 'hive',
|'database' = 'default',
|'table' = 'behavior'
|);""".stripMargin)
tableEnv.executeSql(s"""
|INSERT INTO hive_sink
|SELECT
| user_id,
| item_id,
| behavior,
| ts
|FROM kafka_source;
|)
|""".stripMargin)
hbase
-- 创建一张连接到 HBase 的 表
CREATE TABLE MyTable (
rowkey INT,
family1 ROW<q1 INT>,
family2 ROW<q2 STRING, q3 BIGINT>,
family3 ROW<q4 DOUBLE, q5 BOOLEAN, q6 STRING>,
PRIMARY KEY (rowkey) NOT ENFORCED
) WITH (
'connector' = 'hbase-2.2',
'table-name' = 'mytable',
'zookeeper.quorum' = 'master:2181,slave1:2181,slave2:2181'
);
-- 假设表 T 的字段结构是 [rowkey, f1q1, f2q2, f2q3, f3q4, f3q5, f3q6]
INSERT INTO MyTable
SELECT rowkey, ROW(f1q1), ROW(f2q2, f2q3), ROW(f3q4, f3q5, f3q6) FROM T;
hive
val settings = EnvironmentSettings.newInstance.useBlinkPlanner.build()
val tableEnv = TableEnvironment.create(settings)
val name = "myhive"
val defaultDatabase = "mydatabase"
val hiveConfDir = "/opt/hive-conf"
// 创建一个 HiveCatalog,并在表环境中注册
val hive = new HiveCatalog(name, defaultDatabase, hiveConfDir)
tableEnv.registerCatalog("myhive", hive)
// 使用 HiveCatalog 作为当前会话的 catalog
tableEnv.useCatalog("myhive")
create catalog myhive with ('type' = 'hive', 'hive-conf-dir' =
'/opt/hive-conf');
use catalog myhive;
// 配置 hive 方言
tableEnv.getConfig().setSqlDialect(SqlDialect.HIVE)
// 配置 default 方言
tableEnv.getConfig().setSqlDialect(SqlDialect.DEFAULT)
-- 设置 SQL 方言为 hive,创建 Hive 表
SET table.sql-dialect=hive;
CREATE TABLE hive_table (
user_id STRING,
order_amount DOUBLE
) PARTITIONED BY (dt STRING, hr STRING) STORED AS parquet TBLPROPERTIES (
'partition.time-extractor.timestamp-pattern'='$dt $hr:00:00',
'sink.partition-commit.trigger'='partition-time',
'sink.partition-commit.delay'='1 h',
'sink.partition-commit.policy.kind'='metastore,success-file'
);
-- 设置 SQL 方言为 default,创建 Kafka 表
SET table.sql-dialect=default;
CREATE TABLE kafka_table (
user_id STRING,
order_amount DOUBLE,
log_ts TIMESTAMP(3),
WATERMARK FOR log_ts AS log_ts - INTERVAL '5' SECOND – 定义水位线
) WITH (...);
-- 将 Kafka 中读取的数据经转换后写入 Hive
INSERT INTO TABLE hive_table
SELECT user_id, order_amount, DATE_FORMAT(log_ts, 'yyyy-MM-dd'),
DATE_FORMAT(log_ts, 'HH')
FROM kafka_table;
CEP
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
// 获取登录事件流,并提取时间戳、生成水位线
val stream = env
.fromElements(
LoginEvent("user_1", "192.168.0.1", "fail", 2000L),
LoginEvent("user_1", "192.168.0.2", "fail", 3000L),
LoginEvent("user_2", "192.168.1.29", "fail", 4000L),
LoginEvent("user_1", "171.56.23.10", "fail", 5000L),
LoginEvent("user_2", "192.168.1.29", "success", 6000L),
LoginEvent("user_2", "192.168.1.29", "fail", 7000L),
LoginEvent("user_2", "192.168.1.29", "fail", 8000L)
)
.assignAscendingTimestamps(_.timestamp)
.keyBy(_.userId)
// 1. 定义 Pattern,连续的三个登录失败事件
val pattern = Pattern
.begin[LoginEvent]("first") // 以第一个登录失败事件开始
.where(_.eventType.equals("fail"))
.next("second") // 接着是第二个登录失败事件
.where(_.eventType.equals("fail"))
.next("third") // 接着是第三个登录失败事件
.where(_.eventType.equals("fail"))
// 2. 将 Pattern 应用到流上,检测匹配的复杂事件,得到一个 PatternStream
val patternStream = CEP.pattern(stream, pattern)
// 3. 将匹配到的复杂事件选择出来,然后包装成字符串报警信息输出
patternStream
.select(new PatternSelectFunction[LoginEvent, String] {
override def select(map: util.Map[String, util.List[LoginEvent]]): String
= {
val first = map.get("first").get(0)
val second = map.get("second").get(0)
val third = map.get("third").get(0)
first.userId + " 连续三次登录失败!登录时间:" + first.timestamp + ", " +
second.timestamp + ", " + third.timestamp
}
})
.print("warning")
env.execute()
}
| Pattern模式 | example | 解释 |
|---|---|---|
| begin 以…开始 | beginType | 开始标志,名字随意 |
| next接着【近邻】 | next(“second”) | 第二次标志,名字随意 |
| followedBy再下一个【宽邻居】 | .followedBy(“follow”).where | 和begin next一样为了定义事件名 |
| followedByAny()【非确定宽松】 | start.followedByAny(“middle”).where(…) | |
| allowCombinations【非确定宽松】 | 重复使用已经匹配的事件 | 和folledByAny相同 |
| ontNext不能紧跟 | start.notNext(“not”).where(…) | |
| notFollowedByAny() | start.notFollowedBy(“not”).where(…) | |
| within时间限制条件 | middle.within(Time.seconds) | |
| where当…时 | where(_.eventType.equals(“fail”)) | 判断,和filter一样 |
| oneOrMore一次或更多 | .oneOrMore | 匹配时间出现1或多次 |
| times匹配指定次数 | .times(2,4) | 比如aa/aaa/aaaa |
| consecutive循环检测[近邻] | .where().times(n).consecutive() | 循环检测3次才可 |
| greedy贪心 | .times(3,4).greedy | 匹配最小3次最多4次,比如aaaa,尽可能多 |
| optional可选 | .times(4).optional | 匹配结果可以不符合出现4次 |
面试题:allowCombinations和followedByAny区别:
allowCombinations是模式中的事件任意排序,顺序可以变,都发生了就行
followedByAny是只要是连续的,顺序不变就行
正是因为个体模式可以通过量词定义为循环模式,一个模式能够匹配到多个事件,所以之前代码中事件的检测接收才会用 Map 中的一个列表(List)来保存。而之前代码中没有定义量词,都是单例模式,所以只会匹配一个事件,每个 List 中也只有一个元素:
val first = map.get("first").get(0)
| Pattern模式 | example | 解释 |
|---|---|---|
| subtype子类型 | subtupe(classOf[type]) | 这个类型才可 |
| where | .where(_.user.startsWith(“A”)) | A开头才可 |
| 组合 | 多个where就是组合条件 | |
| or | or和where一样 | 满足一个就行 |
| 终止条件until | oneOrMore或oneOrMore.optional 结合until使用 |
where可以传函数:
迭代条件
只有当所有事件的amount之和小于100且以A开头才可以
middle.oneOrMore
.where((value, ctx) => {
lazy val sum = ctx.getEventsForPattern("middle").map(_.amount).sum
value.user.startsWith("A") && sum + value.amount < 100
})
chatGPT写的代码:
例子中,我们定义了一个名为 pattern 的模式,该模式指定了要匹配的事件序列,即:先出现一个名称为 “foo” 的事件(被标记为 “start”),然后是另一个名称为 “foo” 的事件(被标记为 “middle”),最后又出现一个名称为 “foo” 的事件(被标记为 “end”)。这些事件出现的时间间隔不能超过 10 秒。我们通过 CEP.pattern 方法将该模式应用于输入流 input,并最终通过 result.select 方法获取匹配结果。
val pattern = Pattern.begin[Event]("start")
.where(_.name == "foo")
.followedBy("middle")
.where(_.name == "foo")
.followedBy("end")
.where(_.name == "foo")
.within(Time.seconds(10))
val result = CEP.pattern(input, pattern)
result.select(pattern => {
val start = pattern("start").head
val end = pattern("end").head
(start.id, end.id, end.timestamp - start.timestamp)
})
第二种:
使用 consecutive() 操作符匹配了连续出现的类型为 EventType.TYPE_B 的事件。
Pattern.<Event>begin("start")
.subtype(Event.class).where(e -> e.getType() == EventType.TYPE_A)
.followedBy("middle")
.subtype(Event.class).where(e -> e.getType() == EventType.TYPE_B)
.consecutive() // 匹配连续出现的 B 事件
.followedBy("end")
.subtype(Event.class).where(e -> e.getType() == EventType.TYPE_C);
模式组:
Pattern套Pattern
// 以模式序列作为初始模式
val start = Pattern.begin(
Pattern.begin[Event]("start_start").where(...)
.followedBy("start_middle").where(...)
)
// 在 start 后定义严格近邻的模式序列,并重复匹配两次
val strict = start.next(
Pattern.begin[Event]("next_start").where(...)
.followedBy("next_middle").where(...)
).times(2)
// 在 start 后定义宽松近邻的模式序列,并重复匹配一次或多次
val relaxed = start.followedBy(
Pattern.begin("followedby_start")[Event].where(...)
.followedBy("followedby_middle").where(...)
).oneOrMore
//在 start 后定义非确定性宽松近邻的模式序列,可以匹配一次,也可以不匹配
val nonDeterminRelaxed = start.followedByAny(
Pattern.begin[Event]("followedbyany_start").where(...)
.followedBy("followedbyany_middle").where(...)
).optional
跳跃策略
| name | fun |
|---|---|
| NO_SKIP不跳过 | 默认,所有匹配都输出 |
| SKIP_TO_NEXT跳至下一个策略 | 和greedy效果相同(a1 a2 a3 b),(a2 a3 b),(a3 b)。 |
| SKIP_PAST_LAST_EVENT跳过所有子匹配 | AfterMatchSkipStrategy.skipPastLastEvent()跳过所有子匹配 |
| SKIP_TO_FIRST[a]跳至第一个 | 通过代码调用 AfterMatchSkipStrategy.skipToFirst(“a”)选择跳至第一个策略,这里传入一个参数,指明跳至哪个模式的第一个匹配事件。找到 a1 开始的匹配(a1 a2 a3 b)后,跳到以最开始一个 a(也就是 a1)为开始的匹配,相当于只留下 a1 开始的匹配。最终得到(a1 a2 a3 b) |
| SKIP_TO_LAST[a]跳至最后一个 | 通过代码调用 AfterMatchSkipStrategy.skipToLast(“a”)跳至最后一个策略,同样传入一个参数,指明跳至哪个模式的最后一个匹配事件。找到 a1 开始的匹配(a1 a2 a3 b)后,跳过所有a1、a2 开始的匹配,跳到以最后一个 a(也就是 a3)为开始的匹配。最终得到(a1 a2 a3 b),(a3 b)。 |
匹配时间的选择提取
val patternStream=CEP.parttern(dataStream,pattern)
val result=patternStream.select(new MyPatternSelectFunction)
class MyPatternSelectFunction extends PatternSelectFunction[Event, String]{
override def select(pattern: Map[String, List[Event]] ): String = {
val startEvent = pattern.get("start").get(0)
val middleEvent = pattern.get("middle").get(0)
startEvent.toString + " " + middleEvent.toString
} }
例子
val pattern = Pattern
.begin[LoginEvent]("fails")
.where(_.eventType.equals("fail")).times(3).consecutive()
// 2. 将 Pattern 应用到流上,检测匹配的复杂事件,得到一个 PatternStream
val patternStream = CEP.pattern(stream, pattern)
// 3. 将匹配到的复杂事件选择出来,然后包装成报警信息输出
patternStream
.select(new PatternSelectFunction[LoginEvent, String] {
override def select(map:util.Map[String, util.List[LoginEvent]]):
String = {
// 只有一个模式,匹配到了 3 个事件,放在 List 中
val first = map.get("fails").get(0)
val second = map.get("fails").get(1)
val third = map.get("fails").get(2);
first.userId + " 连续三次登录失败!登录时间:" + first.timestamp + ", "
+ second.timestamp + ", " + third.timestamp
}
})
.print("warning")
PatternFlatSelectFunction
patternStream.flatSelect(new PatternFlatSelectFunction[LoginEvent, String] {
override def flatSelect(map: util.Map[String, util.List[LoginEvent]],
out: Collector[String]):Unit = {
val first = map.get("fails").get(0)
val second = map.get("fails").get(1)
val third = map.get("fails").get(2)
out.collect(first.userId + " 连续三次登录失败!登录时间:" + first.timestamp +
", " + second.timestamp + ", " + third.timestamp)
}
}).print("warning")
匹配时间的通用检测方法
patternStream.process(new PatternProcessFunction[LoginEvent, String] {
override def processMatch(map: util.Map[String, util.List[LoginEvent]] , ctx:
Context,
out: Collector[String]): Unit ={
val first = map.get("fails").get(0)
val second = map.get("fails").get(1)
val third = map.get("fails").get(2)
out.collect(first.userId + " 连续三次登录失败!登录时间:" + first.timestamp +
", " + second.timestamp + ", " + third.timestamp)
}
}).print("warning")
patternProcessFunction的侧输出流
class MyPatternProcessFunction extends PatternProcessFunction[Event, String]
with TimedOutPartialMatchHandler[Event] {
// 正常匹配事件的处理
override def processMatch(match: Map[String, List<Event]] , ctx: Context, out:
Collector[String]) throws Exception{
...
}
// 超时部分匹配事件的处理
Override def processTimedOutMatch(match: Map[String, List[Event]], ctx:
Context): Unit {
val startEvent = match.get("start").get(0)
val outputTag = new OutputTag[Event]("time-out")
ctx.output(outputTag, startEvent)
} }
PatternTimeoutFunction
val timeoutTag = new OutputTag[String]("timeout");
// 将匹配到的,和超时部分匹配的复杂事件提取出来,然后包装成提示信息输出
val resultStream = patternStream
.select(timeoutTag,
// 超时部分匹配事件的处理
new PatternTimeoutFunction[Event, String] {
override def timeout(pattern: Map[String, List[Event]],
timeoutTimestamp: Long): String {
val event = pattern.get("start").get(0)
"超时:" + event.toString()
}
},
// 正常匹配事件的处理
new PatternSelectFunction[Event, String] {
override def select(pattern: Map[String, List[Event]] ): String{
...
}
} )
// 将正常匹配和超时部分匹配的处理结果流打印输出
resultStream.print("matched")
resultStream.getSideOutput(timeoutTag).print("timeout")
CEP应用
val pattern = Pattern
.begin[OrderEvent]("create")
.where(_.eventType.equals("create"))
.followedBy("pay")
.where(_.eventType.equals("pay"))
.within(Time.minutes(15)) // 限制在 15 分钟之内
=======
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
// 获取订单事件流,并提取时间戳、生成水位线
val stream = env
.fromElements(
OrderEvent("user_1", "order_1", "create", 1000L),
OrderEvent("user_2", "order_2", "create", 2000L),
OrderEvent("user_1", "order_1", "modify", 10 * 1000L),
OrderEvent("user_1", "order_1", "pay", 60 * 1000L),
OrderEvent("user_2", "order_3", "create", 10 * 60 * 1000L),
OrderEvent("user_2", "order_3", "pay", 20 * 60 * 1000L)
)
.assignAscendingTimestamps(_.timestamp)
.keyBy(_.orderId) // 按照订单 ID 分组
val pattern = Pattern
.begin[OrderEvent]("create")
.where(_.eventType.equals("create"))
.followedBy("pay")
.where(_.eventType.equals("pay"))
.within(Time.minutes(15))
val patternStream = CEP.pattern(stream, pattern)
val payedOrderStream = patternStream.process(new
OrderPayPatternProcessFunction)
payedOrderStream.print("payed")
payedOrderStream.getSideOutput(new
OutputTag[String]("timeout")).print("timeout")
env.execute()
}
class OrderPayPatternProcessFunction extends PatternProcessFunction[OrderEvent,
String] with TimedOutPartialMatchHandler[OrderEvent] {
override def processMatch(map: util.Map[String, util.List[OrderEvent]],
context: PatternProcessFunction.Context, collector: Collector[String]): Unit =
{
val payEvent = map.get("pay").get(0)
collector.collect("订单 " + payEvent.orderId + " 已支付!")
}
override def processTimedOutMatch(map: util.Map[String,
util.List[OrderEvent]], context: PatternProcessFunction.Context): Unit = {
val createEvent = map.get("create").get(0)
context.output(new OutputTag[String]("timeout"), "订单 " +
createEvent.orderId + " 超时未支付!用户为:" + createEvent.userId)
}
}
处理迟到数据CEP版
val patternStream = CEP.pattern(input, pattern)
// 定义一个侧输出流的标签
val lateDataOutputTag = new OutputTag[String]("late-data")
val result = patternStream
.sideOutputLateData(lateDataOutputTag) // 将迟到数据输出到侧输出流
.select(
// 处理正常匹配数据
new PatternSelectFunction[Event, ComplexEvent] {...}
)
// 从结果中提取侧输出流
val lateData = result.getSideOutput(lateDataOutputTag)
处理迟到数据原始版
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
val stream = env.fromElements(
LoginEvent("user_1", "192.168.0.1", "fail", 2000L),
LoginEvent("user_1", "192.168.0.2", "fail", 3000L),
LoginEvent("user_2", "192.168.1.29", "fail", 4000L),
LoginEvent("user_1", "171.56.23.10", "fail", 5000L),
LoginEvent("user_2", "192.168.1.29", "success", 6000L),
LoginEvent("user_2", "192.168.1.29", "fail", 7000L),
LoginEvent("user_2", "192.168.1.29", "fail", 8000L)
)
.keyBy(_.userId)
val alertStream = stream.flatMap(new StateMachineMappter)
alertStream.print("warning")
env.execute()
}
class StateMachineMappter extends RichFlatMapFunction[LoginEvent, String] {
lazy val currentState = getRuntimeContext.getState(
new ValueStateDescriptor[State]("state", classOf[State])
)
override def flatMap(value: LoginEvent, out: Collector[String]): Unit = {
if (currentState.value() == null) {
currentState.update(Initial)
}
val nextState = transition(currentState.value(), value.eventType)
nextState match {
case Matched => out.collect(value.userId + " 连续三次登录失败")
case Terminal => currentState.update(Initial)
case _ => currentState.update(nextState)
}
}
}
case class LoginEvent(userId: String, ipAddress: String, eventType: String,
timestamp: Long)
sealed trait State
case object Initial extends State
case object Terminal extends State
case object Matched extends State
case object S1 extends State
case object S2 extends State
def transition(state: State, event: String): State = {
(state, event) match {
case (Initial, "success") => Terminal
case (Initial, "fail") => S1
case (S1, "fail") => S2
case (S2, "fail") => Matched
case (Terminal, "fail") => S2
case (S1, "success") => Terminal
case (S2, "success") => Terminal
}
}
运行代码,可以看到输出与之前 CEP 的实现是完全一样的。显然,如果所有的复杂事件处理都自己设计状态机来实现是非常繁琐的,而且中间逻辑非常容易出错;所以 Flink CEP 将底层 NFA 全部实现好并封装起来,这样我们处理复杂事件时只要调上层的 Pattern API 就可以,
无疑大大降低了代码的复杂度,提高了编程的效率
运行
```flink run --class 主类 jar包`
2196

被折叠的 条评论
为什么被折叠?



