前言:
目前姐妹们对“启发式算法缺点”大概比较关注,咱们都想要了解一些“启发式算法缺点”的相关知识。那么小编同时在网上汇集了一些对于“启发式算法缺点””的相关资讯,希望同学们能喜欢,兄弟们一起来了解一下吧!按:去年此时发表了一篇文章 《流计算引擎数据一致性的本质》,主要论述了流计算引擎中的数据一致性问题,事实上,该文章只能算作流计算数据一致性的上篇,如何通过流计算中得到真正准确、符合业务语义的数据,需要作进一步阐述。强迫症接受不了这种半拉子工程,所以今年还是陆陆续续把下篇(流计算引擎数据正确性的挑战) 撰写完成。上下两篇文章的主要论点,分别对应了流计算领域中的两大难题:端到端一致性和完整性推理。
流计算为实时数仓、数据湖、实时业务指标、商业智能等提供了一个快捷的数据构建管道,在人们的认知中,流计算提供的主要增益是速度(低延迟),其次才是正确的数据结果。在流计算这个领域,得到正确的计算结果并不容易,它需要精巧的引擎设计、准确的业务数据定义、上下游系统的配合等。本文介绍了在流计算中数据正确性的内涵和为了解决此问题所面临的挑战,以此引出流计算引擎的关键设计:流数据完整性推理,本文首次给出了完整性的形式化定义和各种引擎上的泛化应用。同时,文章也会对比目前主流几款流计算引擎,如 Flink、Kafka Streams 等在数据完整性推理上所做的设计和取舍。希望通过此文章,能为大家使用流计算引擎提供更多输入,为获得正确计算数据提供若干实践参考。
一、流计算中的正确性
正确(Correctness)的数据指的是:通过流计算引擎计算得到的结果正确地反映了真实物理世界中的对象。如用户在一段时间内连续支付了三笔账单共计100元,对于「用户历史支付总额」这个指标来说, 当用户支付行为完成后,我们应当观察到有且仅有一份指标,且其数值为100。由于流元素无界无序的特性,上述这种逻辑上的推导,在流计算中并不容易实现,可能会存在一些「不正确」的结果,如指标值小于 100,存在多个指标值等。为了得到精确的结果,目前在工程实践中还大量存在一种将流批结合起来的 Lamda 架构,将实时构建的增量数据和通过批处理得到存量数据组合,为下游业务提供更为快速且质量可控的数据。
不正确的数据可能会影响决策者(人或机器)对业务和市场的判断,导致缓慢或错误的商业决策。虽然目前有很多流批一体的计算模式实践,但这些解决方案更多地是在解决诸如传统 Lamda 计算架构在平台运维、计算资源上的复杂性,因流计算本身导致的数据质量问题,并没有因为流引擎的发展发生有太多的改善。下面一部分将会分析流计算中导致数据处理不正确的原因,以及为了得到正确结果所需要的充要条件。
二、实现正确性的充要条件
流计算的本质是通过异步消息的方式进行分布式计算,但由于时钟同步、网络延迟、服务器挂起(如垃圾回收)等原因,流数据产生的逻辑顺序,和到达流处理系统或流处理系统算子的物理顺序通常是不一致的,流计算引擎必须能够处理无序数据,否则会导致不正确的计算结果。
2.1 一致性不等于正确性
数据计算领域有两个概念:数据的一致性和数据的正确性,它们很容易被混淆,但从概念本身来看,数据一致性≠数据正确性,两个概念的定义如下:
1.正确性:流计算的结果正确地反映了真实物理世界中的对象。
2.一致性:跨越所有流计算上下游系统的数据反映了相同的信息。
一致性通常和「exactly-once」这个术语在一起,表示流计算引擎可以从故障恢复到一致的状态,最终的计算结果不包含重复的输入项或丢失任何数据。换句话说,流计算引擎的输出结果就像数据只处理一次而没有任何故障发生一样。正确性的要求比一致性更为严格。如果引擎无法实现一致性地数据处理,则正确性也是无法实现的,如果数据是正确的,则必定是一致的,即数据一致性是数据正确性的必要非充分条件。譬如,流数据源中的4条记录,由于引擎故障/不正确地处理等,在引擎内部总共被处理了5次,则结果也可能是不正确的。反过来,如果引擎实现了一致性,则也可能由于源头数据无序、延迟等问题,最终得不到正确的结果。
计算正确性由数据完整性和引擎一致性共同保证。如果将流计算过程视为一个函数映射:output = f(input),上述模型中,数据完整性是对于无界、无序数据集的约束,即明确了输入 in,引擎数据一致性则明确了数据处理过程(包括输出)f,因此输出就是确定的(引擎端到端一致性,可参考《流计算引擎数据一致性的本质》一文)。
2.2 流数据完整性的必要性
什么是流计算中的数据完整性呢?由于流计算引擎中的数据是无界且无序的,因此我们必须以某种方式将这一类不确定性的数据集,转化为逻辑上的「当前分区」,以便对分区内确定性的数据片段进行分析,「当前分区」可以是过去的一段时间(e.g. 滑动窗口),也可能是过去的若干条记录。从另一个角度来看,无论是流计算还是批计算,要想得到正确的计算结果,最终都需要有确定性的输入数据集,批计算模式只是一个分区涵盖了整个有边界的数据集的一种特例场景而已。目前流计算引擎被外界诟病「不够准」的原因,很大程度上是因为现有的技术方案在对无界无序流数据分区能力不够好导致的。
完整性推理是一种表征数据就绪的手段,即使输入流可能无序到达,流计算引擎也不会用不完整的输入计算作为最终的输出结果。完整性要求计算引擎能够及时追踪当前计算进度,并估算发出的输出结果与其输入流对应的完成程度。这种对数据完整性的推理,在很多流计算场景都是至关重要的:如在基于流的告警系统中,流计算引擎必须生成单个且正确的告警指标,提前发出部分结果是没有意义的,这就要求流计算引擎这种分布式系统要具备一种能推断「告警指标所需的数据已全部就绪」的能力;又比如在通过流式 CEP 进行业务缺失值检测的场景里,如果没有完整性推理,则无法区分真实的数据缺失和数据滞后抵达这两者间的差异。
完整性推理对于引擎本身的状态管理也很有用。如 Apache Spark Structured Streaming 和 Kafka Streams 使用类似的延迟算法(事件时间-固定的过期时间),对计算过程中的状态存储进行回收,以降低内存的消耗。
三、流数据完整性的通用解法
这一部分将从原始的完整性问题出发,提出完整性的形式化表达模型,然后会在这个一般化的框架下,泛化出几个目前使用最常见的完整性推理方案。这种自顶向下的展开,可以让我们更为清晰地看到流计算领域中关于完整性推理的本质。很多我们熟知的概念(如水印),其实只是某一种框架的具体实现(What),更好的方式应该是从模型角度抽象出来(Why)。从模型而不是框架去思考,可以让我们避免被很多引擎「概念」误导。
3.1 完整性推理的形式化定义
运行在流计算引擎上的数据处理程序,一般被定义为一个有向无环图(Timely DataFlow 除外)。图中的节点表示有状态的数据处理算子(Operator),有向边表示算子之间的数据通道。数据流拓扑中的一个算子(Operator)会被映射到一个或多个物理执行节点上,这些节点分布在整个服务器集群中。进入流计算引擎中的元素称之为事件(Event),每个事件都有事件时间(Event Time)和处理时间(Processing Time)两个属性,其中处理事件指事件被处理时,引擎机器的系统时间。事件时间是事件发生的时间,通常是在事件到达引擎之前就确定的,并且可以从每个事件中获取到事件时间戳,从定义来看,事件的事件时间是天然小于处理时间的。
流计算引擎对完整性的推理能力,可简单描述为:系统需要能够产生一种完整性信号 Signal,以某种方式广播至整个数据流拓扑,拓扑中的每个算子(Operator)需要根据此信号,来同步当前自身的数据处理进度。如果定义流计算的输入集合为:E,t 时刻(处理事件)以来的输入集合为 E(t),包括正在处理的数据和缓冲的数据,引擎此时状态为 State(t),State(t) 包括各个算子的状态、数据源的消费偏移量(或文件读取偏移等)等,ET 函数表示获取事件元素的事件时间(或其他属性)。算子处理事件时,完整性意味着对于任意事件 e ∈ E(t),存在:
Signal(t) < ET(e)
算子可以通过此信号,结合事件的逻辑时间(即事件时间),推断目前输入集合 E(t) 的数据是否完整。一般地,完整性信号可以表示为「从 (t-n, t) 之间所有事件的事件时间最小值」,最小值可以通过某种算法 F 结合当前数据源输入、引擎状态等共同计算得到的,如下所示:
Signal(t) = Min{ET(e) | e ∈ E(t) - E(t-n)} = F(E(t), State(t))
理想情况下,上述完整性约束在输入集合 E(t) 随着 t 变化的整个过程中都应该满足,但这种非常强的保证在现实场景中很难实现(即算法 F),因此我们一般会放宽某种限制(如时间),在一段时间内得到完整性推断即可。
Signal(t) = F(t) = Min{Sort(ET(e(i)), ET(e(i+1))......ET(e(i+n)))}, t1 < t < t2 ;①
Signal(t) = F(t) = Min{ET(e(i)), ET(e(i+1))......ET(e(i+n))}, t < t2 -> +∞ ;②
Signal(t) = F(t) = ET(e(i)) - Δ, -∞ < t < +∞;③
上述完整性信号生成算法实现中,①表示可以取一段时间内排序后的事件时间的最小值作为信号量;②表示可以统计过去一段时间内事件时间的最小值作为信号量;③表示可以通过始终从事件时间中减去一个固定大小的数值,作为完整性的信号量。在本文的第四部分,可以看到上述三种形式化的泛化表达,分别对应了工程上三种完整性推理方案实现:重排序、低水印和宽限时间。
3.2 完整性推理的系统设计
从系统架构的角度来看,流计算引擎在实现数据完整性推理时,必要的三个模块是:生产、传播、消费。生产模块用于产生完整性信号,这里面会有一些简单的启发式算法,或一些自适应的复杂算法,算法主要结合输入源本身的一些指标,如输入事件中的事件、源消费偏移量、数据源上游生产的状态等。生产模块是完整性推理中最复杂的一部分,不同的引擎从性能、复杂度、用户体验等角度,会有不同的设计和折中。传播模块即完整性信号从产生到广播至整个数据流拓扑的过程,其过程有可能是通过在输入源注入特殊元素实现,也可能通过流元素自身携带的某些特征来实现,或者从数据流拓扑外直接传送信号给每个算子。完整性信号的消费过程比较简单,算子接受到信号后,一般用于关闭某个计算窗口或者淘汰状态。
3.3 完整性推理的工程实现
业界对流数据的完整性推理,根据其是否需要对事件重排序,大致分为两类:一类被称之为顺序处理 (In Order Processing, IOP) 流系统,该类系统对流元素进行缓冲后重排序。IOP 系统的典型代表有 Trill(微软开源)、Spark Streaming(D-Streams)、Aurora 等。另一类系统被称之为无序处理(Out of Order Processing, OOP)系统,这一类系统不会缓冲数据以强制排序,而是通过信号来追踪数据流的处理进度。本文中论述的完整性推理内容,主要针对的就是这一类系统,它们包括 MillWheel、Flink、Kafka Streams 等。
在 IOP 系统中,主要通过缓冲+重排序的方式,可以为系统提供可预测的完整性语义,其中每个事件的到来保证不会出现更早的事件,从而显着简化了顺序系统的设计。重排序方案存在的最大的问题就是会大幅提升计算时延,因为通常很难获得无序的先验时间或空间界限,同时重排序会带来一定的存储压力,需要与顺序无关的算子行为,如求和、求平均、计数等,可以不依赖严格的顺序,重排序会牺牲这一类算子的特性。
在 OOP 系统中,我们使用一个较弱的属性概念来实现完整性推理,主要方案有标记(Punctuation)、低水印(Low Watermark)、宽限时间(Slack Time)、心跳检测(Heartbeat)等。Punctuation 是一种通过数据流拓扑传递信息的通用机制,该方案的想法是在流数据中加入一些特殊的标识,来标识一段一段的数据,因此就可以从逻辑上将无界无序的流数据,划分为多个有限的数据集。Punctuation 方案的原始论文发表于2003年,由于 Punctuation 的设计地太过普适,在工程实践时成本非常高,因此后续很多引擎借鉴了 Punctuation 方案的思路,设计出了在架构复杂度、用户体验、功能完备等方面更为合理的一些方案,如低水印(Low Watermark)、宽限时间(Slack Time)、心跳检测(Heartbeat),下图展示了这展示了这三种方案的一些异同。
宽限时间(Slack Time)宽限时间是一种简单的完整性度量机制,一般用流元素的事件时间减去一个固定长度的时间(元素抵达算子时的最长滞后时间)来量化,这个固定的时间可以由用户基于实际的算子周期(如两倍窗口大小)来配置。宽限时间方案不用在流中注入特殊消息,实现比较简单,但其缺点是完整性推理能力不足。
低水印(Low Watermark)。低水印是嵌入到数据流中的特殊消息,但它不像普通的消息,它一般被表示为「可能出现在流中的最小时间戳」,低水印的思想是:当算子接收到水印时,会得到一些额外的关于流的信息:没有比水印时间更迟的数据抵达了,因此可以将窗口中的数据计算并输出到下游。低水印的生成有启发式算法(e.g. 统计 Kafka 中所有消费分区的最低偏移)和一些自适应的算法(e.g. 结合数据源特征)。
心跳检测(Heartbeat)心跳检测由携带有关数据流的进度信息的外部信号组成,信号包含一个时间戳,大于心跳时间戳的流元素构成「完整数据集」。心跳检测信号由输入源生成,也可以由系统通过观察环境参数(例如网络延迟、输入源之间的时钟偏移等)来推断。心跳检测方案的优点在于它是一种对用户隐藏的引擎内部机制,与低水印方案一样,针对不同的数据源特征,生成理想的用于完整性推理的心跳检测信号具有挑战性。
上面提及的方案中,除了适用于 IOP 系统的重排序方案,标记、低水印、宽限时间、心跳检测这四个方案之间有着微妙的联系和区别。低水印、宽限时间、心跳检测均可视为标记的一种特例实现,宽限时间简化了信号传播的路径:通过流元素本身携带的信息,结合宽限周期计算即可。心跳检测将从数据源得到的完整性信号直接送至引擎入口(Ingestion point)。心跳检测和低水印都携带了关于数据源的信息,但区别在于:低水印将「完整性」信息从数据源传送至到了输出源的整个拓扑,而心跳检测信号只有数据源的进度信息。
3.4 完整性推理和计算延迟
流处理的理念是在线处理数据,因此较低的处理延迟至关重要。实现强完整性的同时,不可避免地会带来延迟,延迟的原因主要由两部分组成:一是由于实现完整性「等待」所造成的,如在使用低水印方案的引擎中,窗口聚合的数据需要在低水印穿过水印边界时,才会将窗口的数据发送至下游;二是某些引擎(如 Cloud DataFlow)出于容错需要将完整性信号进行持久化,会导致全链路的处理时延上升。
在下游可以接受聚合结果中间值的计算中,一个平衡完整性和延迟的方案是引入周期性触发器(Trigger):重复(并且最终一致)地更新地物化窗口内的数据,类似于数据库世界中使用物化视图获得的语义,这样可以在实现完整性的同时,下游也可以及时获取到最新的计算结果。但在下游不能接受中间结果的计算中,只能输出唯一的一个结果,对于延迟数据,引擎不能无限制地等下去,当引擎接收到完整性信号将结果输出后,如果还有迟到的数据,又该如何处理呢?一般的处理策略有丢弃、重复计算或旁路处理,重复计算可以实现更好的正确性,但需要长时间保留状态,从处理延迟和存储成本考虑,只能实现有限程度的「延迟宽限」。总之,虽然目前实现完整性推理的方案较多,但把处理时延等因素考虑进来后,可以看到完整性推理之路还任重道远。
四、流数据完整性的引擎实现
目前很多流计算引擎都使用低水印进行流数据完整性推理,如 Apache Flink、Google DataFlow 就将水印置于 API 中,使其直接面向用户。与处理有界或有序数据相比,在无序数据流中理解和编码有一定门槛,从收敛(用户使用)复杂性的角度,Kafka Streams、Spark Structured Streaming 使用了弱化版的低水印(宽限时间),分别用于实现最终一致的结果,或用于管理由于迟到数据带来的状态存储成本。下面我们将结合上一章中列举的完整性推理方案和业界最成熟的几款流计算引擎,剖析不同引擎在完整性推理的功能完善度、易用性、复杂度等架构要素上的思考与取舍,从这几款引擎出发,能大致能看到众多引擎在无序数据管理上的发展趋势。
4.1 MillWheel
在具体展开 MillWheel 之前,先简单解释一下 MillWheel/Cloud DataFlow/Beam 这三者之间的关系。MillWheel 是 Google Cloud DataFlow 底层的流计算引擎(现在谷歌内部逐渐在被 Windmill 取代),它解决了数据一致性和完整性推理的难题,可以实现健壮的流式数据处理,Cloud Dataflow 最为突出的贡献是为批处理和流数据处理提供了统一的模型。Beam 主要由一个编程模型、通用 API 层和可移植层组成,底层没有具体的执行引擎,它更像是 SQL 在关系数据库中地位,尝试着成为流批数据处理的标准语言。
完整性推理过程:MillWheel/Cloud DataFlow 采用了低水印的核心思想,实现了支持事件时间处理语义的完整性推理。在生产阶段,MillWheel/Cloud DataFlow 数据流拓扑中每个算子的物理节点会追踪节点上的数据处理进度(已处理+将要处理),在将进度信息持久化到内存和本地状态的同时,定期将进度信息发送到全局水印聚合系统(Central Authority),每一个算子的低水位信息,被定义为该算子对应所有节点上报的处理进度的最小值。
传播阶段,经聚合系统计算得到的最终的低水印信息,将被发送给对应算子物理节点上,最终该节点的水印=Min(当前节点处理进度,上游全局水印最小值)。消费阶段,MillWheel/Cloud DataFlow 中的节点会通过计算得到的低水印信息,遍历之前隐式(e.g. 窗口触发)或显式(编程方式)注册的定时器(Timer),依次执行时间戳小于低水印的定时器回调,完成结果的发送或状态的更改。
架构设计折中:由于 MillWheel/Cloud DataFlow 需要在节点持久化低水印,同时更新水印时需要进行中心化上报,因此这两部分的 IO 开销,会造成流处理延迟的上升。采用中心化上报的方式,源于 Cloud DataFlow 中的算子是动态分区的,数据源非常复杂,需要更复杂的低水印生成机制来保证完整性推理。节点持久化低水印,在引入延迟的同时,带来的一个优势是 MillWheel/Cloud DataFlow 应用的 Failover 较快(状态粒度更细)。与此同时,全局的水印汇聚可以对引擎当前的数据处理进度做更好的评估,如可以在计算延迟和准确性上做一个妥协,计算得到一个 98% 的低水位线,对应于系统中 98% 的流数据的处理进度,从而整体得到更快的处理效率。
4.2 Apache Flink
Apache Flink 中的完整性推理方案,其设计思路源于 DataFlow 模型,核心还是围绕低水印设计的。
完整性推理过程:生产阶段,Flink 程序可以在源节点或专用水印生成节点中生成水印,源节点可以根据进入引擎的流数据信息,或者数据源的其他信息(如 Kafka 分区、偏移量或时间戳等)来计算水印,一些专用水印生成节点能根据它们观察到的流元素的时间戳来计算水印。传播阶段,在整个数据流拓扑中,水印作为特殊的一类元数据消息与常规流数据一起发送给下游节点,当下游节点收到新的水印消息时,它会取所有输入水印的最小值作为当前节点的水印,同时更新当前节点水印,节点仅转发大于前一个水印的水印来保证完整性信号的严格单调性。消费阶段,同 MillWheel/Cloud DataFlow 类似,当水印抵达节点时,有一系列定时器将会被触发,结果被发送到下游,新的水印值从会被广播到所有下游的节点,从而使得整个分布式应用实现了状态同步。
架构设计折中:在 Flink 中,由于节点并没有对低水印进行持久化,因此当节点发生故障时,整个 Pipeline 必须挂起,从上个检查点开始恢复,故障节点的低水位线将设置为低值常量,一旦节点在其所有输入边上收到新的低水印消息,水印才会被重新设置。不持久化低水印带来的优势是 Flink 端到端的数据处理延迟较低,缺点是 FO 的时间比 MillWheel/Cloud DataFlow 长。在完整性推理的设计上,Flink 在当今众多引擎中是最成熟的。
4.3 Apache Kafka Streams
虽然 Cloud DataFlow 和 Flink 都使用了低水印进行流数据完整性推理,但 Apache Kafka Streams 的工程师们并没有基于低水印方案去构建完整性推理机制,原因主要有以下两个方面:
其一,Apache Kafka Streams 提出了「持续增量处理流表」模型(类似 DataFlow 中的流表概念,可以参考 Streaming System 一书)中,如果通过流逐步更新表中结果,同时发出中间结果,那么何时关闭窗口的概念就变得不那么重要了(个人认为这个论点在实践中其实是存在问题的,比如下游很难界定不同结果的同一性,对于延迟处理的累积(accumulations)和撤回(retractions)逻辑的实现一般比较复杂);
其二,工程师们认为 DataFlow 模型中的低水印方案太过于复杂(比如有至少8种 Trigger 需要结合水印使用),需要有更为简洁和直观的完整性解决方案提供给用户使用。基于以上两点,Apache Kafka Streams 使用了上面提到的宽限时间来解决完整性问题。
完整性推理过程:Apache Kafka Streams 没有在流中嵌入特殊的元信息,也不依赖于系统级的低水印时间戳,而是允许通过在每个算子上配置宽限期来进行细粒度的完整性确定。生产阶段,每一个事件流经算子时,算子会使用「事件时间-Slack Time」作为完整性信号,随着流数据的持续流入,事件时间也随之增加,这样就可以得到类似「低水印」的进度推断。传播阶段,由于信号可以由事件本身携带的信息计算得到,因此就没有信号的传播过程,当然这样会存在一些问题,例如当上游算子过滤掉了大量数据,下游的算子可能会因为长时间没有收到数据,而无法及时推进当前算子的流处理进度,这样一定程度上加大处理延迟。消费阶段,当由宽限时间计算得到的时间戳大于窗口上界时,窗口会被关闭,状态将会被释放。
架构设计折中:如果把 Cloud DataFlow 和 Flink 的低水印方案稍泛化一下,则可以清晰地看到,这里的宽限时间可以视为水印方案的一个子集,即类似「高水印」这样的设计。宽限时间方案的优势是,由于将低水印的概念从全局拓展到了算子级别,因此我们可以解耦不同算子的等待时间,如6小时的聚合窗口和10分钟的聚合,一般来说其需要等待乱序数据的时间是不一样的,通过不同的宽限时间设置,可以实现更灵活的同步控制。
宽限时间的缺点主要是由于「信号使用了当前事件信息」这一点带来的,除了上面提到的因为过滤下游算子无法及时获取处理度的缺点,由于数据流拓扑缺乏全局的同步特性,某些场景下这种推理可能会造成结果的不正确。假设聚合算子上游的某个节点由于 GC 或宿主机压力等原因,处理的进度有所滞后,但其他节点处理正常,由于聚合算子收到的事件时间在逐渐增加,因此当超过宽限期后,窗口会关闭,当滞后节点赶上处理进度时,发送给下游聚合算子的数据已经无法使用了(窗口已经关闭了)。为了解决上述几个问题,Apache Kafka Streams 计划也使用 DataFlow 模型类似的方式,在流数据注入携带进度信息的元数据来实现进度追踪,但元数据不一定是简单的时间戳,可能是类似 Vector Clock 这样的方案,在实现细粒度算子控制的基础上,实现全局数据流图同步。
4.4 Apache Spark Streaming
由于 Spark 中的流处理肇始于批处理设计,因此其对如端到端一致性、时间事件的支持并不太好。从 Spark 2.1 开始,在新的 Apache Structured Streaming API (SPARK-18124)中才引入了基于宽限时间的类水印的数据完整性方案,用户通过指定事件时间列和延迟数据的宽限时间,可以对延迟数据进行管理,以便于引擎控制流状态的内存使用,如丢弃延迟事件,并删除永远不会更新的旧状态(e.g. 聚合或连接)等。
完整性推理过程:Spark Structured Streaming 中的水印是全局的,在每个批次计算触发结束后,水印都会被重新计算,具体计算逻辑为:新的水印 = MAX(触发器执行前的看到的最大时间戳,触发器执中数据中的最大时间戳)-宽限时间。在存在有多个输入源的场景中,Spark Structured Streaming 会跟踪每个输入流的情况,单独计算出水印,然后选择最小值作为全局水印。基于全局水印,Spark Structured Streaming 可以维护到达的数据的状态,通过将其与迟到的数据聚合来更新它,小于水印的延迟数据将被聚合,超过水印的数据将被丢弃。
架构设计折中:由于 Spark Structured Streaming 水印的设计初衷是用于计算中的状态管理,因此完整性推理的能力比较薄弱。譬如,由于使用了全局水印,则无法进行在一个数据流拓扑中进行链式聚合(下游聚合算子的输入是上游聚合算子的输出),可能会导致不正确的聚合结果。此外,在水印 API 的设计上,其适用范围也有限,如水印的时间列必须和后续的分组列保持一致,这大大限制聚合算子的功能。
4.5 引擎完整性实现方案对比
下面从完整性推理信号的生成、传播等角度,对比上面提到的四种流计算引擎的实现。
五、总结和展望
通过流计算引擎计算得到正确的结果,需要在引擎一致性和数据的完整性上共同努力。引擎一致性的实质是分布式应用的容错问题,目前在工程实践中已经有体系化且成熟的方案,各引擎发展的方向也是相似的的,即「事务性地输出结果(状态)」。
数据完整性保证了无序无界数据在流计算中有确定性的数据集,在需要单个聚合结果、缺失检测、增量处理等场景中至关重要。如何实现数据完整性推理,是目前流计算领域中最难被用户理解和且完成度不是很好的几个问题之一,完整性推理的本质是需要有一种完整性信号生成算法Signal,使得对于任意的事件 e 和处理时间 t,有 Signal(t) < e 的事件时间。
在目前的工程实践中,完整性推理方案有重排序、标记、低水印、宽限事件、心跳检测等,其中低水印和宽限时间使用最广泛,这两种方案大多是启发式的,只能给出尽可能精确的完整性估计,从现实成本和实际需求考虑,目前还不存在绝对正确的完整性推理算法。
并不是所有的流计算引擎都能得到正确的结果,了解实现数据完整性所的各种约束,对我们进行正确的技术选型至关重要。MillWheel 和 Flink 使用了低水印去实现整个数据流拓扑处理进度的同步,低水印以特殊流元素或旁路传播的方式,让整个数据流拓扑中的每个算子都可以清晰地了解到当前数据处理的进度。Spark Structured Streaming 和 Kafka Streams 采用了宽限时间方案,简化了架构构建和维护的复杂性,降低用户对完整性的理解成本,但与此同时,这两个引擎在完整性推理功能也存在一些不足。
在绝对正确的完整性和延迟之间存在悖论的情况下,流计算引擎也在尝试从不同的方向去优化现在的技术方案:如通过数据源的生产方(而不是引擎)发送完美水印来降低引擎设计成本、内建若干自适应的引擎水印算法以改善启发式算法的不足等。与此同时,相较于引擎一致性,数据完整性这个概念离用户更近,很多完整性的定义需要用户共同参与去完成,在我个人看来,目前众多引擎中的完整性解决方案,对于引擎框架的使用者来说还是存在较高使用门槛的,如何为框架用户提供一种更友好方式来表达实现正确结果所需的指标约束,是需要解决的关键命题。假以时日,如果上述问题能得到较好解决,真正的流批一体、流计算超越批处理才能成为现实。
作者:唐烨(齐光)
来源:微信公众号:阿里开发者
出处:
标签: #启发式算法缺点