Bootstrap

73、Flink 的 DataStream API 生产实践总结

0、汇总

1.可以使用 Maven 命令、CURL 命令、IDEA 手动创建 Flink 项目;

2.可以使用 Maven Shade 插件将必需的依赖项打包进应用程序 jar 中;

3.应该在 Flink 集群的 lib 文件夹内配置需要的(核心)依赖项;

4.应该将程序中(核心)依赖项的生效范围置为 provided(需要对它们编译,但不应将它们打包进项目生成的应用程序 JAR 文件中),避免与集群一些依赖项的版本冲突;

4.flink-connector- 是一个精简 JAR,仅包括连接器代码,但不包含最终的第三方依赖项;flink-sql-connector- 是一个包含连接器第三方依赖项的 uber JAR;

5.uber/fat JAR 主要与 SQL 客户端一起使用,但也可以在任何 DataStream/Table 应用程序中使用它们;

6.某些连接器可能没有相应的 flink-sql-connector- 组件,因为它们不需要第三方依赖项。

7.Flink 发行版的 lib 目录里包括常用模块在内的各种 JAR 文件,若要禁止加载只需将它们从 classpath 中的 lib 目录中删除即可;

8.Flink 在 opt 文件夹下提供了额外的可选依赖项,可以通过移动这些 JAR 文件到 lib 目录来启用这些依赖项;

9.如果只使用 Flink 的 Java API,可以使用任何 Scala 版本;如果使用 Flink 的 Scala API,则需要选择与应用程序的 Scala 匹配的 Scala 版本;

10.2.12.8 之后的 Scala 版本与之前的 2.12.x 版本二进制不兼容,使 Flink 项目无法将 2.12.x 版本直接升级到 2.12.8 以上;为此,需要在构建时添加 -Djapicmp.skip 以跳过二进制兼容性检查;

11.Flink 发行版默认包含执行 Flink SQL 任务的必要 JAR 文件(位于 lib 目录),但默认情况下不包含 Table Scala API,需手动添加;

12.要将 Flink 与 Hadoop 一起使用,需要有一个包含 Hadoop 依赖项的 Flink 系统,而不是添加 Hadoop 作为应用程序依赖项,通过
[export HADOOP_CLASSPATH=`hadoop classpath`] Flink 将使用 HADOOP_CLASSPATH 环境变量指定的 Hadoop 依赖项;

13.如果在 IDE 开发或测试期间需要 Hadoop 依赖项(用于 HDFS 访问),应该限定这些依赖项的使用范围(如 test 或 provided)。

14.StreamExecutionEnvironment 包含了 ExecutionConfig,它允许在运行时设置作业特定的配置;

15.通过 getRuntimeContext() 方法在 Rich* function 中可以访问到 ExecutionConfig;

16.打包后程序运行时,JAR 文件 manifest 中的 program-class 属性会优先于 main-class 属性;对于 JAR manifest 中两个属性都不存在的情况,命令行和 web 界面支持手动传入类名参数;

17.使用 savepoints 时,应该考虑设置最大并行度,此设置会限定整个程序的并行度上限,当作业从一个 savepoint 恢复时,可以改变特定算子或者整个程序的并行度;

18.默认的最大并行度等于 operatorParallelism + (operatorParallelism / 2) 值四舍五入到大于等于该值的一个整型值,并且这个整型值是 2 的幂次方,默认最大并行度下限为 128,上限为 32768;

19.为最大并行度设置一个非常大的值将会降低性能,因为一些 state backends 需要维持内部的数据结构,而这些数据结构将会随着 key-groups 的数目而扩张(key-group 是状态重新分配的最小单元);

20.从之前的作业恢复时,改变该作业的最大并行度将会导致状态不兼容;

21.设置 Job 的并行度,可以从算子层面(operator)、执行环境层面(env)、客户端层面(-P)、系统层面(flink-conf.yaml);

22、当 Lambda 表达式使用 Java 泛型时,需要显式地声明类型信息;使用显式的 ".returns(...)" 、使用类来替代、使用匿名类来替代,使用 Tuple 的子类如 DoubleTuple 来替代;

23、建议始终以 parallelism > 1 的方式在本地测试 pipeline,以识别只有在并行执行 pipeline 时才会出现的 bug。

24、可以通过 Parametertool、Commons CLI、argparse4j 来获取外部配置参数;

25、可以通过 ParameterTool 读取来自 .properties 文件、命令行、系统属性的配置参数;

26、可以在 ExecutionConfig 中注册全局作业参数,在 rich 函数中通过 getRuntimeContext().getGlobalJobParameters() 获取全局作业参数。

27、可以在 process 方法中,通过 Context 将数据发送到由 OutputTag 标识的旁路输出中,用来拆分数据;

28、可以在 DataStream 运算结果上使用 getSideOutput(OutputTag) 方法获取旁路输出流。

29、转换算子

Map[DataStream → DataStream]:输入一个元素,转换后输出一个元素;

FlatMap[DataStream → DataStream]:输入一个元素,转换后产生零个、一个或多个元素;

Filter[DataStream → DataStream]:为每个元素执行一个boolean function,并保留那些 function 输出值为 true 的元素;

