Flink完美搭档,数据存储层上的Pravega,Flink窗口的应用与实现

Flink 完美搭档:数据存储层上的 Pravega


本文将从大数据架构变迁历史,Pravega 简介,Pravega 进阶特性以及车联网使用场景这四个方面介绍 Pravega,重点介绍 DellEMC 为何要研发 Pravega,Pravega 解决了大数据处理平台的哪些痛点以及与 Flink 结合会碰撞出怎样的火花。

大数据架构变迁

Lambda 架构之痛

如何有效地提取和提供数据,是大数据处理应用架构是否成功的关键之处。由于处理速度和频率的不同,数据的摄取需要通过两种策略来进行。上图就是典型的 Lambda架构:把大数据处理架构分为批处理和实时流处理两套独立的计算基础架构。

对于实时处理来说,来自传感器,移动设备或者应用日志的数据通常写入消息队列系统(如 Kafka), 消息队列负责为流处理应用提供数据的临时缓冲。然后再使用 Spark Streaming 从 Kafka 中读取数据做实时的流计算。但由于 Kafka 不会一直保存历史数据,因此如果用户的商业逻辑是结合历史数据和实时数据同时做分析,那么这条流水线实际上是没有办法完成的。因此为了补偿,需要额外开辟一条批处理的流水线,即图中" Batch "部分。

对于批处理这条流水线来说,集合了非常多的的开源大数据组件如 ElasticSearch, Amazon S3, HDFS, Cassandra 以及 Spark 等。主要计算逻辑是是通过 Spark 来实现大规模的 Map-Reduce 操作,优点在于结果比较精确,因为可以结合所有历史数据来进行计算分析,缺点在于延迟会比较大。

这套经典的大数据处理架构可以总结出三个问题:

  • 两条流水线处理的延迟相差较大,无法同时结合两条流水线进行迅速的聚合操作,同时结合历史数据和实时数据的处理性能低下。
  • 数据存储成本大。而在上图的架构中,相同的数据会在多个存储组件中都存在一份或多份拷贝,数据的冗余无疑会大大增加企业客户的成本。并且开源存储的数据容错和持久化可靠性一直也是值得商榷的地方,对于数据安全敏感的企业用户来说,需要严格保证数据的不丢失。
  • 重复开发。同样的处理流程被两条流水线进行了两次,相同的数据仅仅因为处理时间不同而要在不同的框架内分别计算一次,无疑会增加数据开发者重复开发的负担。

流式存储的特点

在正式介绍 Pravega 之前,首先简单谈谈流式数据存储的一些特点。

如果我们想要统一流批处理的大数据处理架构,其实对存储有混合的要求。

  • 对于来自序列旧部分的历史数据,需要提供高吞吐的读性能,即 catch-up read
  • 对于来自序列新部分的实时数据,需要提供低延迟的 append-only 尾写 tailing write 以及尾读 tailing read

重构的流式存储架构

像 Kafka,Cassandra 等分布式存储组件来说,其存储架构都从上往下遵循从专有的日志存储,到本地文件,再到集群上的分布式存储的这种模式。

而 Pravega 团队试图重构流式存储的架构,引入 Pravega Stream 这一抽象概念作为流式数据存储的基本单位。Stream 是命名的、持久的、仅追加的、无限的字节序列。

如上图所示,存储架构最底层是基于可扩展分布式云存储,中间层表示日志数据存储为 Stream 来作为共享的存储原语,然后基于 Stream 可以向上提供不同功能的操作:如消息队列,NoSQL,流式数据的全文搜索以及结合 Flink 来做实时和批分析。换句话说,Pravega 提供的 Stream 原语可以避免现有大数据架构中原始数据在多个开源存储搜索产品中移动而产生的数据冗余现象,其在存储层就完成了统一的数据湖

重构的大数据架构

我们提出的大数据架构,以 Apache Flink 作为计算引擎,通过统一的模型/API来统一批处理和流处理。以 Pavega 作为存储引擎,为流式数据存储提供统一的抽象,使得对历史和实时数据有一致的访问方式。两者统一形成了从存储到计算的闭环,能够同时应对高吞吐的历史数据和低延时的实时数据。同时 Pravega 团队还开发了 Flink-Pravega Connector,为计算和存储的整套流水线提供 Exactly-Once 的语义。

Pravega 简介

