分享

流计算简介(下篇)

 蜗牛的书馆 2019-05-13

文章内容由K2研究院原创,转载请注明来源。

上期回顾

1

流计算的概念

2

流计算的技术要素

   上篇链接:流计算简介(上篇)

3

流计算框架比较

本节将先后介绍Structured Streaming、Flink和Kafka,并从架构的角度讨论其异同。在讨论各个框架的时候,不会谈如何构建一个具体的应用,大家阅读对应的用户文档可以很快编写一些简单实用的流计算应用。基于前面的流计算关键概念介绍,本节将从架构上讨论各个框架的异同,以期大家能对各个技术的来龙去脉、适用场景和未来发展趋势有更深入的了解。因为Spark流行甚广,下面先从Spark的Structured Streaming开始。

3.1

Structured Streaming

Spark是由UC Berkeley AMPLab在2010年开源的项目,其核心思想来自一系列论文。Spark中最重要的概念之一是RDD(resilient distributed datasets),与Mapreduce相比,Spark利用RDD把数据与计算解耦开来。在Spark的框架下,计算是无状态的,所有的状态信息都在RDD里。每个Spark任务都是一个计算工作流,每个步骤的输入和输出都是RDD,中间的计算过程无状态。所以,在工作流执行过程中,集群如果出现故障导致计算失败,只需要从最近一个持久化的RDD重新计算。

早期的Spark并没有流计算的概念。直到Spark Streaming加入项目,Spark才从批计算的模式向批流结合的统一框架演进。但Spark Streaming更像是批计算的衍生,所以在API上并不适合流计算场景,所以项目最终提出Structured Streaming正式作为Spark引擎对外提供的批流结合的API形式。其处理模式上仍然沿用基于RDD的批计算模式——源源不断的数据被划分为连续小批(micro batch),每个小批看做一个RDD,在处理小批的模式上仍然与原始的Spark是完全一样的。

要支持流计算就必须支持时间窗口定义。前面讨论过,窗口实际上对应一系列需要缓存的计算中间结果。在Structured Streaming里,与原始的Spark一样,这些结果仍然是以RDD的形式存在的。考虑一个具体的实例来理解基于小批的计算模式是如何支持时间窗口的。我们仍然沿用前面提到的计算窗口内平均温度的例子:以发生时间每5分钟统计一次平均温度,即计算8:00~8:05时间范围内的平均温度。假设在9:00时刻系统接收到第一批数据中存在8:00~8:05内的温度读数,那么处理完之后缓存的中间结果为(我们暂且忽略其它窗口内的中间结果):

每次更新中间结果需要同时使用新的数据和前一次的输出,按照这样的形式随着数据不断到来,中间结果也在不断更新,直到水位线超过时间窗口后得到最终的计算结果。所以从处理模式来看,Structured Streaming与Spark的批计算并没有本质不同,只是在开始计算前系统并没有得到全部数据,一些输入的RDD是在计算开始之后才源源不断到来的,如上图所示。同样,如果在计算过程中系统发生故障,可以从发生失败之前最后一次持久化的应用状态开始往后计算;在数据源和输出支持的情况下,Structured Streaming支持Exactly-once的计算语义。

由于Structured Streaming的计算过程是无状态的,所以一些计算的并行化程度可以随着数据负载的变化而变化。例如,如果我们想监控某社交网站的聊天记录,根据其中包含的关键词进行报警,那么在一天中不同时间段产生的日志量是有较大波动的。在日志负载较高的时候,我们希望流计算引擎能自动扩容,占用更多的资源来处理日志;反之,我们则希望流计算占用的资源缩小。Structured Streaming在这时候可以根据每批数据的多少来自动伸缩。

由于数据到来后总是需要等待一段时间才会被处理,这自然增大了数据处理延迟。对于一些对实时性要求很高的应用Structured Streaming可能无法满足要求。为了弥补这种短板,从Spark 2.3之后加入了称为Continuous Processing的处理模式。其实现原理上不再有小批的概念,而是在原始的数据流中插入一些标记符(marker)来将数据分段,这与Flink的实现非常类似。目前Continuous Processing截止到最新的2.4版本仍处于实验状态,并且只能够支持映射类的算子,例如map、filter等等,这样设计是考虑这些算子本身不需要记录状态,实现上相对容易。当然,Continuous Processing模式下的算子不再具备自动伸缩的能力。

3.2

Flink

Flink是一个开源的支持批流结合的分布式计算框架。2014年之后,Flink的创建者成立了Data Artisans(后更名为Ververica)公司与开源社区一同完善Flink的开发工作。从Structured Streaming的发展过程中,我们可以清晰的看到Spark是由批计算逐步支持流计算的,而对比来看Flink则是在一开始更多考虑的是流计算。

对比Structured Streaming,Flink最显著的区别在处理模式。在Flink里不再有小批的概念,数据一旦抵达流计算引擎即可以被处理,而不需要等待。所以Flink比起Structured Streaming更加适合那些对实时性要求高的应用场景。前面提到过Structured Streaming在2.3版本引入了Continuous Processing的概念,通过在原始数据流里插入一些标记来将数据分段。而这种标记的概念在Flink里早就存在,称为Barrier,这种设计来源于Chandy-Lamport 算法。Flink定期将数据段计算的中间结果持久化以便故障后可以及时恢复。在实现Exactly-once语义时,Flink采用Two-phase commit来完成。

Flink与Structured Streaming另一个重要的区别来自状态的维护上。在Spark里,算子并没有状态,而状态都是以RDD的形式保存的;但在Flink里,算子是可以有状态的,如下图所示。

