流与表的对偶性模型

流与表:同一枚硬币的两面

马蒂亚斯·J·萨克 斯∗Confluent公司帕洛阿 尔塔,美国 matthias@confluent.io

王国章Confluent公 司帕洛阿尔托,美国 g uozhang@confluent.io 马蒂亚斯·魏德利希柏林洪 堡大学德国柏林 m atthias.weidlich@hu‐berlin.de
约翰‐克里斯托夫·弗赖塔格柏 林洪堡大学德国柏林 fr eytag@informatik.hu‐berlin.de

流处理已成为一种范式,适用于需要对无界数据序列进行低延迟操作符评估的应用。在存在分布式数据源的情况下,定义流处理的语义具有挑战性。在此类场景中,流中的数据物理和逻辑顺序可能变得不一致。现有模型要么忽略这些不一致性,要么通过数据缓冲与重排序技术来处理,从而影响了处理延迟。
本文中,我们引入双流模型来探讨数据流处理中的物理和逻辑顺序。该模型将操作符的结果表示为连续更新的流,从而形成了结果与流的对偶性。因此,它提供了一种自然的方式来以连续方式应对流式数据中物理顺序与逻辑顺序之间的不一致性,而无需显式缓冲与重排序。我们进一步讨论了在正确性、延迟和处理成本方面实现该模型时所面临的权衡与挑战。基于Apache Kafka的案例研究展示了该模型在满足实际需求方面的有效性。

CCS概念

•信息系统→数据流;流管理;

∗附加单位:柏林洪堡大学,mjsax@informatik.hu‐berlin.de
允许出于个人或课堂教学目的,免费复制或打印本文部分或全部内容, 但前提是不得以盈利或商业利益为目的进行复制或分发,且所有复制 品均须在首页注明本声明及完整引用信息。本作品中第三方组件的版 权必须予以尊重。对于所有其他用途,请联系版权所有者/作者。
BIRTE’18,2018年8月27日,里约热内卢,巴西©2018年版权由版权 所有者/作者持有。ACMISBN978‐1‐4503‐6607‐6/18/08。
https://doi.org/10.1145/3242153.3242155

关流处键理词,处理模型,语义

美国计算机协会参考格式:
马蒂亚斯·J. Sax,王国章,马蒂亚斯·魏德利希,和约翰‐克里斯托弗· 弗赖塔格。2018年。流与表:同一枚硬币的两面。收录于国 际实时商业智能与分析研讨会(BIRTE’18),2018年8月 27日,巴西里约热内卢。美国计算机协会,美国纽约州纽约 市,10页。https://doi.org/10.1145/3242153.3242155

1 引言

流处理已成为开发实时应用的一种范式。它基于对无界 数据序列的连续操作评估,支持以连续方式实现大规模 数据的低延迟处理[11]。因此,流处理范式被证明特别 适合在大型组织中支撑业务逻辑的实现。它通过异步消 息传递[19],为大型系统中独立组件(即“微服务”) 之间的通信提供核心支撑。

然而,定义流处理的语义具有挑战性。人们需要应对由数据流的特性、操作符的表达能力以及实现流式模型时面临的权衡所带来的挑战:

数据相关挑战 :现代流处理应用的数据源本质上是分布式的(例如,在物联网用例中),并且通常会为流数据分配时间戳,从而形成逻辑顺序。然而,由于时钟同步不完美、网络延迟或数据源在断开连接期间缓冲数据,数据到达流处理系统的物理顺序可能与此逻辑顺序不一致[25],[4](例如,手机在飞行期间产生数据并在重新连接到网络后推送这些数据)。因此,流处理模型必须能够处理乱序数据到达。

操作符相关挑战 :流处理模型应避免隐式的操作符语义,以实现确定且定义明确的处理结果[12]。特别是,操作符可能是有状态的,并且需要基于分配给流中数据的时间戳,即数据流的逻辑顺序[6]。此外,数据流的无限性意味着处理成本、延迟和结果完整性之间存在权衡。通过显式地结合操作符属性(例如,可分配的、代数的和整体函数[13])对这种权衡进行建模,可以帮助用户理解系统行为。

系统相关挑战 :流处理的理念是数据的在线处理,因此低处理延迟至关重要。因此,在实现流处理模型时,无法依赖集中式算法来实现操作符的分布式评估。同时,应避免数据缓冲(例如,重新排序数据以解决逻辑顺序与物理顺序之间的不一致性)[1, 27]应避免数据缓冲,因为它会损害处理延迟[17]。对于可能延迟数小时的记录(例如,飞行期间断开连接的手机),将处理延迟与乱序数据的处理解耦至关重要。