KeyBy[DataStream → KeyedStream]:在逻辑上将流划分为不相交的分区,具有相同 key 的记录都分配到同一个分区;在内部 keyBy() 是通过哈希分区实现的;当类为 POJO 类,却没有重写 hashCode() 方法而是依赖于 Object.hashCode() 实现或它是任意类的数组时不能作为 key;

Reduce[KeyedStream → DataStream]:在相同 key 的数据流上“滚动”执行 reduce,将当前元素与最后一次 reduce 得到的值组合然后输出新值;

Window[KeyedStream → WindowedStream]:在已经分区的 KeyedStreams 上定义 Window,Window 根据某些特征对每个 key Stream 中的数据进行分组;

WindowAll[DataStream → AllWindowedStream]:在 DataStream 上定义 Window,Window 根据某些特征对所有流事件进行分组,注意所有记录都将收集到 windowAll 算子对应的一个任务中[并行度为1];

WindowReduce[WindowedStream → DataStream]:对窗口应用 reduce function 并返回 reduce 后的值;

Union[DataStream* → DataStream]:将两个或多个数据流联合来创建一个包含所有流中数据的新流,如果一个数据流和自身进行联合,这个流中的每个数据将在合并后的流中出现两次;

Window Join[DataStream,DataStream → DataStream]:根据指定的 key 和窗口 join 两个数据流;

Interval Join[KeyedStream,KeyedStream → DataStream]:根据 key 相等并且在指定的时间范围内(e1.timestamp + lowerBound <= e2.timestamp <= e1.timestamp + upperBound)的条件将分别属于两个 keyed stream 的元素 e1 和 e2 Join 在一起;

Window CoGroup[DataStream,DataStream → DataStream]:根据指定的 key 和窗口将两个数据流组合在一起;

Connect[DataStream,DataStream → ConnectedStream]:“连接” 两个数据流并保留各自的类型,connect 允许在两个流的处理逻辑之间共享状态;

CoMap, CoFlatMap[ConnectedStream → DataStream]:在连接的数据流上进行 map 和 flatMap;

Cache[DataStream → CachedDataStream]:把算子的结果缓存起来,目前只支持批模式下运行的作业;算子的结果在算子第一次执行的时候会被缓存起来,之后的作业中会复用该算子缓存的结果;如果算子的结果丢失了,它会被原来的算子重新计算并缓存;

30、物理分区算子

Global Partitioner[DataStream → DataStream]:将数据输出到下游的0号分区中;

Custom Partitioner[DataStream → DataStream]:使用自定义的 Partitioner 为每个元素选择目标分区;

Shuffle Partitioner[DataStream → DataStream]:将元素随机均匀的输出到下游的分区中;

Rebalance Partitioner[DataStream → DataStream]:将元素以循环的方式输出到下游的分区中;

Rescale Partitioner[DataStream → DataStream]:基于上下游算子的并行度,将元素以循环的方式输出到下游的分区中;

Broadcast Partitioner[DataStream → DataStream]:将所有元素广播到下游的每个分区中;

31、算子链和资源组

将两个算子链接在一起,可以使它们在同一个线程中执行,从而提升性能;Flink 默认会将能链接的算子尽可能的进行链接;

可以调用 StreamExecutionEnvironment.disableOperatorChaining() 方法对整个作业禁用算子链;

可以调用 startNewChain() 方法基于当前算子创建一个新的算子链[只能在 DataStream 转换操作后调用,因为只对前一次的数据转换生效];

可以调用 disableChaining() 方法禁止和当前算子链接在一起[只能在 DataStream 转换操作后调用,因为只对前一次的数据转换生效];

可以调用 slotSharingGroup() 方法为当前算子配置 slot 共享组,Flink 会将同一个 slot 共享组的算子放在同一个 slot 中,而将不在同一个 slot 共享组的算子保留在其它 slot 中,可用于隔离 slot;slot 共享组的默认名称是“default”,可以调用 slotSharingGroup(“default”) 来显式地将算子放入该组;

32、Task 和 Slot 数量的计算

当算子 A 被设置在单独的 SharingGroup 中时,算子 A 有几个并行度就需要几个 slot;

当多个算子能被算子链连在一起时,算子有几个并行度就需要几个 slot;

当算子 A 不能被算子链 B 连在一起,且算子 A 被设置在与算子链 B 相同的 SharingGroup 中时,算子 A 的并行度大于算子链 B 的并行度部分会占用额外的 slot 数;

能被算子链连在一起的算子,可以在同一个线程中执行,只创建一个 Task;

不能被算子链连在一起的算子,每个并行度需要单独创建一个 Task 执行;

33、算子链连接的条件

上下游的并行度一致;
下游节点的入度为1(即下游节点没有来自其它节点的输入);
上下游节点都在同一个 slotSharingGroup 中;
下游节点的 chain 策略为 ALWAYS;
上游节点的 chain 策略为 ALWAYS 或 HEAD;
两个节点间数据分区方式是 Forward Partition;
用户没有禁用算子链;

34、名字和描述

name 主要用在用户界面、线程名、日志、指标等场景,名字需要尽可能简洁,避免对外部系统产生大的压力;description 主要用在执行计划展示及用户界面展示等场景;

建议为算子设置 name\description\uid\parallelism\maxParallelism 参数;

35.Flink 窗口在 keyed streams 要调用 keyBy(...) 后再调用 window(...),而 non-keyed streams 直接调用 windowAll(...);

