04
Event-time 真正的事件产生的时间。 事件日志记录受网络传输延迟,并不得到真正的事件产生时间。

时间三兄弟
  • EventTime 事件真正产生的时间。这个字段是要带在数据里面的,不然无法获取。
  • IngestionTime 进入引擎的时间,受网络影响,时间不确定。大小介于二者间。
  • ProcessingTime 算子执行时的当前系统的时间,时间不确定

设置时间语义,按时间三兄弟中哪个来处理。
从1.12起,默认的时间语义从ProcessingTime改成了EventTime,如果数据源没有可记录EventTime的字段,或没有设置waterMark,就会报错。需要手动设置为ProcessingTime。

env.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime)
Window

对流处理,数据是源源不断进入的,无界的(批处理是有界的)。不可能无界限一直一直计算,Window 把一个无界流数据进行切分,得到有界流(流转批),在窗口内进行一组计算。 (和 ss 的微批是相反的思想(批转流))

窗口的分类

根据是否有keyBy分两类

  • Keyed Window ⇒ 使用 stream.keyBy.window 划分窗口
  • Non-Keyed Window ⇒ 使用 stream.windowAll 划分窗口
Assigner

负责将每条数据分发到正确的window中去

计数的 CountWindow

是根据元素的个数来划分,达到指定数量就划分。与时间是没关系。 如果keyby,要某个key达到指定数量才会统计这个key,而不统计其他key,只能其他key也达到指定数量才触发自己key的统计。

计时的 TimeWindow

达到时间长度就切分。

滚动窗口 TumblingWindow

window size可以是CountSize也可以是TimeSize(又既可以是EventTime也可以是ProcessingTime )。不带key时用windowAll,带key用window。

滑动窗口 SlidingWindow

window size 和 slide size是不等的,window size是一个窗口的大小,代表这个窗口从开始道结束的间隔;slide size可以理解是前后两个窗口开始时间的间隔。一个元素可能会被落在多个window中;window间可能有重叠。可以用来分析一种趋势、走势。

如图示,假设每个window的大小是10s(window size),而每隔5s(slide size)就开启下一个window。则一个元素会被落在两个window中。

flink TimeService 使用 flink eventtime processtime_big data

滚动窗口可以理解为window size 和 slide size相等的的滑动窗口,是滑动窗口的特例。上一个窗口结束同时开启才下一个窗口,所以不会重叠。

时间间隔 SessionWindow

只有没有操作的时间间隔达到指定size的间隔后,才开始算新window。数据不断,就一直是旧window(保持在当前window)

总结

flink TimeService 使用 flink eventtime processtime_flink_02

WindowFunction

window之后,数据在窗口内进行每组window进行计算,如sum(0)。

增量:ReduceFunction, AggregateFunction  数据来了就会进行算的;但是不适合每项业务的,如排序、最值。
全量:ProcessWindowFunction  窗口OK了才进行计算  全量数据。

自定义 AggregateFunction,重写 createAccumulator()、add()、getResult()、merge()方法

05
自定义 ProcessFunction,重写 processElement()

Watermark 水印

理想情况下先操作的数据应该先到达,受网络原因,先产生的数据不一定先到,需要解决,以EventTime来确定数据顺序。

延迟(乱序)数据处理:不能等待数据全部到达再处理,等待时间不可预测,等待过久就变成了批处理。

乱序数据处理:window + Watermark 。Watermark 是衡量EventTime进展的机制。是一条特殊的数据记录。

使用:在获取环境时,设置为EventTime;获取数据源时同时指定watermark的字段(一般情况都在数据源时设置,但支持在transformation时)。

EventTimeSessionWindows
// 旧API
val lines = environment
	  .socketTextStream("gargantua", 9527)
	  // Time.seconds(0) 代表容忍度为0
      .assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[String](Time.seconds(0)) {
        // 设置哪个字段作为watermark的时间
        override def extractTimestamp(element: String): Long = {
           element.split(",")(0).toLong
        }
     })
// 新API
val lines = environment
	  .socketTextStream("gargantua", 9527)
      .assignTimestampsAndWatermarks(WatermarkStrategy.forBoundedOutOfOrderness[String](Duration.ofSeconds(0))
        	.withTimestampAssigner(new SerializableTimestampAssigner[String] {
        		// 设置哪个字段作为watermark的时间
          		override def extractTimestamp(element: String, recordTimestamp: Long): Long = element.split(",")(0).toLong
     }))

// 在transformation
 .window(EventTimeSessionWindows.withGap(Time.seconds(5)))

之前学习SessionWindow时是以ProcessingTime做时间语义,就会以终端的时间来作为window划分的time size。如果以EventTime(watermark指定数据里某个字段作为EventTime),那么就会关联数据里的这个字段的时间作为划分window的依据(具体计算过程?)。

TumblingEventTimeWindows

需要注意,当前数据所携带的时间 >= 上一个window的结束边界的时间,则触发上一个window。但当前数据不会纳入上一个window的统计;它是属于下一个window的,作为下一个window的第一条数据。

延迟的数据(属于上一个window的范围,但上一个window已经被触发过了的数据):

  1. 通过设置容忍度,如Time.seconds(2)代表可以允许数据延迟2s以内。即增加了2s的触发间隔,但不改变window的大小。例如window size为5s,则在>=第7s的数据才能触发一个window,但这个window统计数据的范围还是前5s的。只是允许了5-7s这个范围时属于1-5s的数据延迟仍然可以进入1-5s的window。超出容忍范围的数据仍然会被丢弃。
  2. 被丢弃的数据只能通过侧流输出。
// 侧流 side output   (OutputTag的类型应该和侧输出流中的类型保持一致,否则会报错 type mismatch)
 ctx.output(new OutputTag[(String,Long,Float)]("high"),(value.name,value.time,value.temperature))

