1.流技术演变
1.1 Apache Storm
Apache Storm是流处理的先锋。Apache Storm提供了低延迟的流处理,但是它为实时性付出了一些代价:很难实现高吞吐,并且其正确性并没有达到通常所需要的水平,换句话说,它并不能保证exactly- once(恰好一次);即使它能够保证的正确性级别,其开销也相当大。
1. 2 Spark Streaming
在低延迟和高吞吐的流处理系统中保持容错性是非常困难的,但是为了得到有保证的准确状态,人们相处了一种替代方法:将连续事件中的流数据分割成微小的批量作业。如果分隔的足够小(即所谓的微批处理作业),计算就几乎可以实现真正的流处理。因为存在延迟,所以不可能做到完全实时。但是每个简单的应用程序都可以实现几秒甚至几亚秒的延迟,这就是Spark批处理引擎上运行的SparkStreaming
然而通过间歇性批处理来模拟流处理,会导致运维和开发相互交错。完成间歇性批处理工作和数据达到时间紧密耦合,任何延迟都可能导致不一致的结果(或者是错误)。这种技术的潜在问题是:时间由系统中生成小批量作业的那一部分全权控制。Spark Streaming等一些流处理框架在一定程度上弱化了这一弊端,但是还不能完全避免。另外使用这种方法的计算有着糟糕的用户体验,尤其是那些对延迟比较敏感的作业,而且人们需要在写业务代码时花费大量的精力来提升性能。
1.3 Flink
Apache Flink是为分布式,高性能,随时可用以及准确的流处理应用程序打造的开开源处理流处理框架。Flink不仅可以提供同时支持高吞吐和exactly-once语义的实时计算,还能提供批量数据处理.Flink将批处理看做是一个特殊的流处理。
2.传统架构和流处理架构
传统的架构
典型的传统架构采用一个中心化的数据库系统,该系统用于存储事务性数据。换句话说,数据库(SQL或者NOSQL)拥有“新鲜”的数据(或者说“准确的数据”),当前这些数据反映了业务状态,如系统当前有多少已登录用户,网站当前有多少活跃用户,以及当前每个用户的账户余额是多少。需要新鲜数据的应用有依靠数据库实现。分布式文件则用来存储不需要经常更新的数据。他们往往也是大规模批量计算所依赖的数据存储方式。
这种架构已经成功的应用了很多年,但随着大型分布式系统中的计算复杂度不断上升,这种架构已经不堪重负,许多公司经常遇到下面问题:
- 在许多项目中,从数据到达到数据分析所需要的工作流程太复杂,太缓慢。
- 传统的数据架构太单一:数据库是唯一正确的数据源,每一个应用程序都需要访问数据库来获得所需的数据。
- 采用这种架构的系统通常拥有非常复杂的异常问题处理方法。当出现异常问题时,很难还能保证系统还能很好的运行。
传统架构的另一个问题是,需要通过在大型分布式系统中不断的更新来维持一致性的全局状态。随着系统的规模扩大,维持实际数据与状态数据的一致性变得越来越困难。
流处理的架构
作为一种新选择,流处理架构解决了企业在大规模系统中遇到的诸多问题。以流为基础的架构设计让数据记录持续的从数据源流向应用程序,并在各个程序间持续流动。没有一个数据库来集中存储全局状态数据,取而代之的是共享且永不停止的流数据。它是唯一正确的数据源,记录了业务数据的历史。在流处理架构中,每个应用程序都有自己的数据。这些数据采用本地数据库或者分布式文件存储。
3.Flink架构图
Flink分别提供面向流处理接口(DataStream API)和面向批处理接口(DataSet API)。因此Flink可以完成流处理,也可以完成批处理。Flink支持的拓展库涉及机器学习(FlinkML),复杂的事件处理(CEP),以及图计算(Celly),还有分别针对批处理和流处理的Table API。
4.流处理系统组成
常见的架构是消息处理层和流处理层组成一个流处理系统
消息传输层:从各种数据源(生产者)采集连续事件产生的数据,并传输给订阅了这些数据的应用程序和服务(消费者)。
流处理层有3个用途:①:持续的将数据在应用程序和系统间移动。②:聚合并处理事件。③在本地维护应用程序的状态。
上图的流处理架构中有两个主要的组成部分:消息传输层和Flink提供的流处理层。
消息传出层负责传输由连续事件产生的消息,能够提供消息传输包括kafka和MapR Streams。MapR Streams是MR融合数据平台的一个主要组成部分,它兼容kakfa API。
5.Flink流处理的说明
5.1 Flink流处理和微批处理对比
举例:通过流数据点击追踪网站的三个点击者A,B,C的活动,对于每个访问者,活动是不连续的。在访问时间段内,事件数据被收集起来;当访问者转身去喝咖啡或者喝茶时,数据就产生了间隙。
当使用微批处理方法或者固定窗口来处理时,由微批处理方法得到的窗口是人为设置的,因此很难与会话窗口吻合。
使用Flink流处理API,可以更灵活的定义计算窗口,开发人员可以设置非活动阈值,若超过这个阈值,就可以判断活动结束。因此Flink流处理能力可以使计算窗口和会话窗口吻合。
5.2 对时间的处理
用流处理器编程和批处理器编程最关键的区别在于对时间的处理。
举一个例子:计数。事件流数据(如微博内容,点击数据和交易数据)不断产生,我们需要用key将事件分组,并且每隔一段时间(比如一小时),就针对每个key对应的事件进行计数。
采用传统的批处理架构:
用定期运行批处理作业来实现应用程序的持续性。数据被持续分隔为文件(如一小时为单位);然后,批处理作业将文件作为输入,以此达到持续处理数据的效果。
这种架构完全可行,但是存在以下问题。
①:太多独立部分,为了计算数据中的事件数,这种架构动用了太多系统。每个系统都有学习成本和管理成本,还可能存在bug。
②:对时间处理的方法不明确。假设需要改为每30分钟计数一次。这个变动设计工作流调度逻辑(而不是应用程序代码逻辑),从而使DevOps问题与业务需求混淆。
③:预警。假设除了每小时计数一次,还需要尽可能早的收到计数预警(比如,计数在超过10时预警)。为了做到这一点,可以定期运行批作业之外,引入Storm来采集消息流,Storm提供近似实时计数,批处理作业提供准确计数。但是这样一来,就向架构增加了一个系统,以及与之相关的新编程模型。上述这种架构就叫Lamda架构。
④:乱序事件流。在现实世界里,大多数事件流都是乱序的,即事件的实际发生顺序(事件数据生成时被附上时间戳,如智能手机记录下用户的登录时间)和数据库记录的顺序不一致。这意味着本属于前一批的数据被错误的归入当前的一批,批处理架构很难解决这个问题,大部分人选择忽略它。
⑤:批处理作业界限不清晰。在该架构中,“每小时”的定义含糊不清,分隔的时间实际取决于不通系统间的交互。充其量只能做到大约一小时分隔一次,而在分割时间点前后的事件有可能被归入前一批,也有可能被归入当前一批。将数据流以小时为单位进行分割,实际上是最简单的方法。假设需要根据产生数据的时间段(如从用户登录到退出)生出聚合结果,而不是简单的以小时为单位进行数据分割,这种架构则无法满足需求。
采用流处理架构计数。
通过流处理架构实现应用程序的持续性。水平圆柱体表示消息系统,消息系统负责为所有的流处理系统提供流数据,产生的结构既是实时的,也是准确的。
事件流由消息系统提供,并且只被单一的Flink作业处理,从而以小时为单位进行计数和预警。Flink作业的减慢或者吞吐量激增只会导致事件在消息传输系统中堆积。以时间为单位把时间流分隔为一批任务(称作窗口),这种逻辑完全嵌入在Flink应用逻辑中。预警由同一个程序产生,乱序事件有Flink自行处理。要从以固定时间分组改为根据产生数据的时间进行分组,只需要在Flink程序中修改对窗口的定义即可。此外,如果应用程序代码有过改动,只需要重播Kafka主题,即可重播应用程序。采用流处理架构,可以大幅减少需要学习,管理,编写代码的系统。Flink应用程序用来技术的代码非常简单,如下所示:
|
事件时间:即时间的实际发生时间。更准确的说,每一个事件都有一个与它相关的时间戳,并且时间戳是数据记录的一部分(比如手机或者服务器的记录)。时间时间其实就是时间戳。
处理时间:即时间被处理的时间。处理时间其实就是处理时间机器所测量的时间。
摄入时间:指数据进入流计算框架的时间。缺乏真实事件时间的数据会被流处理器附上时间戳,即流处理器第一次看到它的时间。
在现实世界里,许多因素(如连接暂时中断,不同原因导致网络延迟,分布式系统中时钟不同步,数据速率陡增,物理原因,或者运气差)使得事件时间和处理时间存在时间偏差(事件时间偏差),事件事件和处理时间顺序通常不一致,这意味着事件乱序到达了流处理器。
有些应用程序(如一些预警应用程序)需要尽快可能得到结果,即使有小的误差也没关系, 它们不必等待迟到的事件,因此适合才去处理时间语义。
有应用程序(如欺诈检测系统或者账单系统)则对准确性有要求:只有在时间窗口内发生的事件才能被计算进来。对于这些应用程序,事件事件语义才是正确的选择。
也有两者都采用的情况,例如又要准确的计数,又要提供异常预警。
5.3 窗口
5.3.1 时间窗口
时间窗口是最简单也是最有用的一种窗口,它支持滚动和滑动。
举例:对传感器输出的数值求和。
一分钟滚动收集最近一分钟的数值,并在一分钟结束时输出总和。
一分钟滑动的窗口,计算最近一分钟的数值总和,但没半分钟滑动一次并输出结果。
第一个滑动窗口对9,6,8,4求和,得到27。半分钟后,窗口滑动对8,4,7,3求和,得到22,以此类推。
在Flink中一分钟的滚动窗口定义如下
|
每30秒滑动一次的滑动窗口定义如下
|
5.3.2 计数窗口
Flink支持另一种常见的窗口叫做计数窗口。采用计数窗口采用的依据不再是时间戳,而是元素的数量。
计数窗口也有滚动和滑动两种,和时间窗口类似,不过滚动的大小和滑动的大小变成了元素的数量。
滚动窗口定义如下:
|
滑动窗口定义如下:
|
虽然计数窗口有用,但是其定义不如时间窗口严谨,因此要谨慎使用。时间不会停止,而且时间窗口总会“关闭”。但就计数窗口而言,假设其定义的元素数量为100个,而某个key对应的元素永远达不到100个,那么窗口就永远不会关闭,被该窗口占用的内存就浪费了。一种解决办法用时间窗口来触发超时。
5.3.3 会话窗口
Flink支持的另一种很有用的窗口就是会话窗口。
会话窗口:指活动阶段,其前后都是非活动阶段。例如,用户与网站进行一系列交互(活动阶段)之后,关闭浏览器或者不在交互(非活动阶段)。会话需要有自己的处理机制,因为他们通常没有固定的持续时间(有的30秒就结束,有些长达一小时)。 或者没有固定的交互次数(有些可能3次点击后购买,另一些可能是40次点击却没有购买)。
在Flink中, 会话窗口是由超时时间设定的,即希望等待多久才认为会话结束。例如:用户处理非活动阶段长达5分钟,则认为会话结束,定义如下:
|
5.3.4 触发器
除窗口之外,Flink还提供触发机制。触发器控制生成结果的时间,即何时聚合窗口内容并将结果返回给用户。每一个默认窗口都有一个触发器。例如采用事件事件的时间窗口将在收到水印时被触发。对于用户来说,除了收到水印时,生成完整,准确的结果之外,也可以实现自定义的触发器(例如每秒提供一次近似结果)。
5.3.5 水印
支持事件时间对于流处理架构而言至关重要,因为事件事件能够保证正确的结果,并使流处理架构拥有重新处理数据的能力。当计算基于事件时间时,如何判断所有事件是否都到达,以及何时计算窗口和输出结果呢?换言之,如何追踪事件时间,并知晓输入数据已经流入到某个事件时间呢?为了追踪事件时间,需要依靠由数据驱动的时钟,而不是系统时钟。
以下图一分钟滚动窗口为例。假设第一个窗口是从10:00:00开始,需要计算10:00:00到10:01:00的数值总和。当时间就是记录一部分时,我们如何知道10:01:00已到呢?换句话说,我们如何知道10:00:59的元素还没到呢
Flink通过水印来推进事件时间。水印是嵌在流中的常规记录,计算程序通过水印获知某个时间点已到。对于上述一分钟的滚动窗口,假设水印标记时间为:10:01:00(或者其他时间,如10:03:00),那么收到水印的窗口就知道不会再有早于该时间的记录出现,因为所有小于等于该时间戳的事件都已经到达。这时窗口可以安全的计算并给出结果(总和)。水印使得事件时间和处理时间无关。迟到的水印(“迟到”是从处理时间的角度而言)并不会影响结果的正确性,而只会影响收到结果的速度。
在Flink中,水印由应用开发人员生成。完美的水印永远不会错:时间戳小于水印的标记时间的事件不会再出现。在特殊情况下(例如:非乱序事件流),最近一次的时间戳就可能是完美水印。启发式水印则相反,它只估计时间,因此有可能出错,即迟到事件-其时间戳小于水印时间,晚于水印时间出现。针对启发式水印,Flink提供了处理迟到元素的机智。
设定水印通常与业务相关。例如:如果知道事件的迟到事件不会超过5秒,就可以将水印标记时间设为最大时间戳减去5秒-相当于窗口延迟5秒评估结果;另外一种做法是,采用一个Flink作业监控事件流,学习事件的迟到规律,以此来构建水印生成模型。
如果水印迟到的太久,收到的结果就可能会很慢,解决的办法就是在水印到达之前数据近似结果。如果水印达到的太早,则可能收到错误的结果,不过Flink处理迟到结数据的机制可以解决这个问题。上述问题看起来很复杂,但是恰恰符合现实世界规律-大部分的事件流都是乱序的,并且通常无法了解它们的乱序程度(因为理论上不能预见未来)。水印时唯一让我们直面乱序事件流并保证正确的机制。否则只能选择忽视,假装错误的结果是正确的。
5.3.6 有状态的计算
流计算分为有状态计算和无状态计算两种。
无状态计算观察每个独立事件,并根据最后一个事件输出结果。例如流处理程序从传感器接受温度度数,并在温度超过90度是发出警告。
有状态计算则会基于多个事件输出结果。例如:计算过去一小时的平均温度。
无状态流计算和有状态流计算区别:
输入记录有黑条表示,无状态流计算每次只转换一条输入记录,并根据最新的输入记录输出最新的结果(白条)。有状态的流计算维护所有已处理记录的状态值,并根据新的输入记录更新状态值,因此输出结果(灰条)反应综合多个事件后的记过。
5.3.7 检查点
当分布式引入状态时,自然引入了一致性问题。一致性实际上是“正确性级别”的另一种说法,即成功处理故障并恢复之后得到的结果,与没有发生故障得到的结果相比。,前者有多正确?举例来说,假设要对最近一小时登陆的用户计数,计数结果是多少?
再流计算中,一致性一般分为三个级别
at-most-once: 这其实是没有正确性保障的委婉说法 ,故障发生后,结果可能丢失。
at-least-once: 这表示计数的结果可能大于正确的值,但绝不会小于正确的值,也就是说,计数程序在发生故障后可能多算,但绝不可能少算。
exactly-once:这指的系统保证发生故障后得到的计数结果的值与正确的值一致。
最先保证exactly-once的系统(Storm Trident和Spark Streaming)性能和表现力这两方面都付出很大的代价。为了保证exactly-once,这些系统无法单独对每一条记录运用应用逻辑,而是同时处理多条(一批)记录,保证对每一批的处理要么全部成功,要么全部失败,这就导致在得到结果前,必须等待一批记录处理结束。因此,用户不得不同时使用两个流处理框架(一个用来保证exactly-once,一个用来对每个元素进行低延迟处理),结果使基础设施更加复杂。曾经,用户不得不在exactly-once与获得低延迟和效率之间权衡利弊。Flink避免了这种权衡。
Flink一个重大价值在于,它既保证了exactly-once,又具有低延迟和高吞吐的处理能力。
检查点:Flink保证exactly-once的机制。
Flink的检查点核心作用保证状态的正确,即使遇到程序中断,也要保证正确。
记住这一基本点之后,我们用一个例子来看检查点是如何运行的。Flink为用户提供了用来自定义状态的工具。例如下面这个scala程序按照输入记录的第一个字段(一个字符串)进行分组并维护第二个字段的计数状态。
|
该程序有两个算子:keyBy算子用来将记录按照第一个元素(一个字符串)进行分组,根据该key将数据进行重新分区,然后将记录再发送给下一个算子:有状态map算子(mapWithState)。map算子在接收到每个元素后,将输入记录的第二字段加入到现有的总数中,再将更新过元素发射出去。
下图中程序的初始状态。注意a,b,c三组的初始计数状态都为0,即三个圆柱上的值。ckpt表示检查点屏障。每条记录在处理顺序上严格遵守检查点之前或者之后的规定。例如["b",2]在检查点之前被处理,["a",2]在检查点之后被处理。
上图中,程序的初始状态:输入6条记录,被检查点屏障隔开,所有的map算子状态均为0(计数还未开始).所有key为a的记录都将被顶层的map算子处理,所有key为b的记录将被中间层的map算子处理,所有key为c 的记录将被下层map算子处理。
当该程序处理输入流中的6条记录时,涉及的操作遍布3个并行实例,那么检查点如何保证exactly-once呢
检查点和普通记录类似,他们都有算子处理,但不参与计算,而是会触发检查点相关行为。当读取数据源数据流时(在本例中与keyBy算子内联)遇到检查电屏障时,它将其在输入流中的位置保存到稳定的存储中。如果输入流来自消息系统Kafka或MapR Stream,这个位置就是偏移量。Flink的存储机制是插件化的,稳定存储可以是分布式文件系统,如HDFS,S3或者MapR-FS,下图展示了这个过程。
图中,当Flink数据源(与keyBy算子内联)遇到检查点屏障时,它会将其在输入流中的位置保存到稳定存储中。这让Flink可以根据该位置重启输入。
检查点屏障就是普通记录一样在算子之间流动,当map处理完前3条记录并收到检查点屏障时,他们会将状态以异步方式写入稳定的存储。
当算子的状态备份和检查点屏障位置备份被确认之后,该检查点操作就可以被标记为完成。我们无需停止或者阻断计算条件下,在一个逻辑时间点(对应检查点屏障在输入流位置)为计算状态拍了快照。通过确保备份的状态和位置指向同一个逻辑时间点。当没有出现故障时,Flink的检查点开销极小,检查点的操作的速度由可稳定存储的可用宽带决定。
如果检查点操作失败,Flink会丢弃该检查点并继续正常执行,因为之后的某一个检查点可能会成功。虽然恢复的时间可能会更长,但对于状态的保证依旧很有力。只有在一系列连续的检查操作失败时,Flink才会抛出错误,因为这通常意味着发生了严重且持久的错误。
在这种情况下,Flink会重新拓扑(可能会获取新的执行资源),将输入流倒回到上一个检查点,然后回复状态值并从该处开始继续计算。上例中["a",2],["a",2],["c",2]这几条记录将会被重播。
上图展示了重新处理的过程,从上一个检查点开始重新计算,可以保证再剩下的记录被处理后,得到的map算子的状态值与没有发生故障时的状态是一致的。值得注意的是,输出流会含有重复的数据。具体来说,["a",2],["a",4],["c",3]会出现两次,如果Flink将输出写入特殊的输出系统(比如文件系统或者数据库),那么就可以避免这个问题,
Flink检查点算法的正式名称是异步屏障快照(asyncchronous barrier snapshotting),该算法大致基于Chandy-Lamport分布式快照算法。
5.3.8 保存点:状态版本控制
检查点有Flink自动生成,用来在故障发生时,重新处记录,从而修正状态。Flink用户还可以通过另一个特性来有意识的管理状态版本,这个特性叫做保存点(savepoint)。
保存点与检查点的工作方式相同,只不过它是由用户通过Flink命令行工具或者Web控制台手动触发,而不是由Flink自动触发。和检查点一样,保存点也被保存在稳定存储中。用户可以从保存点重启作业,而不用从头开始。保存点可以被视为作业在某个特定时间点的快照(该时间即为保存点被触发的时间点)。
对版本保存点的另一种理解是,它在明确的时间点保存应用程序状态的版本。这个版本控制系统保存应用程序的版本很相似。最简单的例子是在不修改应用程序代码情况下,每个固定时间拍快照,即照原样保存应用程序的状态版本。如下图所示
在图中,v.0是应用程序在一个正在运行的版本,我们分别在t1和t2时刻触发了保存点。因此可以在这之后的任意时间返回这两个时间点,并且重启应用程序。更重要的是,可以从保存点启动被修改过得应用程序版本。举例来说:可以修改应用程序代码(假设称新版本v.1),然后可以在t1时候运行修改后的代码。这样一来v.0和v.1两个版本同时运行,并在之后的时间里获取各自的保存点。如下图所示。
保存点可用于应对流处理作业在生产环境中遇到的许多挑战。
(1)应用程序代码升级 : 假设你在已经处于运行状态的应用程序中发现了 一 个 bug,并且希望之后的事件都可以用修复后的新版本来处理。通过触 发保存点并从该保存点处运行新版本,下游的应用程序并不会察觉到不 同(当然,被更新的部分除外)。
(2)Flink版本更新: Flink 自身的更新也变得简单,因为可以针对正在运行 的任务触发保存点,并从保存点处用新版本的 Flink重启任务。
(3)维护和迁移:使用保存点,可以轻松地“暂停和恢复”应用程序。这对 于集群维护以及向新集群迁移的作业来说尤其有用 。 此外,它还有利于 开发、测试和调试,因为不需要重播整个事件流 。
(4)假设模拟与恢复:在可控的点上运行其他的应用逻辑,以模拟假设的场
景,这样做在很多时候非常有用。
(5) A/B 测试:从同-个保存点开始,并行地运行应用程序的两个版本,有 助于进行 A/B 测试。
5.3.9 端到端的一致性和作为数据库的流处理器
输入数据来自 一 个分区存储系统(如 Kafka 或者 MapR Streams 这样的消 息队列)。图 5 11 底部的详情图展示了 Flink 拓扑,其中包含 3 个算子。 source 读取输入数据,根据 key 分区,并将数据路由到有状态的算子实例 (这既可以map算子,也可以是窗口聚合算子)。有状态的算子将状态内容(比如前例中的计数结果)或者一些衍生结果写入sink, 再由 sink将结果传送到输出存储系统中(例如文件系统或数据库)。接着,查询服务(比如数据库查询 API)就可以允许用户对状态进行查询(最简单的例子就是查询计数结果),因为状态己经被写入输出存储系统了。
在将状态内容传送到输出存储系统的过程中,如何保证 exactly-once 呢?这 叫作端到端的一致性。本质上有两种实现方法,用哪一种方怯则取决于输 出存储系统的类型,以及应用程序的需求。
(1)第一种方法是在 sink 环节缓冲所有输出,并在 sink 收到检查点记录时, 将输出“原子提交”到存储系统。这种方也保证输出存储系统中只存在有一致性保障的结果,井且不会出现重复的数据。从本质上说,输出存 储系统会参与 Flink的检查点操作。要做到这一点,输出存储系统需要 具备“原子提交”的能力。
(2)第二种方告是急切地将数据写入输出存储系统,同时牢记这些数据可能 是“脏”的,而且需要在发生故障时重新处理。如果发生故障,就需要将输出、输入和 Flink 作业全部回滚,从而将“脏”数据覆盖,并将已经写 入输出的“脏”数据删除。注意,在很多情况下,其实并没有发生删除操作。例如,如果新记录只是覆盖旧纪录(而不是添加到输出中),那么 “脏”数据只在检查点之间短暂存在,井且最终会被修正过的新数据覆盖
值得注意的是,这两种方怯恰好对应关系数据库系统中的两种为人所熟知的事务隔离级别:己提交读( read committed)和未提交读( read uncommitted)。 己提交读保证所有读取(查询输出)都只读取已提交的数据,而不会读取 中间、传输中或“脏”的数据。之后的读取可能会返回不同的结果,因为数据可能已被改变。未提交读则允许读取“脏”数据;换句话说,查询总是看到被处理过的最新版本的数据。
某些应用程序可以接受弱一点的语义,所以 Flink提供了支持多重语义的多 种内置输出算子,如支持未提交读语义的分布式文件输出算子。用户可以 根据输出存储系统的能力和应用程序的需求选择合适的语义。
根据输出存储系统的类型, Flink及与之对应的连接器可以一起保证端到端 的一致性,并且支持多种隔离级别。
现在回过头看看图 5-11 中的应用程序架构。 之所以本例需要有输出存储系统,是因为外部无法访问 Flink的内部状态,所以输出存储系统成了查询目标。但是,如果可以直接查询状态,则在某些情况下根本就不需要输出存储系统,因为状态本身就已经包含了查询所需的信息。这种情况在许多应 用程序中真实存在,直接查询状态可以大大地简化架构,同时大幅提升性 能,如图 5-12所示。
Flink社区正致力于完善可查询状态特性。 Flink提供一个查询API,通过该API可以对 Flink发出查询请求,然后得到当前的状态值。 从某种意义上 说,在有限的情景下, Flink可以替代数据库, 并同时提供写路径 (输入流 不断更新状态)和读路径(可查询状态)。尽管这对于许多应用程序都行得 通,但可查询状态受到的限制还是比通用数据库大得多。
6.流处理架构用例和性能测试
6.1案例:欺诈检测
传统的欺诈检测模型将包含每张信用卡的最后一次刷卡地点的文件直接存储于数据库系统中。但是这样集中式数据库系统中,其他消费者并不能轻易使用消费者刷卡行为的数据。因为访问数据库系统可能会影响欺诈检测系统的正常运工作;在没有经过认真仔细的审查之前,其他消费者绝不会被授权更改数据库。这将导致整个流程变慢,因为必须执行各种仔细地检查,以避免核心业务系统收到破坏或者影响。
与传统方法相比,上图的所示的流处理架构设计将欺诈检测器的输出发送个外部的消息队列(kafka或者MapR Streams),再由如Flink这样的流处理器更新数据库,而不是直接将输出发送给数据库。这使得刷卡行为的数据可以通过消息队列被其他服务使用。例如刷卡性分析器。上一次刷卡行为的数据被存储在本地的数据库系统中,不会被其他服务访问。这样的设计避免因为增加新的服务而带来的过载风险。
6.2 性能测试
6.2.1 Yahoo! Streaming Benchmark
2015 年 12 月, Yahoo!的 Storm 团队发表了一篇博客文章,并在其中展示了 Storm、 Flink 和 Spark Streaming 的性能测试结果。该测试对于业界而言极具价值,因为它是流处理领域的第一个基于真实应用程序的基准测试。
该应用程序从 Kafka 消费广告曝光消息,从 Redis 查找每个广告对应的广告宣传活动,并按照广告宣传活动分组,以 10秒为窗口计算广告浏览量。10 秒窗口的最终结果被存储在 Redis 中,这些窗口的状态也按照每秒记录 一次的频率被写入 Redis,以方便用户对它们进行实时查询。在最初的性能 测评 中,因为 Storm 是无状态流处理器(即它不能定义和维护状态),所以 因此作业也按照无状态模式编写。所有状态都被存储在 Redis 中,如下图所示。
结果如下:
在性能测评中, Spark Streaming遇到了吞吐量和延迟性难两全的问题。随着批处理作业规模的增加,延迟升高。如果为了降低延迟 而缩减规模,吞吐量就会减少。 Storm和 Flink则可以在吞吐量增加时维持低延迟。
为了进一步测试 Flink 的性能,测试人员设置了 一系列不同的场景,并逐步 测试。
6.2.2 变化1:使用Flink状态
最初的性能恻评专注于在相对较低的吞吐量下, 测量端到端的延迟, 即使在极限状态下, 也不关注容错性。 此外,应用程序中的 key基数非常小 (100), 这使得测试结果无法反映用户量大的情况, 或者 key空间随着时间增长的情况(如 tweet)。 2016 年 2 月, data Artisans 的博客发 表了一篇文 章 ,对 Yahoo! Streaming Benchmark进行了拓展,并专注于解决上述问题。 由于最初的测试结果显示 SparK Streaming 的性能欠佳,因此这次的测试对 象只有Storm和Flink, 它们在最初的测试中有着类似的表现。
第 1个变化是利用 Flink提供的状态容错特性重新实现应用程序,如图 5-15 所示。这使得应用程序能保证 exactly-once。
6.2.3 改进数据生成器并增加吞吐量
第 2个变化是通过用每秒可以生成数百万事件的数据生成器来增加输入流的数据量。结果如图 5-16 所示。
storm能够承受每秒40万事件,但受限于CPU; Flink则可以达到每秒300 万事件(7.5倍),但受限于 Kafka集群和 Flink集群之间的网络。
6.2.4 变化 3:消除网络瓶颈
为了看看在没有网络瓶颈问题时 Flink 的性能如何,我们将数据生成器移到 Flink 应用程序的内部。图 5-17 展示了这个流程。在这样的条件下, Flink 可以保持每秒 1500 万事件的处理速度(这是 Storm 的 37.5 倍),如图 5-16 所示。将数据生成器整合到 Flink应用程序中,可以测试性能极限,但这种做法井不现实,因为现实世界中的数据必须从应用程序的外部流入。
6.2.5 变化4: 使用MapR Streams
另一种避免网络瓶颈并测试Flink性能的方也是使用 MapR Streams。在另一个测试中, 同样的 Flink应用程序通过MapRStreams接收数据。
使用 MapR Streams之后,流处理被整合进整个平台,从而使得 Flink可以 与数据生成任务和数据传输任务一起运行,这样就避免了连接 Kafka集群和Flink集群时遇到的大部分问题。在这种高性能配置和更快的网络硬件环境下,Flink能够支撑每秒1000万事件的处理速度。
6.2.6 变化5: 增加key基数
最后一个变化是增加 key基数(广告宣传活动的数量)。在最初的测试中, key 基数只有 100。这些 key 每秒都会被写入 Redis,以供查询。当 key 基数 增加到 100 万肘,系统的整体吞吐 量 减少到每秒 28 万事件,因为向 Redis写入成了系统瓶颈。使用 Flink可查询状态的一个早期原型(如图 5-18所 示),可以消除这种瓶颈,使系统的处理速度恢复到每秒 1500万事件,并 且有 100万个 key可供查询,如图 5 19所示。
通过避免流处理瓶颈,同时利用 Flink 的有状态流处理 能力,可以使吞吐量达到 Storm 的 30 倍左右,同时还能保证 exactly-once 和高可用性。大致来说,这意味着与 Storm相比, Flink的硬件成本或云计 算成本仅为前者的 1/30,同样的硬件能处理的数据量则是前者的 30 倍。
7 Flink批处理技术
从原则上说,批处理是一种特殊的流处理 : 当输入数据是有限的,并且只 需要得到最终结果时,对所有数据定义一个全局窗口并在窗口里进行计算 即可。但是,这样做的效率如何呢?
传统上,有限数据流由专用的批处理器处理;某些时候,它比流处理器更高效。但是,在流处理器中整合高效、大规模的批处理所需的大部分优化方案,是可行的做法。这正是 Flink 所做的工作,而且这样做的效率很高。
同样的后端(流处理引擎)被用来处理有限数据和无限数据 。在流处理引擎之上, Flink 有 以下机制:
- 检查点机制和状态机制:用于实现容错、有状态的处理 ;
- 水印机制:用于实现事件时钟;
- 窗口和触发器:用于限制计算范围,并定义呈现结果的时间。
8 Flink批处理技术 案例研究
在 2015年的 Flink Forward研讨会上, Dongwon Kim1展示了他所做的性能测试 。 他对 MapReduce、 Tez、 Spark 和 Flink 在执行纯批处理任务时的性能做了比较。测试的批处理任务是 TeraSort和分布式散列连接。
第一个任务是TeraSort,即测量为1TB数据排序所用的时间。就上述系统而言, TeraSort本质上是分布式排序问题,如图 6-4所示。它由以下几个阶 段组成:
(1)读取阶段:从 HDFS 文件中读取数据分区;
(2)本地排序阶段:对上述分区进行部分排序;
(3)混洗阶段:将数据按照 key重新分布到处理节点上;
(4)终排序阶段:生成排序输出;
(5)写入阶段:将排序后的分区写入HDFS 文件。
Hadoop 发行版包含对 TeraSort的实现,同样的实现也可以用于 Tez,因为 Tez可以执行通过 MapReduce API编写的程序 。 Spark和Flink的TeraSort实现由 DongwonKim提供。用来测量的集群由42台机器组成,每台机器包含12个CPU内核、 24GB内存,以及6块硬盘。
图 6-5展示了测试结果 。结果显示 ,Flink 的排序时间比其他所有系统都少。 MapReduce 用了 2157 秒 , Tez 用了 1887 秒, Spark 用了 2171 秒 , Flink则只用了 1480秒。
第二个任务是一个大数据集 (240GB)和一个小数据集(256MB)之间的分布式散列连接。结果显示, Flink仍然是速度最快的系统,它所用的时间分别是TEZ和 Spark的 1/2和 1/4,如图 6-6所示。
产生以上结果的总体原因是, Flink 的执行过程是基于流的,这意味着各个处理阶段有更多的重 叠 ,并且混洗操作是流水线式的,因此磁盘访问操作更少。相反, MapReduce、 Tez和 Spark是基于批的,这意味着数据在通过网络传输之前必须先被写入磁盘。 该测试说明,在使用 Flink时,系统空闲时间和磁盘访问操作更少 。
值得一提的是,性能测试结果中的原始数值可能会因集群设置、配置和软件版本而异。如果现在再测试一遍,那么得到的数值的确可能不一样(上述测试用到的软件版本分别是: Hadoop 2.7.1、 Tez 0.7.0、 Spark 1.5.1,以及 Flink 0.9.1,这些系统在如今都有了更新的版本)。本节的重点是,即使是批处理器所擅长的任务,流处理器( Flink)在经过适当的优化后也仍然可以表现得和批处理器( MapReduce、Tez 和 Spark) 一 样好,甚至更好。因 此, Flink 可以用同一 个数据处理框架来处理无限数据流和有限数据流,并且不会牺牲性能 。