Pravega 的设计宗旨是为流的实时存储提供解决方案。应用程序将数据持久化存储到 Pravega 中,Pravega 的 Stream 可以有无限制的数量并且持久化存储任意长时间,使用同样的 Reader API 提供尾读 (tail read) 和追赶读 (catch-up read) 功能,能够有效满足离线计算和实时计算两种处理方式的统一。

Pravega 基本概念

结合上图简要介绍 Pravega 的基本概念:

  • Stream

Pravega 会把写入的数据组织成 Stream,Stream 是命名的、持久的、仅追加的、无限的字节序列。

  • Stream Segments

Pravega Stream 会划分为一个或多个 Segments,相当于 Stream 中数据的分片,它是一个 append-only 的数据块,而 Pravega 也是基于 Segment 基础上实现自动的弹性伸缩。Segment 的数量也会根据数据的流量进行自动的连续更新。

  • Event

Pravega's client API 允许用户以 Event 为基本单位写入和读取数据,Event 具体是Stream 内部字节流的集合。如 IOT 传感器的一次温度记录写入 Pravega 就可以理解成为一个 Event.

  • Routing Key

每一个 Event 都会有一个 Routing Key,它是用户自定义的一个字符串,用来对相似的 Event 进行分组。拥有相同 Routing Key 的 Event 都会被写入相同的 Stream Segment 中。Pravega 通过 Routing Key 来提供读写语义。

  • Reader Group

用于实现读取数据的负载均衡。可以通过动态增加或减少 Reader Group 中 Reader的数量来改变读取数据的并发度。更为详细的介绍请参考 Pravega 官方文档:

http://pravega.io/docs/latest/pravega-concepts

Pravega 系统架构

在控制层面,Controller 作为 Pravega 集群的主节点对数据层面的 Segment Store做管理,提供对流数据的创建,更新以及删除等操作。同时它还承担实时监测集群健康状态,获取流数据信息,收集监控指标等功能。通常集群中会有3份 Controller 来保证高可用。

在数据层面,Segment Store 提供读写 Stream 内数据的 API。在 Pravega 里面,数据是分层存储的:

  • Tier 1 存储

Tier1 的存储通常部署在 Pravega 集群内部,主要是提供对低延迟,短期的热数据的存储。在每个 Segment Store 结点都有 Cache 以加快数据读取速率,Pravega 使用Apache Bookeeper 来保证低延迟的日志存储服务。

  • Long-term 存储

Long-term 的存储通常部署在 Pravega 集群外部,主要是提供对流数据的长期存储,即冷数据的存储。不仅支持 HDFS,NFS,还会支持企业级的存储如 Dell EMC的 ECS,Isilon 等产品。

Pravega 进阶特性

读写分离

在 Tier1 存储部分,写入数据的时候通过 Bookkeeper 保证了数据已经在所有的 Segment Store 中落盘,保证了数据写入成功。

读写分离有助于优化读写性能:只从 Tier1 的 Cache 和 Long-term 存储去读,不去读 Tier1 中的 Bookkeeper。

在客户端向 Pravega 发起读数据的请求的时候,Pravega 会决定这个数据究竟是从Tier1 的 Cache 进行低延时的 tail-read,还是去 Long-term 的长期存储数据(对象存储/NFS)去进行一个高吞吐量的 catch-up read(如果数据不在 Cache,需要按需load 到 Cache 中)。读操作是对客户端透明的。

Tier1 的 Bookkeeper 在集群不出现故障的情况下永远不进行读取操作,只进行写入操作。

弹性伸缩

Stream 中的 Segment 数量会随着 IO 负载而进行弹性的自动伸缩。以上图为例子简单阐述:

  • 数据流在 t0 时刻写入 Pravega,根据路由键数据会路由到 Segment0 和Segment1 中,如果数据写入速度保持恒定不变,那么 Segemnt 数量不会发生变化。
  • 在 t1 时刻系统感知到 segment1 数据写入速率加快,于是将其划分为两个部分:Segment2 和 Segment3。这时候 Segment1 会进入 Sealed 状态,不再接受写入数据,数据会根据路由键分别重定向到 Segment2 和 Segment3.
  • 与 Scale-Up 操作相对应,系统也可以根据数据写入速度变慢后提供 Scale-Down 操作。如在 t3 时刻系统 Segment2 和 Segment5 写入流量减少,因此合并成新的 Segment6。

端到端的弹性伸缩

