Flink学习总结
1、Flink 特点
(1)批流统一
同一套代码,可以跑流也可以跑批
同一个 SQL,可以跑流也可以跑批
(2)性能卓越
高吞吐
低延时
(3)规模计算
支持水平扩展架构
支持超大状态与增量检查点机制
(4)生态兼容
支持与 yarn 集成
支持与 kubernetes 集成
支持单机模式运行
(5)高容错
故障自动重试
一致性检查点
保证故障场景下精确一次的状态一致性
(6)高准确性
提供了事件时间和处理时间的语义,对于乱序事件流,事件时间语义仍然能提供一致且准确的结果
(7)可连接到常用的存储系统,如 kafka、hive、JDBC、HDFS、Redis
(8)高可用
flink 加上 K8s,YARN 和 Mesoso 的紧密集成,以及从故障中快速恢复和动态扩展任务的能力,能够做到极少的停机和 24 小时的运行
2、flink 是什么
核心目标:数据流上的有状态计算
具体说明:flink 是一个框架和分布式处理引擎,用于对无界和有界数据流进行有状态计算
3、有界流与无界流
(1)无界数据流(水龙头的水不断流出)
【1】有定义流的开始,没有定义流的结束
【2】它们会无休止的产生数据
【3】无界流的数据必须持续处理,即数据被社区后需要立即处理
(2)有界数据流(水桶里的水流出、水坝)
【1】有定义流的开始,也有定义流的结束
【2】有界流可以在社区所有数据后再进行计算
【3】有界流所有数据可以被排序,所以并不需要有序摄取
4、有状态的流处理
把流处理需要的额外数据保存成一个 “状态”,然后针对这条数据进行处理,并且更新状态,这就是所谓的有状态的流处理
下图的例子就是将传入的数据缓存为状态来进行累加再进行输出
状态在内存中:优点是速度快;缺点是可靠性差
状态在分布式系统中:优点是可靠性高;缺点是速度慢
5、Flink 和 SparkStreaming 的区别
(1)和 spark 比对
【1】Spark 以批处理为根本;Flink 以流处理为根本
【2】Spark 采用 RDD 模型,传输的是小批数据 RDD 的集合;Flink 采用数据流模型,传输的是事件序列
【3】Spark 是批次计算,一个批次完成后才会计算下一个;Flink 是流执行计算,一个事件在一个节点处理完后可以直接发往下一个节点进行处理
(2)和 Streaming 比对
6、flink 应用场景
(1)电商和市场营销
实时数据报表、广告投放、实时推荐
(2)物联网 IOT
传感器实时数据采集和显示、实时报警,交通运输业
(3)物流配送和服务业
订单状态实时更新,通知信息推送
(4)银行和金融业
实时结算和通知推送,实时监测异常行为
7、Flink 分层 API
8、批次处理和流处理代码的不同
(1)执行环境不同
(2)批次处理使用 groupby; 流处理使用 keyby
(3)流处理必须调用环境执行 execut()
9、集群角色
前两个表示的是 jobmanager 控制节点绑定的主机以及能够访问的 IP
0.0.0.0 代表全部 IP 都可以访问
rest 参数表示的是 UI 绑定的主机和能访问的 IP
taskmanager 表示的是普通节点参数
10、部署模式
主要有三种:
会话模式
单作业模式
应用模式
区别:
集群的生命周期以及资源的分配方式;以及应用的 main 方法到底在哪里执行,客户端还是 Jobmanager
(1)会话模式
启动一个集群,保持一个会话,所有提交作业会竞争资源
适用于单个规模小、执行时间短的大量作业
因为资源共享所以会导致很多问题
(2)单作业模式
为了能够隔离资源,考虑一个作业启动一个集群,实际应用的首选模式,需要借助资源管理框架来启动集群,例如 YARN 和 K8S 等
因为每个作业单开一个集群因此资源消耗大
(3)应用模式
由于在客户端上执行应用代码会占用大量带宽以及客户端资源,因此在应用模式中,应用会直接提交到 JobManager 上去运行,并且一个应用单开一个 JobManager,执行结束就关闭
11、Standalone 运行模式(了解)
12、K8S 运行模式(了解)
方便,但是坑很多
13、历史服务器
14、Flink 运行架构
附注:
(1)每个 job 都会对应一个 jobmaster, 对于一个新的 job 来说会有新的 jobmaster 对应
(2)分发器用于提交应用,并且负责为每一个新提交的作业启动一个新的 jobmaster
15、并行度
一个算子一个节点忙不过来,便把它分成多个算子子任务分布进行处理,从而实现了并行计算,它们完全独立运行
同一个算子,有几个人(子任务)在同时干活,并行度就是多少。在全局范围内取最大并行度为总并行度
若不指定默认为电脑线程数
并行度指定有三种方法:
代码:算子 > 代码:env > 提交时指定 > 配置文件
(1).setParallelism(2) 是单一算子任务并行度设置,优先级最高
(2)env.setParallelism(3) 是全局并行度设置,未指定单独算子并行度时优先级最高
(3)-p 提交时指定并行度优先级比 env 低
(4)在配置文件里指定,优先级最低
16、算子链
算子间的数据传输
(1)一对一
算子之间不需要重分区,也不需要重新调整数据的顺序,map\filter\flatmap 等算子都是一对一的关系,类似于 spark 中的窄依赖
(2)重分区
这种模式下的数据流分区会发生改变,算子会根据数据传输策略把数据发送到不同的下游目标任务,类似于 spark 中的 shuffle
(3)将并行度相同的一对一算子操作,可以直接链接在一起形成一个大任务,每个 task 会被一个线程执行,这样的技术被称为” 算子链 “
条件:
【1】没有数据重分区,一对一
【2】并行度相同
算子链 API:
【1】全局禁用算子链 env.disableOperatorChaining();
【2】某个算子不参与链化 算子 a.disableChaning(); 算子 a 不会和前后算子串联
【3】从某个算子开启新链条:
算子 A.startNewChain() 算子 a 不与 a 之前的串在一起,而是从 a 开始正常链化
17、任务槽
每个 Taskmanager 对每个人物运行占用资源做出的划分叫任务槽
每个任务槽其实表示了 TaskManager 拥有计算资源的一个固定大小的子集,这些资源就是用来单独执行一个子任务的
slot 仅仅用于隔离内存,不会设计 CPU 隔离,具体应用时,可以将 slot 数量配置为机器的 CPU 核心数,避免不同人物之间对 CPU 的竞争
任务对任务槽的共享:
同一个作业中,不同算子的并行子任务可以放到同一个 slot 上执行
(1)均分隔离内存,不隔离 CPU
(2)可以共享
同一个 job 中,不同算子的子任务才可以共享同一个 slot,同时在运行的前提是,属于同一个 slot 共享组,默认是 “default”,不同组之间的任务是完全隔离的,必须分配在不同的 slot 上
18、任务槽和并行度的关系
(1)任务槽是静态概念,指 taskmanager 的并发执行能力,犹如房间数量,是固定的;而并行度是动态概念,代表当前同时工作的算子子任务数量,犹如使用房间的人,数量不固定
(2)slot 数量 >= job 并行度(算子最大并行度),job 才可以运行
任务槽的数量是指所有 Taskmanager 中槽位的总数
(3)如果是 yarn 模式会动态申请
申请的 TM 数量 = job 并行度 / 每个 TM 的 slot 数,向上取整
19、作业提交流程
(1)Standalone
(2)逻辑流图 / 作业图 / 执行图
在该流程中比较重要的点有:
【1】当并行度相同且为一对一模式时可以进行链化
【2】进入 jobmanager 执行流程后最主要的一点就是将并行度展开进行操作
【3】物理图不是数据结构,只有前三个在 flink 中可以找到
【4】执行图最为重要,是调度层的核心
(3)yarn 应用模式
在这种模式下,资源管理器需要想 yarn 申请资源,并由 yarn 来进行资源调配,并最终让资源在资源管理器中注册
20、DataStream API
(1) 创建环境
【1】getExecutionEnvironment
最简单的方式,就是直接调用 getExecutionEnvironment 方法。
【2】createLocalEnvironment
这个方法返回一个本地执行环境。
【3】createRemoteEnvironment
这个方法返回集群执行环境。
(2)执行模式
【1】流执行模式(Streaming)
这是 DataStream API 最经典的模式,一般用于需要持续实时处理的无界数据流。默认情况下,程序使用的就是 Streaming 执行模式。
【2】批执行模式(Batch)
专门用于批处理的执行模式。
【3】自动模式(AutoMatic)
在这种模式下,将由程序根据输入数据源是否有界,来自动选择执行模式。
(3)触发执行程序
需要注意的是,写完输出(sink)操作并不代表程序已经结束。因为当 main()方法被调用时,其实只是定义了作业的每个执行操作,然后添加到数据流图中;这时并没有真正处理数据——因为数据可能还没来。Flink 是由事件驱动的,只有等到数据到来,才会触发真正的计算,这也被称为 “延迟执行” 或“懒执行”。
所以我们需要显式地调用执行环境的 execute() 方法,来触发程序执行。execute() 方法将一直等待作业完成,然后返回一个执行结果(JobExecutionResult)。
21、源算子
新的 source 写法
env.feomSource(Source 的实现类,watermark,名字)
22、flink 支持的数据类型
23、转换算子
(1)map 映射
(2)filter 过滤
(3)flatmap 扁平映射
最重要的特点是一进多出
map 通过使用 return 控制一进一出
flatmap 通过使用 collector 控制一进多出
24、聚合算子
(1)keyby 按键分区
keyby 实际上是逻辑意义上的分组而不是物理意义上的分组,即它不会对物理资源进行划分。多并行度时每个度代表一个子任务,涵盖一定的资源,keyby 只会对这些资源分区,但它们还是在同一个子任务内,因此 keyby 不能设置并行度
(2)简单聚合算子
【1】在 keyby 之后才能调用
【2】只有 Tuple 类型适合传输位置索引,POJO 不行
【2】max/maxby 的区别
max: 只会取比较字段的最大值非比较字段保留第一次的值
maxby: 取比较字段的最大值,同时会比较非比较字段,并取非比较字段的最大值
(3)reduce 算子
1 需要再 keyby 后使用
2 输入类型 = 输出类型,类型不能改变
3 每个 key 的第一条数据来的时候,不会执行 reduce 方法,而是会保存起来,直接输出
4 reduce 方法中的两个参数:
value1 : 之前的计算结果,存状态
value2 : 现在来的数据
25、用户自定义函数(UDF)
(1)富函数类
RichXXXFunction: 富函数
【1】多轮生命周期管理方法
open(): 每个子任务在启动时,调用一次
close(): 每个子任务,在结束时调用一次
如果 flink 程序异常挂掉,不会调用 close
如果是正常调用 cancel 命令,可以 close(web ui)
【2】 多了一个运行时上下文
可以获取一些运行时的环境信息,例如子任务编号,名称,其他的....
26、物理分区算子
(1)随机分区 shuffle
random.nextInt(下游算子并行度)
(2)轮询分区 reblance
nextChannelToSendTo = (nextChannelToSendTo + 1) % 下游算子并行度
如果是数据源倾斜的场景,source 读进来后调用 rebalance 就可以解决数据源的数据倾斜
(3)缩放 rescale 局部组队 加快效率
(4)广播 broadcast
发送给下游的所有子任务
这种方式其实不应该叫做 “重分区”,因为经过广播之后,数据会在不同的分区都保留一份,可能进行重复处理。可以通过调用 DataStream 的 broadcast() 方法,将输入数据复制并发送到下游算子的所有并行任务中去。
(5)全局分区 global 全部只发往第一个子任务
全局分区也是一种特殊的分区方式。这种做法非常极端,通过调用. global() 方法,会将所有的输入流数据都发送到下游算子的第一个并行子任务中去。这就相当于强行让下游任务并行度变成了 1,所以使用这个操作需要非常谨慎,可能对程序造成很大的压力。
(6)按指定分区发送 keyby 指定分区发送
7 种分区器 + 1 种自定义
27、分流
(1)使用 filter
问题:同一个数据要被处理两遍,性能差
(2)侧输出流
每条数据只需要处理一遍即可
总结步骤;
【1】使用 process 算子
【2】定义 outputTag 对象
【3】调用. output
【4】通过主流获取侧流
28、合流
(1)union 合并数据流
【1】流的数据类型必须一致
【2】 一次可以合并多条流
(2)connect 连接
connect 合流
【1】一次只能连接两条流
【2】流的数据类型可以不一样
【3】连接后可以调用 map、flatmap、process 来处理,但各处理各的
(3)CoMapFunction
(3)CoProcessFunction
ConnectionStreams 也可以直接调用. keyby()进行按键分区的操作,得到的还是一个 ConnectedStreams
在多并行度的情况下要进行 keyby
29、输出算子
flink 1.12 以前:.addSink
flink 1.12 以后:.sinkTo
(1)连接到外部系统
支持对象:
(2)输出到文件
行编码: FileSink.forRowFormat(basePath,rowEncoder)
批量编码: FileSink.forBulkFormat(basePath,bulkWriterFactory)
(3)输出到 kafka
如果需要精准一次写入 kafka 需要满足以下全部条件:
【1】开启 checkpoint
【2】设置事务前缀
【3】设置事务超时时间, checkpoint 间隔时间 < 事务超时时间 < max 的 15 分钟
如果要制定 kafka 的 key
可以自定义反序列化器
【1】实现一个接口,重写反序列化方法
【2】指定 key,转换成字节数据组
【3】指定 value 转换成字节数组
【4】返回一个 ProducerRecord
(4)输出到 JDBC
【1】只能用老的 sink 写法: addsink
【2】JDBCSink 的 4 个参数:
第一个参数: 执行的 sql,一般就是 insert into
第二个参数: 预编译 sql, 对占位符填充值
第三个参数: 执行选项 ---》 攒批、重试
第四个参数: 连接选项 ---》 url、用户名、密码
(5)自定义 Sink 输出
30、窗口
(1)概念
用处:在实时处理统计中用于统计一段时间内的数据
方式:将无限数据切割成有限的数据块进行处理
动态创建
(2)分类
【1】按照 驱动类型区分
依照窗口替换,即” 换桶接水 “的条件区分为时间窗口和计数窗口
【2】按照窗口分配数据的规则分类
依照分配数据的规则可以分为滚动窗口、滑动窗口、会话窗口、全局窗口
滚动窗口进行均匀分片,窗口之间没有重叠,也不会有间隔,是首尾相接的状态。每个数据都会被分配到一个窗口,且只属于这个窗口。适用于对每个时间段做聚合统计
滑动窗口大小固定,但是窗口间不会首尾相接,可以错开一定位置。滑动步长代表的是计算频率。当滑动步长小于窗口大小时就会出现窗口重叠,此时数据会被分配到多个窗口当中。滑动窗口适合计算结果更新频率很高的场景
步长会触发数据的收集,而窗口长则是统计区间长度
例如一个窗口长为 5,步长 2,那么每有两条数据就收集一次数据,每有五条数据被收集就会新开一个窗口,并间隔 2,从第二条数据开始就会触发窗口
只要经过一个步长(2)就一定会有一个窗口触发,刚开始时可以想象成有一个虚拟窗口存在触发
会话窗口基于 “会话” 来对数据进行分组,长度不固定。会话超时时间表示两个会话之间的最小距离。若数据到来时间间隔小于指定大小,则在同一窗口,否则新数据属于新窗口,前窗口关闭。会话窗口之间一定不重叠,且留有时间间隔。适用于保持会话的场景下
窗口没有结束的时候,默认不会触发计算,需要有自定义触发器
(3)API
【1】按键分区和非按键分区
没有 keyby 的窗口, 没有进行 keyby,不会分成多条逻辑流,只会在一个任务上执行,并行度为 1
有 keyby 的窗口,数据流会根据 key 被分为多条逻辑流,这就是 KeyedStream。窗口计算会在多个并行子任务执行,且会基于每个 key 进行单独处理
【2】窗口分配器
【3】窗口函数
(4)增量聚合 Reduce
【1】相同 key 的第一条数据来的时候,不会调用 reduce 方法
【2】增量聚合:来一条数据就会计算一次,但是不会输出
【3】在窗口触发的时候,才会输出窗口的最终计算结果
(5)聚合函数 AggregateFunction
三个参数:
输入类型 IN 输入数据的类型
累加器类型 ACC 累加器的类型,存储的中间计算结果的类型
输出类型 OUT 输出的类型
(6)全窗口函数
(7)增量聚合 aggregate + 全窗口 process
【1】增量聚合函数处理数据:来一条计算一条
【2】窗口触发时,增量聚合的结果(只有一条) 传递给全窗口函数
【3】 经过全窗口函数的处理包装后输出
两者结合的优点:
【1】增量聚合: 来一条计算一条,存储中间的计算结果,占用空间少
【2】全窗口函数,可以通过上下文实现灵活的功能
(8)其他 API
触发器 Trigger & 移除器 Evictor
触发器、移除器:现成的几个窗口,都有默认的实现,一般不需要自定义
触发器用于触发计算,触发条件为
时间进展 >= 窗口的最大时间戳 (end - 1ms)
移除器用于移除某些数据的逻辑
【1】窗口什么时候触发输出 ?
时间进展 >= 窗口的最大时间戳 (end - 1ms)
【2】窗口是怎么划分的 ?
start = 向下取整,取窗口长度的整数倍
end = start + 窗口长度
窗口左闭右开 =》 属于本窗口的最大时间戳 = end - 1ms
【3】窗口的生命周期
创建:属于本窗口的第一条数据来的时候 现 new 的,放入一个 singleton 单例的集合中
销毁:(关窗) 时间进展 >= 窗口的最大时间戳 (end - 1ms) + 允许迟到的时间(默认 0)
31、时间语义
事件时间:创建时间,更加准确,比较重要
处理时间:逻辑处理时间
32、水位线
(1)事件时间和窗口
自定义逻辑时钟的好处在于不依赖于处理时间(系统时间),进而导致统计处理的结果能够正确。在实际应用场景中,处理时间只会稍稍延迟
(2)水位线
用于衡量事件时间进展的标记,就被称作水位线(watermark),可以看作为一条特殊的数据记录,用于指示当前的数据时间
【1】有序流的水位线
【2】乱序流的水位线
在乱序流且数据量较小的情况下,会对时间戳进行比对,只留下大的
在乱序流且数据量较大时,可以周期性地生成水位线,只需要保存最大的时间戳
【3】延迟等待
【4】总结
(3)水位线和窗口的工作原理
每个窗口可以理解为一个水桶,但这些窗口只使用一遍,且他们是独立的,不同的
在第一个窗口中,不同时间戳的数据会被分别放入不同的窗口中;由于存在延迟等待(2s),因此等到了 12 的时候第一个窗口才会开始处理,而第一个窗口中的 11 和 12 会交给下一个窗口
(4)水位线生成的总体原则
保证绝对正确:等待足够长的延迟
保证处理够快:水位线延迟设得低一些,实时性强
【1】内置 watermark 的生成原理
* 都是周期性生成的 默认 200ms
* 有序流 watermark = 当前最大的事件时间 - 1ms
* 乱序流 watermark = 当前最大的事件时间 - 延迟时间 - 1ms
(5)水位线的传递 (watermark)
当多个并行度算子的水位线被发送到同一个算子进行处理时,该算子会选择最小的算子作为自己的水位线进行传播
watermark 多并行度下的传递
【1】接收到上游多个,取最小
【2】往下游发送,广播
可能导致的问题:最小 watermark 的上游任务迟迟不更新 watermark 导致窗口无法触发。
解决方法:设置空闲等待,一定时间不更新则不再使用
(6)迟到数据处理
【1】推迟水印推进
【2】设置窗口延迟关闭
推迟关窗时间,在关窗之前,迟到数据来了,还能被窗口计算,来一条迟到数据触发一次计算
(7)总结
【1】乱序与迟到的区别
乱序: 数据的顺序乱了,时间小的比时间大的晚来
迟到: 数据的时间戳 < 当前的 watermark
【2】迟到、乱序的数据处理
1)watermark 中指定乱序等待时间
2)如果开创,设置窗口允许迟到
=》 推迟关窗时间,在关窗之前,迟到数据来了,还能被窗口计算,来一条迟到数据触发一次计算
=》关窗后,迟到数据不会被计算
3)关窗后的迟到数据,放入侧输出流
【3】如果 watermark 等待 3s,窗口允许迟到 2s,为何不是直接设置 watermark 等待或者窗口允许迟到 5s?
=》watermark 等待时间不会设太大 =》 影响计算延迟
=》窗口允许迟到是对大部分吃到数据的处理,尽量让结果准确。
如果只设置允许迟到 5s,就会导致频繁重新输出
【4】设置经验
1)watermark 等待时间,设置一个不算特别大的,一般是秒级,在乱序和延迟取舍
2)设置一定的窗口允许迟到,只考虑大部分的迟到数据,极端小部分迟到很久的数据不管
3)极端小部分迟到很久的数据,放到侧输出流,获取到之后可以做各种处理
33、基于时间的合流——双流联结
(1)窗口联结
Window join
【1】落在同一个时间窗口范围内才能够匹配
【2】根据 keyby 的 key 才能够进行关联匹配
【3】只能拿到匹配上的数据,类似有固定时间区间的 inner join
(2)间隔联结
【1】 只支持事件时间
【2】指定上界、下界的偏移,符号代表时间往前,正好代表时间往后
【3】 process 中,只能处理 join 上的数据
【4】 两条流关联后的 watermark,以两条流中最小的为准
【5】如果当前数据的事件时间 《 当前的 watermark,就是迟到数据,主流的 process 不处理
=》between 后,指定将左流或右流的迟到数据,放入侧输出流
34、处理函数
(1)功能和使用
(2)定时器
35、TopN 案例:实时统计一段时间内的出现次数最多的水位
(1)思路一:使用所有数据到一起,用 hashmap 来存,key = vc ;value = count 值
【1】遍历数据, 统计各个 vc 出现的次数
【2】对 count 值进行排序,利用 List 实现
【3】取出 count 最大的 2 个 vc
(2)思路二:使用 KeyedProcessFunction 实现
【1】按照 vc 做 keyby,开窗,分别 count
==> 增量聚合,计算 count, 先对位于窗口内的数据进行计数
==》全局窗口对各个数据计算结果 count 进行封装,带上窗口结束时的标签
==》为了让同一个窗口时间范围的计算结果到一起去
【2】对同一个窗口范围的 count 值进行处理: 排序,取前 n 个
==》 按照 windowEnd 做 keyby
==》 使用 process,来一条调用一次,需要先存,分开存 HashMap,key=windowEnd,value = List
==》使用定时器,对存起来的结果进行排序、取前 n 个
36、侧输出流
OutputTag 的作用:
定义旁路输出的 tag,旁路输出时会绑定该 tag。这里需要定义 tag 的名称, 相当于标记这个旁路流出的数据键值是什么
37、状态管理
(1)分类
【1】
托管状态:像把钱存银行,不管钱会被拿来干什么,只负责存取,优先采用
原始状态:自己开辟一块内存,就像自己家里买个保险柜存钱,自己管理
【2】托管状态里又可以细分
算子状态、按键分区状态
经过 keyby 函数的状态都叫按键分区状态
算子状态:
该状态作用范围限定在当前的算子任务实例中相当于一个本地变量
按键分区状态
在同一子任务中,不同键值的状态彼此独立,不可交叉访问
(2)按键分区状态
【1】值状态
相比于使用全局变量的优势
对于不同的类别,全局变量的数量取决于类别数量,需要使用 hashmap 等数据结构存储,效率不如使用值状态。值状态的初始化必须放到 open 函数中进行
【2】列表状态
【3】Map 状态(MapState)
【4】规约状态(ReducingState)
【5】聚合状态(AggregatingState)
【6】状态生存时间
‘.newBuilder: 设定的状态生存时间
.setUpdateType: 制定什么时候更新状态失效时间
.setStateVisibility: 所谓的 “状态可见性”,是指因为清除操作并不是实时的,所以当状态国旗后还有可能继续存在,这时候如果对它进行访问,,能否正常读取到就是一个问题了
(3)算子状态
【1】列表状态 & 联合列表状态
算子状态中 list 要和 unionlist 的区别:并行度改变时,怎么重新分配状态
list 状态:轮询均分给新的并行子任务
unionlist 状态: 原先的多个个子任务的状态,合并成一份完整的 list。广播给新的子任务,每人一份完整的,自己取需要的
【2】广播状态
(4)状态后端
管理本地状态的存储方式和位置
【1】分类
1)HashMapStateBackend 哈希状态后端 - 默认
把状态存放在内存里,保存在 Taskmanager 的 JVM 堆上
2)RocksDB 内嵌 RocksDB 后端
数据持久化到本地硬盘,保存在 TaskManager 的本地数据目录里,被存储为序列化的字节数组
【2】如何选择
1)HashMapBackend 是内存计算,读写速度快,但是由于使用的是 Taskmanager 的内存,因此存在内存资源限制
2)RocksDB 是硬盘存储,适合于超级海量状态的存储,但是由于是硬盘存储且需要序列化因此读写效率慢
【3】配置方式
配置方式
-
配置文件 默认值 flink-conf.yaml
-
代码中指定
-
提交参数指定
38、容错机制
(1)检查点
(2)检查点的保存
检查点的保存应该具有一致性,及所有节点的保存进度应该是一致的
(3)具体流程
(4)检查点算法
(5)分布式快照算法
【1】Barrier 对齐的精准一次
由于需要进度一致,因此当一个分界线到达而另一个没有时,需要等待另一个到达,期间未到达分界线方向的数据继续流入,已到达分界线方向的数据会被缓存,但不会被持久化存储,但会造成阻塞
【2】Barrier 对齐的至少一次
在这种模式下,当其中一个分界线未到达时,另一个已到达的分界线数据会继续流入以便后续的持久化保存。但这会带来一个问题,即若发生错误需要恢复,恢复后会对这些后续流入的数据进行重复计算
【3】非 Barrier 对齐的精准一次
在该方式中,只要有一个分界线到达,该分界线就可以以 “插队” 的形式到到达缓冲数据的最前面,目的是为了不阻塞,可以直接往下一个节点走,并将未到达分界线和到达分界线之间的所有数据进行持久化存储。在恢复的时候,除了将对应节点的数据恢复,那些缓存的数据也会被放回到对应的位置。但这种方法无形中增加了 IO 压力已经还有备份大小。因此只适用于对齐时间比较久的情况。
【4】总结 * 检查点算法总结
barrier 对齐: 一个 Task 收到所有上游同一个编号的 barrier 之后,才会对自己的本地状态做备份
1) 精准一次:在 barrier 对齐过程中,barrier 后面的数据阻塞等待(不会越过 barrier)
- 至少一次:在 barrier 对齐过程中,先到的 barrier,其后面的数据不阻塞,接着计算
非 barrier 对齐: 一个 Task 收到第一个 barrier 时,就开始执行备份
-
先到的 barrier,将本地状态备份,其后面的数据接着计算输出
-
未到的 barrier,其前面的数据接着计算输出,同时也保存到备份中
-
最后一个 barrier 到达该 Task 时,这个 Task 的备份结束
39、通用增量
(1)带状态的算子任务将状态更改写入变更日志,记录状态
changelog 会记录状态的数据变化操作并进行同步。例如增加一个数据 4: +I 4
state Table 用于记录当前状态变化后的结果
(2)状态物化,状态表定期保存,独立于检查点
状态表自己独立定期保存,和检查点周期没有关系
(3)状态物化完成后,状态变更日志就可以被截断到相应的点
(4)使用方式
【1】配置文件指定
【2】在代码中设置
(5)最终检查点
40、保存点
(1)用途
【1】版本管理和归档存储
将老版本代码的运行状态保存,等待新版本上线后再通过保存点进行恢复
需要注意的是,保存点能在程序更改时兼容的前提是状态的拓扑结构和数据类型不变
但是下图这种添加了算子的情况是可以恢复的,前提是每个算子都提前设置了算子 UID
【2】更新 flink 版本
【3】更新应用程序
【4】调整并行度
【5】暂停应用程序
(2)和检查点的区别
保存点和检查点的最大区别在于触发的时机。检查点是 flink 自动管理,定期创建的,是自动存盘;而保存点不会自动创建,必须由用户手动触发,是手动存盘
41、状态一致性
最多一次:数据处理过一次后便不再理会
至少一次:一条数据可能被处理多次(重复)
精确一次:刚好给一次,并处理一次,不多不少
42、端到端精确一次
43、输出端保证
保证精准一次一致性写入的方式有两种:
(1)幂等写入
利用 mysql 主键,它是唯一的,反复写入也只会保留最新的一条
(2)事务写入
【1】预写日志
该方式中,当算子处理完相关数据后不会直接写出去,而是缓存起来。
接着做完 checkpoint 后,所有的状态和缓存数据都被备份后,会将缓存的数据一次性写出。为了防止写出失败,所以添加了 ACK 应答机制。
若数据已经写过去了但 ACK 应答失败,则判定这个检查点是不可用的,一旦发生故障则会往更前面寻找成功的检查点,进而导致已经缓存的数据被重复写,因此只能保证至少一次的语义
【2】两阶段提交(2PC)
用来解决分布式场景下的事务
1)上一次检查点完成,barrier 后续的数据开始进行预提交(sink 多个子任务都在往外写),这个时候外部系统还无法读取这些数据,只是会给这些到达的数据打上标签,等待收齐后再提交
2)当新的检查点完成时,各个节点进行正式提交,此时这些预提交的数据将在外部系统中可见。若该批次提交的数据中有提交失败的则需要回滚重新提交,要么一起成功,要么一起失败
3)新的 sink 架构使用的是 TwoPhaseCommittingSink 架构
4)注意
5)流程
44、Flink SQL
(1)流处理中的表
对于无界流数据,表中的数据需要不断更新查询结果,永不停止
(2) 动态表和持续查询
【1】动态表
表中的数据在不断变化
【2】持续查询
持续查询的结果又是一个动态表
(2)用 SQL 持续查询
【1】更新 (update) 查询
可以对已经输出的数据进行更新
【2】追加(append)查询
只要输出就不会再变化
(3)将动态表转换为流
【1】仅仅追加流
通过 Insert 来修改动态表,新增一行
【2】撤回流
包含两类,添加(add)消息和撤回(retract)消息。添加用 add,撤回用 retract,更新用被更改行的 retract 消息和更新后行(新行)的 add 消息
(4)时间语义属性的定义
【1】事件时间
一定要添加单引号,这是特殊语法
【2】处理时间
45、DDL 数据定义
(1)库
修改数据库: ALTER DATABASE
删除数据库:DROP DATABASE
【1】RESTRICT 删除非空数据库会触发异常,默认使用
【2】CASCADE 删除非空数据库也回删除所有相关的表和函数
(2)表
【1】physical_column_definition
物理列是数据库中的常规列,定义了物理介质存储的数据中字段的名称、类型
【2】 metadata_column_definition
元数据列是 SQL 的扩展,用于记录数据的数据信息,例如书序从 kafaka 记录中读取和写入的时间戳
【3】PRIMARY KEY
主键约束,表中的一列或者一组数据是唯一的,非空的,只支持 not enforce, 必须加上
【4】with
用于创建表的表属性,用于指定外部存储系统的元数据信息。配置属性时,表达式 key1=vall 的键和值都应该是字符串的面值
【5】LIKE
基于现有表的定义创建表
46、操作
(1)With 子句
(2)去重子句
SELECT DISTINCT
47、多维分析
根据 Products(supplier_id, product_id, rating) 来判断各个属性的对应列
48、分组窗口聚合
目前只支持基于时间的窗口,不支持基于元素个数
时间属性、窗口长度
时间属性、滑动步长,窗口长度
时间属性、会话间隔
举例:
(1)滚动窗口示例
每 5 秒一个滚动窗口
(2)滑动窗口示例
每 2 秒一个滑动窗口
(3)会话窗口示例
5 秒钟没有数据来才会输出
49、TVF 窗口表值函数聚合
(1)滚动窗口
(2)滑动窗口
在 VTF 中,要求窗口长度 = 滑动步长的整数倍,因为底层会优化成多个小滚动窗口,从而提高性能问题(按照步长将滑动窗口分割成小滚动窗口)
(3)累积窗口
把大窗口拆分成许多的小窗口,每个小窗口可以有自己的开始和结束时间,因此窗口最大长度必须是累积步长的整数倍
50、Over 聚合
over 聚合不像 group by 那样将每个组的结果行数减少为 1 行
(1)语法
order by 必须是时间戳列(事件时间、处理时间),只能升序
(2)按照时间区间进行聚合 (range)
(3)按照行数聚合(rows)
51、TOP-N 特殊语法
在这里的 order by 中是可以升序也可以降序的
(1) 示例
52、Deduplication 去重
排序字段必须是时间属性列,但可以降序;
当 row_number = 1 时:
若排序字段是时间属性列,则被认为是去重
若排序字段不是时间属性列,则为 Top-N
(1)语法
(2)示例
如果是按照时间属性字段降序,表示取最新一条,会造成不断的更新保存最新的一条。如果是升序,表示取最早的一条,不用去更新,性能更好
53、联结(join)查询
(1)常规联结查询
只要来到的数据都会存储在状态里
注意:
(2)附注:
为什么在设置有序和乱序窗口时都需要 - 1ms?
这主要是为了防止在阈值时间内上传多条数据导致无法及时接收。例如阈值 t <= 5s,那么当收到一条数据的 watermark=5s 时,就会关闭窗口输出,若在这一秒内同时上传了多条数据,且 watermark 都为 5,就可能 导致部分数据无法及时进入窗口,因此需要 - 1ms,代表只承诺在这个阈值前的所有数据都上传到窗口
54、间隔联结查询
(1)要点
【1】两表的联结
间隔联结不需要 JOIN 关键字,直接在 FROM 后将要连接的两表列出来用逗号分隔即可。
【2】联结条件
不需要使用 ON, 而是使用 WHERE 写入条件:
联结条件等式 + and between ... and ...
【3】时间间隔限制
(2)示例
55、维表联结查询
(1)lookup_join
每次进入的数据在 MySQL 中查询维度表所需要的值都是变化的,也就是说,查询相同的键值可能得到不同的结果。他们的查询结果彼此之间是独立的
56、order by
实时任务重,order by 子句中必须要有时间属性字段,并且必须写在最前面且为升序
57、SQL Hints
通过使用该语句可以修改一张表的某个具体参数,而不需要删除整张表来重新修改
/+ OPTIONS(...)/
58、集合操作
(1)UNION 和 UNION ALL
UNION: 将集合合并并去重
UNION ALL: 合并但是不去重
(2)Intersect 和 Intersect All
Intersect :交集并去重
Intersect ALL : 交集不去重
(3)Except 和 Except ALL
Except: 差集并去重
Except ALL: 差集不去重
(4)In 子查询
结果只能有一列,和 Inner Join 相似
59、系统函数
(1)标量函数
【1】比较函数
【2】逻辑函数
【3】算术函数
【4】字符串函数
没有 split 函数
【5】时间函数
(2)聚合函数
60、Module 操作
(1)语法
如果模块中有同名函数,那么会优先启用第一个模块中的函数
61、upsert
使用 upsert 时,如果插入数据的键值已经存在,则进行更新;若没有则进行插入。
在映射表中,只有定义了主键才可以使用 upsert 执行,否则就只能使用追加
62、File 连接遇到的问题
63、Catalog
(1)类型
【1】GenericInMemoryCatalog:基于内存的 Catalog
【2】JdbcCatalog:将 Flink 通过 JDBC Catalog 实现,将元数据存储在数据库中
不支持建表,只是打通了 flink 和 mysql 的连接,可以读写 mysql 现有的表
【3】HiveCatalog:两个用途,一是单纯作为 Flink 元数据的持久化存储,二是作为读写现有 Hive 元数据的接口
附注:
Hive MetaStore 以小写形式存储所有元数据对象名称
GenericInMemoryCatalog 会区分大小写
【4】自定义 Catalog
(2)必要参数
64、表环境的创建
65、创建表
(1)连接器表
(2)虚拟表
66、表和流的转换
(1)将流变成表
【1】调用 fromDataStream()方法,可以在该方法中增加参数来制定哪些属性作为表中的字段名,并使用 as()来进行重命名。
【2】调用 createTemporaryView()方法,通过该方法创建虚拟视图,从而直接在 SQL 中引用这张表
(2)将表转换成流
【1】调用 toDataStream()方法,对表进行数据流转换
【2】toChangelogStream(),将表转换成更新日志流
67、自定义函数
(1)调用流程
【1】注册函数,使用 createTemporarySystemFunction()方法,传入注册的函数名以及 UDF 类的 class 对象
【2】使用 Table API 调用函数
【3】在 SQL 中调用函数
(2)标量函数
从输入和输出表中行数据的关系来看,标量函数是 “一对一” 的转换。可以把 0 个、1 个或多个标量值转换成一个标量值。
【1】自定义函数的实现类
【2】注册函数
【3】调用自定义函数
(3)表函数
从输入和输出表中行数据的关系来看,表函数是一个 “一对多” 的转换。可以把 0 个、1 个或多个标量值转换成任意多行数据。
(4)聚合函数
“多对一” 转换,自定义聚合函数会把一行或多行数据(一个表)聚合成一个标量值。
继承抽象类 AggregateFunction,有两个参数,<T,ACC>,T 表示聚合输出的结果类型,ACC 则表示聚合的中间状态类型。
过程:
【1】创建一个累加器 accumulator 来存储聚合的中间结果。于 AggregateFunction 非常类似,可以看做是一个聚合状态。createAccumulator()方法可以创建一个空的累加器
【2】对于输入的每一行数据都会调用 accumulate()方法来更新累加器进行聚合
【3】当所有的数据都处理完成后,通过调用 getValue() 方法来计算并返回最后的结果
(5)表聚合函数
“多对多” 转换,需要继承 TableAggregateFunction,和 AggregateFunction 非常相似,有两个参数,<T,ACC>,T 表示聚合输出的结果类型,ACC 则表示聚合的中间状态类型。但方法不太一样
【1】创建一个累加器 accumulator 来存储聚合的中间结果。
【2】对于输入的每一行数据都会调用 accumulate()方法来更新累加器进行聚合
【3】使用 emitValue() 输出计算结果。该方法没有输出类型,输入参数有两个,第一个是 ACC 类型的累加器,第二个是输出数据的收集器 out, 类型为 Collect