36.Keyed Windows 使用示例

stream
       .keyBy(...)               <-  仅 keyed 窗口需要
       .window(...)              <-  必填项:"assigner"
      [.trigger(...)]            <-  可选项:"trigger" (省略则使用默认 trigger)
      [.evictor(...)]            <-  可选项:"evictor" (省略则不使用 evictor)
      [.allowedLateness(...)]    <-  可选项:"lateness" (省略则为 0)
      [.sideOutputLateData(...)] <-  可选项:"output tag" (省略则不对迟到数据使用 side output)
       .reduce/aggregate/apply()      <-  必填项:"function"
      [.getSideOutput(...)]      <-  可选项:"output tag"
      
37.Non-Keyed Windows 使用示例

stream
       .windowAll(...)           <-  必填项:"assigner"
      [.trigger(...)]            <-  可选项:"trigger" (else default trigger)
      [.evictor(...)]            <-  可选项:"evictor" (else no evictor)
      [.allowedLateness(...)]    <-  可选项:"lateness" (else zero)
      [.sideOutputLateData(...)] <-  可选项:"output tag" (else no side output for late data)
       .reduce/aggregate/apply()      <-  必填项:"function"
      [.getSideOutput(...)]      <-  可选项:"output tag"

38.一个窗口在第一个属于它的元素到达时就会被创建,然后在时间(event 或 processing time)超过窗口的“结束时间戳 + 用户定义的 allowed lateness“时被完全删除;

39.Flink 仅保证删除基于时间的窗口,其他类型的窗口不做保证, 比如全局窗口;

40.每个窗口会设置自己的 Trigger 和 function (ProcessWindowFunction、ReduceFunction、或 AggregateFunction),该 function 决定如何计算窗口中的内容,而 Trigger 决定窗口中的数据何时可以被 function 计算;

41.Trigger 还可以在 window 被创建后、删除前的这段时间内定义何时清理(purge)窗口中的数据;此处数据仅指窗口内的元素,不包括窗口的 meta data,即窗口在 purge 后仍然可以加入新的数据。

42.可以指定一个 Evictor,在 trigger 触发之后,Evictor 可以在窗口函数的前后删除数据;

43.在定义窗口前需确定 stream 是 keyed 还是 non-keyed,keyBy(...) 会将无界 stream 分割为逻辑上的 keyed stream;

对于 keyed stream,数据的任何属性都可以作为 key,使用 keyed stream 允许窗口计算由多个 task 并行,因为每个逻辑上的 keyed stream 都可以被单独处理,属于同一个 key 的元素会被发送到同一个 task;

对于 non-keyed stream,原始的 stream 不会被分割为多个逻辑上的 stream, 所有的窗口计算会被同一个 task 完成,也就是 parallelism 为 1;

44. WindowAssigner 负责将 stream 中的每个数据分发到一个或多个窗口中,Flink 提供了默认的 window assigner,即 tumbling windows、sliding windows、session windows 和 global windows;也可以继承 WindowAssigner 类来实现自定义的 window assigner;

45.基于时间的窗口用 start timestamp(包含)和 end timestamp(不包含)描述窗口的大小;Flink 处理基于时间的窗口使用的是 TimeWindow, 它有查询开始和结束的 timestamp 以及返回窗口所能储存的最大 timestamp 的方法 maxTimestamp();

46.滚动窗口(Tumbling Windows),滚动窗口的 assigner 分发元素到指定大小的窗口,滚动窗口的大小是固定的,且各自范围之间不重叠;

47.滑动窗口(Sliding Windows),滑动窗口的 assigner 分发元素到指定大小的窗口,窗口大小通过 window size 参数设置,滑动窗口需要一个额外的滑动距离(window slide)参数来控制生成新窗口的频率;如果 slide 小于窗口大小,滑动窗口可以允许窗口重叠,此时一个元素可能会被分发到多个窗口;

48.滚动窗口和滑动窗口的 assigners 可以设置 offset 参数,这个参数可以用来对齐窗口,不设置 offset 时,窗口的起止时间会与 linux 的 epoch 对齐,一个重要的 offset 用例是根据 UTC-0 调整窗口的时差,在中国可能会设置 offset 为 Time.hours(-8);

49.会话窗口(Session Windows),会话窗口的 assigner 会把数据按活跃的会话分组,会话窗口不会相互重叠,且没有固定的开始或结束时间,会话窗口在一段时间没有收到数据之后会关闭,会话窗口的 assigner 可以设置固定的会话间隔(session gap)或用 session gap extractor 函数来动态地定义多长时间算作不活跃;当超出了不活跃的时间段,当前的会话就会关闭,接下来的数据将被分发到新的会话窗口;

50.会话窗口并没有固定的开始或结束时间,在 Flink 内部,会话窗口的算子会为每一条数据创建一个窗口,然后将距离不超过预设间隔的窗口合并,想要让窗口可以被合并,会话窗口需要拥有支持合并的 Trigger 和 Window Function, 例如 ReduceFunction、AggregateFunction 或 ProcessWindowFunction;

51.全局窗口(Global Windows),全局窗口的 assigner 将拥有相同 key 的所有数据分发到一个全局窗口,此窗口模式仅在指定了自定义的 trigger 时有用,否则计算不会发生,因为全局窗口没有天然的终点去触发其中积累的数据;

