来吧朋友们,是时候来点干货了。
第一章主要集中在3个主要方面:术语,精确定义了我经常使用到的词汇如“流”。批式VS流式,比较了两类系统的理论性质,假定了只有两个事情是流式系统必须要超越批式的---正确性和时间归因工具。数据处理范式,讲了下批式和流式引擎处理有界和无界数据时的理论方法。
在这一章节,我们会进一步关注第一章所提到的数据处理范式,但是会聚焦在更多细节上,以及带上一些具体的例子。当我们结束本章的时候,我们将学会我所认为的鲁棒的无序数据处理所需要的最核心的原则和概念,包括时间归因的工具,它是真正能使你超越经典的批式处理的关键。
为了给你一种已经在使用的感觉,我使用了Apache Beam代码中的一部分,结合时序图来提供这些概念的可视化。Apache Beam是一个统一的编程模型,并且为批式和流式处理提供一个可移植层,提供了不同语言(例如Java和Python)的一系列的具体SDK。使用Apache Beam的管道能移植跑在任意所支持的执行引擎上(Apache Apex, Apache Flink, Apache Spark,Cloud Dataflow等)。
我在这里使用Apache Beam作为例子的实现不是因为这是一本关于Beam的书(实际上也不是),而是因为它最完整的提现了本书所描述的概念。回到“Streaming 102”最初写的时候(),它也是真正意义上已有的中唯一一个提供了我们所有例子中所必须的表现力。一年半之后,我很乐于看到现在改善了很多,大多数主流系统已经转而或者正在转而要支持像本书所描述的类似的模型。所以请放心我们所提及的概念,虽然透过Beam所提及的,在某种程度上将与你遇到的其他系统等价。
路线图
为了给本章设置一些阶段,我想阐述五个主要概念,这些概念在之后的讨论中将会逐渐巩固,而且绝大多数是在第一部分。我们其实已经学习完其中的两个了。
在第一章中,我首先确立了事件时间()和处理时间()之间的关键区别。它给本书中提到的主要概念其中之一提供了基础,如果你比较关注正确性和事件真正发生的时间,你必须分析固有的事件时间相关的数据,而不是他们在分析时候所遇到的执行时间。
然后我介绍了窗口的概念(例如为数据集按时间边界进行分区),这是一个在技术上被用来处理无界数据源的通用方法,而且可能永远不会被摒弃。固定和滑动窗口是窗口策略中相对简单的,但也有更复杂的一些窗口,例如会话(一种被数据本身的特征所限定的窗口,例如每个被不活跃期所分割的会话),在这也能看到更广泛的应用。
除了这两个概念外,我们还会仔细看下如下三个概念:
触发器
触发器是一种声明窗口的输出通过一些外部信号何时具化结果的机制。触发器提供了结果何时被发射的灵活度。在某种意义上,你可以把他们视为结果何时被具象化的流量控制机制。另一方面你也可以把它视为像是相机释放快门一样,使得你能宣告何时拿到计算的结果时候的快照。
触发器也使得窗口形成时候观察到多次窗口结果成为可能。这转而又对随时间推移逐渐精炼结果敞开了大门,允许随着数据的到来来提供推测性结果,和处理上游数据随史家或数据迟来的改动(例如移动设备场景)。
水印
水印是一种与事件时间输入完备性的概念。
累积
累积模式明确规定了对于同一个窗口间被观察到的多个结果之间的关系。这些结果可能完全脱节了,即指随时间变化导致的独立的增量,或者结果之间是相互重叠的。不同的累积模式有着不同的语义并且相应的代价也不同,因此需要在不同的使用场景中找到较为贴近的一个。
而且,因为我认为以下的方式可能对理解这些概念之间的关系是比较容易的,我们就重提了一些旧的或者探索了一些新的方式,主要通过回答四类问题,每一个我提的问题都是对无界数据处理问题很关键。
What:要计算出来什么样的结果?这个问题由管道中的转换操作来回答。包括了像是求和、建立直方图、训练及其学习模型等。它也是经典批式处理所要回答的问题
Where:事件时间的结果在哪里计算?这个问题由管道中对事件时间窗口的使用来回答。它包括了以下内容,如第一章所提到的一些通用例子(固定的、滑动的和会话),如一些看起来不必使用窗口的用例(如时间不可知处理,经典批式处理大体也可以放到这个分类里),以及其他窗口的更复杂的类型,例如限时拍卖。同时如果你为系统中到来的每一个元素分配了注入时间作为事件时间的话,则也需要注意它也会包含处理时间窗口
When:处理时间下的结果何时被具现化?这个问题由触发器和水印(可选的)的使用来回答。在这一主题下会有无限的变化,但最通用的模式是那些会涉及到重复更新的(例如物化视图语义),或是在相关的输入可被认为是完成之后利用水印为每一个窗口提供一个单独的输出(例如,经典的批式处理语义按每一个窗口应用),或者是以上两者的结合。
How:改进的结果怎么关联?这个问题由累积所使用到的类型回答:抛弃(对于那些结果是独立和不同的应用),积累(对于那些迟到的结果需要建立在之前的结果之上的案例),或者积累且撤回(对于那些既需要积累值还需要撤消针对之前触发器所触发发射出去的值)
我们会在本书的剩余部分看到以上的每个问题的更多细节。并且我将会标注颜色尝试使得这些问题更加清楚表征这些概念是和哪些问题相关的,What/Where/When/How。不用谢,:)
批式的基础:What和Where
好了,派对开始吧,首站:批式处理。
What:转换
应用在经典批式处理中的转换回答了该问题:“What:要计算出来什么样的结果?”。即便你可能已经在经典批式处理中熟悉过了,我们仍会以那为开始,因为它是我们在那所要增加的所有概念的基石。
在本章剩余部分(实际上,会贯穿全书),我们看一个单独的例子:计算一个由9个值组成的数据集中按键值分割的和。我们设想如下场景,我们写了一个基于队伍的移动游戏,我们想创建一个管线来计算每个队伍的分值,分值通过对用户手机所上报的独立分值累积所得。如果我们抓取在名为“用户分数”的SQL表中的9个样例分值,它可能呈现出如下的样子:
SELECT * FROM UserScores ORDER BY EventTime;
---------------------------------------------------
| Name | Team | Score | EventTime | procTime |
| Julie | TeamX | 5 | 12:00:26 | 12:05:19 |
| Frank | TeamX | 9 | 12:01:26 | 12:08:19 |
| Ed | TeamX | 7 | 12:02:06 | 12:05:39 |
| Julie | TeamX | 8 | 12:03:06 | 12:07:06 |
| Amy | TeamX | 3 | 12:03:39 | 12:06:13 |
| Fred | TeamX | 4 | 12:04:19 | 12:06:39 |
| Naomi | TeamX | 3 | 12:06:39 | 12:07:19 |
| Becky | TeamX | 8 | 12:07:26 | 12:08:39 |
| Naomi | TeamX | 1 | 12:07:46 | 12:09:00 |
请注意在本例子中的所有分值都是来自同一队伍的不同玩家,这简化了例子,使得图表中的有限数量的维度。而且因为我们按照队伍分组,我们其实只关注最后三列:
分值
单个玩家某个事件的分值
事件时间
分值对应的事件时间。也就是事件发生的时间
处理时间
处理分值的时候,也就是分值在管道中被观察到的时间
对于每一个管线中的例子,我们看下时序图,它能够突出数据随时间的演进。这个图表标出了9个分值在我们所关注的两个时间维度中,x轴对应事件时间,y轴对应处理时间。图2-1说明了输入数据静态时候的布局的样子。
图图图图
图2-1
之后的时序图要么是动画性质的要么是一系列帧所组成(打印和其他数字格式),能让你看到数据是怎么随时间而处理的(稍候我们将在看第一个图)。
在每一个例子之前是一个Apache Beam Java SDK伪代码的简短片段,这可以让管道的定义更加具体便于理解。就伪代码的意义而言,我有时会变通下规则使得例子更加明了,可能会删除些详细信息(比如省略具体的IO源),或简化命名(在Beam的Java 2.x或更早的版本中触发器的名字实在太冗长,为清楚起见我会用简化命名)。除了这些小事之外,基本都是Beam的代码了(本章所有例子的代码在GitHub均可找到)。
如果你已经对类似于Spark或Flink的东西熟悉了,你应该会比较容易理解Beam代码所表述的东西。但为了让你能速成,在Beam中有两个基本要素:
PCollections
它们代表那些可以执行并行转换(因此在名字开头会有一个P, parallel)操作的数据集(可能是巨大的数据集)
PTransforms
这些是应用在PCollections来产生一个新的PCollections。PTransforms会执行按元素方面进行转换,他们可能进行分组、聚合在一起的元素,它们也可能是其他PTransforms组合而成的,如图2-2所示。
图图图图图图图
图2-2
我们例子的用意在于我们通常假定我们从一个已经加载进来的名为input的PCollection<KV<Team, Integer>>开始(它是一个由Teams和Integers所对应的键值对组成的一个PCollections)。在现实世界的管线中,我们会通过从IO源读取一个PCollection<String>得到原始数据(例如日志记录),然后将其通过解析日志记录成适当的KV对将其转换成一个PCollection<KV<Team, Integer>>。在第一个例子中为了易懂,我包含了所有的这些步骤,但在之后的例子中,我会省略掉IO和解析部分。
对于一个管线,简化从IO源读取数据解析team和分值对,计算魅族的总分后,我们就有了如例2-1所示的代码。
Example 2-1. 累和总线
PCollection<String> raw = IO.read(...);PCollection<KV<Team, Integer>> input = raw.appley(new ParseFn());PCollection<KV<Team, Integer>> totals = input.apply(Sum.integersPerKey());
键值对数据由IO源读取,用Team(队伍名所对应的字符串)作为键以及用Integer(例如队伍中每个独立成员的分值)作为值。每个键所对应的值之后会被累积到一起求和生成每个键的最终和(例如每个队伍的总和)即为输出集合。
对于之后的所有例子来说,在看完我们所要分析的被描述成管线的代码片段后,我们之后会看到时序图,这个时序图展示了对于我们具体数据中的每个键的执行过程。在实际的管线中,你可以认为是类似的操作将会在多个机器上并行执行,但对于我们的例子来说,让事情简单会更加清晰些。
如前所述,Safari editions将完整的执行呈现为一个动画片,鉴于是出版物和其他电子格式使用了关键帧的静态序列来提供一种管线如何随时间进展的感觉。对于这些例子,我们也提供了一个URL来呈现完整的动画版在www.streamingbook.net。
每个图都通过两个维度来标出了输入和输出:事件时间(x轴)和处理时间(y轴)。这样管线中所观察到的真实时间就会自底向上的进行,通过水平的粗黑线来表示随时间推移在处理时间轴的上升。输入数据是一个个圆,圆内的数字代表特定记录的值。它们最开始都是浅灰色,当管线观察到它们时变黑。
管线观察到值时,累加它们时是处于中间状态,并且最终会具化聚合的结果作为输出。状态和输出用矩形代替(灰色是状态,蓝色是输出),挨着顶端的是聚合的值,矩形所覆盖的区域代表事件时间和处理时间累加到结果的一部分。例2-1中的管线,当在经典批式引擎中执行它可能看起来和图2-3类似。
图图图
图2-3
因为是批式管线,它会一直累加状态直到它看到所有的输入(由顶端的绿色虚线表示),也就是生成单个输出为48的时候。在这个例子中,我们会计算所有事件时间的和因为我们不必是哟个任何特定的窗口转换,由此只需要矩形所对应的状态以及覆盖整个x轴的输出。如果我们想处理一个无界数据源,可是经典批式处理并不够用,我们不能等待输入结束,因为它实际上永远不会结束。我们想要的其中一个概念就是我们第一章介绍过的窗口。因此,引出我们的第二个问题---“结果在哪里按照事件时间进行计算?”,我们现在简要的再讨论下窗口。
Where:窗口
像第一章讨论的,窗口化是一个按时间边界将数据源切分的过程。通用的窗口策略包括固定窗口、滑动窗口和会话窗口,如图2-4描述。
图图图图图
图2-4
为了在实际中更好的感知窗口长什么样,我们来看下整数累和的管线任务并且将其窗口化成固定的、两分钟的窗口。用Beam,更改就是用transform简单的加个额外的窗口,也就是例2-2中高亮的部分。
Example 2-2 用窗口累加的代码
PCollection<KV<Team, Integer>> totals = input
.apply(Window.into(FixedWindows.of(TWO_MINUTES)) //Highlights .apply(Sum.integersPerKey());
回忆下Beam提供了统一的模型使其可同时工作在批式和流式上,因为批式语义实际上就是流式的一个子集。因此,我们先来执行这个管线任务在批式引擎上,这个机制更加直接,而且让我们可以直接与流式对比一些东西。图2-5呈现出了结果。
图图图图图
图2-5
之前输入是直到它们完全被消耗完才累加到状态中,然后再产生结果。在这个例子中,不是输出一个结果,我们得到了四个结果,四个2分钟时间窗口每个对应一个结果。
在这点上我们重新回顾了两个在第一章介绍过的主要概念,事件时间和处理时间域的对应关系和窗口。如果我们想再往前走,我们需要开始添加一些最开始提到过的新的概念:触发器、水印和累加。
转到流式:When和How
目前我们看到了在批式引擎上的窗口管线。但理想情况下,我们想要更低延迟的结果,我们也想原生地处理无界数据源。切换到流式引擎是一个正确的步骤,但我们之前等待输入源整体消耗完之后再生成结果的蹙额略不再可行了。开始介绍触发器和水印。
When:触发器上最奇妙的是触发器本身就是奇妙的东西
触发器提供了这个问题的答案:“结果在处理时间的情况下什么时候被具化”触发器声明在处理时间下的一个窗口的结果何时应该产生(虽然触发器本身可能会基于现实情况所发生的时域自己做决定,如在事件时间域内的水印推进,我们稍后会看到)。一个窗口的每个特定输出被称为窗口的窗格。
虽然可以想象会存在相当广泛的触发语义,但实际上仅有两种通用的有用的触发器类型,而且实际上应用总是归结为使用一种或两者的组合:
重复更新触发器
这种是随着内容的演化周期性的生成更新过的窗格给一个窗口。这些更新可以被任何一个记录所具化,或者发生在一些延迟的处理时间之后,例如一分钟一次。这种周期性的给一个重复更新触发器是一个主要的平衡延迟和代价的运用。
完整性触发器
这种具化一个窗口的窗格仅在窗口的输入被认为完成到了某个阈值。这类触发器最类似于我们在批式处理中所熟知的的情况,只有输入完整后我们提供结果。与基于触发器的方法不同的是完整性的概念作用于是单个窗口,而不是一定要整个输入的完整。
网友评论