Loading

Flink总结

date: 2020-10-20 16:20:00
updated: 2021-01-12 17:57:00

Flink总结

1. 状态一致性

  • at most once
  • at least once
  • exactly once

2. 端到端的状态一致性

  • source端(可重设数据的读取位置)
    • kafka 可以根据offset回放数据
  • 内部保证(checkpoint)
    • 每隔一段时间去做一次ck,jobManager会在数据流中加入一个barrier,上游的计算可能有快有慢,如果是精确一次,计算快的算子会把数据先缓存下来,如果是最少一次,那就会加入到计算当中。barrier会广播到下游,所有的task遇到barrier之后都会对自己的状态进行保存,下游收到所有的上游过来的barrier之后,才会进行ck。
    • 同时设置的还有两次ck的时间间隔、允许几个ck存在、超时时间等
    • ck的保存位置有三种:memory、filesystem、rocksDB。内存中主要是开发测试用,速度快,但是不稳定;放到文件中访问速度快,状态信息不会丢失,但是大小会受到 TaskManager 内存限制(默认是5M),生产可用;rocksDB会先放到数据库,类似于key-value数据存储,当最终ck的时候会放到文件中,缺点是访问的速度下降,但是可以存储大量的状态信息
  • sink端(从故障恢复时数据不会重复写入外部系统,比如幂等写入,事务写入)
    • 如果是redis、hbase,通过幂等性,可以保证至少一次 => 精确一次
    • 如果是kafka通过二阶段提交,在sink的时候会先预提交,然后去做ck状态保存,ck失败没关系,因为预提交用户看不到数据;ck成功之后再真正提交,如果这时候提交失败,当flink去做下一次计算的时候会发现commit失败了,会先去进行commit,然后再做计算。

3. kafka

0.8版本kafka:offset是保存在zookeeper中的,但是由于zk不支持高并发,当consumer多了之后,可能会出现问题
=> 0.10版本kafka:在kafka中有一个 _consumer_offset 的 topic,可以自己去保存

在 FlinkKafkaConsumerBase 类里,有一个 ListState ,即 flink 本身就保存了kafka的offset,然后kafka又有一个topic里保存offset。那么flink默认是使用自己的 ListState,当ck成功之后会去同步到kafka,通过 setCommitOffsetsOnCheckpoints 这个配置来设置是否当ck成功后offset会同步kafka

4. watermark

通过 assignTimestampsAndWatermarks 方法来设置如何提取事件时间,以及允许的最大乱序时间。默认是周期性每200毫秒生成一次。也可以不要周期性,每来一条数据就尝试获取一次。

当watermark时间大于窗口时间,那么窗口就会被激活,然后进行计算,输出结果。通过 allowLateness() 可以延长窗口被销毁的时间,如果有延迟很久的数据的话,可以再更新窗口

只有当watermark更新了之后,才会去触发 onTimer 定时器

5. savepoint

相当于重量级的checkpoint,需要用户手动触发,主要用于 作业升级、代码修改等,需要针对每一个算子来设置uid,flink默认的uid生成是根据代码结构,如果uid不一样了就无法获得之前的状态。

6. QueryableState

每一个状态都保存下来了,然后还需要写入到sink端,如果state是可查询的,那么直接就可以不用写,而把state去当成一个类似数据库一样的东西去使用

7. state类型

  • Operator State:state是task级别的state,说白了就是每个task对应一个state
    • ListState等,一般不使用,比如kafkaconsumer里有一个liststate保存offset partition等信息
  • KeyedState:主要用前三个
    • ValueState
    • ListState
    • MapState
    • ReducingState
    • AggregatingState

使用思路就是

  1. 不管是open() 方法也好,还是哪个位置,需要先注册,通过 getRuntimeContext().get...(stateDescriptor) 来注册,标明name和其中的类型
  2. 在使用的时候,先进行初始化的判断,然后更新,如果是通过定时器的方式来实现的,比如是继承了 processFunction 的方式来实现,那么就需要在 onTimer() 方法中去清空所有的状态

8. 案例

8.1 热门商品的统计 TopN

  1. 设置时间语义和最大乱序时间即watermark
  2. 数据进行过滤,然后按照 keyBy("itemId").timeWindow(窗口大小,滑动距离).aggerate(窗口增量函数,全窗口函数) 分组后,开滑动窗口,然后去自定义实现 aggerate 方法,增量函数就是一个点击量的累加,即每当来一条数据的时候就会去计算一次,全窗口函数就是对当前窗口内的所有数据去进行一个封装
  3. 根据同一窗口的所有数据,再根据窗口时间进行分组,即 keyBy("windowEnd").process(new topN(N的大小)) 实现对同一个窗口中的数据进行排序去topN的功能。在 open 方法中去注册一个state,保存窗口内所有输出的数据;在 processElement 方法中每来一条数据,就add进state中,并且去更新定时器,这里的定时器就用当前的窗口时间+1即可 ctx.timeService().registerEventTimer(windowEnd + 1)。flink底层去注册时间戳,是按照时间戳来区分的,如果时间戳一样,那么不管注册多少次,都是一个定时器。在 onTimer() 方法中拿到所有的数据,转换成 arrayList,通过list.sort 方法排序,然后去封装数据

保存在listState会存在一定的问题,在处理延迟数据的时候,可能会导致重复数据的输出
换成mapState

8.2 dau/uv

思路

  1. 按人进行keyby

  2. 自定义实现继承 processFunction 方法,在open方法里去初始化两个state,一个是负责浏览次数累加,一个是定时器

  3. 在 processElement 方法中初始化定时器,设置为当天的零点,在 onTimer 方法中每当激活了定时器就需要输出

  4. 设置时间语义和最大乱序时间即watermark

  5. timewindow定义在1小时,然后实现自定义trigger,每来一条数据处理一次或每20s处理一次,通过布隆过滤器来实现去重。去重不使用set是因为如果数据量很大,会导致内存溢出;然后去实现processFunction,每当一条数据来的时候就会触发process方法,可以去连续redis或者去保存中间的状态

9. 定时器

如果要用到延迟处理,一般都需要用到 KeyedProcessFunction。定时器的分类是根据注册的时间戳不同来分类,同一个时间戳,不管注册几次,都是一个定时器。底层通过小顶堆,获取到当前所有时间戳最小的那一个,然后去触发,触发结束后删除,再进行排序

10. 窗口

window的属性只有两个,start和end,当数据来了之后,跟wm进行比较,判断在哪一个区间内,然后去激活窗口的process方法做处理。

对于窗口来说,window本身没有意义(两个字段只是划分区间),window里面的中间计算结果才是有意义的(中间的状态保存最重要)

posted @ 2021-02-07 15:41  猫熊小才天  阅读(545)  评论(0编辑  收藏  举报