在过去的几十年中,文献中提出了大量的流处理模型。然而,我们认为现有模型仅针对上述挑战的一部分。
例如,关于连续查询语言(CQL)的开创性工作[5]为关系操作符提供了明确定义的语义,但忽略了由乱序数据引发的问题。其他模型则专注于处理无序数据,却未能解决其他一些相关挑战:流标点[27]会导致处理延迟增加;Borealis[2]的“时间旅行”机制存储了流的历史,模糊了在线数据处理的概念;而现有的更新/删除数据流[7]模型缺乏对操作符的形式语义,或未应对分布式处理[17, 21]的挑战。因此,我们的研究聚焦于以下问题:如何设计一种处理模型,以支持在可能无序的数据流上评估表达性操作符,并可通过分布式、在线算法实现。

在本文中,我们提出了双流模型来回答上述问题,并将现有方法统一到一个整体模型中。该模型的核心思想是将操作符的结果(通过表的关联概念表示)表示为连续更新的流。这引发了表与流的对偶性,使得能够对流式数据的物理和逻辑顺序之间的不一致性进行推理。

更具体地说,在下一节介绍数据流的背景之后,本文的贡献以及论文结构总结如下:

  • 我们提出了双流模型(第3节)。它提出了流与表的对偶性(代表操作器结果)。后者由变更日志流建模,从而能够对数据的逻辑顺序和物理顺序中的不一致性进行推理。
  • 我们展示了该模型如何实现对广泛流式算子的语义定义(第4节)。我们还强调了我们的新模型如何在正确性、延迟和处理成本方面显式地体现实现上的权衡。
  • 我们展示了模型的有效性(第5节)。为此,我们报告了一项案例研究,通过在ApacheKafka中实现我们的模型来满足实际需求。
  • 最后,我们在第6节中结合相关工作回顾了我们的贡献,然后在第7节中结束本文。

2 背景

本节将从数据模型和操作符的角度回顾数据流处理的基本概念。

2.1 数据流

数据流是不可变的仅追加序列记录[1, 6]。我们采用一种模型,其中每条记录包含一个偏移量、一个时间戳和一些负载。
记录的偏移量是其唯一的序列号,在追加到数据流时分配(可能由某个中间件层完成)。来自多个源的记录按照其到达顺序以交错方式追加,使得用自然数表示的偏移量为数据流的所有记录定义了一个严格的全序。

每条记录都有一个由其源分配的时间戳(有时称为事件时间或应用时间)。我们用自然数来建模时间。多条记录可以具有相同的时间戳,但具有相同时间戳的记录数量是有限的。

为了捕获记录的负载,我们采用基于键值对的模型。其中可选的键用于标识记录所携带的特定数据,该数据由其值来表示。我们分别用 DK和 DV表示键和值的域。值可以是标量、复合数据类型(如关系元组)或嵌套数据类型(如基于XML的数据类型)。

形式上,我们将记录r建模为一个元组o,t,k,v ∈ N0× N0 × DK × DV。记录的偏移量、时间戳、键和值分别表示为r.o, r.t,r.k和r.v。

数据流表示为S=(r0,r1,r2,...),其中ri是一条记录,具有ri.o= i。流中的所有记录必须具有相同的模式 S= DK × DV,这也称为流的模式。具有模式 S 的数据流S 表示为S[S],所有此类流的域是SJSK。

记录记录记录记录记录 第1个第2个第3个第4个第5个
| 1|2|3|4|5||
| —|—|—|—|—|—|
| 5|6|6|3|8||
| A|B|A|B|B||
| 7.2|14.7|8.9|12.1|16.7||
偏移量:
时间戳:
Key:
值 处理流
到追流加

数据流通过向初始为空的序列追加记录而创建。设S= (r0,...,rn)是一个具有模式 S的数据流。给定一个符合 S 的时间戳和键值对组成的元组t,k,v,将一条新记录追加到 S,从而得到流S′=(r0,...,rn,rn+1),其中rn+1= n+ 1, r.t,r.k,r.v。

我们的模型的基本概念如图1所示。相应的数据流包含五条记录,其中DK={A,B}为键的域, DV= Q为值的域。例如,流中的第二条记录表示为2,6,B,14.7。请注意,记录通过其偏移量进行编号。

我们的模型定义了数据流中记录的物理顺序和逻辑顺序。前者是由偏移量(因此也称为偏移顺序)决定的严格全序,表示记录追加到流中的顺序。而逻辑顺序则是由记录的时间戳所决定的。由于时间戳与偏移量不同,不一定唯一,因此我们使用记录的偏移量作为“决胜依据”[15],以在所有记录上得到一个严格且全序的逻辑顺序。

定义2.1(逻辑记录顺序) 。设r和r ′为数据流S中的记录。r记录早于(或在......之前)记录r′,记作r< r ′,当且仅当:
r.t< r′.t ∨(r.t= r′.t ∧ r.o< r′.o)
我们还说r′晚于(或在......之后)r。