52.窗口函数(Window Functions)有三种:ReduceFunction、AggregateFunction 或 ProcessWindowFunction;

- ReduceFunction、AggregateFunction 执行更高效,因为 Flink 可以在每条数据到达窗口后进行增量聚合;
- 而 ProcessWindowFunction 会得到能够遍历当前窗口内所有数据的 Iterable,以及关于这个窗口的 meta-information;

53.ProcessWindowFunction 可以与 ReduceFunction 或 AggregateFunction 合并来提高效率,既可以增量聚合窗口内的数据,又可以从 ProcessWindowFunction 接收窗口的 metadata;

54.ReduceFunction:指定两条输入数据如何合并起来产生一条输出数据,输入和输出数据的类型必须相同;

55.AggregateFunction:接收三个参数,输入数据的类型(IN)、累加器的类型(ACC)和输出数据的类型(OUT);

56.AggregateFunction 接口有如下方法:

把每一条元素加进累加器:add
创建初始累加器:createAccumulator
合并两个累加器:merge
从累加器中提取输出数据:getResult

57.ProcessWindowFunction:具备 Iterable 能获取窗口内所有的元素,以及用来获取时间和状态信息的 Context 对象,但因为窗口中的数据无法被增量聚合,而需要在窗口触发前缓存所有数据;

58.增量聚合的 ProcessWindowFunction:ProcessWindowFunction 可以与 ReduceFunction 或 AggregateFunction 搭配使用,使其能够在数据到达窗口的时候进行增量聚合,当窗口关闭时,ProcessWindowFunction 将会得到聚合的结果;即实现了增量聚合窗口的元素并且也从 ProcessWindowFunction 中获得了窗口的元数据;

59.除了访问 keyed state,ProcessWindowFunction 还可以使用作用域仅为“当前正在处理的窗口”的 keyed state;

60.process() 接收到的 Context 对象中有两个方法允许访问以下两种 state:

- globalState(),访问全局的 keyed state
- windowState(),访问作用域仅限于当前窗口的 keyed state

61.如果可能将一个 window 触发多次(比如迟到数据会再次触发窗口计算,或自定义了根据推测提前触发窗口的 trigger),这时可能需要在 per-window state 中储存关于之前触发的信息或触发的总次数;

62.当使用窗口状态时,一定记得在删除窗口时清除这些状态,应该定义在 clear() 方法中;

63.Trigger 决定了一个窗口(由 window assigner 定义)何时可以被 window function 处理;每个 WindowAssigner 都有一个默认的 Trigger,如果默认 trigger 无法满足需要,可以在 trigger(...) 调用自定义的 trigger;

64.Trigger 接口提供了五个方法来响应不同的事件:

- onElement() 方法在每个元素被加入窗口时调用。
- onEventTime() 方法在注册的 event-time timer 触发时调用。
- onProcessingTime() 方法在注册的 processing-time timer 触发时调用。
- onMerge() 方法与有状态的 trigger 相关。该方法会在两个窗口合并时,将窗口对应 trigger 的状态合并,比如使用会话窗口时。
- clear() 方法处理在对应窗口被移除时所需的逻辑。

前三个方法通过返回 TriggerResult 来决定 trigger 如何应对到达窗口的事件,应对方案有以下几种:

- CONTINUE: 什么也不做
- FIRE: 触发计算
- PURGE: 清空窗口内的元素
- FIRE_AND_PURGE: 触发计算,计算结束后清空窗口内的元素

65.当 trigger 触发时,它可以返回 FIRE 或 FIRE_AND_PURGE;FIRE 会保留被触发的窗口中的内容,而 FIRE_AND_PURGE 会删除这些内容,Flink 内置的 trigger 默认使用 FIRE,不会清除窗口的内容;Purge 只会移除窗口的内容,不会移除关于窗口的 meta-information 和 trigger 的状态;

66.Flink 包含一些内置 trigger:

- ContinuousProcessingTimeTrigger:根据间隔时间周期性触发窗口或者当 Window 的结束时间小于当前 ProcessTime 触发窗口计算
- ProcessingTimeoutTrigger:当内置触发器满足超时时间时,触发窗口的计算
- ProcessingTimeTrigger:ProcessingTimeWindows 默认使用,会在处理时间越过窗口结束时间后直接触发
- ContinuousEventTimeTrigger:根据间隔时间周期性触发窗口或者当 Window 的结束时间小于当前的 watermark 时触发窗口计算
- EventTimeTrigger:EventTimeWindows 默认使用,会在 watermark 越过窗口结束时间后直接触发
- PurgingTrigger:接收另一个 trigger 并将它转换成一个会清理数据的 trigger
- NeverTrigger:GlobalWindows 默认使用,任何时候都不触发窗口计算
- DeltaTrigger:根据接入数据计算出来的 Delta 指标是否超过指定的 Threshold 去判断是否触发窗口计算
- CountTrigger:在窗口中的元素超过预设的限制时触发

67.Flink 的窗口模型允许在 WindowAssigner 和 Trigger 之外指定可选的 Evictor,通过 evictor(...) 方法传入 Evictor,Evictor 可以在 trigger 触发后、调用窗口函数之前或之后从窗口中删除元素;

