正文
接下来,我们将讨论了容错流式架构的几种方法,从记录确认 (record-acknowledgements) 到微批处理 (micro-batching),事务更新(transactional updates)和分布式快照(distributed snapshots)。 我们将从以下几个维度讨论各个系统的优缺点,同时最终选出一个适合流式处理的最优 Feature 组合。 我们将讨论:
-
完全一次保证:故障后应正确恢复有状态运算符中的状态
-
低延迟:越低越好。 许多应用程序需要亚秒级延迟
-
高吞吐量:随着数据速率的增长,通过管道推送大量数据至关重要
-
强大的计算模型:框架应该提供一种编程模型,该模型不限制用户并允许各种各样的应用程序在没有故障的情况下,容错机制的开销很低
-
流量控制:来自慢速操作员的背压应该由系统和数据源自然吸收,以避免因消费者缓慢而导致崩溃或降低性能
我们遗漏了一个共同特征,即失败后的快速恢复,不是因为它不重要,而是因为(1)所有讨论的系统都是基于完全并行的分布式处理系统,恢复是基础能力;以及(2)在有状态的应用程序中,状态恢复的瓶颈通常在于存储而非计算框架。
Record acknowledgements (记录确认,代表系统 Apache Storm)
虽然流处理已经在诸如金融等行业中广泛使用多年,但直到最近流式处理才能为大数据的基础设施的一部分。 这些都得益于开源的流式大数据处理引擎成熟和发展。 Apache Storm 是开源生态中第一个广泛使用的大规模流处理框架。 Storm 使用上游备份机制和记录确认机制来保证在失败后重新处理消息。 请注意,Storm 不保证状态一致性,任何可变状态处理都委托给用户来处理(Storm 的 Trident API 确保状态一致性,将在下一节中介绍)。
译者注: 以下内容理解需要读者一定的 Apache Storm 基础,请参看 Apache Storm 官方文档有关 Storm 关键概念的描述。
记录确认的容错方式如下:当前 Operator 处理完成每条记录时都会向前一个 Operator 发回针对这条记录处理过的确认。
Topology 的 Source(译者注: Storm 的 Source 节点指 Storm 一个作业中负责从流式源头读取数据的 Operator) 会保留其产生的所有记录备份用来处理 Fail 情况。 当源头一条记录的所有派生记录都被整个 Topology 处理完成,Source 节点就可以删除其备份;当系统出现部分 Fail 情况,例如一条记录并没有收到其下游的派生记录的确认,Source 就会重新发送该记录到下游的 Topology 以便重新进行计算。 这种处理机制可以保证整个处理过程不会丢失数据,但很有可能导致同一条记录被多次发送到下游进行处理(我们称之为“at least once”)。 Storm 使用一种巧妙的机制来实现这种容错方式,每个源记录只需要几个字节的存储来跟踪确认。 Twitter Heron 保持与 Storm 相同的确认机制,但提高了记录重放的效率(从而提高了恢复时间和整体吞吐量)。
单独的记录确认容错体系结构,无论其性能如何,都无法提供 exactly-once(精确一次) 的保证,Storm 将规避重复数据的问题交给了流式处理应用开发者去处理。 当然,对于某些应用程序而言,数据小部分重复可以接受的,但仍然有更多的场景无法接受数据不准确的情况。另外,Storm 的容错机制还带来了吞吐不够以及流控问题, 特别是在 backpressure(反压) 情况下,记录确认的容错方式会导致上游节点错误地认为数据处理出现了 Fail(实际上仅仅是由于 backpressure 导致记录处理不及时,而无法 ack)。上述 Storm 的种种问题最终演化出基于微批处理的流式架构。
Micro batches(微批处理,代表系统 Apache Storm Trident,Apache Spark Streaming)
上节讨论到,Storm 和以及更早前的流式传输系统无法提供对大规模应用程序至关重要的一些 Feature,特别是高吞吐量,快速并行恢复,以及托管状态的一次性语义。 这导致了下一阶段的流式系统演化。
之后,具备容错能力的下一个发展阶段到了微批处理,或者说流离散化 (stream discretization,即将连续的流切分为一个个离散的、小批次的微批进行处理)。这个出发点非常简单:流式处理系统中的算子都是在 record 级别进行计算同步和容错,由此带来了在 record 如此低层次上进行处理的复杂和开销。很简单嘛,我们就把连续的数据流不要切分到 record 级别,而是收敛切分为一批一批微批的、原子的数据进行类似 Batch 的计算。这样,每个 batch 的数据可能会成功或者失败处理,我们就对当前失败的这一小批数据进行处理即可。
微批处理本质上一种批处理模型,显然可以利用现有的批处理引擎就可以完成流式计算。例如,可以在批处理引擎(Spark)提供流功能(这是 Spark Streaming 背后的基本机制),当前它也可以应用于流引擎之上(例如, Storm)提供一次性保证和状态恢复(这是 Storm Trident 背后的想法)。 在 Spark Streaming 中,每次的微批量计算都是一个 Spark 作业,而在 Trident 中,每个微批次都是一个大型记录,微批次中的所有记录都会合并进入一个大型记录。
基于微批处理的系统可以实现上面列出的相当多的需求(确切一次保证,高吞吐量),但它们还有很多不足之处:
-
编程模型:为了实现其目标,例如,Spark Streaming 将编程模型从流式更改为微批处理。 这意味着用户不能再在检查点间隔的倍数之外的时段中窗口数据,并且模型不能支持许多应用程序所需的基于计数或会话窗口。 这些都是应用程序开发人员需要的需求。具有可以改变状态的连续运算符的纯流模型为用户提供了更大的灵活性。
-
流量控制:使用基于时间的数据切分为微批的处理方式仍然具有 backpressure 固有问题。 如果某个下游的 Operator 处理较慢(例如,计算密集型 Operator 处理性能跟不上或者向外部存储写出数据较慢),此时如果负责数据流切分的 Operator 速度快于下游的阻塞节点,就会导致数据切分比原有的配置时间更长。 这导致越来越多的批次在内存排队等待被处理,最终内存 OOM,或者微批的时间间隔增大导致数据不精确。
-
延迟:微批处理显然加大了流计算延迟,一个微批作业的延迟最好情况也只能到微批的间隔时间。 通常情况下,亚秒级别的延迟对于一些简单应用程序足够,但一个较为复杂的流式处理任务,例如单个作业内部存在多个阶段,每个阶段存在大量分布式数据 shuffle 情况,很容易将整个作业延迟拉长的数秒甚至数十秒。