我们在思考流处理问题上花了很多时间,更酷的是,我们也花了很多时间帮助其他人认识流处理,以及如何在他们的组织里应用流处理来解决数据问题。
我们首先要做的是纠正人们对流处理(作为一个快速变化的领域,这里有很多误见值得我们思考)的错误认识。
在这篇文章里,我们选出了其中的六个作为例子。因为我们对Apache Flink比较熟悉,所以我们会基于Flink来讲解这些例子。
- 谬见1:没有不使用批处理的流(Lambda架构)
- 谬见2:延迟和吞吐量:只能选择一个
- 谬见3:微批次意味着更好的吞吐量
- 谬见4:Exactly once?完全不可能
- 谬见5:流只能被应用在“实时”场景里
- 谬见6:不管怎么样,流仍然很复杂
谬见1:没有不使用批处理的流(Lambda架构)
“Lambda架构”在Apache Storm的早期阶段和其它流处理项目里是一个很有用的设计模式。这个架构包含了一个“快速流层”和一个“批次层”。
之所以使用两个单独的层,是因为Lambda架构里的流处理只能计算出大致的结果(也就是说,如果中间出现了错误,那么计算结果就不可信),而且只能处理相对少量的事件。
算Storm的早期版本存在这样的问题,但现今的很多开源流处理框架都具有容错能力,它们可以在出现故障的前提下生成准确的计算结果,而且具有高吞吐的计算能力。所以没有必要再为了分别得到“快”和“准确”的结果而维护多层架构。现今的流处理器(比如Flink)可以同时帮你得到两种结果。
好在人们不再更多地讨论Lambda架构,说明流处理正在走向成熟。
谬见2:延迟和吞吐量:只能选择一个
早期的开源流处理框架要么是“高吞吐”的,要么是“低延迟”的,而“海量且快速”一直未能成为开源流处理框架的代名词。
不过Flink(可能还有其它的框架)就同时提供了高吞吐和低延迟。这里有一个基准测试结果的样例。
让我们从底层来剖析这个例子,特别是从硬件层,并结合具有网络瓶颈的流处理管道(很多使用Flink的管道都有这个瓶颈)。在硬件层不应该存在需要作出权衡的条件,所以网络才是影响吞吐量和延迟的主要因素。
一个设计良好的软件系统应该会充分利用网络的上限而不会引入瓶颈问题。不过对Flink来说,总是有可优化的空间,可以让它更接近硬件所能提供的效能。使用一个包含10个节点的集群,Flink现在每秒可以处理千万级别的事件量,如果扩展到1000个节点,它的延迟可以降低到几十毫秒。在我们看来,这种水平已经比很多现有的方案高出很多。
谬见3:微批次意味着更好的吞吐量
我们可以从另一个角度来讨论性能,不过先让我们来澄清两个容易混淆的概念:
微批次
微批次建立在传统批次之上,是处理数据的一个执行或编程模型。“通过这项技术,进程或任务可以把一个流当作一系列小型的批次或数据块”。
缓冲
缓冲技术用于对网络、磁盘、缓存的访问进行优化。Wikipedia完美地把它定义为“物理内存里的一块用于临时储存移动数据的区域“。
那么第3个缪见就是说,使用微批次的数据处理框架能够比每次处理一个事件的框架达到更高的吞吐量,因为微批次在网络上传输的效率更高。
这个缪见忽略了一个事实,流框架不会依赖任何编程模型层面的批次,它们只会在物理层面使用缓冲。
Flink确实也会对数据进行缓冲,也就是说它会通过网络发送一组处理过的记录,而不是每次发送一条记录。从性能方面说,不对数据进行缓冲是不可取的,因为通过网络逐个发送记录不会带来任何性能上的好处。所以我们得承认在物理层面根本不存在类似一次一条记录这样的情况。
不过缓冲只能作为对性能的优化,所以缓冲:
- 对用户是不可见的
- 不应该对系统造成任何影响
- 不应该出现人为的边界
- 不应该限制系统功能
所以对Flink的用户来说,他们开发的程序能够单独地处理每个记录,那是因为Flink为了提升性能隐藏了使用缓冲的细节。
事实上,在任务调度里使用微批次会带来额外的开销,而如果这样做是为了降低延迟,那么这种开销会只增不减!流处理器知道该如何利用缓冲的优势而不会带来任务调度方面的开销。
谬见4:Exactly once?完全不可能
这个缪见包含了几个方面的内容:
- 从根本上说,Exactly once是不可能的
- 从端到端的Exactly once是不可能的
- Exactly once从来都不是真实世界的需求
- Exactly once以牺牲性能为代价
让我们退一步讲,我们并不介意“Exactly once”这种观点的存在。“Exactly once”原先指的是“一次性传递”,而现在这个词被随意用在流处理里,让这个词变得令人困惑,失去了它原本的意义。不过相关的概念还是很重要的,我们不打算跳过去。
为了尽量准确,我们把“一次性状态”和“一次性传递”视为两种不同的概念。因为之前人们对这两个词的使用方式导致了它们的混淆。Apache Storm使用“at least once”来描述传递(Storm不支持状态),而Apache Samza使用“at least once”来描述应用状态。
一次性状态是指应用程序在经历了故障以后恍如没有发生过故障一样。例如,假设我们在维护一个计数器应用程序,在发生了一次故障之后,它既不能多计数也不能少计数。在这里使用“Exactly once”这个词是因为应用程序状态认为每个消息只被处理了一次。
一次性传递是指接收端(应用程序之外的系统)在故障发生后会收到处理过的事件,恍如没有发生过故障一样。
流处理框在任何情况下都不保证一次性传递,但可以做到一次性状态。Flink可以做到一次性状态,而且不会对性能造成显著影响。Flink还能在与Flink检查点相关的数据槽上做到一次性传递。
Flink检查点就是应用程序状态的快照,Flink会为应用程序定时异步地生成快照。这就是Flink在发生故障时仍然能保证一次性状态的原因:Flink定时记录(快照)输入流的读取位置和每个操作数的相关状态。如果发生故障,Flink会回滚到之前的状态,并重新开始计算。所以说,尽管记录被重新处理,但从结果来看,记录好像只被处理过一次。
那么端到端的一次性处理呢?通过恰当的方式让检查点兼具事务协调机制是可能的,换句话说,就是让源操作和目标操作参与到检查点里来。在框架内部,结果是一次性的,从端到端来看,也是一次性的,或者说“接近一次性”。例如,在使用Flink和Kafka作为数据源并发生数据槽(HDFS)滚动时,从Kafka到HDFS就是端到端的一次性处理。类似地,在把Kafka作为Flink的源并且把Cassandra作为Flink的槽时,如果针对Cassandra的更新是幂等时,那么就可以实现端到端的一次性处理。
值得一提的是,利用Flink的保存点,检查点可以兼具状态版本机制。使用保存点,在保持状态一致性的同时还可以“随着时间移动”。这样可以让代码的更新、维护、迁移、调试和各种模拟测试变得简单。
谬见5:流只能被应用在“实时”场景里
这个谬见包括几点内容:
- “我没有低延迟的应用,所以我不需要流处理器”
- “流处理只跟那些持久化之前的过渡数据有关系”
- “我们需要批处理器来完成笨重的离线计算”
现在是时候思考一下数据集的类型和处理模型之间的关系了。
首先,有两种数据集:
- 没有边界的:从非预定义的端点持续产生的数据
- 有边界的:有限且完整的数据
很多真实的数据集是没有边界的,不管这些数据时存储在文件里,还是在HDFS的目录里,还是在像Kafka这样的系统里。举一些例子:
- 移动设备或网站用户的交互信息
- 物理传感器提供的度量指标
- 金融市场数据
- 机器日志数据
实际上,在现实世界中很难找到有边界的数据集,不过一个公司所有大楼的位置信息倒是有边界的(不过它也会随着公司业务的增长而变化)。
其次,有两种处理模型:
- 流:只要有数据生成就会一直处理
- 批次:在有限的时间内结束处理,并释放资源
让我们再深入一点,来区分两种没有边界的数据集:连续性流和间歇性流。
使用任意一种模型来处理任意一种数据集是完全可能的,虽然这不是最优的做法。例如,批次处理模型被长时间地应用在无边界的数据集上,特别是间歇性的无边界数据集。现实情况是,大多数“批处理”任务是通过调度来执行的,每次只处理无边界数据集的一小部分。这意味着流的无边界特质会给某些人带来麻烦(那些工作在流入管道上的人)。
批处理是无状态的,输出只取决于输入。现实情况是,批处理任务会在内部保留状态(比如reducer经常会保留状态),但这些状态只限在批次的边界内,而且它们不会在批次间流窜。
当有人尝试实现类似带有“事件时间戳”的时间窗,那么“批次的边界内状态”就会变得很有用,这在处理无边界数据集时是个很常用的手段。
处理无边界数据集的批处理器将不可避免地遇到迟到事件(因为上游的延迟),批次内的数据有可能因此变得不完整。要注意,这里假设我们是基于事件时间戳来移动时间窗的,因为事件时间戳是现实当中最为准确的模型。在执行批处理的时候,迟到的数据会成为问题,即使通过简单的时间窗修复(比如翻转或滑动时间窗)也解决不了这个问题,特别是如果使用会话时间窗,就更难以处理了。
因为完成一个计算所需要的数据不会都在一个批次里,所以在使用批次处理无边界数据集时,很难保证结果的正确性。最起码,它需要额外的开销来处理迟到的数据,还要维护批次之间的状态(要等到所有数据达到后才开始处理,或者重新处理批次)。
Flink内建了处理迟到数据的机制,迟到数据被视为真实世界无边界数据的正常现象,所以Flink设计了一个流处理器专门处理迟到数据。
有状态的流处理器更适合用来处理无边界数据集,不管数据集是持续生成的还是间歇生成的。使用流处理器只是个锦上添花的事情。
缪见6:不管怎么样,流仍然很复杂
这是最后一个缪见。你也许会想:“理论虽好,但我仍然不会采用流技术,因为……”:
- 流框架难以掌握
- 流难以解决时间窗、事件时间戳、触发器的问题
- 流需要结合批次,而我已经知道如何使用批次,那为什么还要使用流?
我们从来没有打算怂恿你使用流,虽然我们觉得流是个很酷的东西。我们相信,是否使用流完全取决于数据和代码的特点。
在做决定之前问问自己:“我正在跟什么样类型的数据集打交道?”
- 无边界的(用户活动数据、日志、传感器数据)
- 有边界的
然后再问另一个问题:“哪部分变化最频繁?”
- 代码比数据变化更频繁
- 数据比代码变化更频繁
对于数据比代码变化更频繁的情况,例如在经常变化的数据集上执行一个相对固定的查询操作,这样会出现流方面的问题。
所以,在认定流是一个“复杂”的东西之前,你可能在不知不觉中已经解决过流方面的问题!你可能使用过基于小时的批次任务调度,团队里的其他人可以创建和管理这些批次(在这种情况下,你得到的结果可能是不准确的,而你意识不到这样的结果是批次的时间问题和之前提过的状态问题造成的)。
为了能够提供一组封装了这些时间和状态复杂性的API,Flink社区为此工作了很长时间。在Flink里可以很简单地处理事件时间戳,只要定义一个时间窗口和一个能够抽取时间戳和水印的函数(只在每个流上调用一次)。处理状态也很简单,类似于定义Java变量,再把这些变量注册到Flink。使用Flink的StreamSQL可以在源源不断的流上面运行SQL查询。
最后一点:对代码比数据变化更频繁的情况该怎么办?对于这种情况,我们认为你遇到了探索性问题。使用笔记本或其它类似的工具进行迭代可能适合用来解决探索性问题。
在代码稳定了之后,你仍然会碰到流方面的问题。我们建议从一开始就使用长远的方案来解决流方面的问题。
流处理的未来随着流处理的日渐成熟和这些缪见的逐步淡去,我们发现流正朝着除分析应用之外的领域发展。正如我们所讨论的那样,真实世界正连续不断地生成数据。
传统的做法会中断这些连续的数据,因为这些数据必须被聚合到一个集中的位置,或者被切分成批次,方便应用程序使用。
像CQRS这样的流处理模式越来越流行,应用程序可以直接基于持续的数据流进行开发,这样可以在本地保留状态,可以更好地隔离应用和团队,可以更好地处理基于时间的数据。
随着Flink不断地演化改进,并被越来越多的企业所采用,我们相信它不仅仅能够用来简化分析管道,还能够为我们带来更强大的计算模型。
本文作者:Kostas Tzoumas
来源:51CTO