68.evictBefore() 包含在调用窗口函数前的逻辑,而 evictAfter() 包含在窗口函数调用之后的逻辑,在调用窗口函数之前被移除的元素不会被窗口函数计算;

69.Flink 有三个内置的 evictor:

- CountEvictor: 仅记录用户指定数量的元素,一旦窗口中的元素超过这个数量,多余的元素会从窗口缓存的开头移除;
- DeltaEvictor: 接收 DeltaFunction 和 threshold 参数,计算最后一个元素与窗口缓存中所有元素的差值,并移除差值大于或等于 threshold 的元素;
- TimeEvictor: 接收 interval 参数,以毫秒表示,它会找到窗口中元素的最大 timestamp 即 max_ts 并移除比 max_ts - interval 小的所有元素;

默认情况下,所有内置的 evictor 逻辑都在调用窗口函数前执行;Flink 不对窗口中元素的顺序做任何保证,即使 evictor 从窗口缓存的开头移除一个元素,这个元素也不一定是最先或者最后到达窗口的;

70.使用 ProcessTime 和 GlobalWindows 时无迟到数据,但使用 event-time 窗口时,数据可能会迟到,默认 watermark 一旦越过窗口结束的 timestamp,迟到的数据就会被直接丢弃;

71.Allowed lateness 默认是 0,在 watermark 超过窗口末端、到达窗口末端加上 allowed lateness 之前的这段时间内到达的元素,依旧会被加入窗口;Flink 会将窗口状态保存到 allowed lateness 超时才会将窗口及其状态删除;但窗口再次触发的结果取决于触发器是否 purge 而导致结果不同;

72.通过 Flink 的侧流输出功能,可以获得迟到数据的数据流;

73.当指定了大于 0 的 allowed lateness 时,窗口本身以及其中的内容仍会在 watermark 越过窗口末端后保留;此时如果一个迟到但未被丢弃的数据到达,它可能会再次触发这个窗口,这种触发被称作 late firing;如果是使用会话窗口的情况,late firing 可能会进一步合并已有的窗口,因为他们可能会连接现有的、未被合并的窗口;late firing 发出的元素应该被视作对之前计算结果的更新,即数据流中会包含一个相同计算任务的多个结果,应用需要考虑到这些重复的结果,或去除重复的部分;

74.窗口操作的结果会变回 DataStream,并且窗口操作的信息不会保存在输出的元素中,如果想要保留窗口的 meta-information,需要在 ProcessWindowFunction 里手动将他们放入输出的元素中;

75.当 watermark 到达窗口算子时,它触发了两件事:

- 这个 watermark 触发了所有最大 timestamp(即 end-timestamp - 1)小于它的窗口;
- 这个 watermark 被原封不动地转发给下游的任务;

76.窗口可以被定义在很长的时间段上(比如几天、几周或几个月)并且积累下很大的状态,当估算窗口计算的储存需求时,注意如下:

- Flink 会为一个元素在它所属的每一个窗口中都创建一个副本;在滚动窗口的设置中一个元素只会存在一个副本,在滑动窗口的设置中一个元素可能会被拷贝到多个滑动窗口中,每个(滑动窗口长度/滑动距离)会存在一个数据副本;
- ReduceFunction 和 AggregateFunction 可以极大地减少储存需求,它们会就地聚合到达的元素,且每个窗口仅储存一个值,而使用 ProcessWindowFunction 需要累积窗口中所有的元素;
- 使用 Evictor 可以避免预聚合,因为窗口中的所有数据必须先经过 evictor 才能进行计算;

77.Window join 作用在两个流中有相同 key 且处于相同窗口的元素上,两个流中的元素在组合之后,会被传递给用户定义的 JoinFunction 或 FlatJoinFunction,可以用它们输出符合 join 要求的结果;

stream.join(otherStream)
    .where(<KeySelector>)
    .equalTo(<KeySelector>)
    .window(<WindowAssigner>)
    .apply(<JoinFunction>)
    
78.一个流中的元素在与另一个流中对应的元素完成 join 之前不会被输出;完成 join 的元素会将他们的 timestamp 设为对应窗口中允许的最大 timestamp;

79.滚动 Window Join:所有 key 相同且共享一个滚动窗口的元素会被组合成对,并传递给 JoinFunction 或 FlatJoinFunction,行为与 inner join 类似,所以一个流中的元素如果没有与另一个流中的元素组合起来,它就不会被输出;

80.滑动 Window Join:所有 key 相同且处于同一个滑动窗口的元素将被组合成对,并传递给 JoinFunction 或 FlatJoinFunction,当前滑动窗口内,如果一个流中的元素没有与另一个流中的元素组合起来,它就不会被输出;

81.在某个滑动窗口中被 join 的元素不一定会在其他滑动窗口中被 join;

82.会话 Window Join:所有 key 相同且组合后符合会话要求的元素将被组合成对,并传递给 JoinFunction 或 FlatJoinFunction,这个操作同样是 inner join,如果一个会话窗口中只含有某一个流的元素,这个窗口将不会产生输出;

83.Interval join:组合元素的条件为两个流(A 和 B)中 key 相同且 B 中元素的 timestamp 处于 A 中元素 timestamp 的一定范围内,即 b.timestamp ∈ [a.timestamp + lowerBound; a.timestamp + upperBound] 或 a.timestamp + lowerBound <= b.timestamp <= a.timestamp + upperBound;