// 或
 window.getSideOutput(outputTag).print("侧流输出...")
SlidingEventTimeWindows
.window(SlidingEventTimeWindows.of(Time.seconds(6), Time.seconds(2)))  // 6s window size ; 2s slide size

滑动窗口每个window都是独立的触发,独立的统计自己范围的数据,只是可能会重复。

Flink架构

flink client(提交flink作业) --> JobManager(主) --> TaskManager(从)

Standalone和集群模式用于学习,生产都是 on Yarn 、on k8s。
Flink on Yarn 时是不需要Flink集群的,同Spark on Yarn 一样,Spark、Flink只作为客户端提交作业。

配置&参数

本地开发时,可以通过设置createLocalEnvironmentWithWebUI()开启Web UI 在8081端口。不使用createLocalEnvironmentWithWebUI()的话也有界面只是端口不固定。
在spark 中只有shuffle时会切分出stage;在flink中,除了shuffle,在并行度发生变化也会切分(只有这两种情况会切分)。

下载解压后目录结构
bin   // 启停脚本
 conf  // 配置文件
 examples
 lib
 licenses
 log
 opt
 plugins
  • conf
    在flink-conf.yaml有设置Web UI默认端口为8081(18081);修改了hosts。
    在log4j.properties默认日志等级是INFO,可以改其他。
  • bin
    flink 启动standalone
[liqiang@Gargantua bin]$ ./start-cluster.sh
Starting cluster.
Starting standalonesession daemon on host Gargantua.
Starting taskexecutor daemon on host Gargantua.
  • 启动后通过jps查看,会有一个 TaskManagerRunner,和一个 StandaloneSessionClusterEntrypoint
    在8081会有页面
  • Web UI
    Web UI 里的日志,就是从./log 中获取的。
    通过UI方式提交job。Web UI 里可以Submit New Job,上传一个作业的jar包就能运行,方便自测。(指定主类、指定env并行度、指定main方法所需参数)
    show plan和执行 : 可与查看这个job的情况:并行度、DAG图、运行时日志、输出结果
  • bin
    flink 运行命令
//所有命令帮助
[liqiang@Gargantua bin]$ ./flink
./flink <ACTION> [OPTIONS] [ARGUMENTS]
  • 通过命令提交一个job到本地。
./flink run \
-c com.example.Demo \
-p 2 \
/home/data/xxx/xxx.jar
  • 列出所有(运行中)job、停止等命令查看命令帮助;指定参数等通过官网查看。
flink list -a
flink cancel <job id>
通过命令提交一个job到Yarn。

提交作业到yarn,就不需要依赖本地启动的standalone或集群。
以Per-Job Mode:

./flink run \
-t yarn-per-job \
-c com.example.Demo \
-p 2 \
/home/data/xxx/xxx.jar

需要指定Hadoop的ClassPath,可以配置到环境变量里。

export HADOOP_CLASSPATH='hadoop classpath'

提交到Yarn上的job,日志和结果输出在Yarn上,在8081界面能也看到运行日志和输出结果(有延迟)。
在Yarn的job列表里也支持跳转到flink 8081。

获取DAG图

通过环境可以获取DAG图的json文件

env.getExecutionPlan

将json文件拿到https://flink.apache.org/visualizer/ 里Paste the plan就能绘制出DAG图。

window触发条件
  • 从watermark角度重新理解窗口触发条件。
    当前数据所携带的时间(watermark ) >= 上一个window的结束边界的时间,则触发上一个window。
    即:watermark time >= last window_end time
  • window划分的初始值都是从第0s开始划分区间的,不依赖第一条数据作为window开始时间。
    如设置window size为3s,则第一个window是[0,3),下一个window是[3,6),依次类推[57,00),与数据上的时间无关。

以案例解释:

第一条数据的时间是 17:40:02,也不会以[2,5)作为第一个window,而是[0,3)。(这样设计可能是考虑到第一条到达的数据也有可能是乱序的,并不能作为window划分的依据)

现在要触发17:40:02这条数据,因为这条数据是在[0,3)这个window,所以当后来的数据(watermark)大于等于17:40:03就能触发,而不用等到数据为17:40:05。

如果设置延迟2s。为了实现延迟2s触发window,每个数据对应的watermark的大小应该等于当前window的最大时间(考虑乱序所以要去最大)减去2s

override def getCurrentWatermark: Watermark = new Watermark(maxTimestamp - maxAllowedUnorderedTime)

假如window size仍为3s,延迟2s,第一条数据17:40:34(watermark 为17:40:32)。这条数据属于[33,36)这个window。如果要触发[33,36)这个window,需要后来的数据的watermark大于等于17:40:36,而watermark为17:40:36的数据对应本身的值应该是17:40:38,即需要后面的数据大于等于17:40:38时才能触发17:40:34这条数据。

在自定义WindowFunction时可以获取窗口开始时间,结束时间。

new Timestamp(window.getStart)
new Timestamp(window.getEnd)

测试数据

36.8,1651311634
36.1,1651311635
35.8,1651311636
36.4,1651311637
36.6,1651311638
37.1,1651311641

测试结果 (17:40:34 转时间戳==> 1651311634)

36.8,1651311634
36.1,1651311635
35.8,1651311636
36.4,1651311637
36.6,1651311638  // 平均温度:36.45
37.1,1651311641  // 平均温度:36.266666666667

计算过程:
17:40:34 所在 window[33,36) , 1651311636+2 = 1651311638, 所以38的数据会触发 34和35,得到平均温度36.45
下一个window[36,39) 1651311639+2 = 1651311641, 所以需要等到 1651311641,将 36、37、38一起触发

E:\WorkSpace\Flink\Flink06\src\main\scala\com\example\watermark\WMApp.scala