大家好,欢迎来到IT知识分享网。
到目前为止,我们已经介绍了流式应用是如何突破传统批处理的一些局限,还了解了开源流式处理框架的发展历程以及Flink流式应用的架构,接下来我们将正式进入流式数据处理的世界。
一 Dataflow 编程概述
在正式介绍流式处理的基础概念之前,我们先了解一下Dataflow编程,并建立起完整的术语体系。
1.1 Dataflow图
1.1.1 Dataflow逻辑图
顾名思义,Dataflow程序描述了数据如何在不同操作之间的流动。Dataflow程序通常被表示为有向图,图中的顶点称为算子,表示计算;而边表示数据依赖关系。算子是 Dataflow 程序的基本功能单元,它们从输入获取数据,并对其进行计算,然后产生数据并发往输出以供后续处理。没有输入端的算子称为source,没有输出端的算子称为Sink。
图 1-1 展示了一个从推文输入流中提取并统计主题标签的Dataflow程序。类似图 1-1 的Dataflow图被称作逻辑图,因为它们表达了高层视角下的计算逻辑。
1.1.2 Dataflow物理图
为了执行Dataflow程序, 需要将逻辑图转化为Dataflow的物理执行性图。图1-2 展示了图 1-1 中逻辑图所对应Dataflow物理图。在Dataflow逻辑图中,顶点代表算子;在Dataflow物理图中,顶点代表任务。“抽取主题标签”和“计数”算子都包含两个并行算子任务,每个任务负责计算一部分输入数据。
1.2 数据并行和任务并行
例如,可以将输入数据分组,让同一操作的多个任务并行执行在不同数据子集上,这种并行称为数据并行(data parallelism)。数据并行非常有用,因为它能够将计算负载分配到多个节点上从而允许处理大规模的数据。
还可以让不同算子的任务(基于相同或不同的数据)并行计算, 这种并行称为任务井行(task parallelism)。通过任务并行,可以更好地利用集群的计算资源。
1.3数据传递策略
数据传递策略定义了如何将数据分配给物理Dataflow图中的不同任务。图1-3 描述了一些常见的数据传递策略类型。
1.3.1 转发策略(forward strategy)
在发送端任务和接收端任务之间一对一地进行数据传输。
1.3.2广播策略(broadcast strategy)
把每条数据发往直接下游的所有算子。
1.3.3 基于键值策略(key-based strategy)
根据键值属性对数据进行分区,并保证键值相同的数据会交由同一任务处理。
1.3.4 随机策略(random strategy)
将数据均匀分配给所有任务,实现计算任务的负载均衡。
二 并行处理流式数据
数据流中的事件包括监控数据、传感器测量值、信用卡交易、气象站观测数据、信息流的交互数据,以及网络搜索等。 接下来,我们将介绍如何利用Dataflow的编程模式并行处理流式数据。
2.1 延迟和吞吐
对于离线数据处理,我们最关心的是总的执行时间。由于流式数据是无限的,所以在数据流的处理世界中时没有总的执行时间这个概念的。流式数据处理最看重的是,到来的数据要及时处理。因此,我们主要从延迟和吞吐这方面的来考察实时应用的性能。
2.1.1 延迟
所谓延迟就是指,从接收事件到输出结果的消耗时长。在流处理中,延迟是以时间片(例如毫秒)为单位测量的。 Flink 可以提供低至几毫秒的延迟,可以说在数据到达的那一刻就开始处理。
2.1.2 吞吐
吞吐是用来衡量系统处理能力(处理速率)的指标,它告诉我们系统每单位时间可以处理多少事件。通常情况下是延迟越低越好,吞吐越高越好。
如果系统吞吐已到极限,一味提高事件到达速率只会让延迟更糟糕。如果系统持续以力不能及的高速率接收数据,那么缓冲区可能会用尽,继而可能导致数据丢失。这种情形通常被称为背压(backpressure),我们在后面会介绍怎么处理这种情况。
2.1.3 延迟与吞吐小结
延迟和吞吐并非相互独立的指标。如果事件在数据处理管道中传输时间太久,我们将难以确保高吞吐;同样,如果系统性能不足,
事件很容易堆积缓冲,必须等待一段时间才能处理。
降低延迟实际上可以提高吞吐。显然,系统执行操作越快,相同时间内执行的操作数目就会越多。事实上,这就是在流处理管道中利用井行实现的效果。通过并行处理多条数据流,可以在处理更多事件的同时降低延迟。
2.2 数据流的操作类型
2.2.1 数据流的操作状态
流数据处理既可以是无状态(Stateless)的,也可以是有状态(Stateful)的。
2.2.1.1 无状态操作
无状态的操作不会维持内部状态,即处理事件时无需依赖己处理过的事件,也不保存历史数据。由于事件处理互不影响且与事件到来的时间无关,无状态的操作很容易并行化。此外,如果发生故障,无状态的算子可以很容易地重启,并从中断处继续工作。
2.2.1.2 有状态操作
有状态算子可能需要维护之前接收的事件信息。它们的状态会根据传入的事件更新,并用于未来事件的处理逻辑中。有状态
的流处理应用在并行化和容错方面会更具挑战性,因为它们需要对状态进行高效划分,并且在出错时需进行可靠的故障恢复。
2.2.2数据接入和数据输出
- 数据接入操作是从外部数据源获取原始数据。可以从 TCP 套接字 、文件、 Kafka主题或传感器数据接口中获取数据。
- 数据输出操作是将数据输出到外部系统,写入的目标可以是文件、数据库、消息队列或监控接口等。
2.2.2.1 转换操作(TRANSFORMATION)
转换操作会逐个读取事件,并对其进行某种转换后产生一条新的输出流。 如图 2-1 所示, 转换逻辑可以使用内置的转换算子,也可以使用用户自定义函数。我们会在后面的章节中介绍不同算子的语义。
2.2.2.2 滚动聚合(ROLLING AGGREGATIONS)
滚动聚合 (如求和、求最小值和求最大值 ) 会根据每个到来的事件持续更新结果。聚合操作都是有状态的,它们通过将新到来的事件合并到已有状态来生成更新后的聚合值。图2-2 展示了一个求最小值的滚动聚合,其算子会维护当前的最小值,并根据每个到来的事件去更新这个值。
2.2.2.3 窗口操作(WINDOW OPERATIONS)
窗口操作会持续创建一些称为“桶”的有限事件集合,并允许我们基于这些有限集进行计算。事件通常会根据其时间或其他数据属性分配到不同桶中。窗口的行为是由一系列策略定义的,这些窗口策略决定了什么时间创建桶,事件如何分配到桶中以及桶内数据在什么时间参与计算。我们可以基于时间(例如最近 5 秒钟接收的事件)、数量 (例如最新 100 个事件)或其他数据属性来定义窗口策略。
- 滚动窗口(Tumbling windows):将事件分配到长度固定且互不重叠的桶中 。在窗口边界通过后,所有事件会发送给计算函数进行处理。基于数量(Count-based)的滚动窗口定义了在触发计算前需要集齐多少条事件。 图2-6 是基于数量的滚动窗口将输入流按每 4 个元素一组分配到不同的桶中。 基于时间(Time-based)的滚动窗口定义了在桶中缓冲数据的时间间隔。 图2-3 是基于时间的滚动窗口将事件汇集到桶中,每 10 分钟触发一次计算。
- 滑动窗口(Sliding windows)将事件分配到大小固定且允许相互重叠的桶中,这意味着每个事件可能会同时属于多个桶。我们通过指定长度和滑动间隔来定义滑动窗口。滑动间隔决定每隔多久生成一个新的桶。在图2-5 中,基于数量的滑动窗口的长度为 4 个事件,滑动间隔为 3 个事件。
- 会话窗口(Session windows )在一些常见的真实场景中非常有用,这些场景既不适合用滚动窗口也不适合用滑动窗口。假设有一个应用要在线分析用户行为,在该应用中我们要把事件按照用户的同一活动或会话来源进行分组。例如,用户浏览一连串新闻文章的交互过程可以看作一个会话。由于会话长度并非预先定义好,而是和实际数据有关,所以无论是滚动还是滑动窗口都无法适用于该场景。而我们需要一个窗口操作,能将属于同一会话的事件分配到相同桶中。会话窗口根据会话间隔(SessionGap)将事件分为不同的会话,该间隔值定义了会话在关闭前的非活动时间长度。图2-6 展示了一个会话窗口。
如果你在收集来自不同传感器的测量值,那么可能会想在应用窗口计算前按照传感器 ID 对数据流进行划分。并行窗口中,每个数据分区所应用的窗口策略都相互独立。图2-7 展示了一个按事件颜色划分、基于数量 2 的并行滚动窗口。
窗口操作与流处理中两个核心概念密切相关:时间语义(time semantics)和状态管理(state management)。时间可能是流处理中最重要的一个方面。当然,如果你的系统无法在故障时保护状态,那一切都是空谈。考虑到流式应用可能需要整日、甚至长年累月 地运行,因此必须保证出错时其状态能进行可靠的恢复,并且即使系统发生故障系统也能提供准确的结果。
三时间语义
在使用Flink编写流式处理程序时,避免不了和时间(Time)、水位线(WaterMark)打交道,理解这些概念是我们开发分布式流式应用的基础。
3.1 流式处理场景下的时间语义
当处理一个持续到达且无穷无尽的流式数据时,时间无疑是应用中最核心的要素。比如,我们想每分钟计算一次流量,那么一分钟在流式应用中的含义到底是什么呢?
假设有某个应用程序会分析用户玩在线手游时产生的事件。小灰是开心消消乐忠实玩家,小灰同学在上班坐地铁的路上都会玩这个游戏,但是在某个路段没有信号,手机突然断网了,小灰继续玩游戏,此时游戏产生的数据会缓存在手机里面。在手机信号好时,缓存在手机里的数据会发送给服务端。思考再来考虑这个问题,大数据团队分析在分析每分钟在线玩游戏场景时,一分钟的含义是什么呢?要把游戏掉线的时间算在内吗?图3-1描述了这个问题。
如果我们仅仅考虑现实中每分钟收到多少数据,那结果必然会受到网络传输速度的影响,而事实上每分钟收到的事件数是由数据本身的时间来定义的。在这个开心消消乐的例子中,我们接触到了三个时间概念:事件生成时间(Event Time)、事件处理时间(Processing Time)和事件接入时间(Ingestion Time),下面我们将分别介绍这3种时间的概念。
3.1.1 事件生成时间(Event Time)
事件生成时间是数据流中事件实际发生的时间,它以附加在数据流中事件生成的时间戳为依据。这些时间戳通常在事件数据进入流处理管道之前就已经有了。如图3-2所示,即便事件有延迟,事件生成时间窗口也能准确地将事件分配到窗口中,从而反映出真实发生的情况。也就是说,无论数据流的处理速度如何、事件到达Flink算子的顺序是否会乱,最终生成的结果都是一样的。
3.1.2 事件处理时间(Processing Time)
事件处理时间是当前流处理算子所在机器上的本地时钟时间。基于处理时间的窗口会包含那些恰好在一段时间内到达窗口算子的事件,这里的时间段是按照机器时间测量的。如图3-3所示,在小灰的例子中,处理时间窗口在他手机离线后会继续计时,因此不会把她离线那段时间的活动考虑在内。也就是说,因为数据到达窗口的速率不同,所以Flink窗口算子中使用处理时间会导致不确定的结果。
3.1.3 事件接入时间(Ingestion Time)
事件在进入Flink的时间,即将每一个事件在数据源算子的处理时间作为事件时间的时间戳,并自动生成水位线(WaterMark会在后面介绍)。 Ingestion Time是 Event Time 和 Processing Time 的混合体,它表示事件进入流处理引擎的时间。和事件生成时间相比,事件接入时间(Ingestion Time)的价值不大,因为它的性能和事件时间类似,但却无法提供确定的结果。
3.1.4 三种时间对比
我们总结一下三种时间的特点,如图3-4所示,
- Event Time:事件创建的时间
- Processing Time:执行操作算子的本地系统时间,与机器相关
- Ingestion Time:数据进入Flink的时间
不知道你会不会有这样一个疑问:既然事件时间已经能够解决所有的问题了,那为何还要用处理时间呢?其实处理时间有其特定的使用场景,处理时间由于不用考虑事件的延迟与乱序,所以其处理数据的延迟较低。因此如果一些应用比较重视处理速度而非准确性,那么就可以使用处理时间。
3.2 水位线(WaterMark)
除了记录本身的时间戳, Flink基于事件时间的应用还必须提供水位线(watermark)。水位线用于在事件时间应用中推断每个任务当前的事件时间。基于时间的算子会使用这个时间来触发计算,并推动进度前进。例如:基于时间窗口的任务会在其事件时间,超过窗口结束边界时,进行最终的窗口计算并发出结果。
水位线就是一种Long类型的时间戳,如图3-5所示,矩形表示一条记录,三角形表示这条记录的事件时间戳(真实发生时间),圆圈表示水位线。可以看到下面的数据是乱序的,比如当算子接收到为2的水位线时,就可以认为时间戳小于等于2的数据都已经到来了,此时可以触发计算。但是事件时间3、事件时间5所对应的记录都在水位线2以后到达。同理,接收到为5的水位线时,就可以认为时间戳小于或等于5的数据都已经到来了,此时可以触发计算。
3.2.1 水位线的两个基本特征
- 必须单调递增,这是为了确保任务中的事件时间时钟正确前进,不会倒退。
- 和记录的时间戳存在联系,一个时间戳为T的水位线表示,接下来所有记录的时间戳一定都大于T。
3.2.2 水位线的作用
水位线的第二个属性,可以用来处理数据流中时间戳乱序的记录,例如图2-15中的时间戳3、5所对应的记录,就是时间乱序的记录。当任务收到一个时间戳小于或等于前一个水位线的记录时,我们把这类数据称作迟到记录。
四 状态和一致性模型
状态在数据处理中无处不在,任何一个稍复杂的计算都要用到它。为了生成结果,函数会在一段时间或基于一定个数的事件来累积状态(例如求和)。以某个滚动聚合算子为例,假设它会输出至今为止所见到的全部事件之和。该算子以内部状态形式存储当前的累加值,并会在每次收到新事件时对其进行更新。
由于流式算子处理的都是无穷无尽的数据,所以必须小心避免内部状态无限增长。为了限制状态大小,算子通常都会只保留到目前为止所见事件的摘要或概览。这种摘要可能是一个数量值,一个累加值,一个对至今为止全部事件的抽样, 一个窗口缓冲或是一个保留了应用运行过程中某些有价值信息的自定义数据结构。
不难想象,支持有状态算子将面临很多实现上的挑战:
4.1 任务故障
在流式作业中,算子的状态十分重要,因此需要在故障时予以保护。如果状态在故障期间丢失,那恢复后的结果就会不正确。流式作业通常会运行较长时间 ,因此状态可能是经过数天甚至数月才收集得到。通过重新处理所有输入来重建故障期间丢失的状态,不仅代价高,而且还很耗时。
什么是任务故障?
对于输入流中的每个事件,任务都需要执行以下步骤:
- 接收事件并将它们存在本地缓冲区;
- 选择性地更新内部状态;
- 产生输出记录。
上述任何一个步骤都可能发生故障,而系统必须在故障情况下明确定义其行为。 如果故障发生在第一步,事件是否会丢失?如果在更新内部状态后发生故障,系统恢复后是否会重复更新?在上述情况下,结果是否确定?
在批处理场景下,上面提到的都算不上问题。由于批处理任务可以轻易“从头再来”所以不会有任何事件丢失,状态也可以完全从最初开始构建。然而在流式场景中,处理故障就没那么容易了。流处理系统通过不同的结果保障来定义故障时的行为。接下来,我们一起来看看流式处理引擎是怎么保证结果正确的。
4.2结果保障
我们说的“结果保障”,指的是流处理引擎内部状态的一致性。也就是说,我们关注故障恢复后应用代码能够看到的状态值。请注意,保证应用状态的一致性和保证输出的一致性并不是一回事儿。一旦数据从sink端写出,除非目标系统支持事务,否则结果的正确性将难以保证。
4.2.1 最多一次(AT-MOST-ONCE)
任务发生故障时最简单的措施就是既不恢复丢失的状态,也不重放丢失的事件。最多一次是一种最简单的情况,它保证每个事件最多被处理一次。换句话说,事件可以随意丢弃,没有任何机制来保证结果的正确性。
4.2.2 至少一次(AT-LEAST-ONCE)
对大多数现实应用而言,用户期望是不丢事件,这类保障称为至少一次。它意味着所有事件最终都会处理,虽然有些可能会处理多次。但如果要计算某个事件在输入流中出现的次数,至少一次保障可能就会返回错误的结果。
4.2.3精确一次(EXACTLY-ONCE)
精确一次是最严格,也是最难实现的一类保障,它表示不但没有事件丢失,而且每个事件对于内部状态的更新都只有一次。本质上,精确一次保障意味着应用总会提供正确的结果,就如同故障从未发生过一样。Flink采用了轻量级检查点机制来实现精确一次结果保障。
4.2.4端到端的精确一次(END-TO-END EXACTLY-ONCE)
到目前为止你看到的保障类型都仅限于流处理引擎自身的应用状态。在实际流处理应用中,除了流处理引擎也至少还要有一个数据来源组件和一个数据终点组件。端到端的保障指的是在整个数据处理管道上结果都是正确的。有时候你可以通过弱保障来实现强语义。一个常见情况就是某个任务执行一些诸如求最大值或最小值的幕等操作。该情况下,你可以用至少一次保障来实现精确一次的语义。
五 总结
这篇文章主要介绍了数据流处理相关的基础知识。我们介绍了Dataflow编程模型以及如何将一个流式应用表示为分布式Dataflow 图。学习了并行处理无限流的需求,了解了延迟和吞吐对于流式应用的重要性。学习了流式应用中时间的含义,并比较了事件时间和处理时间的概念。最后我们介绍了状态对流式应用的重要性,以及如何应对故障并确保结果正确。
到目前为止,我们考虑的流处理相关概念都还是独立于 Apache Flink 的。后面我将继续输出Flink的系列问题。
希望这篇文章能起到抛砖引玉的作用,如有描述不当或者错误之处,还请大家在评论区留言指正。
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/6286.html