记录追加到流中的物理顺序可能与其逻辑顺序[6]不一致(参见图1)。在实践中,这种乱序数据非常常见,通常由时钟同步不完美、网络延迟或数据缓冲引起,尤其是在分布式应用中[4]。我们将包含乱序数据的流称为无序流,以区别于有序流。

2.2 流式算子

上述仅追加数据流模型隐式定义了物理存储和处理模型。数据按偏移顺序存储,并通过对输入数据流进行单次线性扫描来执行处理。这一点非常重要,因为它意味着记录将按物理顺序(即记录追加到流中的顺序)进行处理。

根据文献,我们考虑诸如filter、projection、 map和flatMap[1]这类无状态操作符。它们被应用于输入流中的每条记录,结果被追加到输出流中(与 map不同,flatMap每条输入记录可能产生零条、一 条或多条输出记录)。由于操作符按偏移顺序应用,因此它们保持物理顺序:记录以与输入流中原始记录相同的顺序被追加到输出流中。

此外,我们还考虑了诸如聚合(agg)和连接( join)等有状态操作符。有状态操作符比无状态操作符更具表达能力,因为它们的计算逻辑在处理过程中可以考虑多个记录而不仅限于单个记录。状态用于“记住”先前的记录或维护一个(部分)结果。对于每个输入记录,并基于当前状态,操作符可能会产生输出记录和/ 或更新其状态。例如,一个用于计数记录的聚合操作符,其状态为当前计数;每处理一条记录,操作符将计数加一,并相应地更新其状态。

3 流与表的对偶性

在大多数现有的流处理模型中,操作符直接生成输出数据流,而操作符状态的处理被视为一种实现细节。也就是说,诸如CQL[5] 这样的模型忽略了数据流的物理和逻辑顺序之间的不一致性可能对操作符状态的计算以及最终输出流产生的影响。乱序记录在流处理模型中被忽略,需要在技术层进行处理。实践中,这要求延迟操作符的评估,直到记录的逻辑顺序被确定。因此,输出流的计算延迟随着记录的“最大延迟”呈线性增长 [17, 24],,即乱序记录r的时间戳与所有偏移量小于r的偏移量的记录中最大时间戳之间的差值。因此,延迟主要由数据流的特性决定,而非操作符的语义和实现。

为了克服此类依赖性,我们提出了一种操作符结果持续更新的模型[8, 18]。这使得我们能够放弃对记录的逻辑顺序和物理顺序一致性的任何假设。此外,操作符的结果既可以静态地看待,即作为处理到某个偏移量的流所产生的物化结果,也可以动态地看待,即作为连续更新的流。这两种视角仅引出了关于时间的编程两种方式:可以处理结果更新的流,或持续查询物化的结果,以实现相同的计算逻辑。

上述思想在我们的双流模型中得以实现,该模型包含表、表变更日志流和记录流的概念。

操作符当前结果的静态视图由一个表给出,该表会在每条输入记录处理后进行更新。根据第2节中定义的模型,表具有主键属性。例如,对于聚合操作符,分组条件(如时间窗口或分组依据子句)定义了表的主键。如果处理一条乱序记录,则会像处理有序记录一样,针对相应的键对表进行更新。因此,处理乱序记录不会增加操作符评估的延迟:更新一个“较早”的条目并不比更新一个“较新”的条目更耗时,因为两者都只是在哈希表中进行一次键值查找。

操作符结果的动态视图是一种表变更日志流,由对表的更新记录组成。更新的语义基于记录的键和时间戳定义,即具有特定键的较晚记录会更新具有相同键的较早记录。因此,在表变更日志流中,记录的键具有主键语义,重放表变更日志流可以将操作符结果物化为一张表。我们将得到的关系称为流与表的对偶性(图2)。

将表变更日志流应用于初始为空的表,会得到一个每键对应最新结果的表。然而,对于具有时态语义的流处理操作符,我们需要推理表在时间上的内容变化。因此,我们认为表同时维护多个版本:表是一组表版本的集合;在每个时间点上使用时间戳作为版本号对应一个版本。编号为t的表版本表示为Tt。随着输入数据被处理,时间推进,表也随之演化:新的表版本被添加,如果处理了乱序数据,旧的表版本可能被更新。为了推理表的状态,我们为其分配一个生成编号。对于每条被处理的输入记录,生成编号递增一。因此,表的生成编号与最新已处理输入记录的偏移量相同。我们将生成编号为o的表T记作T(o)。注意,每个表生成可能包含多个表版本。

