对流处理的误解

我们花了很多时间来思考流处理。更酷的是:我们也花了很多时间帮助其他人思考流处理以及如何使用流应用解决他们的数据问题。这个过程的第一步是纠正对现代流处理的误解(作为一个快速变化的领域,这里有很多误见值得我们思考)。在这篇文章中,我们选择了其中的 6 个进行讲解,由于 Apache Flink 是我们最熟悉的开源流处理框架,所以我们会基于 Flink 来讲解这些例子。

  • 误解1:如果不使用批处理就不能使用的流(Lambda架构)
  • 误解2:延迟和吞吐量:只能选择一个
  • 误解3:微批处理意味着更好的吞吐量
  • 误解4:Exactly-Once?完全不可能
  • 误解5:流处理只能被应用在’实时’场景里
  • 误解6:不管怎么样,流太复杂了

1. 误解1:如果不使用批处理就不能使用的流(Lambda架构)

Lambda 架构 在 Apache Storm 和其它流处理项目的早期阶段是一个很有用并且出名的设计模式。这个架构包含了一个快速流层和一个批处理层。

之所以使用两层的原因是 Lambda 架构里的流处理只能计算出近似结果(例如,如果发生故障,结果是不可信的),而且只能处理相对少量的事件。虽然这些问题只存在于 Apache Storm 的早期版本中,与现今开源流处理不相关。现今的很多开源流处理框架都具有容错能力,即使出现故障也能产出准确的结果,而且具有高吞吐的计算能力。所以没有必要为了得到’快’而’准确’的结果维护多层架构。现今的流处理器(例如,Flink)可以满足你这两方面的需求。好在人们不再更多地讨论 Lambda 架构,这表明流处理正在走向成熟。

2. 误解2:延迟和吞吐量:只能选择一个

早期的开源流处理框架要么是高吞吐,要么是低延迟,因此开源流处理框架不是’海量数据、快速’场景的选择。但是 Flink(可能还有其他流流处理器)同时提供了高吞吐量和低延迟。这里有一个基准测试结果的示例

让我们从一个基本的角度来研究这一点,特别是从硬件层。让我们考虑一个存在网络瓶颈的流处理 Pipeline(至少我们看到的许多使用 Flink 的 Pipeline 存在这种情况)。在硬件层面,不需要存在这样的权衡。网络容量才是影响最大吞吐量和可达到的最低延迟的主要因素。

一个设计良好的软件系统应可以达到网络上限而不会引入瓶颈问题。虽然 Flink 的性能还是有优化的空间,使其更接近硬件所能达到的水平。但在这一点上,Flink 已经证明可以在 10 节点的集群上每秒处理 1000 万个事件,如果扩展到 1000 个节点,可以同时实现几十毫秒的延迟。根据我们的经验,这种性能水平对于大多数实际部署来说绰绰有余。

误解3:微批处理意味着更好的吞吐量

我们可以从另一个角度来讨论性能,不过先让我们来澄清两个容易混淆的概念:

  • 微批处理(Micro-batching):是建立在传统批处理模型之上的数据处理执行和编程模型。通过这项技术,进程或任务可以把一个流当作一系列小型的批次或数据块。
  • 缓冲(Buffering):一种用于访问网络、磁盘、缓存等的优化技术。维基百科中是这样定义的:物理内存里的一块用于临时储存移动数据的区域。

常见的误解是使用微批处理的数据处理框架会比每次处理一个事件的流处理框架有更高的吞吐量,因为微批处理在网络上传输的效率更高。该误解忽略了一个事实,流处理框架不会依赖任何处理和编程模型层面的批处理,但会在物理层面进行缓冲。Flink 确实也会对数据进行缓冲,这也就意味着 Flink 会通过网络一次发送一组处理过的记录,而不是一次只发送一条记录。从性能方面说,不对数据进行缓冲是不可取的,因为通过网络逐个发送记录不会带来任何性能上的好处。所以我们承认,在物理层面根本不存在一次发送一条记录的事情。但缓冲仅用作性能优化。因此,缓冲:

  • 对用户是不可见的
  • 不应该对系统造成任何影响
  • 不应该强加人为的限制
  • 不应该限制系统功能

