0%

大数据计算引擎之Flink流处理基础

数据流程序描述了数据如何在算子之间流动。数据流程序通常表示为有向图,其中节点称为算子,用来表示计算。算子是数据流程序的基本功能单元。

流处理基础

流编程简介

数据流图(DataFlow Graph)

DataFlow Graph
如上图所示,数据流图被称为逻辑流图。为了执行一个数据流程序,Flink会将逻辑流图转换为物理数据流图。

数据并行和任务并行

我们可以以不同的方式利用数据流图中的并行性:

  • 我们可以对输入数据进行分区,并在数据的子集上并行执行具有相同算子的任务并行。即数据并行,数据并行是有限度的,因为他允许处理大量数据,并将计算分散到不通透计算节点上。
  • 我们可以将不同算子在相同或不同的数据上并行执行。即任务并行,可以更好地利用资源。

数据交换策略

数据交换策略定义了在物理执行流图中如何给数据分配给任务。数据交换策略可以由执行引擎自动选择,具体取决于算子的语义或我们明确指定的语义。
数据交换侧略

  • 前向策略将数据从一个任务发送到接收任务。如果两个任务都位于同一台物理计算机上(这通常由任务调度器确保),这种交换策略可以避免网络通信。
  • 广播策略将所有数据发送到算子的所有的并行任务上面去。因为这种策略会复制数据和涉及网络通信,所以代价相当昂贵。
  • 基于键控的策略通过Key值(键)对数据进行分区保证具有相同Key的数据将由同一任务处理。
  • 随机策略统一将数据分配到算子的任务中去,以便均匀地将负载分配到不同的计算任务。

并行处理流数据

延迟和吞吐量

流处理程序是连续运行的,输入可能是无界的,所以数据流处理中没有总执行时间的概念。相反,流处理程序必须尽可能快的提供输入数据的计算结果。因此,引入延迟和吞吐量来表征流处理的性能要求。

延迟

延迟表示处理事件所需要的时间,是接收事件和看到输出中处理此事件的效果之间的时间间隔。

吞吐量

吞吐量是衡量系统处理能力的指标,也就是处理速率。
吞吐量以每个时间为单位系统所能处理的事件数量或操作数量来衡量。阿紫流式系统中,我们希望确保我们的系统可以处理最大的预期事件到达的速率,即我们主要关心的在于确定的峰值吞吐量是多少,当系统处于最大负载性能怎样。

为了更好地理解峰值吞吐量的概念,让我们考虑一个流处理 程序没有收到任何输入的数据,因此没有消耗任何系统资源。当第一个事件进来时,它会尽可能以最小延迟立即处理。例如,如果你是第一个出现在咖啡店的顾客,在早上开门后,你将立即获得服务。理想情况下,您希望此延迟保持不变 ,并且独立于传入事件的速率。但是,一旦我们达到使系统资源被完全使用的事件传入速率,我们将不得不开始缓冲事件。在咖啡店里 ,午餐后会看到这种情况发生。许多人出现在同一时间,必须排队等候。在此刻,咖啡店系统已达到其峰值吞吐量,进一步增加 事件传入的速率只会导致更糟糕的延迟。如果系统继续以可以处理的速率接收数据,缓冲区可能变为不可用,数据可能会丢失。这种情况是众所周知的 作为背压,有不同的策略来处理它。

延迟和吞吐量的对比

此时,应该清楚延迟和吞吐量不是独立指标。如果事件需要在处理流水线中待上很长时间,我们不能轻易确保高吞吐量。同样,如果系统容量很小,事件将被缓冲,而且必须等待才能得到处理。

例子来阐明一下延迟和吞吐量如何相互影响。首先,应该清楚存在没有负载时的最佳延迟。也就是说,如果你是咖啡店的唯一客户,会很快得到咖啡。然而,在繁忙时期,客户将不得不排队等待,并且会有延迟增加。另一个影响延迟和吞吐量的因素是处理事件所花费的时间或为每个客户提供服务所花费的时间。想象一下,期间圣诞节假期,咖啡师不得不为每杯咖啡画圣诞老人。这意味着准备一杯咖啡需要的时间会增加,导致每个人花费 更多的时间在等待咖啡师画圣诞老人,从而降低整体吞吐量。