前面我们提到过算子的状态一般是计算的中间结果,除此以外对于数据源算子还需要记录数据的消费情况。例如在Flink里的Kafka数据源算子就需要记录被消费的topic里各个partition的offset情况。借用Flink文档里的一句话来概括算子的状态:

At a high level, we can consider state in stream processing as memory in operators that remembers information about past input and can be used to influence the processing of future input.

所以在Flink里不仅仅有算子和流的概念,还有状态概念;而在Structured Streaming里,虽然也有状态的概念,但状态和流在实现上并没有区分开。

前面在讨论Structured Streaming时,我们提到一些算子(主要是映射类算子)可以根据负载动态调整并行度。对于并行度变化,我们可以再展开一些讨论。

  • 首先,一部分算子的并行度是无法变化的,例如Kafka的数据源算子,其并行度是由对应topic的partition数量决定的;

  • 其次,一部分算子由于自身是无状态的,例如map、filter这类映射型算子,所以其内部执行的并行度本质上是可以随着负载变化动态调整的。在Flink里暂时没有支持并行度的自动调整,可能会在未来的版本里加入支持。目前可以在代码里通过setParallelism来定义并行度。

  • 最后,对于那些有状态的算子,并行度变化不仅意味着引入更多的资源,还意味着状态的迁移。

为了理解最后一种情况,我们可以举个例子:假设我们希望建立一套流计算应用来统计某网站用户每5分钟的操作次数。每个用户有唯一的userId,那么在构建Flink处理项目时,我们可以编写如下Java代码:

final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); DataStream<String> events = [...] //用户使用行为日志 DataStream<Tuple2<String, Integer>> results = events .keyBy('userId') .timeWindow(Time.minutes(5)) .sum(1).setParallelism(5); results.print(); env.execute('User Event Count');

上面的代码片段里,数据流通过keyBy方法将原始数据分流到5个逻辑节点来处理。假设网站一共有100个用户,那么理想情况下每个节点需要处理来自20个用户的数据。如果我们希望改变节点的并行度,比如改成10,那么意味着我们用10个节点计算,每个节点处理10个用户。如果流计算应用已经运行了一段时间,我们如果想从并行度5改成并行度10,不仅需要额外的5个逻辑节点,还需要把原来5个逻辑节点上缓存的中间状态分发到新的节点上。目前Flink允许修改有状态算子的并行度,但必须先停止正在运行的项目,修改后再重新启动。

3.3

 Kafka Streams

Kafka Streams是基于Kafka的一套用于开发流计算应用的库。所以,从应用开发上看,Kafka Streams比起Flink和Structured Streaming显得更轻量级。后两者一般借助其它的资源管理服务,如Yarn、Mesos或Kubernetes来管理集群的计算和内存资源(它们自身也有简单的独立管理资源能力)。如果基于Kafka Streams开发一个流计算应用,那么应用运行起来只是本地的一个进程,而不是运行在某种资源管理服务上。所以如果希望增加应用的并行度,在资源充足的前提下,可以在一个节点上运行多个进程,或者在其它机器上运行更多的进程,这些进程(包括进程内的多个线程)会由Kafka Streams库统一调度。如果希望做资源隔离,则可以将进程运行在容器里(比如Docker),利用Kubernetes这样的框架来管理容器的生命周期和服务的伸缩。

从处理模式来看,Kafka Streams更加接近Flink,而不是类似Structured Streaming的基于小批的处理。所以,Kafka Streams也能达到较好的实时性。由于Kafka Streams依赖用户启动更多的进程来增加处理的吞吐量,所以同Flink一样,它也不具备自动随着输入负载自动伸缩的能力。但结合容器和Kubernetes,通过外部的一些策略可以比较简单的实现自动伸缩。这方面比起Flink来要相对灵活。

Kafka Streams需要依托在Kafka集群上,其中间结果的存储需要保存在Kafka。这些中间结果,既包括算子的状态(类似Flink里的状态),也包括一些聚合操作所需要的数据存储。例如在下面的处理逻辑上:

从map到groupById之间需要将数据shuffle一次。在Flink或Structured Streaming里,这些中间数据会缓存在计算节点的本地存储上。但在Kafka Streams中,这些数据会存储在Kafka里临时的topic里。这意味着Kafka Streams的流计算应用需要占用Kafka集群的一些资源。所以,在使用Kafka Streams之前需要对集群资源做详细的测试。

3.4

流计算框架介绍小结

面对具体的应用场景,可以通过对比来选择适合的流计算框架。例如,如果已经有Hadoop环境,可以选择Flink或Structured Streaming;如果没有,而是有Kafka集群,则可以考虑使用Kafka Streams(当然也可以使用Flink的独立集群模式)。如果应用更加偏批计算,例如希望每天分析前一天的所有数据,那么使用Structured Streaming可以享受到自动资源伸缩的好处;如果应用更加偏流计算,比如希望根据数据里的特定模式报警,则Flink要更胜一筹。此外,从开发的历程上看,Flink比起另外两者在成熟度上更有优势,无论是UI交互、观测metrics还是API的灵活性都要更加丰富和完善。


Structured Streaming

Flink

Kafka streams

依赖资源管理(如Yarn、Mesos等)

可选,但一般依赖

可选

不依赖

实时性要求

成熟度

一般

成熟

一般

无状态自动伸缩

支持

暂不支持

暂不支持

总结

本文介绍了流计算的演进和核心概念,在此基础上介绍了一些流计算框架。希望给读者从概念理解和应用选型上提供一些参考。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多