示例 3.1(演化的表) 。假设图1中的流为表变更日志流。物化该流将实现表的演化从第1代到第5代的表,包含表版本T3,T5,T6,T8(更新已高亮):
T(1)={T5={⟨A, 7.2⟩}}
T(2)={T5={⟨A, 7.2⟩}, T6={⟨B, 14.7⟩}}
T(3)={T5={⟨A, 7.2⟩}, T6={⟨A, 8.9⟩, ⟨B, 14.7⟩}}
T(4)={T3={⟨B, 12.1⟩}, T5={⟨A, 7.2⟩, ⟨B, 12.1⟩}, T6={⟨A, 8.9⟩, ⟨B, 14.7⟩}}
T(5)={T3={⟨B, 12.1⟩}, T5={⟨A, 7.2⟩, ⟨B, 12.1⟩}, T6={⟨A, 8.9⟩, ⟨B, 14.7⟩}, T8={⟨A, 8.9⟩, ⟨B, 16.7⟩}}

请注意,在从第3代到第4代的过程中处理了一条乱序记录。因此,不仅创建了新的表版本3,而且T5也已更新。

最后,我们的模型包含记录流,其中记录表示事实(而不是像表变更日志流中的更新)。例如,可以使用记录流来捕获用于网页监控的点击流:单个用户的每次点击都由流中的一个记录来捕获。

记录流和表变更日志流表示第2节中介绍的数据流的特殊情况,参见图3。对于建模不可变事实的记录流,每条记录在整个流中必须具有唯一键。我们引入一个特殊的键 ⊥with⊥, ⊥,用于表示记录流中记录的唯一键(r.k= ⊥)。对于表变更日志流,我们假设每条记录都有一个键,即r.k, ⊥,并且唯一键的数量是有限的。这意味着从表变更日志流物化出的表是有限的。

4 流处理操作符

在本节中,我们将介绍模型中的流处理操作符。我们根据操作符的属性区分不同类型的操作符。操作符可能是无状态或有状态的,可能具有一个或多个输入流,并且可能仅定义在特定类型的输入流上。图4总结了不同操作符在各种流/表之间的所有转换。记录流通常被转换为记录流(第4.1节),但聚合操作符除外,它生成结果表(第4.2节)。表总是被转换为新表(第4.3节),除非与流进行连接,此时结果为输出流(第4.4节)。

此外,如第3节所述,表和表变更日志流可以相互转换。

为了对有序和无序输入流进行抽象,我们为记录流和表变更日志流定义了流等价。有序流是一个包含有序流和无序流的等价类中的规范代表。因此,我们仅基于有序输入流提供操作符定义。通过利用流等价性,我们在不重新定义操作符的情况下,将原始定义扩展到无序输入流。

我们通过记录输入流和过滤操作符的示例来阐述记录流的流等价定义。请注意,本例中记录具有k= ⊥,因此在以下讨论中我们省略键。

示例 4.1(过滤有序/无序记录流) 。给定一个有序记录流和一个无序记录流S[S]和S ′[S],它们包含相同的记录r、s、 t、v。让一个过滤操作符应用于每个流,得到以下结果:
filter(S)= filter((r, s, t, v))=(r, t, v)
filter(S ′ )= filter((r, s, v, t))=(r, v, t)

两个输入流和两个输出流都包含相同的数据(即时间戳和值),但由于记录出现的顺序不同,它们并不equal。然而,在比较两个记录流时,只需考虑时间和值,应忽略描述物理顺序的偏移量。

如果两个记录流包含相同的数据(即时间戳和值),我们认为它们彼此等价。形式上,我们基于多重集相等性[8]定义记录流等价性如下:

定义 4.2(记录流等价性) 。两个记录流S[S]和 S ′ [S]是等价的,记作S[S] ≡S ′ [S],当且仅当它们对应的时间戳‐值对的多重集相等:
S ≡ S ′ ⇔ MS(πT, V(S))= MS(πT, V(S ′ )) (1)
其中MS是一个将序列转换为多重集的操作符, π是一个流投影,用于返回每条记录的时间戳和值属性。

4.1 无状态操作符

在第2.2节中讨论的无状态操作符,包括过滤、投影、 映射和扁平映射,可以在我们的模型中以简单的方式实现于记录流。这些操作符按偏移顺序应用于输入流中的每条记录,并生成一个(保持顺序的)输出记录流。输出记录继承输入记录的时间戳。基于记录流等价性,我们对无序输入流上的无状态操作符定义正确性如下:

定义 4.3 (算子正确性) 。给定一个有序数据流S[S]和一个无序数据流S′[S]。一个无状态流处理算子O是正确的,当且仅当:
S ≡ S′ ⇒ O(S) ≡ O(S′) (2)