那么,你可以同时获得低延迟和高吞吐量吗?或者这是一个无望的努力?我们可以降低得到咖啡的延迟 ,方法是:聘请一位更熟练的咖啡师来准备咖啡。在高负载时,这种变化也会增加吞吐量,因为会在相同的时间内为更多的客户提供服务。 实现相同结果的另一种方法是雇用第二个咖啡师来利用并行性。这里的主要想法是降低延迟来增加吞吐量。当然,如果系统可以更快的执行操作,它可以在相同的时间内执行更多操作。 事实上,在流中利用并行性时也会发生这种情况。通过并行处理多个流,在同时处理更多事件的同时降低延迟。

数据流上的操作

数据摄入和数据吞吐量

数据摄取和数据出口操作允许流处理程序与外部系统通信。数据摄取是操作从外部源获取原始数据并将其转换为其他格式(ETL)。实现数据提取逻辑的运算符被称为数据源。

转换算子


转换算子是单遍处理算子,碰到一个事件处理一个事件。这些操作在使用后会消费一个事件,然后对事件数据做一些转换,产生一个新的输出流。转换逻辑可以集成在操作符中或有UDF函数提供。自然开发人员可以自定义计算逻辑。

滚动聚合

滚动聚合是一种聚合方式,例如:sum , minimun, maximum ,为每个输入事件不断更新。聚合操作是有状态的,并将当前状态与传入事件一起计算以产生更新的聚合值。

窗口操作符

转换和滚动聚合一次处理一个事件产生输出事件并可能更新状态。但是,有些操作必须收集并缓冲数据以计算其结果。例如考虑不同流之间的链接或整体聚合这样的操作。
窗口还可以在语义上实现关于流的比较复杂的查询。我们看到额滚动聚合的方式,以聚合值编码·整个流的历史数据来为每个事件提供低延迟的结果,但如果我们只是对最近的数据感兴趣会怎样?

