1.为什么是Flink
Flink 可以支持本地的快速迭代,以及一些环形的迭代任务。Flink 可以定制化内存管理,相对于 Spark,Flink 并没有将内存完全交给应用层。
而在业界比较常见的三款流式计算框架(SparkStreaming、Storm、Flink)中,我们选择了 Flink 的原因有以下几点:
- 首先业务需求需要对事件(用户行为)提供实时的响应(毫秒级),对低延迟的要求比较高,因此我们放弃了使用 SparkStreaming,因为 SparkStreaming 是基于微批的处理思想,只能提供准实时(秒级)。而 StructuredStreaming 目前还比较早期,故暂时不考虑。
- 相较于 Storm,我们希望选择一个批流统一的技术栈和生态圈,并且有更加活跃的社区。从目前的版本迭代以及项目的 star 数来看,Flink 显然更胜一筹。
- Flink 丰富的状态类型和天然的分布式的状态存储和恢复机制,让开发人员可以更加聚焦在业务实现中,而不再需要考虑依赖外部存储来保留中间结果,并且在服务异常情况下的数据恢复问题。
- 我们的不少业务场景有对窗口操作(Window)的依赖,Flink 提供了比 Storm 更加丰富的窗口操作。
2.平台架构
采用的是lambda架构:
收集数据API – 事件日志 – flume – kafka – 实时flink(离线hadoop) – 实时特性计算habse(数据分析elasticsearch,导出事件消息队列)
3.实时事件
有以下几个需要注意的问题:
1.所有的实时计算逻辑都依赖于用户行为事件,而确保任务是按事件发生的先后顺序处理,是我们必须保证的,否则数据就会出现不一致的情况。因此,我们通过在数据接入层进行了按用户 ID 做分组,保证单个用户的数据落在 Kafka 的同一个 partition 中。
2.Flink 任务中的时间处理类型我们应该选择哪个?
默认情况下,Flink 使用的是 ProcessingTime 来作为 TimeCharacteristic。但是我们需要依赖于事件发生时间作为我们的 watermark 生成及 session 窗口触发,因此我们选择 TimeCharacteristic 为 EventTime
。
3.单用户的 session 窗口如何生成,以及 session 窗口如何实现会话开始 / 结束的触发?
目前 Flink 的窗口函数中已经支持了 SessionWindow 的窗口机制。因此我们只需要根据 UID 分组然后直接使用即可。但是,默认情况下所有的 Window 只会在结束之后,触发我们的窗口函数。因此为了满足在事件不同条件下的触发,我们需要自定义 Trigger,并且通过利用 State 机制来传递当前的触发类型,来以便后续的 window 处理程序能够识别出不同的触发类型,从而进行不同的逻辑处理。
4.当事件流在一段时间内处于空闲状态,如何保证 session window 能够被照常触发?
考虑到我们在容忍一定延迟数据的情况下,希望在 session window 结束时能够被尽早的触发。所以如果指定时间内(此处我们设置为 session 的时长,即可认为是 session timeout 时间)依然没有数据流入系统,那么我们将会尝试干预 watermark 机制,当它感知到当前流处于 idle 状态时,会尝试自动递增上升 watermark,当当前的 watermark 累计递增了 session timeout 时间时,就相应的达到了 session window 的及时触发效果。当然,这样做的风险是 watermark 水位被提高了,但在我们场景中这个是可以接受的。目前在 Flink 社区中也有关于 stream idle 的讨论,感兴趣的可以参考一下【FLINK-5018】。
5.session window 窗口太大(定义为 30 分钟),导致大量的 state 如何处理?
默认情况下,Flink 使用 JobManager 的堆内存进行状态的存储,同时可以配合使用 FileSystem 进行状态的持久化,但是对于我们的场景中,状态太大,已经超过了默认的 state 大小(默认为 5M),同时在每次进行 checkpoint 的时候,默认都是全量的 checkpoint, 导致了 checkpoint 处理过慢而超时,甚至数据对齐导致的背压,严重影响了程序的处理能力。因此,我们选择使用 rocksdb 来进行状态存储,主要他可以支持增量的 checkpoint,并且减少了 JobManager 的压力。
6.实时 Flink 任务中如何动态订阅配置信息,并同步到所有相关的 Task 中?
目前我们的所有配置变更都存储在 Kafka 中,任何依赖配置的变更,都可以通过订阅 Kafka topic 来获得最新的配置信息。最初的时候,我们通过使用 Flink 的 connect API 将数据流和配置流关联起来。当有新的配置变化后,可以通过 broadcast 将所有配置同步到各个 Task 上。但是,存在的问题是 broadcast 只能将配置信息下发给直属下游,而并不是它的所有下游节点,即缺少一个全局的配置。因此,我们的解决思路是,通过在每个 TaskManager 实例启动时,实例化一个 Kafka Consumer,然后通过采用监听者模式,去推送最新的配置给当前 JVM 中的所有订阅该配置类型的算子实例。这样就保证了单个 JVM 中共享同一套配置信息,同时任意的算子实例都可以订阅自己依赖的配置,而无需关心上下游的问题。