定义4.3指出,如果两个输入流是等价的,则两个输出流必须属于同一个等价类。此外,有序流是每个等价类的规范代表。因此,基于产生有序输出流的有序输入流来定义操作符语义,足以隐式地定义无序输入流和输出流的操作符语义。

4.2 记录流聚合

根据第3节中介绍的双流模型,记录流聚合操作符agg 会产生一个持续更新的结果表。为了定义聚合操作符在无序输入流上的正确性,我们使用表相等性:对于两条等价的输入记录流,要求agg操作符对二者返回相同的结果表。

定义 4.4 (算子正确性) 。给定一个有序数据流S[S] 和一个无序数据流S′ [S]。当且仅当满足以下条件时,agg操作符是正确的:
S ≡ S′ ⇒ agg(S)= agg(S ′ ) (3)

请注意,表相等性意味着两个表必须包含完全相同的表版本。此外,物化输出变更日志流必须能够还原出原始的结果表。这意味着表的变更日志记录会被分配其所 有输入记录中的最大时间戳。结果表的相等性要求决定了聚合操作符必须正确地更新其状态,并在处理乱序数据时发出相应的更新记录。我们通过一个示例来说明正确的行为。

示例 4.5(无序记录的聚合) 。考虑一个在包含三条记录的记录流上进行计算的求和操作符。这些记录以时间戳‐值对的形式定义(省略了键,所有记录的键均设为= ⊥),如下所示:
⟨3, 2.3⟩, ⟨7, 4.4⟩, ⟨5, 6.1⟩

正确的聚合结果在相应的有序输入流上定义为一个表T,该表具有三个版本T3,T5,T7。此表在三代中演变如下(我们仅显示求和):
⟨3, 2.3⟩ → T(1): T3: 2.3
⟨5, 6.1⟩ → T(2): T3: 2.3 T5: 8.4
⟨7, 4.4⟩ → T(3): T3: 2.3 T5: 8.4 T7: 12.8

对应的输出表变更日志流包含三条记录3,2.3、5,8.4、7,12.8。使用包含乱序记录的输入流进行处理会以不同的方式演化输出表:
⟨3, 2.3⟩ → Tˆ(1): Tˆ3: 2.3
⟨7, 4.4⟩ → Tˆ(2): Tˆ3: 2.3 Tˆ7: 6.7
⟨5, 6.1⟩ → Tˆ(3): Tˆ3: 2.3 Tˆ5: 8.4 Tˆ7: 12.8
相应的表变更日志流包含四条记录:
⟨3, 2.3⟩, ⟨7, 6.7⟩, ⟨5, 8.4⟩, ⟨7, 12.8⟩

注意,表版本ˆT7 ∈ ˆT(2)和对应的变更日志记录 7,6.7是正确的。有序和无序输入记录流的长度为2的流前缀不等价,因此在每种情况下处理完前两条记录后(定义4.4),两个输出表T(2)和ˆT(2)无需相等[8]。

结果完整性与运行时成本 :如上所定义,聚合操作的结果表会无限增长,因为保留了所有表版本的完整历史。因此,在实际应用中需要对结果表的大小进行限制[8]。为此,我们引入一个保留时间参数,用于定义结果表中的元素应保留多长时间。随着事件时间的推进,早于 “当前事件时间减去保留时间”的元素可以从表中删除。

从结果表中清除数据意味着不再接受对这些元素的更新,相应的输入记录将被丢弃。引入保留时间参数使得结果完整性与存储需求之间的权衡变得明确。例如,如果用户知道迟到数据在特定应用中存在上限,则可以相应地配置保留时间参数,以确保结果的完整性,并估算内存/存储需求。因此,保留时间参数在某种程度上类似于标点/水印机制[3, 27]。然而,保留时间不会引入额外延迟,因为结果表会立即更新,而标点/水印机制则会延迟计算直到水印通过。

Grouping and Windowing 流处理中的聚合通常基于分组属性(类似于关系模型中的分组操作符)和时间窗口。在我们的模型中,这两个概念通过在结果表的主键中编码分组属性和窗口ID[20]来支持。该表存储每个键和每个窗口的当前聚合值。

4.3 表操作符

到目前为止,我们讨论了表可以从表变更日志流中物化,或者作为聚合操作的结果。对于前者,我们引入了一个将表变更日志流物化的表操作符。此外,我们的模型能够直接实现对表的过滤、有限2形式的投影和映射、 agg以及主键等值连接操作符。所有这些操作符生成的结果表会在输入表每次更新时相应地被更新。上述表操作符的语义遵循时间关系模型:对输入表版本t的更新会触发输出表版本t的更新。此外,表间连接是基于相应的表版本定义的,即版本为t的结果表应由左表版本t与右表版本t连接生成。