窗口操作不断从无限事件流中创建有限的事件集,好让我们执行有限集的计算。通常会基于数据属性或基于时间的窗口来分配事件。 要正确定义窗口运算符语义,我们需要确定如何给窗口分配事件以及对窗口中的元素进行求值的频率是什么样的。 窗口的行为由一组策略定义。窗口策略决定何时创建新的窗口以及要分配的事件属于哪个窗口,以及何时对窗口中的元素进行求值。 而窗口的求值基于触发条件。一旦触发条件得到满足,窗口的内容将会被发送到求值函数,求值函数会将计算逻辑应用于窗口中的元素。 求值函数可以是sum或minimal或自定义的聚合函数。 求值策略可以根据时间或者数据属性计算(例如,在过去五秒内收到的事件或者最近的一百个事件等等)。 接下来,我们描述常见窗口类型的语义。

  • 滚动窗口是将事件分配到固定大小的不重叠的窗口中。当通过窗口的结尾是,全部时间被传送到求值函数进行处理。基于计数的滚动窗口定义了在除法求值之前需要收集多少事件。如图所示:一个基于计数的翻滚窗口,每四个元素一个窗口,基于时间的滚动窗口定义一个时间间隔,包含在此时间间隔内的事件.
    滚动窗口
    下图显示了基于时间的滚动时间的滚动窗口,将事件手机到窗口中每10分钟触发一次计算。
    滚动时间窗
  • 滑动窗口事件将分配到固定大小的重叠的窗口中去。因此,事件可能属于多个桶。我们通过提供窗口的长度和滑动距离来定义滑动窗口。滑动距离定义了创建新窗口的间隔,基于滑动计数的窗口。如下图所示:长度为4个事件,3个为滑动距离
  • 会话窗口再常见的真实场景中很有用。考虑一个分析在线用户行为的应用程序,在程序中,我们想把源自同一时期的用户活动或会话事件分组在一起。会话有一系列相邻时间发生的事件组成,接下来有一段时间没有活动。例如:用户在APP上浏览一系列的新闻,然后关闭APP,那么浏览新闻这段时间的浏览事件就是一个会话。
  • 会话窗口事先没有定义窗口的长度,而是取决于数据的实际情况。我们将同一会话中的事件分配到同一个窗口中去。而不同会话可能窗口长度不一样。会话窗口会定义一个间隙来区分不同的会话。间隙:用户一段时间内部活动,就认为用户的会话结束了。下图为一个会话窗口:
  • 到目前为止,所有窗口类型都是在整条流上去做窗口操作。但实际上可能要将一条流分流成多个逻辑流并定义并行窗口。如图所示:一个基于计数的长度为2的并行滚动窗口,根据时间颜色分流。

    时间语义

    在流处理中,窗口操作与两个主要概念密切相关:时间语义和状态管理。时间也许是流处理最重要的方面。即使低延迟是流处理的一个有吸引力的特性,它的真正价值不仅仅是快速分析。真实世界的系统,网络和通信渠道远非完美,流数据经常被推迟或无序(乱序)到达。理解如何在这种条件下提供准确和确定的结果是至关重要的。 更重要的是,流处理程序可以按原样处理事件制作的也应该能够处理相同的历史事件方式,从而实现离线分析甚至时间旅行分析。

    处理时间

    处理时间是处理流的应用程序的机器的本地时钟的时间(墙上时钟)。处理时间的窗口包含了一个时间段内来到机器的所有事件。

    事件时间

    事件事件是流中的事件实际发生的时间。事件时间基于流中的事件所包含的时间戳。通常情况下,在事件进入流处理程序前,事件数据就已经包含了时间戳。
    事件时间使得计算结果的过程不需要依赖处理数据的速度。基于事件时间的操作是可以预测的,而计算结果也是确定的。无论流处理程序处理流数据的速度快或是慢,无论事件到达流处理程序的速度快或是慢,事件时间窗口的计算结果都是一样的。

    水位线(Watermarks)

    在我们对事件时间窗口的讨论中,我们忽略了一个很重要的方面:我们应该怎样去决定何时触发事件时间窗口的计算?也就是说,在我们可以确定一个时间点之前的所有事件都已经到达之前,我们需要等待多久?我们如何知道事件是迟到的?在分布式系统无法准确预测行为的现实条件下,以及外部组件所引发的事件的延迟,以上问题并没有准确的答案。在本小节中,我们将会看到如何使用水位线来设置事件时间窗口的行为。

水位线是全局进度的度量标准。系统可以确信在一个时间点之后,不会有早于这个时间点发生的事件到来了。本质上,水位线提供了一个逻辑时钟,这个逻辑时钟告诉系统当前的事件时间。当一个运算符接收到含有时间T的水位线时,这个运算符会认为早于时间T的发生的事件已经全部都到达了。对于事件时间窗口和乱序事件的处理,水位线非常重要。运算符一旦接收到水位线,运算符会认为一段时间内发生的所有事件都已经观察到,可以触发针对这段时间内所有事件的计算了。

水位线提供了一种结果可信度和延时之间的妥协。激进的水位线设置可以保证低延迟,但结果的准确性不够。在这种情况下,迟到的事件有可能晚于水位线到达,我们需要编写一些代码来处理迟到事件。另一方面,如果水位线设置的过于宽松,计算的结果准确性会很高,但可能会增加流处理程序不必要的延时。

状态和持久化模型

在数据处理中,状态时普遍存在的。任何稍微复杂一点的计算,都会涉及到状态。为了产生计算结果,一个函数在一段时间内的一定数量的事件上来累加状态,例如聚合计算或者模式匹配。有状态的运算符试用输入的事件以及内部保存的状态来计算得到输出。

当我们考虑一下使用批处理系统来分析一个无界数据集时,会发现状态的重要性显而易见。在现代流处理器兴起之前,处理无界数据集的一个通常做法是将输入的事件攒成微批,然后交由批处理器来处理。当一个任务结束时,计算结果将被持久化,而所有的运算符状态就丢失了。一旦一个任务在计算下一个微批次的数据时,这个任务是无法访问上一个任务的状态的(都丢掉了)。这个问题通常使用将状态代理到外部系统(例如数据库)的方法来解决。相反,在一个连续不间断运行的流处理任务中,事件的状态是一直存在的,我们可以将状态暴露出来作为编程模型中的一等公民。当然,我们的确可以使用外部系统来管理流的状态,即使这个解决方案会带来额外的延迟。