所以对 Flink 的用户来说,他们可以按照单独处理每个记录的方式开发程序,但 Flink 使用缓冲来实现其底层性能优化。事实上,微批处理会以调度任务的形式引入相当大的开销,而如果这样做是为了降低延迟,那么这种开销只会只增不减!流处理器知道该如何利用缓冲的优势而不会带来任务调度方面的开销。

4. 误解4:Exactly-Once?完全不可能

这个误解包含了如下几个方面的内容:

  • 在现实中 Exactly-once 语义是不可能的
  • Exactly-once 语义不可能是端到端的
  • Exactly-once 语义从来都不是现实世界的需求
  • Exactly-once 语义是以牺牲性能为代价的

之前 Exactly-Once 仅指 Exactly-Once Delivery,而现在这个词被随意用在流处理里,使得这两个词比以前更容易混淆,也失去了它原本的意义。不过,相关概念仍然很重要,因此我们不会完全跳过它。为了尽可能准确,我们将 Exactly-Once 拆分为状态的 Exactly-Once 和传递的 Exactly-Once。

由于之前人们对这两个词的错误使用导致了这两个不同概念的混淆。例如,Apache Storm 使用 At-Least-Once 来描述传递(Storm 不支持状态),而 Apache Samza 使用 At-Least-Once 来描述应用状态。

状态 Exactly-once 意味着发生故障后,应用程序状态就像没有发生故障一样。例如,我们在维护一个计数器应用程序,在发生故障后,既不会多计数也不能少计数。在这种情况下使用 Exactly-Once 这个词是因为应用程序状态中每条消息都只处理了一次。传递 Exactly-once 意味着发生故障后,接收方(应用程序之外的某个系统)接收到处理后的事件就好像没有发生故障一样。

虽然流处理框架不可能在每个场景中保证传递的 Exactly-once,但可以做到状态的 Exactly-once。Flink 可以做到状态的 Exactly-once,并不会对性能造成显著影响。与 Flink 的 Checkpoint 配合,还能实现 Sink 上的传递 Exactly-once 语义保证。Flink Checkpoint 是应用程序状态的周期性、异步和一致的快照。这就是 Flink 在发生故障时仍然能保证状态 Exactly-once 的原因:Flink 会定时记录(快照)输入流的读取位置和每个算子的相关状态。如果发生故障,Flink 就会回滚到之前的状态,并重新开始计算。

因此,即使重放记录,结果状态中记录也好像只处理了一次。那么端到端的 Exactly-once 处理呢?可以通过让 Checkpoint 兼具事务协调机制来实现,换句话说,就是让 Source 和 Sink 算子参与到 Checkpoint 里来。在框架内部,结果是 Exactly-once 的,从端到端来看,也是 Exactly-once 的,或者说接近一次性。例如,在使用 Kafka 作为 Source,滚动文件(HDFS)作为 Sink 时,从 Kafka 到 HDFS 可以实现端到端的 Exactly-once 处理。类似地,Kafka 作为 Source,Cassandra 作为 Sink 时,如果对 Cassandra 做幂等更新时,那么就可以实现端到端的 Exactly-once 处理。

5. 误解5:流处理只能被应用在’实时’场景里

这个误解包括如下几个方面的内容:

  • 我没有低延迟的应用,所以我不需要流处理器
  • 流处理只跟那些持久化之前的过渡数据有关系
  • 我们需要批处理器来完成笨重的离线计算

现在是时候思考一下数据集类型与执行模型类型之间的关系了。有两种数据集

  • 无限:连续产生的数据,没有终点
  • 有限:有限且完整的数据

事实上,许多现实世界的数据集是无限数据集。无论数据存储在 HDFS 上的文件或者目录中,还是存储在 Apache Kafka 等基于日志的系统中,都是如此。有如下示例:

  • 用户与移动设备或网站的交互
  • 物理传感器提供的测量数据
  • 金融市场数据
  • 机器日志数据

实际上,在现实世界中很难找到一个有限数据集,不过一个公司的大楼位置信息倒是有限的(不过它也会随着公司业务的增长而变化)。其次,有两种处理模型:

  • 流处理:只要有数据生成就会一直处理
  • 批处理:在有限的时间内运行完处理,并释放资源