示例4.6(表‐表连接) 。图5展示了输入表A(包含版本1、5、6)与输入表B(包含版本2、3、6)的内连接示例。请注意,如果在某个时间t某张表没有更新,则可以安全地使用较早的表版本(即t −1)进行连接,因为Tt−1 = Tt。在此示例中,由于不存在表B1,因此在时间戳1时无法连接A1,故没有结果表R1。由于A2不存在,B2与A1连接生成R2。将B2更新为B3会导致R2更新为R3。类似地, R5是更新后的A5与B3连接的结果。注意,在时间戳6时,两张表同时更新,因此R6= A6 ▷◁ B6。

我们可以将结果表描述为物化视图,并使用关系型数据库系统中已知的技术来维护结果表,当输入表被更新时[9, 14]。在实践中,并非所有表都需要物化并且可以采用成本模型来决定,某个操作符是使用物化表,还是仅在相应的表变更日志流上进行操作。

4.4 基于流的连接

我们的模型还包含多种用于流的等值连接操作符,即流‐流连接和流‐表连接。

流‐流连接 基于连接属性以及一个滑动窗口,该窗口实际上在两个流上定义了一个事件时间带连接:如果两条记录具有相同的连接属性,并且它们的时间戳彼此接近(即其时间戳差值小于或等于窗口大小),则这两条记录将进行连接。连接结果记录将被分配两条输入记录中较大的时间戳。

流表连接 执行主键表查找‐连接操作,并生成输出流。对于每条流记录,使用连接属性在表中查找相等的主键——如果在表中找到了该键,则该流记录与表记录进行连接,连接后的记录被追加到输出流中。流表连接是一种时间性连接:表查找是基于与流记录时间戳相对应的表版本进行的。因此,流记录只能与时间戳小于或等于该记录时间戳的表记录进行连接。连接结果记录继承输入流记录的时间戳。

示例4.7(流表连接) 。图6展示了一个与示例3.1中表T(5)的流‐表连接。流输入记录表示为时间戳‐键‐值三元组,例如,2,B,3.5表示一个时间戳为2、连接键为 B、值为3.5的记录。第一条流记录2,B,3.5未进行连接,因为在该记录的时间戳t= 2处尚无可用的表。记录5,A,4.2成功与表版本5连接,生成输出记录5,A,4.2 ▷◁ 7.2。对于第三条记录6,C,6.4,表中没有键k= C对应的元素,因此不产生连接结果。最后,记录7,B,1.2生成连接记录7,B,1.2 ▷◁ 14.7。注意,最后一条结果记录是与表版本6进行连接的结果。

流表连接也可以是左外连接,确保每个流输入记录恰好有一个连接结果。对于流‐流连接,内连接、左外连接和全外连接语义可以直接在我们的模型中实现。

乱序记录 :在连接中处理乱序记录需要多种策略。对于流表连接,乱序记录无需特殊处理。然而,如果乱序的表更新未得到妥善处理,则可能导致错误的连接结果。假设图6中从A,a,2到A,a ′,5的表更新被延迟,那么流记录A, α ′,6将与第一个表版本进行连接,并错误地输出A, α ′ ▷◁ a,6。为了处理这种情况,需要进行缓冲流表连接操作符中的记录流输入记录会因延迟表更新而重新触发连接计算。因此,如果发生延迟表更新,相应的更新记录将被发送到下游,以“覆盖”之前发出的连接记录。注意,流表连接的结果不是记录流,而是普通数据流,因为它可能包含更新记录。

对于左连接和全外流‐流连接,采用相同的技术。没有连接伙伴的记录会立即作为左/全外连接结果发出,并且如果另一条流中稍后出现匹配的连接记录,则可能会被更新。该技术允许以完全非阻塞方式实现连接操作。

对于内流‐流连接,输出流仍然是一个记录流,因为内流‐流连接是一种单调操作,永远不会产生需要后续更新的早期结果。

两种连接都受为表引入的保留时间参数约束。如果在相应的表版本或流记录被清除之后处理延迟流记录,则内连接不会产生连接结果。

5 案例研究:Apache Kafka

在本节中,我们阐述了如何利用双流模型来设计和实现一个实际的流处理系统。我们以Apache Kafka[16],这一在工业界广泛采用的大规模流式平台作为研究中的具体示例。Kafka将数据流存储在主题中,供客户端进行生产和消费。每个主题被维护为一个分区日志,生产者客户端可以持续向其中追加不可变记录序列。消费者客户端可以订阅一个或多个主题,并按分区以追加顺序读取日志中的记录。日志中的记录以键-值对的形式存储。每条记录都有一个由生产者客户端在生成时设置的内嵌时间戳字段。此外,当记录被追加到日志时,会被分配一个偏移量,用于唯一标识其在日志中的位置。