Pravega 是以 Kubernetes Operator 来对集群各组件进行有状态的应用部署,这可以使得应用的弹性伸缩更为灵活方便。

Pravega 最近也在和 Ververica 进行深度合作,致力于在 Pravega 端实现 Kubernetes Pod 级别的弹性伸缩同时在 Flink 端通过 rescaling Flink 的 Task 数量来实现弹性伸缩。

事务性写入

Pravega 同样提供事务性的写入操作。在提交事务之前,数据会根据路由键写入到不同的 Transaction Segment 中,这时候 Segment 对于 Reader 来说是不可见的。只有在事务提交之后,Transaction Segment 才会各自追加到 Stream Segment 的末尾,这时候 Segment 对于 Reader 才是可见的。写入事务的支持也是实现与 Flink 的端到端 Exactly-Once 语义的关键。

Pravega vs. Kafka

首先最关键的不同在于两者的定位:Kafka 的定位是消息队列,而 Pravega 的定位是存储,会更关注于数据的动态伸缩,安全性,完整性等存储特性。

对于流式数据处理来说,数据应该被视为连续和无限的。Kafka 作为基于本地文件系统的一个消息队列,通过采用添加到日志文件的末尾并跟踪其内容( offset 机制)的方式来模拟无限的数据流。然而这种方式必然受限于本地文件系统的文件描述符上限以及磁盘容量,因此并非无限。

而两者的比较在图中给出了比较详细的总结,不再赘述。

Pravega Flink Connector

