通过上面的介绍,我们已经了解了Flink的主要应用场景,就是处理大规模的数据流。那为什么一定要用Flink呢?数据处理还有没有其他的方式?要解答这些疑问,就需要从流处理和批处理的概念讲起。
数据处理有不同的方式。
对于具体应用来说,有些场景的数据是一个一个到来的,是一组有序的数据序列,我们把它叫作数据流;而有些场景的数据则是一批同时到来的,是一个有限的数据集,这就是批量数据(有时也直接叫数据集)。
容易想到,处理数据流,当然应该来一个就处理一个,这种数据处理模式就叫作流处理,因为这种处理是即时的,所以也叫实时处理。与之对应,处理批量数据自然就应该一批读入、一起计算,这种数据处理模式就叫作批处理,也叫作离线处理。
那在真实的应用场景中,到底是数据流更常见,还是批量数据更常见呢?
在日常生活中,这两种形式的数据都有,如图1-4所示。例如,我们日常发信息,可以一句一句地说,也可以写一大段信息一起发过去。一句一句的信息就是一个一个的数据,它们构成的序列就是一个数据流;而一大段信息是一组数据的集合,对应的就是批量数据(数据集)。
图1-4 Flink处理的两种数据形式
当然,有经验的人都知道,一句一句地说,你一言我一语,有来有往,这才叫聊天;一大段信息直接发过去,别人看着都眼晕,很容易就没下文了——如果是很重要的整篇内容(如表白信),那么写成文档或邮件发过去效果可能会更好。
因此,可以看到,聊天这个生活场景的数据的生成、传递和接收处理都是流式的;而写信这个生活场景的数据的生成尽管也是流式的(字总得一个一个地写),但我们可以把它们收集起来,统一传输、统一处理(当然,我们还可以进一步“较真”:处理也是流式的,字得一个一个地读)。无论传输和处理的方式是什么样的,数据的生成一般都是流式的。
在IT应用场景中,这一点会体现得更加明显。企业的绝大多数应用程序都在不停地接收用户请求、记录用户行为和系统日志,或者持续接收采集到的状态信息。因此,数据会在不同的时间持续地生成,形成一个有序的数据序列,这就是典型的数据流。
流数据更真实地反映了我们的生活方式,真实场景中产生的一般都是数据流,那处理数据流就一定要用流处理的方式吗?
这个问题似乎问得有点“无厘头”,不过仔细一想就会发现,很多数据流的场景其实也可以用“攒批”的方式来处理。例如,对于聊天,我们可以收到一条信息就回一条,也可以攒很多条一起回复;对于应用程序,也可以把要处理的数据先收集齐,然后一并处理。
但是这样做的缺点也非常明显:数据处理不够及时,实时性变差了。而流处理是真正的即时处理,它没有攒批的等待时间,因此,处理速度会更快,实时性会更好。
另外,在批处理的过程中,还必须有一个固定的时间节点,用来结束攒批的过程并开始计算,而数据流是连续不断的,我们没有办法在某一时刻说:“好了,现在收集齐所有数据了,可以开始分析了。”如果需要实现持续计算,就必须采用流处理的方式来处理数据流。
很显然,对于流式数据,用流处理是最好、最合理的方式。
但是传统的数据处理架构并不是这样的。无论是关系型数据库还是数据仓库,都倾向于先收集数据,再进行处理。为什么不直接用流处理的方式呢?这是因为分布式批处理在架构上更容易实现。想想生活中发消息聊天的例子,我们就很容易理解了:如果来一条消息就立即处理,那么这样做一定会很受人欢迎,但是这要求我们必须时刻关注新消息,会耗费大量精力,工作效率会受到很大的影响;如果隔一段时间查看一下新消息,进行批处理,那么压力明显就小多了。当然,这么做的代价是可能无法及时处理某些重要消息而造成严重的后果。
想要弄清楚流处理的发展和演变,先要了解传统的数据处理架构。
IT互联网企业往往会用不同的应用程序处理各种业务,如内部使用的企业资源规划(ERP)系统、客户关系管理(CRM)系统、面向客户的Web应用程序。这些系统一般都会分层设计:计算层就是应用程序本身,用于数据计算和处理;存储层往往是传统关系型数据库,用于数据存储,如图1-5所示。
图1-5 传统事务处理系统架构
可以发现,这里的应用程序在处理数据的模式上有共同之处:接收的数据是持续生成的事件,如用户的点击行为、客户提交的订单或操作人员发出的请求。在处理事件时,应用程序需要先读取远程数据库的状态,然后按照处理逻辑得到结果,将响应返给用户并更新数据库状态。一般来说,一个数据库系统可以服务多个应用程序,它们有时会访问相同的数据库或表。
这就是传统的事务处理架构。系统处理的连续不断的事件其实就是一个数据流,而对于每个事件,系统都在收到之后进行相应的处理,这也是符合流处理的原则的。因此可以说,传统的事务处理就是最基本的流处理架构。
对于各种事件请求,事务处理的方式能够保证实时响应,好处是一目了然的。但是,这样的架构对表和数据库的设计要求很高;当数据规模越来越庞大、系统越来越复杂时,可能需要对表进行重构,而且一次联表查询也会花费大量的时间,甚至不能及时得到返回结果。于是,作为程序员,就只好将更多的精力放在表的设计和重构,以及SQL的调优上,而无法专注于业务逻辑的实现,这种工作费力费时,却无法直接体现在产品上。
那有没有更合理、高效的处理架构呢?
不难想到,如果我们对事件流的处理非常简单,比如收到一条请求就返回一个“收到”,就可以省去数据库的查询和更新了,但是这样的处理是没什么实际意义的。在现实应用中,往往还需要一些额外数据。此时,可以把需要的额外数据保存成一个“状态”,然后针对这条数据进行处理并更新状态。在传统架构中,这个状态是保存在数据库里的,这就是所谓的有状态的流处理。
为了加快访问速度,可以直接将状态保存在本地内存中,如图1-6所示。当应用收到一个新事件时,它可以从状态中读取数据,也可以更新状态;而当状态从内存中读/写的时候,就和访问本地变量没什么区别了,实时性可以得到极大的提升。
另外,当数据规模增大时,我们也不需要重构,只需要构建分布式集群,各自在本地计算就可以了,可扩展性也变得更好。
因为采用的是一个分布式系统,所以还需要保护本地状态,防止发生故障时丢失数据。我们可以定期地将应用状态的一致性检查点存盘,写入远程持久化存储中,遇到故障时再去读取而进行恢复,保证更好的容错性。
图1-6 有状态的流处理
有状态的流处理是一种通用且灵活的设计架构,可用于许多不同的场景,具体来说,有以下几种典型应用。
1.事件驱动型(Event-Driven)应用
事件驱动型应用是一类具有状态的应用,它从一个或多个事件流中提取数据,并根据到来的事件触发计算、状态更新或其他外部动作,比较典型的就是以Kafka为代表的消息队列,几乎都是事件驱动型应用。
这其实跟传统事务处理在本质上是一样的,区别在于基于有状态的流处理的事件驱动应用不再需要查询远程数据库,而是在本地访问它们的数据,如图1-7所示,这样在吞吐量和延迟方面就可以有更好的性能。
另外,远程持久化存储的检查点保证了应用可以从故障中恢复。检查点可以异步和增量地完成,因此对正常计算的影响非常小。
图1-7 传统事务处理与事件驱动型应用对比
2.数据分析(Data Analysis)型应用
所谓数据分析,就是指从原始数据中提取信息和发掘规律。传统的数据分析一般先将数据复制到数据仓库(Data Warehouse)中,进行批量查询,如果数据有了更新,就必须将最新数据添加到要分析的数据集中,然后重新运行查询或应用程序。
如今,Apache Hadoop生态系统的组件已经是许多企业大数据架构中不可或缺的组成部分。因此,现在的做法一般是将大量数据(如日志文件)写入Hadoop的分布式文件系统(HDFS)、S3或HBase等批量存储数据库中,以较低的成本进行大容量存储;然后可以通过SQL-on-Hadoop类的引擎查询和处理数据,如大家熟悉的Hive。这种处理方式是典型的批处理,其特点是可以处理海量数据,但实时性较差,因此也叫离线分析。
如果我们有一个复杂的流处理引擎,那么数据分析其实也可以实时执行。流式查询或应用程序不再读取有限的数据集,而是接收实时事件流,不断生成和更新结果,结果要么被写入外部数据库,要么被作为内部状态进行维护。
Apache Flink同时支持批处理与流处理的数据分析应用,如图1-8所示。
图1-8 数据分析型应用的批处理与流处理
与批处理分析相比,流处理分析最大的优势就是低延迟,真正实现了实时。另外,流处理不需要单独考虑新数据的导入和处理,实时更新本来就是流处理的基本模式。当前企业对流式数据处理的一个热点应用就是实时数据仓库,很多企业正是基于Flink来实现的。
3.数据管道(Data Pipeline)型应用
ETL即数据的提取、转换、加载,是在存储系统之间转换和移动数据的常用方法。在数据分析应用中,通常会定期触发ETL任务,将数据从事务数据库系统复制到分析数据库或数据仓库中。
数据管道的作用与ETL的作用类似,可以转换和扩展数据,也可以在存储系统之间移动数据。不过,如果我们用流处理架构搭建数据管道,那么这些工作就可以连续运行,而不需要再去周期性触发了。例如,数据管道可以用来监控文件系统目录中的新文件,将数据写入事件日志中。连续数据管道的明显优势是降低了将数据移动到目的地的延迟,而且更加通用,可应用于更多的场景。
周期性ETL与数据管道的区别如图1-9所示。
图1-9 周期性ETL与数据管道的区别
有状态的流处理架构其实并不复杂,很多用户基于这种思想开发了自己的流处理系统,这就是第一代流处理器,Apache Storm就是其中的代表。Storm可以说是开源流处理的先锋,最早是由Nathan Marz和BackType的一个团队开发的,后来才成为Apache软件基金会的下属项目。Storm提供了低延迟的流处理,但是它也为实时性付出了代价:很难实现高吞吐,而且无法保证结果的正确性。用更专业的话说,它并不能保证“精确一次”(exactly-once);即便是它能够保证的一致性级别,开销也相当大。状态一致性和exactly-once会在后续章节展开讨论。
对于有状态的流处理,当数据越来越多时,必须用分布式的集群架构来获取更高的吞吐量。但是分布式架构会带来另一个问题:怎样保证数据处理的顺序是正确的呢?
对于批处理来说,这并不是一个问题,因为所有数据都已收集完毕,可以根据需要选择、排列数据而得到想要的结果。但是如果我们采用“来一个处理一个”的流处理,就可能出现“乱序”现象:本来先发生的事件,因为分布处理滞后了。那么应该怎么解决这个问题呢?
以Storm为代表的第一代分布式开源流处理器主要专注于具有毫秒级延迟的事件处理,特点就是快;而对于准确性和结果的一致性是不提供内置支持的,因为结果有可能取决于事件到达的时间和顺序。另外,第一代流处理器通过检查点来保证容错性,但是在故障恢复的时候,即使事件不会丢失,也有可能被重复处理,因此,无法保证exactly-once。
与批处理器相比,可以说第一代流处理器牺牲了结果的准确性,用来换取更低的延迟。而批处理器恰好相反,牺牲了实时性,换取了结果的准确性。
我们自然想到,如果可以让二者相结合,不就可以同时提供低延迟和准确的结果了吗。正是基于这样的思想,Lambda架构被设计出来,如图1-10所示,可以认为这是第二代流处理架构,但事实上它只是第一代流处理器和批处理器的简单合并。
图1-10 Lambda架构示意图
Lambda架构的主体是传统批处理架构的增强。它的批处理层(Batch Layer)就是由传统的批处理器和存储空间组成的;而实时层(Speed Layer)则由低延迟的流处理器组成。数据到达之后,两层处理双管齐下,一边由流处理器进行实时处理,一边写入批处理器存储空间,等待批处理器进行批量计算。流处理器快速计算出一个近似结果,并将它们写入流处理表中;而批处理器会定期处理存储空间中的数据,将准确的结果写入批处理表中,并从流处理表中删除不准确的结果。最终,应用程序会合并流处理表和批处理表中的结果,并展示出来。
Lambda架构现在已经不再是最先进的了,但仍应用在许多地方。它的优点非常明显,就是兼具了批处理器和第一代流处理器的优点,同时保证了低延迟和结果的准确性。而它的缺点同样非常明显:首先,Lambda架构本身就很难建立和维护;其次,它需要我们对一个应用程序做出两套语义上等效的逻辑实现,因为批处理和流处理是两套完全独立的系统,它们的API也完全不同,为了实现一个应用,付出双倍的工作量,这对程序员显然不够友好。
之前的分布式流处理架构都有明显的缺陷,人们也一直没有放弃对流处理器的改进和完善。终于,在原有流处理器的基础上,新一代分布式开源流处理器诞生了。为了与之前的系统区分,我们一般称之为第三代流处理器,其代表就是Flink。
第三代流处理器通过巧妙的设计,完美地解决了乱序数据对结果正确性的影响,并且这一代系统做到了exactly-once的一致性保障,是第一个具有一致性和准确结果的开源流处理器。另外,先前的流处理器仅能在高吞吐和低延迟中二选一,而第三代流处理器能够同时提供这两个特性。因此可以说,这一代流处理器仅凭一套系统就完成了Lambda架构两套系统的工作,它的出现使Lambda架构黯然失色。
除了低延迟、容错性和结果准确性,新一代流处理器还在不断地添加新的功能,如高可用的设置,以及与资源管理器(如YARN或Kubernetes)的紧密集成等。
下面我们会将Flink的特性做一个总结,从中可以体会到新一代流处理器的强大。