[TOC]
Dataflow 图
顾名思义, Dataflow 程序描述了数据如何在不同操作之间流动。 Dataflow 程序通常表示为有向图。图中顶点称为算子,表示计算:而边表示数据依赖关系。算子是 Dataflow程序的基本功能单元,它们从输入获取数据,对其进行计算, 然后产生数据并发往输出以供后续处理。没有输入端的算子称为数据掘,没 有输出端的算子称为数据汇。 一个 Dataflow 图至少要有一个数据源和一个数据汇。图 2-1展示了一个从推文输入流中提取并统计主题标签的 Dataflow程序。
image.png国 2-1 :一个持续统计主题标签数目的 Dataflow逻辑图 (顶点表示算子, 边表示数据依赖)
类似图 2-1 的 Dataflow 图被称作逻辑图,因为它们表达了高层视角下的计算 逻辑。为了执行Dataflow程序, 需要将逻辑图转化为物理Dataflow图,后 者会指定程序的执行细节。例如: 当我们使用分布式处理引擎时,每个算子 可能会在不同物理机器上运行多个并行任务。医I 2-2 展示了图 2-1 中逻辑图 所对应的物理 Dataflow 图。在逻辑 Dataflow 图中,顶点代表算子;在物理 Dataflow 图中,顶点代表任务。 “抽取主题标签”和“计数”算子都包含两 个并行算子任务,每个任务负责计算一部分输入数据 。
image.png数据并行和任务并行
Dataflow 图的井行性可以通过多种方式 加 以利用。首先,你可 以 将输入数 据分组,让同一操作的多个任务并行执行在不同数据子集上,这种井行称为 数据井行( data para!!巳!ism)。数据并行非常有用,因为它能够将计算负载 分配到多个节点上从而允许处理大规模的数据。再者,你可以让不同算子的任务(基于相同或不同的数据)并行计算,这种并行称为任务井行( task parallelism)。通过任务并行,可以更好地利用集群的计算资掘。
数据交换策略
数据交换策略定义了如何将数据项分配给物理 Dataflow 图中的不同任务。这 些策略可以由执行引擎根据算子的语义自动选择,也可以由 Dataflow 编程人 员显式指定。接下来,我们结合图 2-3 来简单了解一下常见的数据交换策略 。
image.png- 转发策略( forward strategy)在发送端任务和接收端任务之 间一对一地进 行数据传输。如果两端任务运行在同 一物理机 器上(通常 由任务调度器决 定),该交换策略可以避免网络通信。
- 广播策略(broadcast strategy)会把每个数据项发往下游算子的全部并行 任务。 i亥策略会把数据复制多份且涉及网络通信,因此代价十分昂贵。
- 基于键值的策略(key-based strategy)根据某一键值属性对数据分区,并保证键值相同的数据项会交由同一任务处理。图2-2中, “抽取主题标签” 算子的输出就是按照键值(主题标签)戈!J分的,因 此下游的计数算子可以 正确计算出每个主题标签的出现次数。
- 随机策略(random strategy)会将数据均匀分配至算子的所有任务,以实 现计算任务的负载均衡。
并行流处理
现在你已经对 Dataflow 编程的基础有所了解 。接下来我们看一下如何将这些 概念应用到井行数据流处理中。在 此之 前我 们 先给出数据流 的 定义 : 数据流 是一个可能无限的事件序列。
数据流中的事 件可以 表示监控数据、传感器 测 量 值、信用卡交易、气象站观 切!|数据、在线用户交互,以及网络搜索等。本节你将学到 如何利用 Dataflow 编程范式并行处理无限数据流。
延迟和吞吐
在第 l 章, 你已经了解到流式应用和传统批处理程序在操作需求上有所差异, 而这些需求差异还体现在性能 i平测方面。对批处理应用而言,我们通常会关 心作业的总执行时间,或者说处理引擎读取输入、执行计算、写回结果总共 需要多长 时间 。但由于流式应用会持续执行且输入可能是无限的, 所以 在数 据流处理 中没有总执行时间的概 念。取 而代 之的是 ,流式 应 用 需要 针对到 来 数据尽 可能快地计 算结果,同 时还 要应对很高 的 事件接 入速 率。 我们用延迟 和吞吐来表示这两方面的性能需求 。
延迟
延迟表示处理一个事件所需的时间。本质上,它是从接收事件到在输出中观 察到事件处理效果的时间 |同隔。为了直观地理解延迟,想 一 下你每天都会光 顾自己喜欢的咖啡店。当你进 门的时候,可能已经有别的顾客在里面了。这 时候你就需要排队,等轮到你的时候再开始点单。收银员收到你的付款后会 把订单交给帮你准备饮品的咖啡师。咖啡制作完成后,咖啡师会叫你的名字, 你来从吧台取走咖啡。所谓服务延迟就是你在店内买咖啡的时间,即从你进 门的 一 刻到你喝到第一口咖啡的时间。
在流处理中,延迟是以时间片(例如毫秒)为单位测量的。根据应用的不同,你可能会关注平均延迟,最大延迟或延迟的百分位数值。例如:平均延迟为 10 毫秒表示平均每条数据会在 10 毫秒 内处理;而第 95 百分位延迟在 10 毫 秒意味着 95% 的事件会在 10毫秒内处理 。平均值会掩盖处理延迟的真实分布, 从而导致难以发现问题。如果咖啡师在给你准备卡布奇诺前刚好把牛奶用光 了,那么你必须等他从供应间再拿 一 些出来。虽然你可能因为这次耽搁而不高兴,但其余大多数顾客可能丝毫不会为此影响,心情。
保证低延迟对很多流式应用(例如:诈骗识别、系统告 警 、网络监测,以及 遵循服务级别协议( SLA)的服务)而言至关重要。低延迟是流处理的一个 关键特性,它滋生出了所谓的实时应用。像 Apache Flink 这样的现代化流处 理引擎可以提供低至几毫秒的延迟。相反,传统批处理的延迟可能从几分钟 到几小时不等。在批处理中,你先要批量收集事件,然后才能处理它们。因 此处理延迟受制于每个批次最迟事件的时间,且天然受到批次大小的影响。 真正的流处理不会引人人为延迟等要素,只有这样才能将延迟将至极低。在 真正的流模型中,事件一到达系统就可以进行处理,延迟会更加真实地反映 出每个事件都要经历的实际处理工作。
吞吐
吞吐是用来衡量系统处理能力(处理速率)的指标,它告诉我们系统每单位 时间可以处理多少事件。回到刚刚咖啡店的例子,如果它的营业时间是早 7 点到晚 7 点,并且 一天 服务了 600 名顾客,那么它的平均吞吐是 so 人/小时。 通常情况下延迟是越低越好,而显然吞吐则是越高越好 。
吞吐的衡量方式是计算每个单位时间的 事件或操作数。但要注意,处理速率 取决于数据到来速率,因此吞吐低不 一 定意味着性能 差。在 流处理系统中, 你通常希望系统有能力应对以最大期望速率到来的事件 。 换言之,首要的关 注点是确定峰值吞吐,即系统楠负载时的性能上限。为了更好地理解峰值吞 吐的概念,我们先假设某个流处理应用没有在接收任何数据,也因此无需占 用任何系统资源。当首个事件进入时,系统会立刻以尽可能低的延迟进行处理。 这就如同你是早晨咖啡店开门后的首位顾客,会立即享受服务。理想情况下, 你会希望延迟保持平稳,不受事件到来速率的影响。但现实中, 一旦事 件到 达速率过高致使系统没有空闲资源,系统就会被迫开始缓冲事件。在咖啡店 的例子中,你很有可能在午 每后见到这种情况 :店内 突然 间涌入大 量 顾客, 点单的人排起了长队 。 此时系统吞吐已到极限, 一味提高事件到达速率只会 让延迟更糟。如果系统持续以力不能及的高速率接收数据,那么缓冲区可能 会用尽,继而可能导致数据丢失。这种情形通常被称为背压( backpressure) , 我们有多种可选策略来处理它。
延迟与吞吐
至此你应该已经清楚,延迟和吞吐并非相 互独立 的指标 。如果事件在数据处 理管道中传输时间太久,我们将难以确保高吞吐;同样,如果系统性能不足, 事件很容易堆积缓冲,必须等待一段时间才能处理。
我们再通过咖啡店的例子来解释一下延迟和吞吐如何相互影响。首先需要明 确的是,在空负载的情况下延迟会达到最优。也就是说,如果咖啡店只有你 一 名顾客, f尔将获得最快的服务。然而,在高峰时段,顾客必须要排队,此 时延迟将增加。影响延迟和相应吞吐的另 一 因素是处理单个事件的时间,即 在咖啡店服务每一名顾客所需的时间。假设现在正值圣诞假期,咖啡师要在 他们完成的每杯咖啡的杯 子上画一个圣诞老人。这意味着准备单杯 咖啡的时 间会延长,继而导致每位顾客在店里花 费的时间增加, 此时 整体吞吐量将会 下降。
既然这样,可以通过某种方式同时获得低延迟和高吞吐吗?还是说这根本不 切实际?在咖啡店的例子中,为了降低延迟,店家可以雇佣更娴熟的咖啡师, 他们制作咖啡会更快 一 些。这样在高峰时段,相同时间内可以服务的顾客数 量多了,吞吐量 自然也会提高。另一个殊途同归的办怯是再雇一个咖啡师, 即利用并行解决问题 。 此处的要点在于:降低延迟实际上可以提高吞吐。显然, 系统执行操作越快,相同时间内执行的操作数目就会越 多 。 事 实上,这就是 在流处理管道中利用井行实现的效果。通过井行处理多条数据流,可 以在处 理更多事件的同时降低延迟。
数据流上的操作
流处理引擎通常会提供一系列内置操作来实现数据流的获取、转换,以及输出 。 这些算子可以组合生成 Dataflow 处理图,从而实现流式应用所需的逻辑。本 节我们将介绍最常见的流式操作。
这些操作既可以是无状态( stateless)的,也可以是有状态( stateful)的。无 状态的操作不会维持内部状态,即处理事件时无需依赖己处理过的事件,也 不保存历史数据。由于事件处理互不影响且与事件到来的时间无关,无状态 的操作很容易并行化。此外,如果发生故障,无状态的算子可以很容易地重启, 并从中断处继续工作。 相反,有状态算子可能需要维护之前接收的事件信息。 它们的状态会根据传入的 事件更新,并用于未来 事件的处理逻辑中。有状态 的流处理应用在并行化和容错方面会更具挑战性,因为它们需要对状态进行 高效 划分,并且 在出错时需进行可靠 的故障恢 复。
数据接入和数据输出
数据接入和数据输出操作允许流处理引擎和外部系统进行通信。数据接入操作是从外部数据惊获取原始数据并将其转换成适合后续处理的格式。实现数据接入操作逻辑的算子称为数据源。数据惊可 以从 TCP 套 接 字 、文件、 Kafka主题或传感器数据接口中获取数据。数据输出操作是将数据以适合外部 系统使用的格式输出。负责数据输出的算子称为数据汇,其写入的目标可以 是文件、数据库、消息队列或监控接口等。
转换操作
转换操作是一类“只过一次”的操作,它 们 会分 别处理每个 事 件。这些操作逐个读取事件,对其应用某些转换并产生一条新的输出流。 如图 2-4所示, 转换逻辑 可以 是算子内置的, 也可以由用 户自定义函数提供。函数 由应用开 发人员编写,可用来实现某些自定义的计算逻辑。
算子 既可以同时接收 多 个输入流或 产生多条 输出流,也可以通过单流分割或 合并多条 流来改变 Dataflow 图的结构
滚动聚合
滚动聚合 (如求和、求最小值和求最大值 ) 会根据每个到来的事件持续更新 结果。聚合操作都是有状态的,它 们通过将 新到来的事件合并到 已有状态 来 生成更新后的聚合值。注意,为了更有效地合并事件和当前 状态并生成单个 结果,聚合函数必须满足可结合( associative)及可交换( commutative)的条件, 否则算子就需要存储整个流的历史记录。图 2-5展示了一个求最小值的攘动 聚合,其算子会维护当前的最小值,并根据每个到来的事件去更新这个值。
image.png窗口操作
转换操作和该动聚合每次处理一个事件来产生输出井(可能)更新状态。然而, 有些操作必须收集并缓冲记录才能计算结果,例如流式 Join 或像是求中位数 的整体聚合( holistic aggregate)。为了在无限数据流上高效地执行这些操作, 必须对操作所维持的数据量加以限制。本节我们将讨论支持该项功能的窗口 操作。
除了产生单个有用的结果,窗口操作还支持在数据流上完成一些具有切实语 义价值的查询。你已经了解攘动聚合是如何将整条历史流压缩成一个聚合值, 以及如何针对每个事件在极低延迟内产生结果。 i亥操作对某些应用而言是可 行的,但如果你只对最新的那部分数据感兴趣该怎么办?假设有 一个应用能 向司机提供实时路况信息以帮助他们躲避拥堵 。在该场景下, 你只想知道在 最近几分钟内某个特定位置有没有发生交通事故,而可能对该位置发生过的 所有事故并不感兴趣。此外,将整条历史流 合并为单个聚合值会丢失数据随 时间变化的信息。例如,你可能想了解某路口每 5 分钟的 车流量。
窗口操作会持续创建 一 些称为“桶”的有限事件集合,并允许我们基于这些 有限集进行计算。事件通常会根据其时间或其他数据属性分配到不同桶中。 为了准确定义窗口算子语义,我们需要决定事件如何分配到桶中以及窗口用 怎样的频率产生结果。窗口的行为是由 一 系列策略定义的,这些窗口策略决 定了什么时间创建桶,事件如何分配到桶中以及桶内数据什么时间参与计算。
其中参与计算的决策会根据触发条件判定,当触发条件满足时,桶内数据会 发送给一个计算函数(evaluation function),由它来对桶中的元素应用计算 逻辑。这些计算函数可以是某些聚合(例如求和、求最小值), 也 可以是 一 些直接作用于桶内收集元素的自定义操作。策略的指定可以基于 时 间(例如 最近 5秒钟接收的事件)、数量 (例如最新 100个事件)或其他数据属性。 我们会在接下来介绍常见窗口类型的语义。
- 攘动窗口(tumbling window)将事件分配到长度固定且互不重叠的桶中。在 窗口边界通过后,所有事件会发送给计算函数进行处理。基于数量的( count based)该动窗口定义了在触发计算前需要集齐多少条事件。图 2-6中基于 数量的攘动窗口将输入流按每 4个元素一组分配到不同的桶中。基于时间的
(time-based)滚动窗口定义了在桶中缓冲数据的时间间隔。图 2-7 中基于时 间的攘动窗口将事件汇集到桶中, 每 10分钟触发一次计算。
- 滑动窗口(sliding window)将事件分配到大小固定且允许相互重叠的桶中, 这意味着每个事件可能会同时属于多个桶。 我们通过指定长度和滑动间隔来 定义滑动窗口。滑动间隔决定每隔多久生成一个新的桶。在图 2-8 中,基于 数量的滑动窗口的长度为 4 个事件,滑动间隔为 3 个事件。
会话窗口(session window)在一些常见的真实场景中非常有用,这些场 景既不适合用攘动窗口也不适合用滑动窗口。假设有 一 个应用要在线分析 用户行为,在该应用中我们要把事件按照用户的同 一活动或会话来源进行 分组。会店由发生在相邻时间内的一系列事件外加一段非活动时间组成。 例如,用户浏览一连串新闻文章的交互过程可以看作一个会话。由于会话 长度并非预先定义好,而是和实际数据有关,所以无论是滚动还是滑动窗 口都无怯用于该场景。而我们需要一个窗口操作,能将属于同一会话的事 件分配到相同桶中。会i舌窗口根据会i舌间隔(session gap)将事件分为不 同的会话, i亥间隔值定义了会 i舌在关闭前的非活动时间长度。图 2-9 展示 了一个会 i舌窗口。
image.png迄今为止你所 见 到的所有窗口都是基于全局流数据的窗口。但在实际应用中, 你可能会想将数据流划分为多条逻辑流井定义 一 些并行窗口。例如,如果你 在收集来自不同传感器的测量值,那么可能会想在应用窗口计算前按照传感 器 ID 对数据流进行划分。并行窗口中,每个数据分区所应用的窗口策略都相 互独立。图 2-10展示了一个按事件颜色划分、基于数量 2的并行该动窗口。
image.png窗口操作与流处理中两个核心概念密切相关 :时 间语义( time semantics)和 状态管理( state management)。时间可能是流处理中最重要的一个方面。尽 管低延迟是流处理中一个很吸引人的特性,但流处理的真正价值远不止提供 快速分析。现实世界的系统、网络及通信信道往往充斥着缺陷,因此流数据 通常都会有所延迟或者以乱序到达。了解如何在这种情况下提供精准、确定 的结果就变得至关重要。此外,处理实时事件的流处理应用还应以相同的方 式处理历史事件,这样才能支持离线分析,甚至时间旅行式分析( time travel analyse)。当然,如果你的系统无告在故障时保护状态,那 一切都是空谈。 至今为止你见到的所有窗口类型都要在生成结果前缓冲数据。实际上,如果 你想在流式应用中计算任何有意义的结果(即便是简单的计数),都需要维 护状态。考虑到流式应用可能需要整日、甚至长年 累月 地运行,因此必须保 证出错时其状态能进行可靠的恢复,并且即使系统发生故障系统也能提供准 确的结果。在本章剩余部分,我们将深入研究流处理中的时间以及在发生故 障时和状态保障相关概念。
时间语义
本节我们将介绍流式场景中时间语义和不同的时间概念。我们将讨论流处理引 擎 如何基于乱序事件产生精确结果,以及如何使用数据流进行历史 事件处 理并实现“时 间旅行” 。
流处理场景下一分钟的含义
当处理 个持续到达且可能无穷的事件流时,时间便成了应用中最为核心的 要素。假如你想持续计算结果,比如每分钟计算一次,那么一分钟在流式应 用环境中的含义到底是什么?
假设有某个应用程序会分析用户玩在线手游时产生的事件。该应用将用户组织成不同团队,并会收集每个团队的活动信息,这样就能基于团队成员完成游戏目标的速度,提供诸如额外生命或等级提升的游戏奖励(例如,如果团 队所有成员在一分钟内消除了 500 个泡泡,他们就会提升一级)。爱丽丝是 个铁杆玩家,每天早晨上班路上都会玩这个游戏。但是有个问题:爱丽丝住 在柏林,每天乘地铁上班。而众所周知,柏林地铁上手机上网信号很差。因 此考虑如下情况:爱丽丝开始消泡泡的时候手机还能联网向分析应用发送事 件。突然,地铁开进隧道,手机断网了。爱丽丝继续玩她的,此时游戏产生 的事件会缓存在手机里。在地铁离开隧道,爱丽丝重新上线后,之前缓存的 事件才会发送给应用。此时应用该怎么办?在上述示例中一分钟的含义又是 什么?需要把爱丽丝离线的时间考虑在内吗?图 2-11 说明了这个问题。
image.png在线游戏这个简单场景展示了算子语义应该依赖事件实际发生时间而非应用收到 事件的 时间。在这个手游例子中,后果可能非常糟礁,以至于爱丽丝和 她团队的其他玩家失 望透 顶,再也不想碰这个游戏 。但其实还有更多 时间敏 感应用,需要我 们 对其处理语义进行保障。如果我 们 仅考虑现实时间一分钟 内收到多少数据,到 rs结果可能会随网络连接速度或处理速度而改变。而事实 上每分钟收到事件数目的是由数据本身的时间来定义的。
在爱丽丝游戏的例子中,流式应用可以使用两个不同概念的时间,即处理时 间( processing time)和事件时间( event time) 。 我们将在接下来的几节对 它们进行介绍。
处理时间
处理时间是当前流处理 算子 所在机器上的本地时钟 时 间。基于处理时间的窗 口会包含那些恰好在 一 段时间内到达窗口算子的事件,这里的 时间 段是按照 机器时间测量的 。 如图二 12 所示,在爱丽 丝 的例子中,处理时间窗口在她手机离线后会继续计时,因此不会把她离线那段时间的活动考虑在内。
事件时间
事件时间是数据流中事件实际发生的时间,它以附加在数据流中事件的时间 戳为依据。这些时间戳通常在事件数据进入流处理管道之前就存在(例如事 件的生成时间)。如图 2-13 所示,目|]便事件有延迟, 事件 时间 窗 口也能准确 地将事件分配到窗口中,从而反映出真实发生的情况。
image.png事件时间将处理速度和结果内容彻底解祸。基于事件时间 的操作是可预测的, 其结果具有确定性。无论数据流的 处理速度如 何 、事件到达算子的 顺序怎样, 基于事件 时间的 窗口都会生成同样的结果。
使用 事件时间要克服的挑战之 一是如何处理延迟事件。普遍存在的无序问题 也可以借此解决。假设有另一位名叫鲍勃的玩家也在玩那个在线手游,他恰 好和爱丽丝在同 一趟地铁上。虽然玩的游戏相同,但鲍勃和爱丽丝的移动网 络供应商不同。当爱丽丝的手机在隧道里没信号的时候,鲍勃的手机依然能 联网向后端游戏应用发送 事件。
依靠事件 时间 ,我们可以保证在数据乱序的情况下结果依然正确, 而 且结合 可重放的数据流,时间戳所带来的确定性允许你对历史数据“快进”。这意 味着你可以通过重放数据流来分析历史数据,就如同它们是实时产生的一样。
此外,你可以把计算“快进”到现在,这样一旦你的程序赶上了当前事件产 生的进度,它能够以完全相同的程序逻辑作为 实 时应用继续运行 。
水位线
在到目前为止有关事件时间窗口的讨论中,我们 一直 忽略了 一 个非常重要的 方面:怎样决定事件时间窗口的触发时机?换言之 ,我们需要等多久才能确 定已经收到了所有发生在某个特定时间点之前的事件?此外,我们如何得知 数据会产生延迟?鉴于分布式系统现实的不确定性以及外部组件可能引发任 意延迟,这两个问题都没有完美的答案。在本节中,我们将了解如何利用水 位线来设定 事件时间窗口的行为 。
水位线是一个全局进度指标 ,表示我们确信不会再有延迟事件到来的某个时 间点。本质上,水位线提供了 一 个逻辑时·钟,用来通知系统当前的事件时间。 当一个算子接收到时间为 T 的水位线,就可以认为不会再收到任何时间戳小 于或等于 T 的事件了。水位钱无论对于事件时间窗口还是处理乱序事件的算 子都很关键。算子 一 且收到某个水位线,就相当于接到信号:某个特定时间 区间的时间戳已经到齐,可以触发窗口计算或对接收的数据进行排序了。
水位线允许我们 在结果的准确性和延迟之间做出取舍。激进的水位线策略保 证了低延迟,但随之而来的 是低可信度。 i亥情况下 ,延迟事件可能会在水位 线之后到来,我们必须额外加一些代码来处理它们。反之,如果水位线过于 保守,虽然可信度得以保证,但可能会无谓地增加处理延迟。
在很多现实应用中,系统无战获取足够多的信息来完美地确定水位线。以手 游场景为例,现实中根本无法得知用户会离线多久。他们可能正在过隧道, 可能正在上飞机,也可能直接退坑不玩了。无论水位线是由用户定义还是自 动生成,只要存在“拖后腿”的任务,追踪分布式系统中的全局进度就可能 出现问题,因此简单地依赖水位 线并不总是可以高枕无 忧 。 而流处理系统很关键的 一 点是能提供某些机制来处理那些可能晚于水位线的迟到事件。根据 应用需求的不同,你可能想直接忽略这些事件,将它们写入日志或利用它们 去修正之前的结果。
处理时间与事件时间
此刻你可能心存疑惑:既然事件时间能够解决所有问题,为何还要去关心处理时 间?事实上,处理时间的确有其特定的适用场景。处理时间窗口能够将延迟降至 最低。由于无需考虑、迟到或乱序的事件,窗口只需简单地缓冲事件,然后在达到 特定时间后立即触发窗口计算即可。因此对于那些更重视处理速度而非准确度的 应用,处理时间就会派上用场。另一种情况是,你需要周期性地实时报告结果而 无论其准确性如何。 一个常见示例应用是实时监控仪表盘,它会接收井展示事件 聚合结果。最后,处理时间窗口能够表示数据流自身的真实情况,这可能会在某 些用例中派上用场。例如,你可能想观察数据流的接入情况,通过计算每秒事件 数来检测数据中断。总而言之,虽然处理时间提供了很低的延迟,但它的结果 依赖处理速度,具有不确定性。事件时间 则 与之相反,能保证结果的准确性,并允许你处理延迟甚至无序的事件。
状态和一致性模型
我们现在要转向流处理中另 一个卡分重要的方面 状态。状态在数据处理 中无处不在,任何 一 个稍复杂的计算者R要用它。为了生成结果,函数会在 一 段时间或基于 一 定个数的事件来累积状态(例如计算聚合或检测某个模式)。 有状态算子同时使用传入的事件和内部状态来计算输出。以某个滚动聚合算 子为例,假设它会输出至今为止所 见到的 全部事件之和。该算子以 内部状态 形式存储当前的累加值,并会在每次收到新事件时对其进行更新。类似地, 假设还有一个算子,会在每次检测到“高温”事件且在随后 10分钟内出现“烟 雾”事件时报警。这个算子需要将“高温”事件存为内部状态,直到接下来 发现“烟雾”事件或超过 10分钟的时间限制。
在使用批处理系统分析无限数据集的情况下,状态的重要性会越发凸显。在 现代流处理引擎兴起之前,处理无限数据的通用办陆是将到来事件分成小批 次,然后不停地在批处理系统上调度并运行作业。每当 一个作业结束,其结 果都会写入持久化存储中,同时所有算子的状态将不复存在。 一 旦某个作业 被调度到下个批次上执行,它将无怯访问之前的状态。该问题通常的解决方 案是将状态管理交由某个外部系统(如数据库)完成。反之,在持续运行的 流式作业中,每次处理事件所用到的状态都是持久化的,我们完全可以将其 作为编程模型中的最高级别。按理说,我们也可以使用外部系统来管理流处 理过程中的状态,只是这样可能会引入额外延迟。
由于流式算子处理的都是潜在无穷无尽的数据,所以必须小心避免内部状态 无限增长。为了限制状态大小,算子通常都会只保留到目前为止所见事件的 摘要或概览。这种摘要可能是一个数量值,一个累加值,一个对至今为止全 部事件的 抽样, 一 个窗口缓冲或是一个保留了应用运行过程中某些有价值信 息的自定义数据结构。
不难想象,支持有状态算子将面临很多实现上的挑战:
状态管理: 系统需要高效地管理状态并保证它 们不 受并发更新的影响。
状态划分: 由于结果需要同时依赖状态和到来的事件,所以状态并行化会变得异常复 杂。幸运的是,在很多情况下可以把状态按照键值划分,井独立管理每 一部分。举例而言,如果你要处理从一组传感器得到的测量值数据流,则可 以用分区算子状态、(partitioned operator state)来单独维护每个传感器的 状态。
状态恢复: 最后 一 个也是最大的挑战在于,有状态算子需要保证状态、可以恢复,并且 即使出现故障也要确保结果正确。
任务故障
在流式作业中,算子的状态十分重要,因此需要在故障时予以保护。如果状 态在故障期间丢失,那恢复后的结果就会不正确。流式作业通常 会运行较长 时间 ,因 此状态可能是经过数天甚至数月才收 集得到。通过重新处理所有输 入来 重 建故障期间丢失的状态,不仅代价高,而且还很耗时。
在本章开头,你学到了如何将流处理程序建模成 Dataflow 图。在实际执行前, 它们需要被翻译成物理 Dataflow 图,其中会包含很多相连的并行任务。每个 任务都要运行一部分算子逻辑,消费输入流并为其他任务生成输出流。典型 的现实系统设 置 都可以轻松做到在很多物理机器上 并行运行数 以百计的任务。 对于长期运行的流式作业而 言 ,每个任务都随时有可能出现故障。如何确保 能够透明地处理这些故障,让流式作业得以继续运行? 事实上,你不仅需要 流处理引擎在出现任务故障时可以继续运行,还需要它能保证结果和 算子 状 态的正确性。我们将在本节 一一 讨论这些问题 。
什么是任务故障 ?
对于输入流中的每个事件,任务都需要执行以下步骤: 1接收事件井将它们 存在本地缓冲区;2选择性地更新内部状态、;1 产生 输出记录。上述任何 一 个步骤都可能发生故障,而系统必须在故障情况下明确定义其行为 。 如果故 障发生在第一步,事件是否会丢失?如果在更新内部状态后发生故障,系统 恢复后是否会重复更新?在上述情况下,结果是否确定?
在批处理场景下,上面提到的都算不上问题。由于批处理任务可以轻易“从 头再来’\所以不会有任何事件丢失,状态也可以完全从最初开始构建。然 而在流式场景中,处理故障就没那么容易了。流处理系统通过不同的结果保 障来定义故障时的行为 。 接下来我们回顾 一 下现代流处理引擎所提供的不同 种类的结果保障以及它们相应的实现机制。
结果保障
在讨论不同类型的保障之前,我们需要澄清 一 些在讨论流处理引擎任务故障 时容易导致困惑的点。在本章剩余部分,当提到“结果保障”,我们指的是 流处理引擎内部状态的 一致性。也就是说,我们关注故障恢复后应用代码能 够看到的状态值。请注意,保证应用状态的 一 致性和保证输出的一致性并不 是一 回事儿。一旦数据从数据汇中写出,除非目标系统支持事务,否则结果 的正确性将难以保证。
至多一次
任务发生故障时最简单的措施就是既不恢复丢失的状态,也不重放丢失的事 件。至多 一 次是 一 种最简单的情况,它保证每个 事件至多被 处理 一次。换句 话说,事件可以随意丢弃,没有任何机制来保证结果的正确性。这类保障也 被称作“没有保障”,因为即便系统丢掉所有事件也能满足其条件。无论如何, 没有保障听上去都是个不靠谱的主意。但如果你能接受近似结果并且仅关注 怎样降低延迟,这种保障似乎也可以接受。
至少一次
对大多数现实应用而言,用户期望是不丢事件,这类保障称为至少一次。它 意味着所有事件最终都会处理,虽然有些可能会处理多次。如果正确性仅依 赖、信息的完整度,那 重复 处理或许可以接受 。 例如,确定某个事件是否在输 入流中出现过,就可以利用至少一次保障正确地实现。它最坏的情况也无非 就是多几次定位到目标事件。但如果要计算某个事件在输入流中出现的次数, 至少一次保障可能就会返回错误的结果。
为了确保至少 一 次结果语义的正确性,需要想办怯从惊头或缓冲区中重 放事件。持久化事件日志会将所有事件写入永久存 ft者,这样在任务故障 时就可以重放它们 。实现该功 能的另一个方告是采用记 录确认( record acknowledgments )。该方邑会将所有事件存在缓冲区中,直到处理管道中所 有任务都确认某个事件已经处理完毕才会将事件丢弃。
精确一次
精确一次是最严格, 也是最难实现的一类保障,它表示不但没有事件丢失, 而且每个事件对于内部状态的更新都只有 一次。本质上,精确一次保障意味 着应用总会提供正确的结果,就如同故障从未发生过一般。
提供精确 一 次保障是以至少 一 次保障为前提,因此同样需要数据重放机制。 此外,流处理引 擎需要确保内部状态 的一致性,即在故障恢复后,引擎需要 知道某个 事件对应的更新是否已经反映 到状态上 。事务性更新是实现该目标 的一个方蓓,但它可能会带来极大 的性能开销。 Flink 采用了 轻量级检查点机 制来实现精确一次结果保障。
端到端的精确一次
至今为止你看到的保障类型都仅限于流处理引擎自身的应用状态。在实际流 处理应用中,除了流处理引擎也至少还要有一个数据来源组件和 一个数据终 点组件。端到端的保障指的是在整个数据处理管道上结果都是正确的。在每 个组件都提供自身的保障情况下,整个处理管道上端到端的保障会受制于保 障最弱的那个组件。注意,有时候你可以通过弱保障来实现强语义。 一个常 见情况就是某个任务执行 一 些诸如求最大值或最小值的幕等操作。该情况下, 你可以用至少 一 次保障来实现精确 一 次的语义。
小结
本章主要教给你数据流处理相关的基础知识。我们介绍了 Dataflow 编程模型 以及如何将一个流式应用表示为分布式 Dataflow 图 。接下来 ,你学习了并行 处理无限流的需求,了解了延迟和吞吐对于流式应用的重要性。本章还涵盖 了基本的流式操作以及如何利用窗口在无限输入上计算出有意义的结果。你 学习了流式应用中时间的含义,并比较了事件时间和处理时间的概念。最后 我们介绍了状态对流式应用的重要性,以及如何应对故|璋并确保结果正确。
到目前为止,我们考虑的流处理相关概念都还是独立于 Apache Flink 的。在后续,我们会介绍 Flink是如何实现这些概念的,以及怎样利用它 的 DataStrearnAPI来编写一些涵盖了目前为止所讲特性应用。
网友评论