让我们再深入一点,有两种无限数据集:连续流(有连续到达数据的流)和间歇流(周期性到达数据的流)。

使用任意一种模型来处理任意一种数据集都是完全可能的,尽管不一定是最优的做法。例如,批处理模型一直被应用在无限数据集上,特别是间歇性的无限数据集。现实情况是,大多数批处理任务是通过调度来执行的,每次只处理无限数据集的一小部分。这意味着流的无限特性会给某些人带来麻烦。

批处理给人的印象是无状态的,因为输出只取决于输入。现实情况是,批处理作业会在内部保留状态(比如 Reducer 经常会保留状态),但这些状态只局限在一个批次内,无法跨多个批次处理关联事件。一旦用户尝试实现事件时间窗口这样简单的东西时,’状态限制在一个批次内’就很重要了,这是处理无限数据集常用的方法。处理无限数据集的批处理器不可避免地遇到迟到事件(因为上游的延迟),批次内的数据有可能因此变得不完整。需要注意的是,这里我们假设是基于事件时间生成窗口,因为事件时间是现实当中最为准确的模型。在执行批处理的时候,即使是简单的固定窗口(比如翻转或滑动窗口)在遇到迟到数据时也会出现问题,当使用会话窗口时更难以处理。如果完成一个计算所需要的数据不在一个批次里,那么在使用批次处理无限数据集时,就很难得到正确的结果。最起码,需要额外的开销来处理迟到的数据,还要维护批次之间的状态(要等到所有数据达到后才开始处理,或者重新处理批次)。Flink 内置了处理迟到数据的机制,在现实世界中处理无限数据时,迟到数据一种很正常的现象,因此,精心设计的流处理器将提供简单的工具来处理迟到数据。

6. 误解6:不管怎么样,流太复杂了

我们到了最后阶段,在这个阶段你会想’这听起来很棒,但我仍然不会使用流处理,因为……’:

  • 流处理框架学习成本太高。
  • 流难以解决时间窗、事件时间、触发器的问题
  • 流需要结合批处理,而我已经知道如何使用批处理,那为什么还要使用流?

我们永远不会仅仅因为我们认为流处理很酷就怂恿你使用流处理。相反,我们认为使用流处理的决定最终应该取决于你的数据的性质和代码的性质。在做决定之前问问自己:’我现在使用什么类型的数据集?’

  • 无限(用户活动数据、日志、传感器数据)
  • 有限

然后再问另一个问题:”什么变更最频繁?”

  • 代码比数据变化更频繁
  • 数据比代码变化更频繁

如果你的数据比代码变更更频繁,例如,在经常变化的数据集上执行一个相对固定的查询操作,我们认为你有流的问题。所以,在认定流是一个”复杂”的东西之前,你可能在不知不觉中已经解决过流方面的问题!你可能使用过基于小时的批次任务调度(在这种情况下,由于批次的时间问题和之前提过的状态问题,你得到的结果可能是不准确的,你可能都没意识到这点)。

Flink 社区长期以来一直致力于提供更高级别的 API,以抽象出更多复杂的时间和状态。例如,在 Flink 中处理事件时间就像定义一个时间窗口和一个提取时间戳和 Watermark 的函数一样简单(每个流只需执行一次)。处理状态就像在 Java 程序中定义变量并将它们注册到 Flink 一样简单。Flink 的 StreamSQL 等努力可以让你在永无止境的流上运行 SQL 查询。

完成思考练习:如果你的代码比数据变更更频繁怎么办?在这种情况下,我们认为你遇到了探索性问题。使用笔记本或其他类似的迭代工具可能非常适合解决探索问题。 但是,一旦你的代码稳定下来,你就会遇到流处理问题。我们建议你从一开始就开始思考流处理的长期解决方案。

7. 流处理的未来

随着流处理的日渐成熟,这些误解在日常讨论中也变得越来越少,我们发现流正朝着除分析应用之外的领域发展。正如我们所讨论的那样,真实世界正连续不断地生成数据。传统上,这种连续的数据流必须中断处理,数据要么集中收集在一个地方,要么分批次切割,以便应用程序与之交互。

原文:Stream Processing Myths Debunked

赏几毛白!