为了更方便与 Flink 的结合使用,我们还提供了 Pravega Flink Connector(
https://github.com/pravega/flink-connectors), Pravega 团队还计划将该 Connector 贡献到 Flink 社区。Connector 提供以下特性:

  • 对 Reader 和 Writer 都提供了 Exactly-once 语义保证,确保整条流水线端到端的 Exactly-Once
  • 与 Flink 的 checkpoints 和 savepoints 机制的无缝耦合
  • 支持高吞吐低延迟的并发读写
  • Table API 来统一对 Pravega Sream 的流批统一处理

车联网使用场景

以无人驾驶车联网这种能够产生海量 PB 级数据的应用场景为例:

  • 需要对车况路况数据做实时的处理以及时对路线规划做出微观的预测和规划
  • 需要对较长期行驶数据运行机器学习算法来做路线的宏观预测和规划,这属于批处理
  • 同时需要结合实时处理和批处理,利用历史数据生成的机器学习模型和实时数据反馈来优化检测结果

而客户关注的关键指标主要在:

  • 如何保证高效地端到端处理速度
  • 如何尽可能减少机器学习模型的训练时间
  • 如何尽可能降低存储数据的消耗与成本

下面给出引入 Pravega 前后的解决方案比较。

解决方案比较

Pravega 的引入无疑大大简洁了大数据处理的架构:

  • Pravega 作为抽象的存储接口,数据在 Pravega 层就实现了一个数据湖:批处理,实时处理和全文搜索都只需要从 Pravega 中获取数据。数据只在 Pravega 存储一份,而不需要像第一种方案中数据冗余地存储在 Kafka,ElasticSearch 和 Long Term Storage 中,这可以极大减少了企业用户数据存储的成本。
  • Pravega 能够提供自动的 Tier Down,无需引入 Flume 等组件来进行额外的 ETL 开发。
  • 组件得到精简,从原来的 Kafka+Flume+HDFS+ElasticSearch+Kibana+Spark+SparkStreaming 精简到 Pravega+Flink+Kibana+HDFS ,减轻运维人员的运维压力。
  • Flink 能够提供流批处理统一的功能,无需为相同的数据提供两套独立的处理代码。

总 结

Flink 俨然已经成为流式计算引擎中的一颗闪亮的明星,然而流式存储领域尚是一片空白。而 Pravega 的设计初衷就是为了填上大数据处理架构这一拼图最后的空白。“所有计算机领域的问题,都可以通过增加一个额外的中间层抽象解决”,而 Pravega 本质就是在计算引擎和底层存储之间充当解耦层,旨在解决新一代大数据平台在数据存储层上的挑战。


可查看更多数仓系列直播视频~

整体思路与学习路径

当我们碰到一项新的技术时,我们应该怎样去学习并应用它呢?在我个人看来,有这样一个学习的路径,应该把它拆成应用和实现两块。首先应该从它的应用入手,然后再深入它的实现。

应用主要分为三个部分,首先应该了解它的应用场景,比如窗口的一些使用场景。然后,进一步地我们去了解它的编程接口,最后再深入了解它的一些抽象概念。因为一个框架或一项技术,肯定有它的编程接口和抽象概念来组成它的编程模型。我们可以通过查看文档的方式来熟悉它的应用。在对应用这三个部分有了初步的了解后,我们就可以通过阅读代码的方式去了解它的一些实现了。

实现部分也分三个阶段,首先从工作流程开始,可以通过 API 层面不断的下钻来了解它的工作流程。接下来是它整体的设计模式,通常对一些框架来说,如果能构建一个比较成熟的生态,一定是在设计模式上有一些独特的地方,使其有一个比较好的扩展性。最后是它的数据结构和算法,因为为了能够处理海量数据并达到高性能,它的数据结构和算法一定有独到之处。我们可以做些深入了解。

以上大概是我们学习的一个路径。从实现的角度可以反哺到应用上来,通常在应用当中,刚接触某个概念的时候会有一些疑惑。当我们对实现有一些了解之后,应用中的这些疑惑就会迎刃而解。

为什么要关心实现

举个例子:

看了这个例子我们可能会有些疑惑:

  • ReduceFunction 为什么不用计算每个 key 的聚合值?
  • 当 key 基数很大时,如何有效地触发每个 key 窗口计算?
  • 窗口计算的中间结果如何存储,何时被清理?
  • 窗口计算如何容忍 late data ?

当你了解了实现部分再回来看应用这部分,可能就有种醍醐灌顶的感觉。

应用场景与编程模型

实时数仓的典型架构

■ 第一种最简单架构,ODS 层的 Kafka 数据经过 Flink 的 ETL 处理后写入 DW 层的 Kafka,再通过 Flink 聚合写入 ADS 层的 MySQL 中,做这样一个实时报表展现。

缺点:由于 MySQL 存储数据有限,所以聚合的时间粒度不能太细,维度组合不能太多。

■ 第二种架构相对于第一种引入了 OLAP 引擎,同时也不用 Flink 来做聚合,通过 Druid 的 Rollup 来做聚合。

缺点:因为 Druid 是一个存储和查询引擎,不是计算引擎。当数据量巨大时,比如每天上百亿、千亿的数据量,会加剧 Druid 的导入压力。

■ 第三种架构在第二种基础上,采用 Flink 来做聚合计算写入 Kafka,最终写入 Druid。

缺点:当窗口粒度比较长时,结果输出会有延迟。

■ 第四种架构在第三种基础上,结合了 Flink 聚合和 Druid Rollup。Flink 可以做轻度的聚合,Druid 做 Rollup 的汇总。好处是 Druid 可以实时看到 Flink 的聚合结果。

Window 应用场景

■ 聚合统计:从 Kafka 读取数据,根据不同的维度做1分钟或5分钟的聚合计算,然后结果写入 MySQL 或 Druid 中。

■ 记录合并:对多个 Kafka 数据源在一定的窗口范围内做合并,结果写入 ES。例如:用户的一些行为数据,针对每个用户,可以对其行为做一定的合并,减少写入下游的数据量,降低 ES 的写入压力。

■ 双流 join:针对双流 join 的场景,如果全量 join 的话,成本开销会非常大。所以就要考虑基于窗口来做 join。

Window 抽象概念

■ TimestampAssigner: 时间戳分配器,假如我们使用的是 EventTime 时间语义,就需要通过 TimestampAssigner 来告诉Flink 框架,元素的哪个字段是事件时间,用于后面的窗口计算。

■ KeySelector:Key 选择器,用来告诉 Flink 框架做聚合的维度有哪些。

■ WindowAssigner:窗口分配器,用来确定哪些数据被分配到哪些窗口。

■ State:状态,用来存储窗口内的元素,如果有 AggregateFunction,则存储的是增量聚合的中间结果。

■ AggregateFunction(可选):增量聚合函数,主要用来做窗口的增量计算,减轻窗口内 State 的存储压力。

■ Trigger:触发器,用来确定何时触发窗口的计算。

■ Evictor(可选):驱逐器,用于在窗口函数计算之前(后)对满足驱逐条件的数据做过滤。

■ WindowFunction:窗口函数,用来对窗口内的数据做计算。

■ Collector:收集器,用来将窗口的计算结果发送到下游。

上图中红色部分都是可以自定义的模块,通过自定义这些模块的组合,我们可以实现高级的窗口应用。同时 Flink 也提供了一些内置的实现,可以用来做一些简单应用。

Window 编程接口

stream        .assignTimestampsAndWatermarks(…)     <-    TimestampAssigner  .keyBy(...)                           <-    KeySelector         .window(...)                          <-    WindowAssigner          [.trigger(...)]                       <-    Trigger           [.evictor(...)]                       <-    Evictor  .reduce/aggregate/process()           <-    Aggregate/Window function

首先我们先指定时间戳和 Watermark 如何生成;然后选择需要聚合的维度的 Key;再选择一个窗口和选择用什么样的触发器来触发窗口计算,以及选择驱逐器做什么样的过滤;最后确定窗口应该做什么样计算。

下面是一个示例:

接下来我们详细看下每个模块。

■ Window Assigner

总结一下主要有3类窗口:

  • Time Window
  • Count Window
  • Custom Window

■ Window Trigger

Trigger 是一个比较重要的概念,用来确定窗口什么时候触发计算。

Flink 内置了一些 Trigger 如下图:

■ Trigger 示例

假如我们定义一个5分钟的基于 EventTime 的滚动窗口,定义一个每2分触发计算的 Trigger,有4条数据事件时间分别是20:01、20:02、20:03、20:04,对应的值分别是1、2、3、2,我们要对值做 Sum 作。

初始时,State 和 Result 中的值都为0。

当第一条数据在20:01进入窗口时,State 的值为1,此时还没有到达 Trigger 的触发时间。

第二条数据在20:02进入窗口,State 中的值为1+2=3,此时达到2分钟满足 Trigger 的触发条件,所以 Result 输出结果为3。

第三条数据在20:03进入窗口,State 中的值为3+3 = 6,此时未达到 Trigger 触发条件,没有结果输出。

第四条数据在20:04进入窗口,State中的值更新为6+2=8,此时又到了2分钟达到了 Trigger 触发时间,所以输出结果为8。如果我们把结果输出到支持 update 的存储,比如 MySQL,那么结果值就由之前的3更新成了8。

■ 问题:如果 Result 只能 append?

如果 Result 不支持 update 操作,只能 append 的话,则会输出2条记录,在此基础上再做计算处理就会引起错误。

这样就需要 PurgingTrigger 来处理上面的问题。

■ PurgingTrigger 的应用

和上面的示例一样,唯一的不同是在
ContinuousEventTimeTrigger 外面包装了一个 PurgingTrigger,其作用是在 ContinuousEventTimeTrigger 触发窗口计算之后将窗口的 State 中的数据清除。

再看下流程:

前两条数据先后于20:01和20:02进入窗口,此时 State 中的值更新为3,同时到了Trigger的触发时间,输出结果为3。

由于 PurgingTrigger 的作用,State 中的数据会被清除。

当后两条数据进入窗口之后,State 重新从0开始累计并更新为5,输出结果为5。

由于结果输出是 append 模式,会输出3和5两条数据,然后再做 Sum 也能得到正确的结果。

上面就是 PurgingTrigger 的一个简单的示例,它还支持很多有趣的玩法。

■ DeltaTrigger 的应用

有这样一个车辆区间测试的需求,车辆每分钟上报当前位置与车速,每行进10公里,计算区间内最高车速。

首先需要考虑的是如何来划分窗口,它不是一个时间的窗口,也不是一个基于数量的窗口。用传统的窗口实现比较困难,这种情况下我们考虑使用 DeltaTrigger 来实现。

下面是简单的代码实现:

如何提取时间戳和生成水印,以及选择聚合维度就不赘述了。这个场景不是传统意义上的时间窗口或数量窗口,可以创建一个 GlobalWindow,所有数据都在一个窗口中,我们通过定义一个 DeltaTrigger,并设定一个阈值,这里是10000(米)。每个元素和上次触发计算的元素比较是否达到设定的阈值,这里比较的是每个元素上报的位置,如果达到了10000(米),那么当前元素和上一个触发计算的元素之间的所有元素落在同一个窗口里计算,然后可以通过 Max 聚合计算出最大的车速。

■ 思考点

上面这个例子中我们通过 GlobalWindow 和 DeltaTrigger 来实现了自定义的 Window Assigner 的功能。对于一些复杂的窗口,我们还可以自定义 WindowAssigner,但实现起来不一定简单,倒不如利用 GlobalWindow 和自定义 Trigger 来达到同样的效果。

下面这个是 Flink 内置的 CountWindow 的实现,也是基于 GlobalWindow 和 Trigger 来实现的。

■ Window Evictor

Flink 内置了一些 Evictor 的实现。

■ TimeEvictor 的应用

基于上面的区间测速的场景,每行进10公里,计算区间内最近15分钟最高车速。

实现上只是在前面基础上增加了 Evictor 的使用,过滤掉窗口最后15分钟之前的数据。

■ Window Function

Flink 内置的 WindowFunction 有两种类型,第一种是 AggregateFunction,它是高级别的抽象,主要用来做增量聚合,每来一条元素都做一次聚合,这样状态里只需要存最新的聚合值。

  • 优点:增量聚合,实现简单。
  • 缺点:输出只有一个聚合值,使用场景比较局限。

第二种是 ProcessWindowFunction,它是低级别的抽象用来做全量聚合,每来一条元素都存在状态里面,只有当窗口触发计算时才会调用这个函数。

  • 优点:可以获取到窗口内所有数据的迭代器,实现起来比较灵活;可以获取到聚合的 Key 以及可以从上下文 Context 中获取窗口的相关信息。
  • 缺点:需要存储窗口内的全量数据,State 的压力较大。

同时我们可以把这两种方式结合起来使用,通过 AggregateFunction 做增量聚合,减少中间状态的压力。通过 ProcessWindowFunction 来输出我们想要的信息,比如聚合的 Key 以及窗口的信息。

工作流程和实现机制

上一节我们介绍了窗口的一些抽象的概念,包括它的编程接口,通过一些简单的示例介绍了每个抽象概念的的用法。

这一节我们深入的研究以下窗口底层是怎么实现的。

WindowOperator 工作流程

首先看下 WindowOperator 的工作流程,代码做了一些简化,只保留了核心步骤。

主要包括以下8个步骤:

1. 获取 element 归属的 windows

2. 获取 element 对应的 key

3. 如果 late data,跳过

4. 将 element 存入 window state

5. 判断 element 是否触发 trigger

6. 获取 window state,注入 window function

7. 清除 window state

8. 注册 timer,到窗口结束时间清理 window

Window State

前面提到的增量聚合计算和全量聚合计算,这两个场景所应用的 State 是不一样的。

如果是全量聚合,元素会添加到 ListState 当中,当触发窗口计算时,再把 ListState 中所有元素传递给窗口函数。

如果是增量计算,使用的是 AggregatingState,每条元素进来会触发 AggregateTransformation 的计算。

看下 AggregateTransformation 的实现,它会调用我们定义的 AgregateFunction 中的 createAccumulator 方法和 add 方法并将 add 的结果返回,所以 State 中存储的就是 accumulator 的值,所以比较轻量级。

Window Function

在触发窗口计算时会将窗口中的状态传递给 emitWindowContents 方法。这里会调用我们定义的窗口函数中的 process 方法,将当前的 Key、Window、上下文 Context、窗口的内容作为参数传给它。在此之前和之后会分别调用 evictBefore 和evictAfter 方法把一些元素过滤掉。最终会调用 windowState 的 clear 方法,再把过滤之后的记录存到 windowState 中去。从而达到 evictor 过滤元素的效果。

Window Trigger

最后看下 Trigger 的实现原理。当我们有大量的 Key,同时每个 Key 又属于多个窗口时,我们如何有效的触发窗口的计算呢?

Flink 利用定时器来保证窗口的触发,通过优先级队列来存储定时器。队列头的定时器表示离当前时间最近的一个,如果当前定时器比队列头的定时器时间还要早,则取消掉队列头的定时器,把当前的时间注册进去。

当这次定时器触发之后,再从优先级队列中取下一个 Timer,去调用 trigger 处理的函数,再把下一个 Timer 的时间注册为定时器。这样就可以循环迭代下去。

总结

本文主要分享了 Flink 窗口的应用与实现。首先介绍了学习一项新技术的整体思路与学习路径,从应用入手慢慢深入它的实现。然后介绍了实时数仓的典型架构发展历程,之后从窗口的应用场景、抽象概念、编程结构详细说明了窗口的各个组成部分。并通过一些示例详细展示了各个概念之间配合使用可以满足什么样的使用场景。最后深入窗口的实现,从源码层面说明了窗口各模块的工作流程。

  • 20
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值