书名:流式系统
ISBN:978-7-115-64548-7
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
著 [美]泰勒·阿基道(Tyler Akidau)
[美]斯拉瓦·切尔尼亚克(Slava Chernyak)
[美]鲁文·拉克斯(Reuven Lax)
译 陈守元
责任编辑 杨海玲
人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
网址 http://www.ptpress.com.cn
读者服务热线:(010)81055410
反盗版热线:(010)81055315
这是一本理论高度概括的书,从定义(what)、位置(where)、时机(when)和方法(how)的角度讲解流式处理最核心、最本质的概念、特性、设计和思考方式。
本书分为两部分:第一部分以Beam的编程模型为抓手讨论流式处理的种种核心问题,重点是相对高层次地讨论批处理数据处理模型以及流式数据处理模型;第二部分讨论流与表的二象性,即两者之间的类比与差异,对概念进行深入探讨,并讨论关于流式处理的“流和表”的思考方式。本书还概要浏览大数据处理系统的家族历史,深入讨论流式系统发展领域的一些重要贡献。
本书覆盖流式系统的核心理论和本质概念,适合大数据系统爱好者、相关专业学生、数据工程师、数据科学家和开发人员阅读和参考。尽管本书只是流式系统的导论读物,但是仍然需要读者了解大数据处理系统的基本原理并具备相关的使用经验。
O’Reilly以“分享创新知识、改变世界”为己任。40多年来我们一直向企业、个人提供成功所必需之技能及思想,激励他们创新并做得更好。
O’Reilly业务的核心是独特的专家及创新者网络,众多专家及创新者通过我们分享知识。我们的在线学习(Online Learning)平台提供独家的直播培训、互动学习、认证体验、图书、视频,等等,使客户更容易获取业务成功所需的专业知识。几十年来O’Reilly图书一直被视为学习开创未来之技术的权威资料。我们所做的一切是为了帮助各领域的专业人士学习最佳实践,发现并塑造科技行业未来的新趋势。
我们的客户渴望做出推动世界前进的创新之举,我们希望能助他们一臂之力。
“O’Reilly Radar博客有口皆碑。”
——Wired
“O’Reilly凭借一系列非凡想法(真希望当初我也想到了)建立了数百万美元的业务。”
——Business 2.0
“O’Reilly Conference是聚集关键思想领袖的绝对典范。”
——CRN
“一本O’Reilly的书就代表一个有用、有前途、需要学习的主题。”
——Irish Times
“Tim是位特立独行的商人,他不光放眼于最长远、最广阔的领域,并且切实地按照Yogi Berra的建议去做了:‘如果你在路上遇到岔路口,那就走小路。’回顾过去,Tim似乎每一次都选择了小路,而且有几次都是一闪即逝的机会,尽管大路也不错。”
——Linux Journal
在这个由数据驱动的时代,我有幸见证了云计算、大数据和人工智能如何成为推动生产力发展的强大引擎,引领社会各行各业经历深刻的变革。我的职业生涯跨越了20多年,专注于大规模分布式系统、大数据处理和人工智能的探索与实践。这段经历让我坚信,这些技术必将成为第四次工业革命“下半场”的核心力量,为社会和产业带来持续而深远的影响。
流式处理作为大数据处理领域中一个新兴且快速发展的分支,在实时数据分析、事件驱动架构及复杂事件处理等方面展现出了巨大的潜力和价值。Apache Flink作为流式处理领域中的佼佼者,其先进的实时流计算框架已经成为工业界实时流计算的事实标准。《流式系统》一书不仅深入探讨了流式处理的理论和实践,更与Apache Flink的设计理念不谋而合,为我们提供了一个全面而深入的视角来理解和应用流式计算技术。
阿里云一直是Apache Flink技术和社区的积极推动者,参与并贡献了大量Apache Flink内核引擎及周边基础设施的内容,创建并推动了整个Apache Flink中文社区的发展,对Apache Flink中文社区的繁荣和推广起到了非常积极的作用。此外,阿里云还在云平台上提供了Serverless Flink全托管产品——实时计算Flink版,帮助用户以最小的代价开启云原生Flink技术并享受其带来的便利。从阿里云和Apache Flink的合作模式中,我看到了一家负责任的开放云计算公司对待先进开源技术社区的态度:从拥抱开源到参与开源,再到贡献开源,最终引领开源。只有这样,云服务提供商和开源社区才能做到互惠互利、共存共荣、协同发展!
最后,我对《流式系统》一书的出版表示祝贺。我相信,这本书将为Apache Flink中文社区的伙伴们提供宝贵的知识资源,帮助他们在云时代享受技术爆炸式增长带来的知识盛宴。让我们一起拥抱这个充满机遇和挑战的新时代,共同探索未知,创造未来。
Enjoy the journey of knowledge and innovation!
汪军华
阿里云副总裁、阿里云计算平台事业部负责人
于杭州西子湖畔
首先向Apache Flink中文社区中一直关心并期待本书的伙伴们表达一下歉意,实在让大家久等啦!
说起翻译本书的渊源,最要感谢的是Apache Flink中文社区的伙伴们,是你们慧眼识珠,发现了这本关于流式系统的权威理论书。从2018年开始,我所在的阿里云实时计算团队开始推进Apache Flink中文社区的相关运营工作,我们团队一直致力于将Apache Flink相关技术引入中文社区,并推动中文社区的开源大数据生态建设。我们团队希望通过一系列开发者社区活动,在中国产业界培育整个开源大数据的用户、社区、市场,努力推动开源大数据技术在中国各行各业的落地和发展,最终实现全社会普惠计算的愿景。
对Apache Flink中文社区的学习者而言,这是一本理论高度概括的好书,它从定义、位置、时机和方法的角度讲解流式处理最核心、最本质的概念、特性、设计和思考方式。但这绝不只是一本“正经教条”的教科书,它除了尝试教选修大数据流式处理相关课程的计算机相关专业的学生有关流式处理领域的理论,还关注工业实用性,对从工业界诞生的且以设计先进和社区活跃迅速抢占全球开源社区流计算市场的新一代流式处理引擎Apache Flink的架构设计大加赞赏。因此,可以说,Apache Flink与本书的联系跨越了学术、工程和社区。在本书最初写作时,Apache Flink的相关核心创始人,包括Stephan和Kostas,都深入参与其中,并对技术和理论内容有所贡献。因此,如果希望从实践入手学习流计算,我建议阅读Apache Flink的官方文档;如果希望从理论入手学习流计算,我建议优先阅读本书。
我要向参与本书翻译的所有人员表示衷心的感谢。特别感谢来自中国信息通信研究院大数据团队的姜春宇、马鹏玮、王妙琼对本书进行了技术审校,你们的参与让本书的技术内容更加可靠。我还要感谢人民邮电出版社杨海玲编辑及其团队,你们对图书质量的严格把控令人钦佩,感谢你们在本书出版过程中的辛勤工作。感谢我的家人,特别是我的妻子唐诗斯女士,你们始终如一的支持帮助我完成了这本译作。
最后,预祝大家阅读本书愉快!
陈守元(巴真)
热衷冒险的读者朋友大家好,欢迎打开这本书!此时此刻,我假定你要么是期待了解更多有关流处理的知识精髓,要么只是希望花几小时阅读有关褐鳟[1]的传奇故事。不管是哪种情况,我都向你致敬!但是,选择后者且对计算机科学没有深入理解的人,在勇敢地深入本书内容之前,可能需要做好失望的心理准备。
[1] 这里的褐鳟就是指本书封面上的动物。这是作者开玩笑的说法:读者可能会根据书的封面插图误认为本书是讲褐鳟的书。——译者注
开卷之前先定基调,有些事情我必须事先提醒一下。首先,本书有多位作者,但我们并没有尝试假装我们都以同样的风格说话和写作。就像我们是奇怪的三胞胎但恰好出生在不同的家庭。尽管这听起来很有趣,但最终的写作结果实际上会让人读起来不那么愉悦。相反,我们尝试选择以自己的语言风格编写本书,并且我们给予本书足够的自我意识,以便可以在适当的时候提到我们每个人,但又不能给予它过多的自我意识,以至于它怨恨我们只把它制作成一本书,而不是像带有苏格兰口音的机械恐龙那样更酷的东西。[2]
[2] 顺便说一句,这原本就是我们要求的动物书封面,但是O’Reilly的编辑认为这不利于制作线稿。我谨对此表示不同意,本书现在的封面上的褐鳟是一种妥协。
封面本来可能是……
本书有3位作者。
Tyler
就是我。如果本书中你没有明确地被告知是谁在说话,你可以假设是我,因为我们在本书后半部分加入了其他作者。当我写到一半想到要回去更新我已经写过的所有东西时,我基本上就像“噢,不要吧”的状态。我是谷歌公司数据处理语言与系统团队[3]的技术负责人,负责谷歌的Cloud Dataflow、Apache Beam,以及谷歌内部的数据处理系统,如Flume、MillWheel和MapReduce。我还是Apache Beam PMC的创始成员。
[3] 或者叫DataPLS,发音类似Datapals,明白了吧?
Slava
他是谷歌MillWheel团队的长期成员,后来是MillWheel系统的继任者Windmill团队的创始成员,这一至今尚未命名的系统主要负责支撑谷歌的Cloud Dataflow中的流式引擎。他是当今世界上在流处理系统中水位和时间语义方面最顶尖的专家之一。你会发现,他是第3章的作者,这不足为奇。
Reuven
他位列这份作者名单的最后一位,是因为他在流式系统领域比Slava和我都更具经验,因此我们担心他放在我们之前我们承受不了这份压力。他曾发起并领导谷歌在通用流处理引擎领域几乎所有的系统级技术创新,其中包括他投入大量精力去研究在使用细粒度检查点的系统上提供高吞吐量、低延时、精确一次的语义。你会发现,他是第5章的作者。他也是Apache Beam PMC成员。
现在,你已经知道自己将从哪些人身上获得信息,接下来就是要了解自己将获得什么信息,让我们回到我想提到的第二件事。本书在概念上有两个主要部分,每部分有4章,每部分之后都有一章相对独立。
阅读的乐趣从第一部分“Beam模型”(第1章到第4章)开始,Beam模型最初是为谷歌的Cloud Dataflow开发的,后来作为Apache Beam项目捐赠给了Apache软件基金会。当前Beam模型在行业的大多数系统中被全部或者部分集成。第一部分的重点是高层次地讨论批加流数据处理模型,由4章组成。
● 第1章涵盖流处理的基础,建立一些术语定义,讨论流式系统的能力,区分两个重要的时间域(处理时间和事件时间),最后研究一些常见的数据处理模式。
● 第2章详细介绍针对乱序数据进行健壮的流处理所涉及的核心概念,定义、位置、时机和方法中每部分都会在具体运行示例的上下文中进行详细分析,并用动图来突出时间的维度。
● 第3章(由Slava编写)对时间进展指标、指标创建的方法以及指标在流水线中传播的方法进行深入探讨。这一章的最后详细分析两种真实场景下水位的实现。
● 第4章从第2章话题中止的地方继续讨论,深入到高级开窗和触发概念,如处理时间窗口、会话和持续触发器。
在第一部分和第二部分之间插入包含重要细节的第5章(由Reuven编写)。在第5章中,Reuven列举提供端到端精确一次(或有效一次)处理语义所带来的挑战,并详细介绍3种不同的精确一次处理方法的实现细节:Apache Flink、Apache Spark 和谷歌Cloud Dataflow。
紧接着是第二部分(第6章到第9章),这一部分对概念进行深入探讨,并研究以底层“流和表”的方式思考流处理,该思想最近由Apache Kafka社区的爱好者传播,当然源头可以追溯到数十年前的数据库社区。这部分内容也由4章组成。
● 第6章介绍流和表的基本思想,通过流和表的角度分析经典的MapReduce方法,然后尝试构建足够通用的流表理论,覆盖全部Beam模型的内容(甚至更多)。
● 第7章讨论流式流水线引入持久状态的动机,研究两种常见的隐式状态,然后分析一个实际的使用场景(广告归因),最终引出通用状态管理机制的必要特征。
● 第8章研究在关系代数和SQL中有关流式的含义,对比当前Beam模型和经典SQL中表和流之间固有的设计偏好,并且提出一组可能的路线,将健壮的流式语义并入SQL中。
● 第9章研究各种不同的连接类型,分析它们在流式上下文中的行为,最后详细研究一个有用但仍未广泛支持的流式连接使用场景:时间有效性窗口。
最后,本书的结尾是第10章,这一章纵览MapReduce数据处理系统家族的重大历史,深入讨论推进流式系统发展到今天的一些重要贡献。
作为最后一点指导,如果你要我描述一下我最希望读者从本书中吸收的知识点,我会列出如下重点。
● 从本书中你能学到的唯一最重要的东西就是流表理论,以及流和表如何相互关联。其他一切知识点均是建立在上述内容之上的。我们直到第6章才开始谈这个主题。不过没关系,它值得等待,因为只有有了前面几章的铺垫,你才能更好地欣赏它的美妙。
● 时变关系启发大家新的思考方式。时变关系是流处理最典型的特征:构建流式系统所有功能的体现,并且将来自批处理的我们熟知和热爱的类似工具与上述流处理联系起来。直到第8章我们才介绍时变关系,同样,之前所有章的铺垫都是为更好地学习它们。
● 一个编写精良的分布式流式引擎是充满魔力的。这个观点其实适用于任意一个分布式系统,但是,你越深入地了解如何构建这些系统以提供处理语义(特别是来自第3章和第5章的案例研究),就越会感受到编写一个分布式流处理引擎背后的工作有多繁重了。
● LaTeX/Tikz是制作图表、动图等的工具。我对于这个工具的评价是:看似可怕、锋利,但确实是令人惊叹。我希望本书中的动图能让我们讨论的复杂话题显得更加简明,从而激励更多的人尝试LaTeX/Tikz。
本书使用了下述排版约定。
中文楷体
表示新术语。
等宽字体(Constant
width
)
表示代码,段落内表示与代码相关的元素,如变量或函数名称、数据库、数据类型、环境变量、语句和关键字。
粗体等宽字体(Constant
width
bold
)
表示命令或用户输入的其他文本。
斜体等宽字体(Constant
width
italic
)
表示该文本应由用户提供的值或由用户根据上下文决定的值替换。
下面提供一些相关的在线资源可以帮助你享受本书。
本书中所有的图可在异步社区本书对应的网页中找到。这对于自带动画效果的图特别有用。
动图是用LaTeX/Tikz绘制的,先转换为PDF,然后通过ImageMagick转换为动画GIF。对于一些刨根问底的读者,在异步社区本书对应的网页中还可以找到用于呈现动图的完整源代码和说明(包括本书、“Streaming 101”和“Streaming 102”博客文章以及原始Dataflow模型的论文)。注意,这大约有1.4万行LaTeX/Tikz代码,这些代码盘根错节地增长,我们当时并没有打算让其他读者来阅读或者修改代码。换言之,那是一张杂乱的、交织在一起的网,建议你就此打住,不要再去探索代码。
尽管本书在很大程度是概念性的,但是仍然有很多代码和伪代码片段用于说明要点。第2章和第4章中关于功能性的核心Beam Model概念的代码,以及第7章中有关状态和计时器概念的代码,可以从异步社区本书对应的网页中获得。理解流式语义是主要目标,代码主要使用Beam PTransform/DoFn
实现并附带单元测试代码。还有一个单独的流水线实现来演示单元测试和实际流水线之间的差异。代码布局如下。
src/main/java/net/streamingbook/BeamModel.java
示例2-1至示例2-9以及示例4-3的Beam PTransform
实现,每个实现都带有额外的方法,用于在使用各章的示例数据集执行时返回预期的输出。
src/test/java/net/streamingbook/BeamModelTest.java
通过与本书中匹配的生成数据集来验证BeamModel.java中的示例PTransform
的单元测试。
src/main/java/net/streamingbook/Example2_1.java
示例2-1流水线的单机版本,可以在本地运行或使用分布式Beam运行引擎。
src/main/java/net/streamingbook/inputs.csv
Example2_1.java的样例输入文件,包含本书的数据集。
src/main/java/net/streamingbook/StateAndTimers.java
使用Beam的状态和计时器原语实现第7章中转化归因示例的Beam代码。
src/test/java/net/streamingbook/StateAndTimersTest.java
验证StateAndTimers.java中的转化归因DoFn
的单元测试。
src/main/java/net/streamingbook/ValidityWindows.java
时间有效性窗口实现。
src/main/java/net/streamingbook/Utils.java
一些共享的工具类方法。
本书是为了帮助你完成工作。一般来说,如果本书提供了示例代码,你就可以在程序和文档中直接使用它。你无须与我们联系获得授权,除非你正在复制代码的重要部分。例如,你的代码中直接使用本书中的若干代码片段不需要授权,但销售或分发O’Reilly图书示例的CD-ROM确实需要授权。通过引用本书内容或者引用示例代码以回答一些问题不需要授权,但将本书中大量的示例代码合并到你产品文档之中确实需要授权。
我们不要求注明出处,但如果注明出处我们会非常感谢。出处通常包括标题、作者、出版商和ISBN。例如“Streaming Systems by Tyler Akidau, Slava Chernyak, and Reuven Lax (O’Reilly). Copyright 2018 O’Reilly Media, Inc., 978-1-491-98387-4.”。
如果觉得使用代码示例超出了合理使用或上面我们所给出的权限,请随时通过permissions@oreilly.com与我们联系。
如果想就本书发表评论或有任何疑问,敬请联系O’Reilly出版社。
美国:
O’Reilly Media, Inc.
1005 Gravenstein Highway North
Sebastopol, CA 95472
中国:
北京市西城区西直门南大街2 号成铭大厦C 座807 室(100035)
奥莱利技术咨询(北京)有限公司
我们还为本书建立了一个网页,其中包含勘误表、示例和其他额外的信息。你可以在O’Reilly官方网站访问本书对应的网页。
关于本书的技术性问题或建议,请发电子邮件到errata@oreilly.com。
要查看更多我们的书籍、课程、会议和最新动态等信息,可访问O’Reilly官方网站。
最后也是最重要的一点是,许多人都特别出色,我们要在这里感谢他们中的一部分,感谢他们对我们撰写这本书给予的帮助。
本书的内容提炼了谷歌、整个行业以及整个学术界无数绝顶聪明人的工作精华。我们对他们表示诚挚的感谢,但遗憾的是我们无法将这些帮助过我们的人一一列出来道谢。
在谷歌的同事中,DataPLS团队(以及这一团队各个前身团队,包括Flume、MillWheel、MapReduce等)中的所有人都功不可没,多年来,他们帮助我们让这些想法落地实现。我们尤其要感谢下面这些人。
● 感谢Paul Nordstrom和来自MillWheel黄金期的MillWheel团队的其他成员Alex Amto、Alex Balikov、Kaya Bekirğlu、Josh Haberman、Tim Hollingsworth、Ilya Maykov、Sam McVeety、Daniel Mills和Sam Whittle建立这样一套全面的、健壮的、可伸缩的底层原语,我们在这些原语之上能够构建本书中讨论的更高级的模型。如果没有他们的远见和技能,今天大规模流处理领域将截然不同。
● 感谢Craig Chambers、Frances Perry、Robert Bradshaw、Ashish Raniwala和Flume团队的其他成员通过Flume实现一套易于表达且功能强大的大数据处理基础,我们基于此统一了流式世界。
● 感谢Sam McVeety,MillWheel论文的主要作者,这篇论文第一次使我们这个令人惊叹的小项目声名远扬。
● 感谢Grzegorz Czajkowski在很多项目交付期限和其他紧急事项迫在眉睫的情况下还一直支持我们的技术布道工作。
从更大视野看,大量的功劳归于Apache Beam、Apache Calcite、Apache Kafka、Apache Flink、Apache Spark和Apache Storm社区的每个人。在过去10多年中,这些项目为推动全世界流处理的最先进技术做出了重大贡献。
为了更加具体地表示感谢,我们要感谢下面这些人。
● 感谢Martin Kleppmann,带头倡导“流和表”的思维方式,并投入大量时间为本书每章的初稿提供了富有见地的技术意见和编辑意见。你是一个充满创意、各方面都非常出色的人。
● 感谢Julian Hyde,感谢你充满洞察的愿景以及对流式SQL的异常热情。
● 感谢Jay Kreps,感谢你与Lambda架构“暴政”做斗争,你原创的“Questioning the Lambda Architecture”文章让Tyler顿时精神抖擞,被感染到也加入了这场辩论。
● 感谢Stephan Ewen、Kostas Tzoumas、Fabian Hueske、Aljoscha Krettek、Robert Metzger、Kostas Kloudas、Jamie Grier、Max Michels和dataArtisans[4]大家庭的其余同事。一直以来,你们总是以开放和协作的方式去推动流式系统发展。感谢大家,流式世界因为你们变得更加美好。
[4] dataArtisans被阿里巴巴收购后,已改名为Ververica。——译者注
● 感谢Jesse Anderson,感谢你勤奋审稿和热情的拥抱。如果见到你,我一定给你一个大大的拥抱。
● 感谢Danny Yuan、Sid Anand、Wes Reisz以及卓越的QCon开发者大会,使我们第一次有机会在2014年旧金山QCon大会上对业界公开谈论我们的工作。
● 感谢O’Reilly的Ben Lorica和标志性的Strata数据会议,一再支持我们努力传播流处理技术,无论是在线上,还是通过书籍,或是亲自宣传。
● 感谢整个Apache Beam社区,特别是我们的提交者(committer),帮助推进Beam的愿景,感谢Ahmet Altay、Amit Sela、Aviem Zur、Ben Chambers、Griselda Cuevas、Chamikara Jayalath、Davor Bonaci、Dan Halperin、Etienne Chauchot、Frances Perry、Ismaël Mejía、Jason Kuster、Jean-Baptiste Onofré、Jesse Anderson、Eugene Kirpichov、Josh Wills、Kenneth Knowles、Luke Cwik、Jingsong Lee、Manu Zhang、Melissa Pashniak、Mingmin Xu、Max Michels、Pablo Estrada、Pei He、Robert Bradshaw、Stephan Ewen、Stas Levin、Thomas Groh、Thomas Weise和James Xu。
如果不对不知疲倦的审稿人表达感谢,那这篇致谢绝对是不完整的,那些充满讲解的评注让平凡的东西变得卓越。感谢Jesse Anderson、Grzegorz Czajkowski、Marián Dvorský、Stephan Ewen、Rafael J. Fernández-Moctezuma、Martin Kleppmann、Kenneth Knowles、Sam McVeety、Mosha Pasumansky、Frances Perry、Jelena Pjesivac-Grbovic、Jeff Shute和William Vambenepe,你们就像是《回到未来2》中时光车的核融合装置。
当然,我们还要感谢我们的创作和出版支持团队。
● 感谢我们的首任编辑Marie Beaugureau给予我们的帮助和支持,以使该项目得以开展,并对我颠覆编辑规范保持一贯的耐心。我们想你!
● 感谢我们的继任编辑Jeff Bleiel接管了我们整个出版项目,并且帮助我们完成了这个项目,感谢你对我们可能无法在最后宽松的期限内完成书稿保持了极大耐心。最终我们完成了!
● 感谢我们的文字编辑Bob Russell比任何人都更仔细地阅读了我们的书。我向你对语法、标点、词汇和Adobe Acrobat批注的熟练掌握表示敬意。
● 感谢我们的制作编辑Nick Adams帮助我们将一团乱如麻的HTML代码整理成一件制作精良的出版物。当我要求你手动忽略Bob的多次有关将我们使用的术语“数据”从复数改为单数的建议时,你没有生我的气。你让本书看起来比我希望的还要好看,谢谢。
● 感谢我们的索引编制者Ellen Trotman-Zaig以某种方式将一个错综复杂的参考网络处理成一个有用且全面的索引。我敬畏你对细节的关注。
● 感谢我们的插图画师Rebecca Panzer美化我们的静态图,并向Nick保证我不需要花更多的周末时间来研究如何重构我的动画以调整为更大的字体。
● 感谢我们的校对员Kim Cofer指出我们的草率和不一致之处,这样其他读者就不需要再受一次折磨。
Tyler想感谢下面这些人。
● 感谢我的合著者Reuven Lax和Slava Chernyak,以前所未有的方式将想法和章节付诸实践。
● 感谢Rob Schlender要在机器人接管世界前给我买一瓶苏格兰威士忌。敬时尚!
● 感谢我的叔叔 Randy Bowen,让我发现我是多么喜欢计算机,尤其是那张自制的Pov-Ray2.x软盘,为我打开了一个全新的世界。
● 感谢我的父母David和Marty Dauwalder,没有你们的奉献精神和难以置信的毅力,这一切都不可能实现。你们是有史以来最好的父母!
● 感谢David L. Vlasuk博士,没有你,我今天就不会在这里了。谢谢你给予我的所有帮助。
● 感谢我的家人Shaina、Romi和Lone Akidau,尽管写作占用了我许多夜晚和周末,但你们仍坚定地支持我完成了这项艰巨的任务。我永远爱你们。
● 感谢我忠实的写作伙伴Kiyoshi,尽管我们一起写这本书的时候,你要么在睡觉,要么在邮差到来时狂叫,但不得不说你做得毫无瑕疵,而且似乎不费吹灰之力。你是你小狗家族的骄傲。
Slava要感谢下面这些人。
● 感谢在MillWheel和后续的流式Dataflow中的水位以及这些系统的许多其他部分的代码设计人员和共同参与者Josh Haberman、Sam Whittle和Daniel Mills。像这样复杂的系统从来都不是凭空设计,没有你们每个人的思考和努力,我们今天不会有如此成就。
● 感谢来自Ververica的Stephan Ewen帮助我认识并理解了Apache Flink的水位机制。
Reuven要感谢下面这些人。
● 感谢Paul Nordstrom的远见,感谢Sam Whittle、Sam McVeetty、Slava Chernyak、Josh Haberman、Daniel Mills、Kaya Bekirog Lu、Alex Balikov、Tim Hollingsworth、Alex Amato和Ilya Maykov在构建最初的MillWheel系统以及撰写后续论文付出的所有努力。
● 感谢来自Ververica的Stephan Ewen帮助审阅了第5章,以及针对Apache Flink内部机制给出的宝贵反馈。
最后,我们都要感谢亲爱的读者,感谢你愿意花钱购买本书,听我们喋喋不休地谈论我们要做的和要玩的东西。把我们所知所想写下来实为人生快事,我们竭尽全力确保你购买本书是物有所值的。但是,如果出于某种原因你不喜欢它……好吧,希望你买了纸质书,这样你至少可以把它扔到房间里,一段时间后当作旧书卖掉。
流式数据处理在大数据时代中占据了举足轻重的地位,理由非常充分,包括以下几点。
● 企业渴望更加及时地洞察自己的数据,从使用批处理转变为使用流式处理是实现低延时的好方式。
● 使用一套专为处理无界数据而设计的数据处理系统显然更容易驾驭现代企业中越来越常见的大量无界数据集。
● 在数据到达时进行数据处理的机制使系统的负载在时间维度分配更加均衡,从而产生更加一致且可预测的资源消耗。
尽管业务驱动激发了人们对流式的浓厚兴趣,但是与批处理系统相比,流式系统长期以来仍然处于相对不成熟的状态。直到最近,流式系统才呈现出向成熟转变的势头。我曾经傲慢地认为,我之前的“Streaming 101”和“Streaming 102”两篇博客文章对流式处理发展方向的变化是有些许贡献的(这两篇博客实际上是本书前几章的基础)。但实际上,有大量业内人士非常乐于看到一套流式系统趋于成熟,也有一大批聪明且能干的朋友热衷于构建一套这样的系统。
以我的观点来看,即使对流式的总体宣传已经取得了实质性的成功,我仍然会在本书中或多或少地引用我在“Streaming 101”中的一些论点。原因有二:其一,尽管众多行业已经开始注意到这场争辩的呼声,但是“Streaming 101”中的诸多论点在今天看来仍然非常适用;其二,有很多人还不了解“Streaming 101”中的论点,本书是我阐释这些论点的进一步尝试。
进入正题之前,我会介绍一些重要的背景信息,这些信息有助于对后续将要探讨的主题构建知识框架。我会从以下3个方面进行介绍。
要精确地谈论复杂的主题,通常需要精确的术语定义。对于一些当前正在使用且被过多解释的术语,我会尽量准确地表述我在使用这些术语时希望传递的信息。
我会指出当前流式系统的各类常见的缺陷,还会提出一些我认为为了今后能够满足现代数据消费者的需求,数据处理系统的构建者需要采用的思维方式。
我会介绍数据处理中相关联的两个基本的时间域,展示它们的关联方式,并指出这两个域给数据处理系统带来的一些难题。
在深入讨论后续内容之前,我想先明确一个概念:什么是流式(streaming)?如今,流式这个术语可以用来描述各种不同的概念(为简单起见,到目前为止我一直在比较宽泛地使用这个术语),这种情况会导致人们对“什么是流式”或者“流式系统能解决什么问题”产生误解。因此,我更愿意将这个术语定义得精确一些。
问题的关键在于,许多应该通过“是什么”来描述的概念(如无界数据处理、近似结果等),已经被非正式地描述为它们之前是“如何”(如通过流式执行引擎)形成的。术语缺乏精确性使流式的真正含义变得不清晰,在某些情况下,这种不清晰会给流式系统本身造成不良的影响,会使人们认为它们的功能局限于历史上所描述的“流式”特征,如只能提供近似的或者推测性的结果。
考虑到设计良好的流式系统与现有的任意批处理引擎一样能够(技术上更是如此)产生正确、一致且可重现的结果,我倾向于单独赋予“流式”这个术语一个非常具体的含义。
一类为无限的数据集而设计的数据处理引擎。[1]
[1] 出于完整性的考虑,需要指出的是,这个定义既包括真正的流式,也包括微批实现。如果你对微批处理系统不熟悉,可以将微批处理系统理解为通过使用重复执行批处理引擎的方式处理无界数据的流式系统。Spark Streaming在微批处理方面是业界的典范。
如果我想谈论低延时、近似的或推测性的结果,我会使用这些具体的词汇,而不是不精确地称它们为“流式”。
你会遇到讨论不同类型的数据的场景,这时精确的术语同样可以起到很好的作用。从我的角度来看,定义一个给定数据集的形态有两个重要的(并且正交的)维度:基数(cardinality)和构成(constitution)。
数据集的基数决定了数据集的大小,基数最突出的方面是决定给定数据集的大小是有限的还是无限的。我倾向于用以下两个术语描述数据集中的粗粒度基数。
一类大小有限的数据集。
一类大小(至少在理论上)无限的数据集。
基数之所以重要,是因为无限数据集的无界性会对消耗这些无限数据集的数据处理框架造成额外的负担。1.2节将对此进行详细介绍。
另外,数据集的构成决定了数据集的物理呈现方式。因此,数据集的构成定义了人们和所讨论的数据进行交互的方式。在第6章之前,我们不会深入研究数据集的构成,但是为了让你对构成有一个简要的感知,我们先介绍两个重要的基本构成。
表是数据集在某个特定时间点的整体视图。SQL系统传统上是在处理表。
[2] 熟悉我的原创博客文章“Streaming 101”的读者可能记得,在讨论数据集时,我强烈建议放弃术语“流”。但是我的这个建议并没有被大家接受,我最初认为是因为这个术语的易用性和大众的使用习惯,但回想起来,我认为我之前完全理解错了。实际上,区分两种不同类型的数据集构成(表和流)有很大的价值。本书后半部分的大部分内容也确实专注于理解这二者之间的关系。
流是数据集逐个元素随时间演变的视图。数据处理系统的MapReduce体系传统上是在处理流。
我们将在第6、8和9章中深入研究流和表之间的关系,还将在第8章中学习将流和表联系在一起的时变关系的统一的底层概念。但在此之前,我们主要讨论流,因为流是现今大多数数据处理系统(包括批处理系统和流式系统)中流水线开发人员直接进行交互的构成。这也是最自然地体现流处理独有的挑战的构成。
接下来讨论一下流式系统能做什么和不能做什么,重点讨论能做什么。我想在本章中阐述的最重要的事情之一就是,一个设计良好的流式系统有多么强大。流式系统在历史上已经被降级至一个能够提供低延时但是输出不准确或推测性结果的小众市场。为了最终能够提供正确的结果,流式系统通常与能力更强的批处理系统结合成一个数据处理架构,即Lambda架构。
如果你不太熟悉Lambda架构,可以将Lambda架构的基本思想理解为在批处理系统旁边运行一个流式系统,二者执行本质上相同的计算。流式系统提供低延时、不准确的结果(要么是因为使用近似算法,要么是因为流式系统本身不提供正确性),随后批处理系统会继续运行并提供正确的输出。Lambda架构最早由推特(Twitter)的Nathan Marz(Storm的作者)提出,最终非常成功,因为它在当时确实是一个了不起的想法。当时流式引擎在正确性上不尽如人意,而批处理引擎又天生稍显笨拙,于是Lambda架构提供了让你“鱼与熊掌兼得”的方法。但是,维护Lambda系统很麻烦——需要构建、提供和维护两个独立版本的流水线,而且在整个流程的最后需要以某种方式将这两个流水线的结果进行合并。
作为多年来致力于强一致性流式引擎的研究人员,我也发现Lambda架构的整个原理有点儿令人厌恶。Jay Kreps的“Questioning the Lambda Architecture”博客文章一出现我就很自然地成了这篇文章的忠实拥护者。这篇文章是当时反对双模式执行的必要性的非常明显的声明之一。令人开心的是,Kreps使用Kafka这样的可重放系统作为流式互联系统,解决了可重复性的问题,进而提出了Kappa架构。Kappa架构的基本思想是使用一个为当前作业量身定制的、设计良好的系统来运行单条流水线。我不确定这个概念是否需要有一个希腊字母名称,但原则上我完全支持这个观点。
批处理和流式的效率差异
我提出过一个观点:效率问题并不是流式系统固有的局限性,而是目前大多数流式系统的设计选择导致的结果。批处理和流式之间的效率差异很大程度上是批处理系统中不断增加的数据捆绑和更高效的混洗传输的结果。现代批处理系统不遗余力地实施复杂的优化,从而允许使用少得惊人的计算资源就能获得显著的吞吐量。没有理由说这些使批处理系统效率大增的奇思妙想不能被运用到一套为无界数据设计的系统中,为用户在我们通常考虑的高延时、较高效率的批处理和低延时、较低效率的流式处理的两种方案中提供灵活的选择。实际上我们在谷歌公司已经用Cloud Dataflow做到了这一点,方法是在统一模型下同时提供批处理运行引擎和流式运行引擎。在我们的使用场景中,我们使用了两套独立的运行引擎,因为正好有两个独立设计的系统针对其特定的使用场景进行了优化。从长远来看,从工程的角度,我希望看到我们将两套独立的系统合并成一套系统,这套系统融合了二者的精华,同时仍然保持让用户选择恰当的效率级别的灵活性。不过,这套系统目前仍未完全实现。老实说,因为有统一的Dataflow模型,将两套独立系统合并成一套系统甚至不是非常必要,所以将两套独立系统合并成一套系统可能永远不会发生。
说实话,我想进一步探讨一些东西。我认为设计良好的流式系统实际上提供了批处理功能的严格超集。模(modulo)运算也许是一个效率增量,应该不需要像现在这样的批处理系统。感谢Apache Flink社区的人们把这个想法铭记在心,构建了一个即使在“批处理”模式下底层也始终是全流式的系统。我太喜欢它了。
所有这一切的必然结果是,流式系统的日趋成熟与能够处理无界数据的稳健框架相结合,将促使Lambda架构慢慢退至它在大数据历史中古董的位置。我相信现在是实现这一目标的时候了。因为要做到这一点,也就是说,要以其人之道,还治其人之身,击败批处理系统,你只需要两样东西——正确性和推断时间的工具。
保证了正确性就可以使流式和批处理平起平坐。从核心上说,正确性可归结为一致性存储。流式系统需要一种随时间对持久状态进行检查点操作的方法(Kreps在他的“Why local state is a fundamental primitive in stream processing”一文中已经谈到了这一点),并且这种方法必须设计得足够好,以便在机器发生故障时保持状态的一致。当Spark Streaming数年前首次出现在大数据场景中时,它就像是黑暗的流式世界中一盏关于一致性的明灯。值得庆幸的是,从那以后情况有了很大的改善,但是需要关注的是,很多流式系统仍然试图在没有强一致性的情况下运行。
因为这一点非常重要,所以再强调一次:强一致性是实现“精确一次”(exactly-once)处理[3]的必要条件,“精确一次”处理对于正确性是必要条件,而正确性又是任何有希望齐平乃至超越批处理系统功能的必要条件。除非你真的不在乎计算结果,否则我恳请你避免使用任何不提供强一致状态的流式系统。批处理系统不要求提前验证它们是否能够产生正确的结果,不要把时间浪费在不能满足正确性的流式系统上。
[3] 如果不熟悉我说的“精确一次”的意思,我在这里解释一下。精确一次指的是某些数据处理框架提供的特定类型的一致性保证。一致性保证通常分为3个主要类别:至多一次处理、至少一次处理和精确一次处理。注意,此处使用的名称指的是最终在流水线生成的输出中观测到的有效语义,而不是流水线可能处理(或尝试处理)任何给定记录的实际次数。出于这个原因,“有效一次”这个术语会用来替代“精确一次”,因为“有效一次”对于描述底层实现的本质更具代表性。Reuven会在第5章中更详细地介绍这些概念。
如果想了解更多关于在流式系统中获得强一致性所需的内容,建议查看MillWheel、Spark Streaming和Flink snapshotting的论文。这3篇论文都用了大量的篇幅来讨论一致性。Reuven将在第5章中对一致性保证进行深入探讨,如果你渴望了解更多内容,可以在一些文献和其他地方找到关于该主题的高质量信息。
这些工具将帮助你超越批处理。好的时间推断工具对处理不同的事件时间偏差的无界无序数据至关重要。越来越多的现代数据集呈现出上述这些数据特征,而现有的批处理系统(以及许多流式系统)缺乏必要的工具来应对上述特征带来的种种困难(尽管现在情况在快速地变化,甚至在我写这本书的时候它也在变化)。本书的大部分内容会用来解释和关注这一要点的方方面面。
首先,大家需要对时间域的重要概念有基本的了解;然后,我会深入讨论刚才提到的不同的事件时间偏差的无界无序数据;最后,我会在本章的其余几节中讨论使用批处理系统和流式系统进行有界数据处理和无界数据处理的常见方法。
如果需要透彻地讲解无界数据处理,必须对时间相关的域有清晰的理解。在任何数据处理系统中,通常有两个我们关心的时间域。
事件实际发生的时间。
事件在系统中被观测到的时间。
并非所有使用场景都需要考虑事件时间(如果你的场景不需要考虑事件时间,你的工作会变得轻松很多),但很多使用场景需要。这样的示例包括随时间的推移描述用户行为、大多数的计费应用,以及很多类型的异常检测等。
在理想情况下,事件时间和处理时间应该总是相等的,即事件在发生时就立即被处理。但现实情况并非如此,事件时间和处理时间之间的偏差不仅非零,而且通常是受底层输入源、执行引擎和硬件等特征共同影响的一个高度可变的函数。能够影响偏差程度的因素包括以下几个。
● 共享资源限制,如网络拥塞、网络分区或非专用环境中的共享CPU。
● 软件原因,如分布式系统处理逻辑、竞争等。
● 数据本身的特性,如键分布、吞吐量变化或无序变化(例如,一架坐满了乘客的飞机,这些乘客在整个航程中离线使用手机后将手机从“飞行模式”退出)。
因此,如果你绘制任何真实系统中的事件时间和处理时间的进度关系,通常会得到类似于图1-1中实线所示的结果。
在图1-1中,斜率为1的虚线表示处理时间和事件时间恰好相等的理想情况,实线表示实际情况。在本示例中,系统的处理能力在处理时间的前期稍微滞后,在处理时间的中期趋近于理想情况,然后在后期再次稍微滞后。一眼就可以看出,在图1-1中有两种存在于不同时间域的偏差。
图1-1 时间域映射。x轴表示系统中事件时间的完整性,即到事件时间中的X时刻为止,所有事件时间小于X的数据都被系统观测到。y轴[4]表示处理时间的进度,即数据处理系统在执行操作时观测到的正常时钟时间
[4] 发布“Streaming 101”以来,许多人向我指出,将处理时间放在x轴上,将事件时间放在y轴上,会更直观。我认同交换两个轴最初会感觉更自然,因为事件时间似乎是处理时间自变量的因变量。然而,这两个变量都是单调的,且密切相关,它们实际上是相互依赖的变量,因此我认为从技术的角度来看,你只需要选择一个轴并坚持下去。数学很难理解(尤其是在美国和加拿大以外的地区,math突然变成了复数形式maths,仿佛要合起伙来整你)。
代表理想情况的虚线和实线之间的垂直距离是处理时间域中的滞后。这段距离告诉你(在处理时间中)事件发生的具体时间点和事件被处理的时间点之间的延迟。这可能是两种偏差中的较自然和直观的一种。
代表理想情况的虚线和实线之间的水平距离是当前时间点流水线中事件时间偏差的量。水平距离告诉你当前时间点流水线落后于理想情况(事件时间)的程度。
实际上,在任何给定的时间点上,处理时间滞后和事件时间偏差是相同的,它们只是看待同一事物的两种方式。[5]关于滞后/偏差的重要结论是:由于事件时间和处理时间之间的总体映射不是静态的(即滞后/偏差可以随时间任意变化),这意味着,如果你在意事件时间(即事件实际发生的时间),就不能仅在流水线观测到数据的上下文中分析数据。遗憾的是,许多为无界数据设计的系统都曾采用了这样的操作方式。为了应对无界数据集的无限性,这些系统通常提供一些对传入数据进行开窗的概念。我们稍后会深入讨论开窗,开窗在本质上就是将数据集以时间为边界切割成有限个数的片段。如果你在意结果的正确性,并且有兴趣在事件时间的上下文中分析数据,就不能像许多系统那样使用处理时间来定义这些时间边界(即处理时间开窗)。由于处理时间和事件时间之间没有一致的相关性,因此某些以事件时间标识的数据将被归入错误的处理时间窗口(由于分布式系统固有的滞后,以及许多类型输入源的在线/离线性质等),从而在某种程度上抛弃了正确性。我们将在后面几节以及本书其余部分的一些示例中更详细地讨论这个问题。
[5] 这个结果真的不应该令人惊讶(但可能只是对我来说是这样的,因此我还是不得不指出这一点),因为在测量这两种类型的偏差/滞后时,我们有效地创建了一个具有理想直线的直角三角形。数学很酷。
遗憾的是,当基于事件时间进行开窗时,整体情况也不是很乐观。在无界数据的上下文中,无序且变化的偏差会导致事件时间窗口出现完整性问题:如果缺少处理时间和事件时间之间的可预测映射关系,那么如何确定给定的事件时间为X的所有数据均已被观测到呢?对于许多真实的数据源,你完全不能确定数据的完整性,但是现在使用的绝大多数数据处理系统都依赖完整性的概念,这使得它们在应用于无界数据集时处于严重的劣势。
我建议,与其把无界数据划分为最终具有完整性的有限数量的批量信息,不如设计一种让我们可以应对由复杂数据集带来的这种不确定性的工具。新数据必将到达,旧数据可能被撤回或更新,我们构建的任何系统都应该有能力独立地应对这些状况。在这些系统中,完整性的概念只是针对特定的和适当的使用场景进行的便捷性优化,在语义层面上不是必要的。
在详细介绍这种方法之前,我们先讨论一下更有用的背景信息:常见的数据处理模式。
此时,我们已经拥有了足够的背景知识,可以开始研究目前在有界数据处理和无界数据处理中常见的核心使用模式。我们基于人们关注的两种主要的引擎(批处理引擎和流式引擎,在这种情况下,我实质上将微批处理归为流式,因为这二者之间的差异在这个层面的讨论中并不十分重要)讨论以上两种类型的处理。
处理有界数据在概念上非常简单,也被大家所熟知。在图1-2中,我们从左侧一个充满无序状态的数据集开始。我们通过一些数据处理引擎(通常是批处理引擎,尽管设计良好的流式引擎也可以完成同样的工作),例如MapReduce,来运行这个数据集,在右侧输出一个具有更大内在价值的新的结构化数据集。
图1-2 有界数据通过传统的批处理引擎进行处理。左侧有限的非结构化数据集通过数据处理引擎运行,生成右侧相应的结构化数据
当然,作为这个方案的一部分,实际上你可以进行的计算有无限变种,但是总体模型非常简单。更有趣的是处理无界数据集的任务。现在我们来了解一下通常用来处理无界数据的各种方式,先介绍传统的批处理引擎使用的方法,再介绍大多数流式引擎或微批处理引擎等为无界数据而设计的系统使用的方法。
尽管批处理引擎并不是明确地为无界数据而设计的,但自从批处理系统第一次被构想出来,它就被用来处理无界数据集。正如人们所期望的,这种方法围绕着将无界数据切分成适合批处理的有界数据集的集合。
使用批处理引擎的重复运行来处理无界数据集的最常见方法是,将输入数据以固定大小的窗口进行开窗,然后将每个这样的窗口作为单独的有界数据源(有时也称为滚动窗口)进行处理,如图1-3所示。特别是对于像日志这样的输入源,事件被写入不同的目录和文件中,这些目录和文件层次的名称进行了编码以便与它们的窗口进行对应。这样看来,似乎事情变得非常简单了,你只需要执行一个基于时间的混洗处理,就可以提前使数据进入适当的事件时间窗口中。
图1-3 通过具有传统的批处理引擎的特定固定窗口进行的无界数据处理。一个无界数据集被预先收集到有限的、固定大小的有界数据窗口中,然后通过连续运行传统的批处理引擎进行处理
在实际使用中,大多数系统仍然需要处理数据的完整性问题(如果由于网络分区的原因,一些事件在发送到日志系统的途中被延迟了,应该如何处理?如果事件需要全局收集,并且必须在处理之前迁移到公共位置,应该如何处理?如果事件来自移动设备,应该如何处理?)。这就意味着,可能需要一些缓解这方面问题的方案(例如延迟处理以确保所有事件都被收集完成,或者当迟到数据到达时对某个给定窗口的整批数据重新处理)。
当你尝试使用批处理引擎将无界数据划分到更复杂的开窗策略(如会话)时,这种方法更不适用。会话通常定义为被一段不活跃的间隔终止的一系列活跃周期(例如对于特定用户)。当使用典型的批处理引擎计算会话时,你经常会得到跨越不同批次被拆分的会话,如图1-4中的虚线标记所示。可以通过增大批规模的方法来减少拆分会话的数量,但这样做的代价是增加延时。另一种方式是增加额外的逻辑来缝合之前进行的会话,但这会增加系统设计的复杂度。
图1-4 通过具有传统的批处理引擎的特定固定窗口将无界数据处理到会话中。一个无界数据集被预先收集到有限的、固定大小的有界数据窗口中,然后通过连续运行传统的批处理引擎将这些窗口细分到多个动态会话
无论采用何种方式,用传统批处理引擎来计算会话的效果都不理想。更好的方式是用流式系统来建立会话,稍后我们会讨论。
与大多数基于批处理的无界数据处理方法的特定性质相反,流式系统天生就是为无界数据构建的。正如前面所讨论的,对于许多真实的分布式输入源,你会发现自己不仅在处理无界数据,还在处理具有以下特征的数据。
● 事件时间方面高度无序,这意味着,如果你需要在事件发生的上下文中分析数据,那么你需要在流水线中进行一些基于时间的混洗操作。
● 事件时间偏差不固定,也就是说,你不能假定自己可以在时间Y的某段固定偏差ε内看到给定的事件时间X的大部分数据。
在处理具有上述特征的数据时,可以采用现有的几种方法。我通常将这些方法分为4组:时间无关、近似算法、基于处理时间开窗和基于事件时间开窗。
我们现在将花一点儿时间来研究上述每种方法。
时间无关处理用于与时间基本无关的场景,即所有相关逻辑都是由数据驱动的。因为这些使用场景的一切都是由更多数据的到达所决定的,所以除基本的数据交付之外,流式引擎实际上没有其他特别需要提供的支持。因此,所有现有的流式系统基本上都支持开箱即用的时间无关的使用场景(当然,你关心正确性,那么应该对一致性保证中的系统间的方差取模)。批处理系统也非常适合对无界数据源进行时间无关处理,只需简单地将无界数据源切割成有界数据集的任意序列,然后独立地处理这些数据集。我们将在本节中介绍几个具体的示例,但考虑到掌握时间无关处理比较简单(至少从时间角度来看),我不会在它上面花费更多的时间。
过滤。时间无关处理的一种非常基本的形式就是过滤,如图1-5所示。假设你正在处理Web流量日志,希望过滤掉所有来自非特定域的流量。你会在每条记录到达时查看它,看它是否属于你感兴趣的域,如果不属于,则丢弃它。这类事情在任何时间只取决于一个元素,因此与数据源是否无界无序,以及是否存在不同的事件时间偏差都不相关。
图1-5 过滤无界数据。将不同类型的数据集合(从左到右流动)过滤为只包含单个类型的同类集合
内连接。另一个时间无关的例子就是内连接,如图1-6所示。当连接两个无界数据源时,倘若你只关心来自两个源的元素到达时的连接结果,那么逻辑中就没有时间元素。一旦看到来自某一个源的一个值,你就可以直接把这个值缓冲在持久状态中。只有当来自另一个源的第二个值到达时,你才需要发出这条连接的记录。(事实上,你可能需要某种垃圾回收策略去清理未发出的部分连接,例如基于时间的回收策略。但是,对于几乎没有甚至完全没有未完成连接的使用场景,回收策略可能根本不是问题。)
图1-6 在无界数据上执行内连接。当观测到来自两个源的匹配元素时就会产生连接
将语义切换到某种外连接引入了我们已经讨论过的数据完整性问题:在观测到连接的一端的元素后,如何知道连接的另一端元素是否会到达?实话告诉你,你不会知道,因此,必须引入超时的概念,即引入一个时间元素。时间元素本质上就是开窗的一种形式,我们将在稍后进行更深入的探讨。
第二大类方法是近似算法,如近似Top-N算法和流式k均值算法等。这些近似算法将无界数据源作为输入,输出基本上满足我们预期的结果,如图1-7所示。近似算法的优点是,设计上开销很低,而且本身就是为无界数据而设计的;缺点是,这类算法数量有限,算法本身通常也很复杂(这使得很难演化出新的算法),而且它们的近似特性限制了它们的效用。
图1-7 计算无界数据的近似值。数据通过一个复杂的算法进行计算,在另一侧输出与预期结果大体相似的结果
值得注意的是,这些算法在设计上通常都引入了时间元素(如某种内置衰减)。算法通常在元素到达系统时处理它们,因此时间元素通常都基于处理时间。这对在其近似值上提供某种可证明误差界的算法而言尤其重要。如果这些误差界是基于按顺序到达的数据预测的,那么当算法使用事件时间偏差变化的无序数据时,这些误差界基本上没有任何意义。我们需要记住这一点。
近似算法本身是一个引人入胜的主题,但由于它在本质上是时间无关处理(对算法本身的时间特征取模)的另一个例子,因此使用起来非常简单,鉴于目前关注的重点,我们不再进一步研究它。
剩下的两类无界数据处理的方法都是开窗的变体。在深入研究它们之间的差异之前,我先清晰地说明我所说的开窗的确切含义,因为我们只是在1.2.2节中简要提到过它。简单地从概念上讲,开窗就是将数据源(无界或有界)按时间切割成有限的数据块,然后进行处理。图1-8展示了3种不同的开窗策略。
图1-8 开窗策略。每个示例都展示3个不同的键,突出显示对齐的窗口(适用于所有数据)和未对齐的窗口(适用于数据的一个子集)之间的差异
我们来仔细观察每种开窗策略。
我们之前讨论过固定窗口。固定窗口把时间按照固定的时间长度切分成片段(segment)。通常(如图1-9所示),固定窗口的片段会统一地应用于整个数据集,这是对齐的窗口的一个例子。在某些情况下,需要对数据的不同子集(如每个键)的窗口进行相移,以便窗口的完成负载能够更均衡地随时间分布,这是未对齐的窗口的例子,因为它随着数据状况的变化而变化。[6]
[6] 在第2章中将详细介绍对齐的固定窗口,在第4章中将详细介绍未对齐的固定窗口。
图1-9 基于处理时间开窗为固定窗口。数据根据其到达流水线的顺序被收集到窗口中
滑动窗口属于广义上的固定窗口,滑动窗口是根据固定的窗口长度以及固定的滑动周期定义的。如果滑动周期小于窗口长度,窗口会出现重叠;如果滑动周期等于窗口长度,就是狭义的固定窗口;如果滑动周期大于窗口长度,就会得到一个比较奇特的“采样窗口”,该窗口只会查询时间维度上的数据子集。与固定窗口一样,滑动窗口通常是对齐的,但在某些使用场景中,滑动窗口也可以出于性能优化的目的而不对齐。注意,图1-8中的滑动窗口是以体现滑动的感觉为目的而绘制的,实际上,所有5个窗口都将应用于整个数据集。
会话是动态窗口的一个例子,会话由一系列事件组成,这些事件由大于某个超时的不活跃间隔终止。会话将一系列与时间相关的事件(如在一次会议中观看的一系列视频)分组在一起,通常用于分析随时间变化的用户行为。会话很有趣,因为它们的长度不能预先定义,它们依赖于所涉及的实际数据。会话是典型的未对齐的窗口的示例,因为在数据的不同子集(如不同的用户)中,会话几乎从不 相同。
我们之前讨论的两个时间域(处理时间和事件时间)是我们真正关心的。[7]开窗在这两个时间域中都是有意义的,所以我们将详细了解这两个时间域,并了解它们的区别。由于处理时间开窗在历史上更为常见,因此我们将从处理时间开窗开始。
[7] 如果你在学术文献或基于SQL的流式系统中进行了足够多的探索,你还会遇到第三个时间域的开窗——基于元组的开窗(其窗口大小以元素个数计算)。然而,基于元组的开窗本质上是基于处理时间开窗的一种形式,在元素到达系统时,它们被分配单调增加的时间戳。因此,我们不再详细讨论基于元组的开窗。
基于处理时间开窗。当基于处理时间进行开窗时,系统本质上是在经过一些处理时间后,将传入的数据缓冲到对应的窗口中。例如,在以5 min为窗口周期的固定窗口策略中,系统会对5 min处理时间内的数据进行缓冲,然后将每5 min处理时间内观测到的所有数据视为一个窗口,并将这些窗口发送至下游进行处理。
基于处理时间开窗有几个很好的特性。
● 简单。基于处理时间开窗的实现非常简单,因为你不需要关注在时间范围内混洗数据的问题。你只需要在数据到达时将数据进行缓冲,然后在窗口结束时将数据发送至下游。
● 判断窗口完整性很简单。由于系统十分清晰地了解窗口的所有输入是否都已经可见,因此对于一个给定的窗口是否完整能够做出准确的判断。这意味着在基于处理时间开窗时,不需要具备任何处理“迟到”的数据的能力。
● 如果你想在观测数据时推断出数据源的信息,基于处理时间开窗恰好合适。许多监控类的场景都属于此类情况。假设需要跟踪每秒发送到服务全球规模的Web服务的请求数,以检测服务是否正常为目的而计算请求率是基于处理时间开窗的完美应用。
除了这些优点,基于处理时间开窗存在一个非常大的缺点:如果所讨论的数据有与其相关的事件时间,而且基于处理时间的窗口反映这些事件实际发生的时间,那么这些数据就必须按事件时间的顺序到达。遗憾的是,以事件时间排序的数据在许多真实的分布式输入源中并不常见。
举一个简单的例子,假设存在一个收集用户统计信息以供后续处理的手机应用。当给定的移动设备在任何一段时间内处于离线状态(短暂的连接中断、跨境飞行中启用飞行模式等)时,这段时间内记录的数据无法进行上传,直到该设备再次处于在线状态。这意味着,数据到达的时间可能偏离事件时间几分钟、几小时、几天、几周甚至更长。在基于处理时间开窗时,期望从包含这类数据到达时间和事件时间偏差过大的数据的数据集中提取任何有用的推断,基本上是不可能的。
另一个例子是,当整个系统正常运行时,许多分布式输入源似乎可以提供按照事件时间排序(或非常接近)的数据。当系统正常时,事件时间与输入源的偏差较低,但这并不意味着可以一直保持这种状态。假设存在一个全球性服务,处理从多个大洲收集的数据。如果带宽受限的洲际线路中发生的网络问题(这种情况非常普遍)进一步降低了带宽或者增加了延时,那么可能突然有一部分输入数据以比之前大得多的时间偏差到达。如果对这些数据基于处理时间开窗,那么这些窗口就不能像之前一样体现窗口中实际发生的数据的情况。相反,它们表示的是事件到达处理流水线时间的窗口,这条流水线由新旧数据随意混合而成。
在上述两个例子中,我们真正想要的是以保证事件到达的顺序的方式,基于事件时间对数据进行开窗。我们真正想要的是事件时间开窗。
基于事件时间开窗。当需要以将反映事件实际发生的时间切成的有限数据块的方式观测一个数据源时,你需要使用事件时间开窗。这是开窗的标准范本。在2016年之前,大多数在用的数据处理系统缺乏对事件时间开窗的原生支持(尽管任何具有良好一致性模型的系统,如Hadoop或Spark Streaming 1.x,都可以作为构建此类开窗系统的合理基础)。我很高兴地看到,今天的世界看起来已有诸多改观,无论是Flink,还是Spark,或是Storm和Apex,这些系统本身都支持某种类型的事件时间开窗。
图1-10展示了一个将无界数据源开窗为1小时固定窗口的示例。
图1-10中的箭头指明了两部分特别有意义的数据,这两部分数据到达与数据所属的事件时间窗口不匹配的处理时间窗口。因此,对于关注事件时间的使用场景,如果将数据基于处理时间开窗,计算结果是不正确的。正如我们所期望的,事件时间正确性是使用事件时间窗口的一个好处。
图1-10 基于事件时间开窗为固定窗口。数据根据其发生的时间被收集到窗口中。箭头指明到达与数据所属的事件时间窗口不匹配的处理时间窗口中的示例数据
在无界数据源上基于事件时间开窗的另一个好处是,可以创建窗口长度是动态的窗口,如会话,在固定窗口上生成会话时无须将数据进行任意的拆分(如在1.2.3节的会话示例中看到的那样),如图1-11所示。
图1-11 基于事件时间开窗为会话。基于事件发生对应的时间,数据被收集到各个会话窗口中,捕获活跃期的数据激增。箭头再次指明对将数据放入正确的事件时间位置来说时间混洗的必要性
当然,如此强大的语义不会免费提供,事件时间窗口也不例外。因为窗口的存在时间(在处理时间中)通常必须比窗口本身的实际长度长,所以事件时间窗口有以下两个明显的缺点。
由于窗口生命周期的延长,系统需要进行更多的数据缓冲操作。值得庆幸的是,持久存储通常是大多数数据处理系统依赖的最便宜的资源类型(其他类型主要是CPU、网络带宽和RAM)。因此,当使用经过良好设计的具有强一致持久状态和良好的内存中的缓存层的数据处理系统时,这个问题通常没有想象的那样令人担忧。此外,很多有用的聚合操作不要求对整个输入集进行缓冲,取而代之的是,使用存储在持久状态中的更小的中间聚合进行增量计算。
考虑到我们没有好的方式来判断什么时候一个给定窗口的数据已经全部到齐,那么如何才能知道窗口的结果何时准备物化输出呢?实际上,我们没办法知道。对于许多类型的输入,系统能够通过某种在MillWheel、Cloud Dataflow和Flink中可以找到的类似于水位(在第3章和第4章中会详细讨论)的概念,对窗口的结束时间进行大致准确的启发式的估计。但是,当把保证结果绝对正确摆在首位时(例如在计费系统场景中),唯一的选择是为流水线的构建者提供一种方式来表达何时物化窗口的结果,以及如何随着时间的变化来改进这些结果。处理窗口完整性(或应对缺少完整性)是一个很有吸引力的话题,但最好在具体示例的上下文中探讨,我们将在后面介绍。
真的有很多知识点!能读到这里不容易!你应该受到表扬!当然,我们才刚刚开始。在开始详细研究Beam模型的方法之前,我们简要回顾一下到目前为止我们学到的内容。在本章中,我们完成了以下工作。
● 澄清术语,将“流式”的定义聚焦于指代以无界数据构建的系统,同时使用更具描述性的术语,例如,近似的/推测性的结果,以得出通常归类在“流式”类别下的不同概念。此外,我们强调了大规模数据集的两个重要维度:基数(有界与无界)和编码(表和流)。编码将占据本书后半部分的大部分内容。
● 评估精心设计的批处理系统和流式系统的相关功能,假定流式实际上是批处理的超集,并且像Lambda架构这样的概念(断言流式比批处理差)注定会随着流式系统的成熟而退出历史舞台。
● 提出了流式系统要赶上并最终超越批处理系统所需的两个高级别概念,分别是正确性和推断时间的工具。
● 确立了事件时间和处理时间之间的重要差异,描述了这些差异为在数据发生时的上下文中分析这些数据带来的困难,并提出了从确保完整性的思考方式转为仅适配数据随时间变化的方法。
● 研究了当前通过批处理引擎和流式引擎对处理有界数据和无界数据常用的主要数据处理方法,大致将处理无界数据的方法分类为时间无关、近似算法、基于处理时间开窗和基于事件时间开窗。
接下来,我们将深入研究Beam模型的细节,从概念的角度看如何将数据处理分解为4个相关轴:定义(what)、位置(where)、时机(when)和方法(how)。我们还会详细介绍如何在多个场景中处理一个简单、具体的示例数据集,突出Beam模型启用的多个使用场景,以及现实中一些具体的API。这些示例将有助于深刻理解本章引入的事件时间和处理时间的概念,同时还将探讨水位这样一些新概念。