自2015年以来,我们与ApacheKafka社区合作,在KafkaStreams(首次发布于ApacheKafka0.10)中实现了我们的模型。KafkaStreams是一个客户端库,使开发者能够基于存储在Kafka中的数据流构建实时处理应用程序。如今,KafkaStreams库已被广泛应用于各个行业,包括《纽约时报》、Pinterest、 LINE、Trivago等。3该库提供了一个高级DSL,供开发者定义其应用程序的处理逻辑,从读取Kafka主题中的数据流开始,将输入流转换为新的流,生成不断演化的表,最终将结果数据流写回Kafka主题。在KafkaStreamsDSL中,有两种一等抽象:KStream 和KTable。KStream 是记录流的抽象,而KTable 则是双流模型中表变更日志流及其对应物化表的抽象。此外, DSL用户可以实时查询KTable的物化状态。

5.1 Kafka Streams DSL 操作符

表1总结了Kafka Streams DSL中可用的操作符及其输入和输出类型。操作符定义与双流模型高度一致。对于生成KTable的任何操作符,DSL用户均可指定是否物化结果表。对于这些物化的KTable,Kafka Streams提供了一组API,允许开发者实时查询其演进状态。

此外,Kafka Streams在复制的内部Kafka主题中维护KTable的变更日志流。Kafka中复制的表变更日志流具有高可用性,因此可作为该KTable演进状态的事实来源。例如,在扩展操作和容错事件期间,KTable的状态始终可以从该变更日志流主题重新物化,这体现了双流模型中流与表的对偶性。

为了实现数据并行性,Kafka Streams DSL要求连接和聚合操作始终基于某些键,以便不同键的操作可以独立执行。特别是,DSL为聚合操作引入了groupBy操作符,类似于关系数据库中的分组操作,该操作以记录的值作为输入,并返回一个分组属性。为了执行此操作符,首先通过中间分区Kafka主题对输入流进行重新分区,然后每个重新分区的流分区会根据分组属性并行地进行聚合。Kafka Streams中的任何操作符实现都不是阻塞的:每当从源Kafka主题接收到一条记录时,它将立即通过Kafka Streams DSL中指定的所有连接的操作符进行处理。

直到它被物化到某个结果KTable,或写回到下沉 Kafka主题。在处理过程中,记录时间戳将根据第4节中定义的每个操作符的语义进行维护/更新。

操作符 第1个输入 第2个输入 输出
过滤,mapValue KStream KStream
过滤,mapValue KTable KTable
映射,扁平映射 KStream KStream
groupBy→agg KStream KTable
groupBy→agg KTable KTable
groupBy+windowBy→ agg KStream KTable
内连接/左连接/右连接 KStream KStream KStream
内连接/左连接/右连接 KTable KTable KTable
内连接/左连接 KStream KTable KStream

5.2 流聚合中的窗口化

Kafka Streams 提供了多个调节参数,使开发者能够通过推理时间在结果完整性、运行成本和延迟之间进行权衡。在本节中,我们将给出这些调节参数的一个具体示例:用于流聚合的窗口操作。

对于KStream的聚合操作符,除了groupBy之外,用户还可以通过windowBy操作符提供另一组分组条件。这种聚合的结果将变为一个窗口化KTable。例如,用户可以生成一个直方图来统计每小时用户点击数,其中按小时划分的规则在windowBy操作符中指定,生成的KTable实际上在每个分组的一小时窗口内包含一个物化状态。时间窗口或会话窗口[4]均可作为窗口类型。时间窗口可以相互重叠,也可以不重叠(即跳跃4或滚动窗口)。会话窗口不会重叠,当在一定时间内未接收到元素时,会话窗口即结束。

图7展示了一个窗口化流聚合的示例。在第1行中,我们首先从名为“click‐topic”的Kafka主题创建一个 KStream。然后在第3行中,我们创建一个窗口化的 KTable,每5分钟统计每个区域的点击次数。聚合键在第4行的groupBy中指定,窗口子句在第5行中指定。在 windowBy操作符内部,我们将窗口长度定义为5分钟,每个窗口每1分钟推进一次(即,这是一个跳跃窗口)。此外,我们定义每个窗口将保留一小时,以便在窗口已过期后一小时内到达的任何乱序数据仍能被处理;即,相应窗口的聚合结果将被更新,并且一条新的更新记录将被追加到表变更日志流中。

此窗口操作可用于确定结果表中每个窗口何时可被视为“完成”:在保留时间过后,窗口将不再被更新,结果即为最终结果,且不再接受任何数据来更新该窗口。通过更长的窗口保留期,可以处理带有较早时间戳的乱序记录。因此,通过自定义更长的保留期,应用程序对乱序数据更具弹性。但更长的保留时间的代价是存储占用增加,以及每个窗口的结果需要更长时间段才能被视为完成。

