首页 > 解决方案 > 为什么围绕 Spark Streaming 微批处理(使用 kafka 作为源)受到如此多的批评?

问题描述

既然任何 Kafka 消费者实际上都是批量消费的,那么与 Kafka Streams(将自己定位为真正的流媒体)相比,为什么围绕 Spark Streaming 微批处理(当使用 Kafka 作为他的源时)受到如此多的批评?

我的意思是:很多批评都围绕在 Spark Streaming 微批处理架构上。而且,通常,人们说 Kafka Streams 是一个真正的“实时”工具,因为它一个接一个地处理事件。

它确实会一一处理事件,但据我了解,它使用(几乎所有其他库/框架)消费者 API。Consumer API 批量轮询主题以减少网络负担(间隔可配置)。因此,消费者将执行以下操作:

while (true) {
        ConsumerRecords<String, String> records = consumer.poll(100);

        ///// PROCESS A **BATCH** OF RECORDS
        for (ConsumerRecord<String, String> record : records) {

            ///// PROCESS **ONE-BY-ONE**
        }
}

所以,虽然说 Spark 是对的:

  1. 由于其微批处理最小间隔将延迟限制在最多 100 毫秒,因此可能具有更高的延迟(请参阅 Spark Structured Streaming DOCs);
  2. 以组的形式处理记录(作为 RDD 的 DStream 或结构化流中的 DataFrame)。

但:

  1. 可以在 Spark 中逐一处理记录 - 只需循环 RDD/Rows
  2. 实际上,Kafka Streams 会轮询一批记录,但会逐个处理它们,因为它在底层实现了 Consumer API。

为了明确起见,我不是从“粉丝”提出质疑(因此,这是一个意见问题),恰恰相反,我真的试图从技术上理解它,以便理解流生态系统中的语义。

欣赏这件事的每一条信息。

标签: apache-sparkspark-structured-streaming

解决方案


免责声明:我参与了 Apache Storm(众所周知,它是一个处理“逐记录”的流式框架,尽管也有 trident API),现在参与了 Apache Spark(“微批处理”)。

流媒体技术的主要关注点之一是“吞吐量与延迟”。从延迟的角度来看,“逐条记录”处理显然是赢家,但“逐条处理”的成本很高,每件小事都变成了巨大的开销。(考虑到系统的目标是每秒处理一百万条记录,那么任何额外的处理开销都会被多路复用一百万。)实际上,也有相反的批评,与“微记录”相比,“按记录读取”的吞吐量较差-批”。为了解决这个问题,流框架在其“内部”逻辑中添加批处理,但以减少延迟的方式添加。(比如配置批处理的大小,以及强制刷新批处理的超时)

我认为两者之间的主要区别在于任务是否“连续”运行并且它们是否正在组成“管道”。

在流式框架中“逐个记录”,当应用程序启动时,所有必要的任务都在物理上计划并一起启动,除非应用程序终止,否则它们永远不会终止。源任务不断地将记录推送到下游任务,下游任务处理它们并推送到下一个下游。这是以流水线方式完成的。除非没有要推送的记录,否则 Source 不会停止推送记录。(有背压和分布式检查点,但让我们抛开细节,专注于概念。)

在流式框架中做“微批处理”,它们必须为每个微批处理确定“批处理”的边界。在 Spark 中,计划(例如,该批次将从源和进程中读取多少条记录)通常由驱动端完成,任务是根据决定的批次进行物理计划的。这种方法为最终用户提供了一项重要的作业 - 什么是“适当”的批处理大小以实现他们所针对的吞吐量/延迟。批次太小会导致吞吐量下降,因为计划批次需要不小的成本(很大程度上取决于来源)。太大的批次会导致不良的延迟。此外,“stage”的概念适用于批处理工作负载(我看到 Flink 在他们的批处理工作负载中采用了舞台)而不适合流工作负载,因为这意味着某些任务应该等待“