由于流处理运算符默认处理的是无界数据流。所以我们必须要注意不要让内部状态无限的增长。为了限制状态的大小,运算符通常情况下会保存一些之前所观察到的事件流的总结或者概要。这个总结可能是一个计数值,一个累加和,或者事件流的采样,窗口的缓存操作,或者是一个自定义的数据结构,这个数据结构用来保存数据流中感兴趣的一些特性。

状态管理
系统需要高效的管理状态,并保证针对状态的并发更新,不会产生竞争条件(race condition)
状态分区
并行会带来复杂性,因为计算结果同事取决于已经保存的状态和输入的事件流。幸运的是,绝大数情况下,我们可以使用key来对状态进行分区,然后独立管理每一个分区。
状态恢复
需要保证有状态的运算符可以恢复,即便出现任务失败,计算也是正确的。

任务失败

什么是任务失败
对于流中的每一个事件,一个处理任务分为以下步骤:

  • 接收事件,并将事件存储在本地缓存中;
  • 可能会更新内部状态;
  • 产生输出记录
    在批处理中,我们可以方便的重新计算,因此所有的问题都不是问题。但是在流的世界中,处理失败不是一件小事。因此需要保证流系统在失败的情况下需要保证结果的准确性。

结果的保证
要注意保证应用程序状态的一致性并不是保证应用程序的输出结果的一致性。一旦输出结果被持久化,结果的准确性就很难保证了。除非持久化系统支持事务。
AT-MOST-ONCE
当任务发生时,最简单的做法就是什么都不干,既不恢复丢失的状态,也不重播丢失的事件。AT-MOST-ONCE语义就是最多处理一次事件。
AT-LEAST-ONCE
我们希望不丢失事件,这种类型的保证便是我们的 AT-LEAST-ONCE语义是所有的事件都得到了处理,而且一些事件还可能被处理多次。如果结果的正确性仅仅依赖于数据的完整性,那么重复是可以接受的。
为了保证在AT-LEAST-ONCE语义下,计算结果也能正确。我们还要有另一套系统来从数据源或者缓存中重新播放数据。

  • 一种方法是持久化的事件日志文件系统会将所有的事件写入到持久化存储中,多以任务失败,这些数据是可以重播的。
  • 还有一种方法可以获得同等的效果,就是使用结果承认机制。这种方法将会把每一条数据都保存在缓存中,直到数据的处理等到所有的任务的承认。一旦得到所有任务的承认,数据将被丢弃。

EXACTLY-ONCE
恰好处理是最严格的的保证,也是最难实现的。恰好处理一次语义不仅仅意味着没有事件丢失,还意味着针对每一个数据,内部状态仅仅更新一次。
提供恰好处理一次语义的保证必须有至少处理一次语义的保证才行,同时还需要数据重放机制。另外,流处理器还需要保证内部状态的一致性。也就是说,在故障恢复以后,流处理器应该知道一个事件有没有在状态中更新。事务更新是达到这个目标的一种方法,但可能引入很大的性能问题。Flink使用了一种轻量级快照机制来保证恰好处理一次语义
端到端恰好处理一次
目前我们看到的一致性保证都是由流处理器实现的,也就是说都是在Flink流处理器内部保证的。而在真实世界中,流处理应用除了流处理器以外还包含了数据源(例如Kafka)和持久化系统。端到端的一致性保证意味着结果的正确性贯穿了整个流处理应用的始终。每一个组件都保证了它自己的一致性。而整个端到端的一致性级别取决于所有组件中一致性最弱的组件。要注意的是,我们可以通过弱一致性来实现更强的一致性语义。例如,当任务的操作具有幂等性时,比如流的最大值或者最小值的计算。在这种场景下,我们可以通过最少处理一次这样的一致性来实现恰好处理一次这样的最高级别的一致性。

这是打赏的地方...
---------Thanks for your attention---------