上述的 a 和 b 为 A 和 B 中共享相同 key 的元素,上界和下界可正可负,只要下界永远小于等于上界即可,Interval join 目前仅执行 inner join;

84.当一对元素被传递给 ProcessJoinFunction,他们的 timestamp 会从两个元素的 timestamp 中取最大值 (timestamp 可以通过 ProcessJoinFunction.Context 访问);

85.Interval join 目前仅支持 event time;默认情况下,上下界也被包括在区间内,但 .lowerBoundExclusive() 和 .upperBoundExclusive() 可以将它们排除在外;

86.Window Join 和 Interval Join 只适合 Inner Join,可以通过 coGroup/connect 实现 Left/Right/Inner Join;

87.ProcessFunction 是底层的数据流处理操作,可访问所有(非循环)流应用程序的基本模块;

- 事件 (数据流中的元素)
- 状态(容错、一致、仅在 keyed stream 上)
- 定时器(事件时间和处理时间,仅在 keyed stream 上)

88.ProcessFunction 允许访问 keyed State,可通过 RuntimeContext 访问;

89.定时器支持处理时间和事件时间,对函数 processElement(…)的每次调用都会获得一个 Context 对象,该对象可以访问元素的事件时间戳和 TimerService;TimerService 可用于注册处理时间和事件时间定时器;对于事件时间定时器,当 watermark 到达或超过定时器的时间戳时,会调用 onTimer(...),在该调用过程中,所有状态的作用域再次限定为创建定时器时使用的 key,从而允许定时器操作 keyed State;

90.对于两个输入的底层 Join 可以使用 CoProcessFunction 或 KeyedCoProcessFunction,通过调用 processElement1(…)和processElement2(…)分别处理两个输入;

91.底层 Join 流程如下:

为一个输入或两个输入创建状态对象;
从输入中接收元素时更新状态;
从另一个输入接收元素后,查询状态并产生 Join 结果;

92.TimerService 按 key 和时间戳消除重复的定时器,即每个 key 和时间戳最多有一个定时器,如果为同一时间戳注册了多个计时器,那么 onTimer() 方法将只被调用一次;

93.Flink 会同步 onTimer() 和 processElement() 的调用,无需担心同时修改状态;

94.定时器是容错的,并与应用程序的状态一起进行 Checkpoint,如果发生故障恢复或从保存点启动应用程序,则会恢复定时器;

95.当应用程序从故障中恢复或从保存点启动时,本应在恢复前启动的检查点中的处理时间定时器将立即启动;

96.除了 RocksDB 后端/与增量快照/与基于堆的定时器的组合,定时器使用异步检查点;但是大量的定时器会增加检查点时间,因为定时器是检查点状态的一部分;

97.由于 Flink 为每个 key 和时间戳只维护一个定时器,可以通过降低定时器的精度来合并定时器以减少定时器的数量;

对于1秒(事件或处理时间)的定时器精度,可以将目标时间四舍五入到整秒,定时器最多会提前1秒触发,但不会晚于要求的毫秒精度,每个键和秒最多有一个计时器;

long coalescedTime = ((ctx.timerService().currentProcessingTime() + timeout) / 1000) * 1000;
ctx.timerService().registerProcessingTimeTimer(coalescedTime);

由于事件时间定时器只在 watermark 到达时触发,可以使用当前 watermark 来注册定时器,并将其与下一个 watermark 合并;

long coalescedTime = ctx.timerService().currentWatermark() + 1;
ctx.timerService().registerEventTimeTimer(coalescedTime);

98.停止处理时间定时器-ctx.timerService().deleteProcessingTimeTimer();停止事件时间定时器-ctx.timerService().deleteEventTimeTimer();如果没有注册具有给定时间戳的定时器,则停止定时器无效;

99.在与外部系统交互(用数据库中的数据扩充流数据)时,需要考虑与外部系统的通信延迟对整个流处理应用的影响:

同步交互:使用 MapFunction 访问外部数据库的数据,MapFunction 向数据库发送一个请求然后一直等待,直到收到响应;大多数情况下,等待占据了函数运行的大部分时间;
异步交互:一个并行函数实例可以并发地处理多个请求和接收多个响应,使函数在等待的时间可以发送其他请求和接收其他响应,使等待的时间可以被多个请求分摊;大多数情况下,异步交互可以大幅度提高流处理的吞吐量;

100.提高 MapFunction 的并行度(parallelism)在有些情况下也可以提升吞吐量,但是这样做通常会导致非常高的资源消耗:更多的并行 MapFunction 实例意味着更多的 Task、更多的线程、更多的 Flink 内部网络连接、更多的与数据库的网络连接、更多的缓冲和更多程序内部协调的开销;

101.异步 I/O 算子使用前提:

需要支持异步请求的数据库客户端;
如果没有支持异步请求的客户端,可以通过创建多个客户端并使用线程池处理同步调用的方法,将同步客户端转换为有限并发的客户端,比正规的异步客户端效率低;

102.Flink 的异步 I/O API 允许用户在流处理中使用异步请求客户端,API 处理与数据流的集成,同时还能处理好顺序、事件时间和容错等;

103.在具备异步数据库客户端的基础上,实现数据流转换操作与数据库的异步 I/O 交互需要以下三部分:

- 实现分发请求的 AsyncFunction;
- 获取数据库交互的结果并发送给 ResultFuture 的回调函数;
- 将异步 I/O 操作应用于 DataStream,作为 DataStream 的一次转换操作,启用或者不启用重试;

104.第一次调用 ResultFuture.complete 后 ResultFuture 就完成了,后续的 complete 调用都将被忽略;

105.Timeout:超时参数定义了异步操作执行多久未完成、最终认定为失败的时长,如果启用重试,则可能包括多个重试请求,可以防止一直等待得不到响应的请求;

106.Capacity: 容量参数定义了可以同时进行的异步请求数;即使异步 I/O 通常带来更高的吞吐量,执行异步 I/O 操作的算子仍然可能成为流处理的瓶颈,限制并发请求的数量可以确保算子不会持续累积待处理的请求进而造成积压,而是在容量耗尽时触发反压;

107.AsyncRetryStrategy: 重试策略参数定义了什么条件会触发延迟重试以及延迟的策略,如固定延迟、指数后退延迟、自定义实现等;

108.超时处理:

当异步 I/O 请求超时的时候,默认会抛出异常并重启作业; 

如果想处理超时,可以重写 AsyncFunction#timeout 方法;重写 AsyncFunction#timeout 时需要调用 ResultFuture.complete() 或者 ResultFuture.completeExceptionally() 以通知 Flink 这条记录的处理已经完成;如果超时发生时不想发出任何记录,可以调用 ResultFuture.complete(Collections.emptyList());

109.AsyncFunction 发出的并发请求经常以不确定的顺序完成,这取决于请求得到响应的顺序;Flink 提供两种模式控制结果记录以何种顺序发出。

- 无序模式: 异步请求一结束就立刻发出结果记录。流中记录的顺序在经过异步 I/O 算子之后发生了改变。当使用处理时间作为基本时间特征时,这个模式具有最低的延迟和最少的开销。此模式使用 AsyncDataStream.unorderedWait(...) 方法。
- 有序模式: 保持了流的顺序。发出结果记录的顺序与触发异步请求的顺序(记录输入算子的顺序)相同;算子将缓冲一个结果记录直到这条记录前面的所有记录都发出(或超时),因为记录或者结果要在 checkpoint 的状态中保存更长的时间,所以与无序模式相比,有序模式通常会带来额外的延迟和 checkpoint 开销。此模式使用 AsyncDataStream.orderedWait(...) 方法。

110.当流处理应用使用事件时间时,异步 I/O 算子会正确处理 watermark。

- 无序模式:Watermark 既不超前于记录也不落后于记录,即 watermark 建立了顺序的边界。只有连续两个 watermark 之间的记录是无序发出的。在一个 watermark 后面生成的记录只会在这个 watermark 发出以后才发出。在一个 watermark 之前的所有输入的结果记录全部发出以后,才会发出这个 watermark。在 watermark 的情况下,无序模式会引入一些与有序模式相同的延迟和管理开销。开销大小取决于 watermark 的频率;
- 有序模式:连续两个 watermark 之间的记录顺序也被保留了。开销与使用处理时间相比,没有显著的差别。

111.异步 I/O 算子提供了完全的精确一次容错保证,它将异步请求的记录保存在 checkpoint 中,在故障恢复时重新触发请求;

112.重试支持为异步 I/O 操作引入了一个内置的重试机制,它对用户的异步函数实现逻辑是透明的。

- AsyncRetryStrategy: 异步重试策略包含了触发重试条件 AsyncRetryPredicate 定义,以及根据当前已尝试次数判断是否继续重试、下次重试间隔时长的接口方法。在满足触发重试条件后,有可能因为当前重试次数超过预设的上限放弃重试,或是在任务结束时被强制终止重试(此时系统以最后一次执行的结果或异常作为最终状态)。
- AsyncRetryPredicate: 触发重试条件可以选择基于返回结果、执行异常来定义条件,两种条件是或的关系,满足其一即会触发。

113.在实现使用 Executor 和回调的 Futures 时,建议使用 DirectExecutor,因为通常回调的工作量很小,DirectExecutor 避免了额外的线程切换开销;回调通常只是把结果发送给 ResultFuture,也就是把它添加进输出缓冲;从这里开始,包括发送记录和与 chenkpoint 交互在内的繁重逻辑都将在专有的线程池中进行处理。

114.DirectExecutor 可以通过 org.apache.flink.util.concurrent.Executors.directExecutor() 或 com.google.common.util.concurrent.MoreExecutors.directExecutor() 获得;

115.默认情况下,AsyncFunction 的算子(异步等待算子)可以在作业图的任意处使用,但它不能与 SourceFunction/SourceStreamTask 组成算子链;

116.以下情况将阻塞 asyncInvoke(...) 函数,从而使异步行为无效:

- 使用同步数据库客户端,它的查询方法调用在返回结果前一直被阻塞;
- 在 asyncInvoke(...) 方法内阻塞等待异步客户端返回的 future 类型对象;

117.启用重试后可能需要更大的缓冲队列容量:

新的重试功能可能会导致更大的队列容量要求,最大数量可以近似地评估如下:
inputRate * retryRate * avgRetryDuration

例如,对于一个输入率=100条记录/秒的任务,其中1%的元素将平均触发1次重试,平均重试时间为60秒,额外的队列容量要求为:
100条记录/秒 * 1% * 60s = 60