当然,我不认为这样的批评意味着微批量是“不可用的”。当您的实际工作负载可以容忍几分钟(甚至几十分钟)的延迟时,您真的需要担心延迟吗?可能没有。您需要关注学习曲线的成本(很可能仅 Spark 与 Spark 和其他,但仅 Kafka 流或仅 Flink 是肯定的。)和维护。此外,如果您有一个需要聚合的工作负载(可能带有窗口),则框架的延迟限制不太重要,因为您可能会将窗口大小设置为分钟/小时。

微批处理也有好处——如果有大量空闲,运行空闲任务的资源就会被浪费,这适用于“记录到记录”的流框架。它还允许对特定的微批处理进行批处理操作,这在流式传输中是不可能的。(尽管您应该记住它仅适用于“当前”批次。)

我认为没有灵丹妙药——Spark 一直引领着“批处理工作负载”,因为它起源于处理 MapReduce 的问题,因此整体架构针对批处理工作负载进行了优化。其他流式框架从“流式原生”开始,因此应该在流式工作负载上具有优势,但在批处理工作负载上不太理想。统一批处理和流式处理是一种新趋势,有时一个(或几个)框架可能会在两种工作负载上提供最佳性能,但我不确定现在是时候了。

编辑:如果您的工作负载以“端到端恰好一次”为目标,则延迟绑定到检查点间隔,即使对于“逐记录”流框架也是如此。检查点之间的记录构成一种批处理,因此检查点间隔将是您新的主要作业。

编辑2:

Q1) 为什么windows 聚合可以让我减少延迟问题?也许真的想足够快地更新有状态操作。

与窗口性质带来的延迟相比,微批处理和逐记录之间的输出延迟不会显着(即使是微批处理在某些极端情况下也可以达到亚秒级延迟)。

但是,是的,我假设只有在窗口过期时才会发生发射(结构化流中的“附加”模式)。如果您想在窗口发生变化时发出所有更新,那么可以,延迟角度仍然存在差异。

Q2) 为什么语义在这种权衡中很重要?听起来它与 Kafka-Streams 相关,例如,当配置了完全一次时,它会减少提交间隔。也许您的意思是,为了获得更好的语义,可能一个接一个地检查点会增加开销,然后影响延迟?

我不知道 Kafka 流的详细信息,所以我的解释不会基于 Kafka 流的工作原理。那将是你的功课。

如果您正确阅读了我的答案,您也同意流式框架不会为每条记录执行检查点 - 开销会很大。也就是说,两个检查点之间的记录将是同一组(某种批次),当故障发生时必须重新处理。

如果有状态恰好一次(有状态操作正好一次,但输出至少一次)对您的应用程序来说已经足够了,那么您的应用程序只需将输出写入接收器并立即提交,以便输出的读者可以立即读取它们。延迟不受检查点间隔的影响。

顺便说一句,有两种方法可以只实现一次端到端(尤其是接收端):

  1. 支持幂等更新
  2. 支持事务更新

情况 1) 立即写入输出,因此不会通过语义影响延迟(类似于至少一次),但存储应该能够处理 upsert,并且在发生故障时会看到“部分写入”所以你阅读器应用程序应该容忍它。

情况 2) 写入输出但在检查点发生之前不提交它们。流框架将尝试确保仅在检查点成功并且组中没有失败时提交和公开输出。有多种方法可以使分布式写入成为事务性的(2PC,协调器执行“原子重命名”,协调器写入任务写入的文件列表等),但无论如何读者都看不到部分写入直到提交发生,因此检查点间隔将极大地影响输出延迟。

Q3)这不一定能解决关于 Kafka 客户端轮询的一批记录的问题。

我的回答解释了一般概念,即使在轮询请求中提供一批记录的源的情况下也适用。

  • Record-by-record:源不断地获取记录并发送给下游操作员。Source 不需要等待下游操作符对先前记录的完成。在最近的流式处理框架中,非洗牌操作员将在一个任务中完全处理 - 对于这种情况,这里的下游操作员在技术上意味着有一个下游操作员需要“洗牌”。
  • 微批处理:引擎为微批处理规划新的微批处理(源的偏移范围等)和启动任务。在每个微批处理中,它的行为与批处理类似。

推荐阅读