在实时计算 PV 信息时,用户短时间内重复点击并不会增加点击次数,基于此需求,我们需要对流式数据进行实时去重。
一想到大数据去重,我们立刻可以想到布隆过滤器、HyperLogLog 去重、Bitmap 去重等方法。对于实时数据处理引擎 Flink 来说,除了上述方法外还可以通过 Flink SQL 方式或 Flink 状态管理的方式进行去重。
本文主要介绍基于 Flink 状态管理的方式进行实时去重。
1.状态管理
虽然 Flink 的很多操作都是基于事件解析器进行一次的事件处理,但也有很多操作需要记住多个事件的信息,比如窗口运算等。这些操作便称为有状态的操作。
有状态的操作有一些经典案例,比如说:
- 计算每分钟/小时/天的统计量等;
- 实时计算 PV、UV,需要维护目前已有的 PV、UV 信息;
- 实时更新机器学习模型,需要记住模型的参数;
我们在上一篇内容中介绍了如何计算分钟级的统计量,我们采用的方法是开一个窗口函数进行统计;而现在的任务是数据去重,对于增量数据来说没法进行开窗运算。
针对这种情况,Flink 提供了基于事件驱动的处理函数(ProcessFunction),其将事件处理与 Timer、State 结合在一起,提供了更加强大和丰富的功能。
Flink 子任务状态更新和获取的流程如下图所示,一个算子子任务接收输入流,获取对应的状态,根据新的计算结果更新状态。
![adb5225367c9353908247d515afe9f62.png](https://i-blog.csdnimg.cn/blog_migrate/6849712738a43efbfb924218798c3761.png)
获取和更新状态的逻辑其实并不复杂,但流处理框架还需要解决以下几类问题:
- 数据的产出要保证实时性,延迟不能太高;
- 需要保证数据不丢不重,恰好计算一次,尤其是当状态数据非常大或者应用出现故障需要恢复时,要保证状态的计算不出任何错误;
- 一般流处理任务都是 7*24 小时运行的,程序的可靠性需要非常高。
基于上述要求,我们不能将状态仅交由内存管理,因为内存的容量是有限制的,当状态数据稍微大一些时,就会出现内存不够的问题。由于 Flink 本身提供了有状态的计算,并且封装了一些底层的实现,比如状态的高效存储、Checkpoint 和 Savepoint 持久化备份机制、计算资源扩缩容等问题,所以我们只需要调用 Flink API,专注于业务逻辑即可。
2.状态类型
Managed State 和 Raw State
Flink有两种基本类型的状态:托管状态(Managed State)和原生状态(Raw State)。从名称中也能读出两者的区别:Managed State 是由 Flink 管理的,Flink 帮忙存储、恢复和优化,Raw State 是开发者自己管理的,需要自己序列化。两者对比如下:
Managed State | Raw State | |
---|---|---|
状态管理方式 | Flink Runtime 托管,自动存储、自动恢复、自动伸缩 | 用户自己管理 |
状态数据结构 | Flink提供的常用数据结构,如 ListState、MapState 等 | 字节数组:byte[] |
使用场景 | 绝大多数 Flink 算子 | 用户自定义算子 |
大部分情况下我们使用 Managed State 便可满足需求。
Keyed State 和 Operator State
我们对 Managed State 继续细分,它又有两种类型:Keyed State 和 Operator State。
Keyed State 是 KeyedStream
上的状态。假如输入流按照 id 为 Key 进行了 keyBy
分组,形成一个 KeyedStream
。下图为 Keyed State,因为一个算子子任务可以处理一到多个Key,算子子任务 1 处理了两种 Key,两种 Key 分别对应自己的状态。
![373e9652973c6c298a6f2c5187c1cd0e.png](https://i-blog.csdnimg.cn/blog_migrate/e688be813def4d1f9fc74a0d51b057b3.png)
Operator State 可以用在所有算子上,每个算子子任务或者说每个算子实例共享一个状态,流入这个算子子任务的数据可以访问和更新这个状态。下图为 Operator State。
![efa14006a28a7ccfa736bdcac33c892a.png](https://i-blog.csdnimg.cn/blog_migrate/dedb44ec8f51d30751935fd980a96943.png)
下图为两者的区别:
Keyed State | Operator State | |
---|---|---|
适用算子类型 | 只适用于KeyedStream 上的算子 |
可以用于所有算子 |
状态分配 | 每个 Key 对应一个状态 | 一个算子子任务对应一个状态 |
创建和访问方式 | 重写 Rich Function 通过里面的 RuntimeContext 访问 | 实现 CheckpointedFunction 等接口 |