即在无序输出模式下,给工作队列增加 60 个容量可能不会影响吞吐量;而在有序模式下,头部元素是关键点,它未完成的时间越长,算子提供的处理延迟就越长;在相同的超时约束下,如果头元素事实上获得了更多的重试,那重试功能可能会增加头部元素的处理时间即未完成时间,也就是说在有序模式下,增大队列容量并不是总能提升吞吐。

118.当队列容量增长时(可以缓解背压),OOM 的风险会随之增加;对于 ListState 存储来说,理论的上限是 Integer.MAX_VALUE,虽然队列容量的限制是一样的,但在生产中不能把队列容量增加到太大,此时增加任务的并行性也许更可行;

119.使用 keyed state,首先需要为 DataStream 指定 key(主键);这个 key 用于状态分区(数据流中的 Record 也会被分区);可以使用 DataStream 中 Java API 的 keyBy(KeySelector) 来指定 key,将生成 KeyedStream;

120.Key selector 函数接收单条 Record 作为输入,返回这条记录的 key,该 key 可以为任何类型,但是它的计算产生方式必须具备确定性,Flink 的数据模型不基于 key-value 对,将数据集在物理上封装成 key 和 value 是没有必要的,Key 是“虚拟”的,用以操纵分组算子;

121.可以通过 tuple 字段索引,或者选取对象字段的表达式来指定 key 即 Tuple Keys 和 Expression Keys;

122.keyed state 接口提供不同类型状态的访问接口,这些状态都作用于当前输入数据的 key 下,即这些状态仅可在 KeyedStream 上使用,支持的状态类型如下:

- ValueState: 保存一个可以更新和检索的值(每个值都对应到当前的输入数据的 key,因此算子接收到的每个 key 都可能对应一个值)这个值可以通过 update(T) 进行更新,通过 T value() 进行检索;
- ListState: 保存一个元素的列表,可以往这个列表中追加数据,并在当前的列表上进行检索,通过 add(T) 或者 addAll(List) 添加元素,通过 Iterable get() 获得整个列表,还可以通过 update(List) 覆盖当前的列表;
- ReducingState: 保存一个单值,表示添加到状态的所有值的聚合,使用 add(T) 增加的元素会用提供的 ReduceFunction 进行聚合;
- AggregatingState: 保留一个单值,表示添加到状态的所有值的聚合,和 ReducingState 相反的是,聚合类型可能与添加到状态的元素的类型不同,使用 add(IN) 添加的元素会用指定的 AggregateFunction 进行聚合;
- MapState: 维护了一个映射列表,可以添加键值对到状态中,也可以获得反映当前所有映射的迭代器,使用 put(UK,UV) 或者 putAll(Map) 添加映射,使用 get(UK) 检索特定 key,使用 entries(),keys() 和 values() 分别检索映射、键和值的可迭代视图,还可以通过 isEmpty() 来判断是否包含任何键值对;

123.所有类型的状态都有一个 clear() 方法,清除当前 key 下的状态数据;

124.状态对象仅用于与状态交互,状态本身不一定存储在内存中,还可能在磁盘或其他位置;从状态中获取的值取决于输入元素所代表的 key,在不同 key 上调用同一个接口,可能得到不同的值;

125.状态通过 RuntimeContext 进行访问,只能在 rich functions 中使用;

126.任何类型的 keyed state 都可以有有效期 (TTL),如果配置了 TTL 且状态值已过期,则会尽最大可能清除对应的值,所有状态类型都支持单元素的 TTL,列表元素和映射元素将独立到期;

127.在使用状态 TTL 前,需要先构建一个 StateTtlConfig 配置对象,然后把配置传递到 state descriptor 中启用 TTL 功能;

128.TTL 的更新策略(默认是 OnCreateAndWrite):

- StateTtlConfig.UpdateType.OnCreateAndWrite - 仅在创建和写入时更新
- StateTtlConfig.UpdateType.OnReadAndWrite - 读取时也更新

129.数据在过期但还未被清理时的可见性配置如下(默认为 NeverReturnExpired):

- StateTtlConfig.StateVisibility.NeverReturnExpired - 不返回过期数据
- StateTtlConfig.StateVisibility.ReturnExpiredIfNotCleanedUp - 会返回过期但未清理的数据

NeverReturnExpired 情况下,过期数据就像不存在一样,不管是否被物理删除,这对于不能访问过期数据的场景下非常有用,比如敏感数据, ReturnExpiredIfNotCleanedUp 在数据被物理删除前都会返回;

注意:

- 状态上次的修改时间会和数据一起保存在 state backend 中,开启该特性会增加状态数据的存储;Heap state backend 会额外存储一个包括用户状态以及时间戳的 Java 对象,RocksDB state backend 会在每个状态值(list 或者 map 的每个元素)序列化后增加 8 个字节;
- 暂时只支持基于 processing time 的 TTL;
- 尝试从 checkpoint/savepoint 进行恢复时,TTL 的状态(是否开启)必须和之前保持一致,否则会遇到 “StateMigrationException”;
- TTL 的配置并不会保存在 checkpoint/savepoint 中,仅对当前 Job 有效;
- 不建议 checkpoint 恢复前后将 state TTL 从短调长,这可能会产生潜在的数据错误;
- 
;