6 相关工作

我们的模型建立在现有的流处理连续查询处理、物化视图维护和关系查询语言的研究基础之上。

特里等人[26] 和刘等人[22, 23]提出了关于连续查询的早期工作。他们使用连续查询在输入数据被更新时精炼结果。因此,该模型类似于物化视图[9]的增量维护。我们的模型借鉴了这些关于增量更新的思想,并将其应用于表和数据流,即用于数据的静态视图和动态视图。

纪事数据模型[14] 基于纪事和关系构建。纪事是一系列无界的事物记录序列,这些记录被应用于一个关系。后者被表示为版本化表,这与我们所提出的表版本概念类似。然而,与我们的双流模型不同,纪事模型忽略了显式时间域中的时间维度。因此,纪事模型无法捕捉数据的乱序到达。

开创性的连续查询语言(CQL)[5]利用关系模型为数据流提供强大的查询语义。它将流建模为表,因此与我们的工作最为接近。然而,CQL不处理乱序记录,而是假设此类问题可在技术层通过缓冲与重排序来解决。我们提出的版本化表概念可视为对CQL的推广:有序流可作为特例进行处理,从而在创建新表版本后立即清除较旧的表版本。Trill[10]支持一种类似于我们工作的关系‐时态流处理模型。但与CQL一样,其并未考虑乱序数据的概念。

劳等人[18]提出定义能够持续更新输出的操作符,从而引出“截至目前的结果”这一概念。他们基于输入前缀定义了操作符的正确性,并对阻塞和非阻塞操作符给出了形式化的定义。然而,与我们的工作不同之处在于,他们的模型仅针对记录流。因此,该模型仅限于单调查询。此外,他们的模型未考虑乱序记录或基于窗口的算子。

包含更新或删除记录的数据流已被Babu和 Widom[7]讨论过。尽管他们的工作提出了总体思路,但并未提供流式算子的语义以及算子正确性的概念。

Borealis系统[2]存储输入流的历史,并允许在迟到的更新记录到达时进行原地更新。在这种情况下,已更新的输入流会被重新回放。我们认为,这种方法处理更新的开销较大,会损害处理延迟。相比之下,我们的模型实现增量更新,仅对实际需要更新的结果进行修改。

处理具有不同时间偏移的无界、无序数据流时,对时间进行推理的能力至关重要。事件时间的概念最早由 Sri‐vastava和Widom[25]提出。该研究指出了相关的同步问题,并建议采用缓冲和重排序技术来处理乱序数据。此外,巴尔加等人[8]为流引入了强时间语义,并借鉴时态数据库系统,提出了不同一致性级别的定义。

SPREAD模型[15]通过比较记录的时间戳和到达顺序来识别乱序数据。其关键见解在于,由一批记录到达所触发的评估(例如,受限于元组时间戳的推进)无法全面捕捉输入数据的时间特性。SPREAD中提出的解决方案是将具有相同时间戳的记录定义为同一批次,即在同一个时间戳内可根据到达顺序多次触发评估。

最后,乱序数据可以通过punctua-tions[27]来处理。它们代表提供关于数据流某些保证的控制消息。水印 [3, 4]是一种特殊的标点形式,用于报告事件时间进展的下界。所讨论的技术有一个共同点,即它们意味着在接收到水印或标点符号到达。正如Maier等人[24],所强调的,这种方法本质上会增加处理延迟。

Li等人[21] 和Krishnamurthy等人[17]意识到,顺序处理存在延迟增加和内存需求增加等缺点。他们提出了类似于我们模型的乱序处理,但未引入变更日志流的概念。因此,他们依赖标点符号来延迟计算[21],且查询结果不会持续地更新[17]。

7 结论

在本文中,我们引入了双流模型。其核心思想是将流处理操作符的结果定义为对表的连续更新。这些更新可以物化到版本化表中,也可以表示为更新记录的流,从而形成流与表的对偶性。我们的双流模型使我们能够整体地分析流中数据物理和逻辑顺序的不一致性:它为持续处理无序流提供了明确定义的操作符语义,而无需显式缓冲与重排序记录。通过这种方式,流处理延迟不会受到影响。

直接在流式模型中处理乱序数据具有更多优势。我们讨论了该模型如何明确地在数据流处理环境中对结果的完成度、处理成本和延迟进行权衡。最后,我们介绍了在ApacheKafka这一广泛采用的开源流处理平台中实现双流模型的方法。

8 致谢

我们感谢MichaelNoll、DamianGuy、HojjatJafarpour、 BillBejeck、SriramSubramanian和JayKreps对本文的深入讨论,以及他们在KafkaStreams上的工作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值