This commit is contained in:
louzefeng
2024-07-09 18:38:56 +00:00
parent 8bafaef34d
commit bf99793fd0
6071 changed files with 1017944 additions and 0 deletions

View File

@@ -0,0 +1,127 @@
<audio id="audio" title="22 | Apache Beam的前世今生" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/fb/f5/fbc0744e1f6737e91001cfd92d53a8f5.mp3"></audio>
你好,我是蔡元楠。
今天我要与你分享的主题是“ Apache Beam的前世今生”。
从这一讲开始我们将进入一个全新的篇章。在这一讲中我将会带领你了解Apache Beam的完整诞生历程。
让我们一起来感受一下Google是如何从处理框架上的一无所有一直发展到推动、制定批流统一的标准的。除此之外我还会告诉你在2004年发布了MapReduce论文之后Google在大规模数据处理实战中到底经历了哪些技术难题和技术变迁。我相信通过这一讲你将会完整地认识到为什么Google会强力推崇Apache Beam。
在2003年以前Google内部其实还没有一个成熟的处理框架来处理大规模数据。而当时Google的搜索业务又让工程师们不得不面临着处理大规模数据的应用场景像计算网站URL访问量、计算网页的倒排索引Inverted Index等等。
那该怎么办呢?这个答案既简单又复杂:自己写一个。
没错当时的工程师们需要自己写一个自定义的逻辑处理架构来处理这些数据。因为需要处理的数据量非常庞大业务逻辑不太可能只放在一台机器上面运行。很多情况下我们都必须把业务逻辑部署在分布式环境中。所以这个自定义的逻辑处理架构还必须包括容错系统Fault Tolerant System的设计。
久而久之Google内部不同组之间都会开发出一套自己组内的逻辑处理架构。因为工程师们遇到的问题很多都是相似的开发出来的这些逻辑处理架构很多时候也都是大同小异只是有一些数据处理上的逻辑差别而已。这无疑就变成了大家一起重复造轮子的情况。
这时候就有工程师想到能不能改善这一种状况。MapReduce的架构思想也就由此应运而生。
## MapReduce
其实MapReduce的架构思想可以从两个方面来看。
一方面它希望能提供一套简洁的API来表达工程师数据处理的逻辑。另一方面要在这一套API底层嵌套一套扩展性很强的容错系统使得工程师能够将心思放在逻辑处理上而不用过于分心去设计分布式的容错系统。
这个架构思想的结果你早就已经知道了。MapReduce这一套系统在Google获得了巨大成功。在2004年的时候Google发布的一篇名为“MapReduce: Simplified Data Processing on Large Clusters”的论文就是这份成果的总结。
在MapReduce的计算模型里它将数据的处理抽象成了以下这样的计算步骤
- Map计算模型从输入源Input Source中读取数据集合这些数据在经过了用户所写的逻辑后生成出一个临时的键值对数据集Key/Value Set。MapReduce计算模型会将拥有相同键Key的数据集集中起来然后发送到下一阶段。这一步也被称为Shuffle阶段。
- Reduce接收从Shuffle阶段发送过来的数据集在经过了用户所写的逻辑后生成出零个或多个结果。
很多人都说这篇MapReduce论文是具有划时代意义的。可你知道为什么都这么说吗
这是因为Map和Reduce这两种抽象其实可以适用于非常多的应用场景而MapReduce论文里面所阐述的容错系统可以让我们所写出来的数据处理逻辑在分布式环境下有着很好的可扩展性Scalability
MapReduce在内部的成功使得越来越多的工程师希望使用MapReduce来解决自己项目的难题。
但是就如我在模块一中所说的那样使用MapReduce来解决一个工程难题往往会涉及到非常多的步骤而每次使用MapReduce的时候我们都需要在分布式环境中启动机器来完成Map和Reduce步骤以及启动Master机器来协调这两个步骤的中间结果Intermediate Result消耗不少硬件上的资源。
这样就给工程师们带来了以下一些疑问:
- 我们的项目数据规模是否真的需要运用MapReduce来解决呢是否可以在一台机器上的内存中解决呢
- 我们所写的MapReduce项目是否已经是最优的呢因为每一个Map和Reduce步骤这些中间结果都需要写在磁盘上会十分耗时。是否有些步骤可以省略或者合并呢我们是否需要让工程师投入时间去手动调试这些MapReduce项目的性能呢
问题既然已经提出来了Google的工程师们便开始考虑是否能够解决上述这些问题。最好能够让工程师无论是新手工程师亦或是经验老到的工程师都能专注于数据逻辑上的处理而不用花更多时间在测试调优上。
FlumeJava就是在这样的背景下诞生的。
## FlumeJava
这里我先将FlumeJava的成果告诉你。因为FlumeJava的思想又在Google内容获得了巨大成功Google也希望将这个思想分享给业界。所以在2010年的时候Google公开了FlumeJava架构思想的论文。
<img src="https://static001.geekbang.org/resource/image/8b/f4/8be0a05a553f505368eecd5acf78b9f4.png" alt="unpreview">
FlumeJava的思想是将所有的数据都抽象成名为PCollection的数据结构无论是从内存中读取的数据还是在分布式环境下所读取的文件。
这样的抽象对于测试代码中的逻辑是十分有好处的。要知道想测试MapReduce的话你可能需要读取测试数据集然后在分布式环境下运行来测试代码逻辑。但如果你有了PCollection这一层抽象的话你的测试代码可以在内存中读取数据然后跑测试文件也就是同样的逻辑既可以在分布式环境下运行也可以在单机内存中运行。
而FlumeJava在MapReduce框架中Map和Reduce思想上抽象出4个了原始操作Primitive Operation分别是parallelDo、groupByKey、 combineValues和flatten让工程师可以利用这4种原始操作来表达任意Map或者Reduce的逻辑。
同时FlumeJava的架构运用了一种Deferred Evaluation的技术来优化我们所写的代码。
对于Deferred Evaluation你可以理解为FlumeJava框架会首先会将我们所写的逻辑代码静态遍历一次然后构造出一个执行计划的有向无环图。这在FlumeJava框架里被称为Execution Plan Dataflow Graph。
有了这个图之后FlumeJava框架就会自动帮我们优化代码。例如合并一些本来可以通过一个Map和Reduce来表达却被新手工程师分成多个Map和Reduce的代码。
FlumeJava框架还可以通过我们的输入数据集规模来预测输出结果的规模从而自行决定代码是放在内存中跑还是在分布式环境中跑。
总的来说FlumeJava是非常成功的。但是FlumeJava也有一个弊端那就是FlumeJava基本上只支持批处理Batch Execution的任务对于无边界数据Unbounded Data是不支持的。所以Google内部有着另外一个被称为Millwheel的项目来支持处理无边界数据也就是流处理框架。
在2013年的时候Google也公开了Millwheel思想的论文。
<img src="https://static001.geekbang.org/resource/image/b4/28/b49ae286ad9952a10d2762e4bafdcb28.png" alt="unpreview">
这时Google的工程师们回过头看感叹了一下成果并觉得自己可以再优秀一些既然我们已经创造出好几个优秀的大规模数据处理框架了那我们能不能集合这几个框架的优点推出一个统一的框架呢
这也成为了Dataflow Model诞生的契机。
## Dataflow Model
在2015年时候Google公布了Dataflow Model的论文同时也推出了基于Dataflow Model思想的平台Cloud Dataflow让Google以外的工程师们也能够利用这些SDK来编写大规模数据处理的逻辑。
<img src="https://static001.geekbang.org/resource/image/b7/10/b7e96f551dd36d11efab22b666558f10.png" alt="">
讲到这么多你可能会有个疑问了怎么Apache Beam还没有出场呢别着急Apache Beam的登场契机马上就到了。
## Apache Beam
前面我说了Google基于Dataflow Model的思想推出了Cloud Dataflow云平台但那毕竟也需要工程师在Google的云平台上面运行程序才可以。如果有的工程师希望在别的平台上面跑该如何解决呢
所以为了解决这个问题Google在2016年的时候联合了Talend、Data Artisans、Cloudera这些大数据公司基于Dataflow Model的思想开发出了一套SDK并贡献给了Apache Software Foundation。而它Apache Beam的名字是怎么来的呢就如下图所示Beam的含义就是统一了批处理和流处理的一个框架。
<img src="https://static001.geekbang.org/resource/image/87/ec/873099f513c51a8ecce528f0d6e600ec.png" alt="unpreview">
这就是Apache Beam的发展历史从中你可以看到它拥有很多优点而这也是我们需要Beam的原因。
在现实世界中很多时候我们不可避免地需要对数据同时进行批处理和流处理。Beam提供了一套统一的API来处理这两种数据处理模式让我们只需要将注意力专注于在数据处理的算法上而不用再花时间去对两种数据处理模式上的差异进行维护。
它能够将工程师写好的算法逻辑很好地与底层的运行环境分隔开。也就是说当我们通过Beam提供的API写好数据处理逻辑后这个逻辑可以不作任何修改直接放到任何支持Beam API的底层系统上运行。
关于怎么理解这个优点其实我们可以借鉴一下SQLStructure Query Language的运行模式。
我们在学习SQL语言的时候基本上都是独立于底层数据库系统来学习的。而在我们写完一个分析数据的Query之后只要底层数据库的Schema不变这个Query是可以放在任何数据库系统上运行的例如放在MySql上或者Oracle DB上。
同样的我们用Beam API写好的数据处理逻辑无需改变可以根据自身的需求将逻辑放在Google Cloud Dataflow上跑也可以放在Apache Flink上跑。在Beam上这些底层运行的系统被称为Runner。现阶段Apache Beam支持的Runner有近十种包括了我们很熟悉的Apache Spark和Apache Flink。
当然最后Apache Beam也是希望对自身的SDK能够支持任意多的语言来编写。现阶段Beam支持Java、Python和Golang。
<img src="https://static001.geekbang.org/resource/image/56/48/56637be15a6c3ad47fda35731ff88448.png" alt="unpreview">
也就是说通过Apache Beam最终我们可以用自己喜欢的编程语言通过一套Beam Model统一地数据处理API编写好符合自己应用场景的数据处理逻辑放在自己喜欢的Runner上运行。
## 小结
今天我与你一起回顾了Apache Beam的完整诞生历程。
通过这一讲,我希望你知道每一项技术都不会毫无理由地诞生,而每一项技术诞生的背后都是为了解决某些特定问题的。了解前人一步步解决问题的过程,有助于我们更有层次地理解一项技术产生的根本原因。在学习一项技术之前,先了解了它的历史源流,可以让我们做到知其然,并知其所以然。
## 思考题
你也能分享一些你所经历过的技术变迁或是技术诞生的故事吗?
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@@ -0,0 +1,132 @@
<audio id="audio" title="23 | 站在Google的肩膀上学习Beam编程模型" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/4c/ad/4c990a90026aedbb1cd394c9eada65ad.mp3"></audio>
你好,我是蔡元楠。
今天我要与你分享的话题是“站在Google的肩膀上学习Beam编程模型”。
在上一讲中我带你一起领略了Apache Beam的完整诞生历史。通过上一讲你应该对于Apache Beam在大规模数据处理中能够带来的便利有了一定的了解。
而在这一讲中让我们一起来学习Apache Beam的编程模型帮助你打下良好的基础以便应对接下来的Beam实战篇。希望你在以后遇到不同的数据处理问题时可以有着Beam所提倡的思考模式。
现在让我们一起进入Beam的世界吧。
## 为什么要先学习Beam的编程模型
可能你会有疑问很多人学习一项新技术的时候都是从学习SDK的使用入手为什么我们不同样的从SDK入手而是要先学习Beam的编程模型呢
我的答案有两点。
第一Apache Beam和其他开源项目不太一样它并不是一个数据处理平台本身也无法对数据进行处理。Beam所提供的是一个统一的编程模型思想而我们可以通过这个统一出来的接口来编写符合自己需求的处理逻辑这个处理逻辑将会被转化成为底层运行引擎相应的API去运行。
第二学习Apache Beam的时候如果只学习SDK的使用可能你不一定能明白这些统一出来的SDK设计背后的含义而这些设计的思想又恰恰是涵盖了解决数据处理世界中我们所能遇见的问题。我认为将所有的SDK都介绍一遍是不现实的。SDK会变但它背后的原理却却不会改变只有当我们深入了解了整个设计原理后遇到各种应用场景时才能处理得更加得心应手。
## Beam的编程模型
那事不宜迟我们来看看Beam的编程模型到底指的是什么
简单来说Beam的编程模型需要让我们根据“WWWH”这四个问题来进行数据处理逻辑的编写。“WWWH”是哪四个问题呢这里我先卖个关子在进入四个具体问题之前我需要先介绍一下根据Beam编程模型所建立起来的Beam生态圈帮助你理解Beam的编程模型会涉及到的几个概念。整个Apache Beam的生态圈构成就如下图所示。
<img src="https://static001.geekbang.org/resource/image/bb/35/bbe679898aef03c77e49ba8d93aca235.png" alt="">
为了帮助你理解,我为这几层加了编号,数字编号顺序是自下而上的,你可以对照查找。
第一层是现在已有的各种大数据处理平台例如Apache Spark或者Apache Flink在Beam中它们也被称为Runner。
第二层是可移植的统一模型层各个Runners将会依据中间抽象出来的这个模型思想提供一套符合这个模型的APIs出来以供上层转换。
第三层是SDK层。SDK层将会给工程师提供不同语言版本的API来编写数据处理逻辑这些逻辑就会被转化成Runner中相应的API来运行。
第四层是可扩展库层。工程师可以根据已有的Beam SDK贡献分享出更多的新开发者SDK、IO连接器、转换操作库等等。
第五层我们可以看作是应用层各种应用将会通过下层的Beam SDK或工程师贡献的开发者SDK来实现。
最上面的第六层,也就是社区一层。在这里,全世界的工程师可以提出问题,解决问题,实现解决问题的思路。
通过第6讲的内容我们已经知道这个世界中的数据可以分成有边界数据和无边界数据而有边界数据又是无边界数据的一种特例。所以我们都可以将所有的数据抽象看作是无边界数据。
同时每一个数据都是有两种时域的分别是事件时间和处理时间。我们在处理无边界数据的时候因为在现实世界中数据会有延时、丢失等等的状况发生我们无法保证现在到底是否接收完了所有发生在某一时刻之前的数据。所以现实中流处理必须在数据的完整性和数据处理的延时性上作出取舍。Beam编程模型就是在这样的基础上提出的。
Beam编程模型会涉及到的4个概念窗口、水位线、触发器和累加模式我来为你介绍一下。
- **窗口Window**
窗口将无边界数据根据事件时间分成了一个个有限的数据集。我们可以看看批处理这个特例。在批处理中我们其实是把一个无穷小到无穷大的时间窗口赋予了数据集。我会在第32讲中对窗口这个概念进行详细地介绍。
- **水位线Watermark**
水位线是用来表示与数据事件时间相关联的输入完整性的概念。对于事件时间为X的水位线是指数据处理逻辑已经得到了所有事件时间小于X的无边界数据。在数据处理中水位线是用来测量数据进度的。
- **触发器Triggers**
触发器指的是表示在具体什么时候,数据处理逻辑会真正地触发窗口中的数据被计算。触发器能让我们可以在有需要时对数据进行多次运算,例如某时间窗口内的数据有更新,这一窗口内的数据结果需要重算。
- **累加模式Accumulation**
累加模式指的是如果我们在同一窗口中得到多个运算结果,我们应该如何处理这些运算结果。这些结果之间可能完全不相关,例如与时间先后无关的结果,直接覆盖以前的运算结果即可。这些结果也可能会重叠在一起。
懂得了这几个概念之后我来告诉你究竟Beam编程模型中的“WWWH”是什么。它们分别是What、Where、When、How。
**What results are being calculated?**
<img src="https://static001.geekbang.org/resource/image/71/bb/71c8ace006d56d7f6fe93cbc56dc91bb.png" alt="">
我们要做什么计算得到什么样的结果Beam SDK中各种transform操作就是用来回答这个问题的。这包括我们经常使用到批处理逻辑训练机器学习模型的逻辑等等。
举个例子我们每次学习大规模数据处理时所用到的经典例子WordCount里我们想要得到在一篇文章里每个单词出现的次数那我们所要做的计算就是通过Transform操作将一个单词集合转换成以单词为Key单词出现次数为Value的集合。
**Where in event time they are being computed?**
<img src="https://static001.geekbang.org/resource/image/34/08/34005c0d4635d26304c6a9c71f857708.png" alt="">
计算什么时间范围的数据?这里的“时间”指的是数据的事件时间。我们可以通过窗口这个概念来回答这个问题。
例如我们有三个不同的数据它们的事件时间分别是12:01、12:59和14:00。如果我们的时间窗口设定为[12:00 , 13:00),那我们所需要处理的数据就是前两个数据了。
**When in processing time they are materialized?**
<img src="https://static001.geekbang.org/resource/image/09/6c/0963adf3a79446382e366c2c82a96e6c.png" alt="">
何时将计算结果输出?我们可以通过使用水位线和触发器配合触发计算。
在之前的概念中我们知道触发器指的就是何时触发一个窗口中的数据被计算出最终结果。在Beam中我们可以有多种多样的触发器信号例如根据处理时间的信号来触发也就是说每隔了一段时间Beam就会重新计算一遍窗口中的数据也可以根据元素的计数来触发意思就是在一个窗口中的数据只要达到一定的数据这个窗口的数据就会被拿来计算结果。
现在我来举一个以元素计数来触发的例子。我们现在定义好的固定窗口Fixed Window时间范围为1个小时从每天的凌晨00:00开始计算元素计数定为2。我们需要处理的无边界数据是商品交易数据我们需要计算在一个时间窗口中的交易总量。
为了方便说明我们假设只接收到了4个数据点它们按照以下顺序进入我们的处理逻辑。
1. 于6月11号23:59产生的10元交易
1. 于6月12号00:01产生的15元交易
1. 于6月11号23:57产生的20元交易
1. 于6月11号23:57产生的30元交易。
接收到第三个数据的时候6月11号这个24小时窗口的数据已经达到了两个所以触发了这个窗口内的数据计算也就是6月11号的窗口内交易总量现在为10+20=30元。
当第四个数据6月11号23:57产生的30元交易进入处理逻辑时6月11号这个24小时窗口的数据又超过了两个元素这个窗口的计算再次被触发交易总量被更新为30+30=60元。你可以看到由于6月12号这个窗口的数据一直没有达到我们预先设定好的2所以就一直没有被触发计算。
**How earlier results relate to later refinements?**
<img src="https://static001.geekbang.org/resource/image/d1/5a/d1e21d08c6ecdc43c92c8bc753b8565a.png" alt="">
后续数据的处理结果如何影响之前的处理结果呢?这个问题可以通过累加模式来解决,常见的累加模式有:丢弃(结果之间是独立且不同的)、累积(后来的结果建立在先前的结果上)等等。
还是以刚刚上面所讲述的4个交易数据点为例子你可能会认为这里我们采取的累加模式是累积其实我们采取的是丢弃。因为我们从始至终只保存着一个计算结果。这里要再引入一个概念每一次通过计算一个窗口中的数据而得到的结果我们可以称之为窗格Pane
我们可以看到当数据处理逻辑第一次产生6月11号这个窗口结果的时候两次交易相加产生的30元成为了一个窗格。而第二次产生窗口结果是60元这个结果又是一个窗格。因为我们只需要计算在一个窗口时间中的交易总量所以第一个窗格随之被丢弃只保留了最新的窗格。如果我们采用的是累积的累加模式呢那这两个交易总量30元和60元都会被保存下来成为历史记录。
Beam的编程模型将所有的数据处理逻辑都分割成了这四个纬度统一成了Beam SDK。我们在基于Beam SDK构建数据处理业务逻辑时只需要根据业务需求按照这四个维度调用具体的API即可生成符合自己要求的数据处理逻辑。Beam会自动转化数据处理逻辑并提交到具体的Runner上去执行。我们可以看到无论是Runner自身的API还是Beam的SDK设计都需要有能力解决上述四个问题。Beam的编程模型是贯穿了Beam生态圈中的每一层的。
在模块四的后续的内容中我们会围绕着这四个问题展开具体的分析看看在Beam的实战中这每一步是如何被解答的。
## 小结
Google如此地推崇Apache Beam开源项目除了借此能够推广自己的云计算平台之外更是借鉴了Apache Hadoop在开源社区中所取得的巨大成功。Google希望为外界贡献一个容易使用而又功能强大的大数据处理模型可以同时适用于流处理和批处理并且还可以移植于各种不同数据处理平台上。
在Beam的生态圈中我们可以看到每一层的设计都是根据Beam的编程模型来搭建的。懂得了Beam编程模型之后我们可以为生态圈中的任意一层做出贡献。
## 思考题
在现实应用中你能否根据Beam的编程模型来分享你会怎么设计自己的数据处理逻辑呢
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/4d/a8/4d90d26b0e793703f02bd8684a0481a8.jpg" alt="unpreview">

View File

@@ -0,0 +1,169 @@
<audio id="audio" title="24 | PCollection为什么Beam要如此抽象封装数据" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/c1/09/c17f55c9c32c2ee42715c56493ef7709.mp3"></audio>
你好,我是蔡元楠。
今天我要与你分享的主题是“为什么Beam要如此抽象封装数据”。
很多人在刚开始接触Apache Beam的时候都会觉得这里面的概念太抽象了。什么PCollection、PValue、Transform……这都是些什么尤其是PCollection完全和先前的技术知识找不到对应。
确实如此。同样作为数据的容器PCollection却并不像Python/Java的List或者C++的vector。PCollection是无序的Beam对于PCollection中元素的处理顺序不作任何保证。所以你不可能说“我想处理PCollection中的第二个元素”因为它就没有“第几个”这种概念。
PCollection也不像Python/Java的Set或者C++的unordered_setPCollection不一定有固定的边界。所以你也不能指望去查找一个PCollection的大小。在PCollection的世界里也没有“固定大小”这样的概念。
作为程序员,我很讨厌重复造轮子,尤其是新瓶装旧酒。的确,有很多开发者为了体现自己项目的复杂度,故意强行引进了很多概念,让大家都似懂非懂的。这就像是为了体现自己知道茴香豆的“茴”有几种写法一样,故意用另一种写法来体现自己“有文化”。
那么Apache Beam引进这么多概念是不是也是故意强行造轮子呢答案是否定的。这一讲我们就要分析PCollection为什么会如此设计。
在之前的章节中我们已经讲到了Apache Beam的“爷爷”——FlumeJava产生的背景。
当时Google的工程师们发现MapReduce使用起来并没有很方便。如果计算过程都能够分解成map、shuffle、reduce步骤的话MapReduce是非常能胜任的。
但是很多现实世界中的问题往往需要一系列的map或reduce步骤。这样的数据流水线就需要额外的代码用来协调这些map或reduce步骤。也需要一些额外的代码去管理分步的map/reduce步骤产生的一些中间结果。以至于新的开发者很难理解复杂的流水线。
清楚这样的背景对于理解Apache Beam的PCollection起源很有帮助。因为这个项目起源只是为了提供比MapReduce更好的开发体验最终的运算引擎仍然是MapReduce。
## 为什么需要PCollection
那么为什么Beam需要PCollection这样一个全新的抽象数据结构呢
我们知道不同的技术系统有不同的数据结构。比如C++里有vector、unordered_map安卓有ListView。相比它们而言其实Beam的数据结构体系是很单调的几乎所有数据都能表达为PCollection。
PCollection就是Parallel Collection意思是可并行计算的数据集。如果你之前学习了Spark的章节就会发现PCollection和RDD十分相似。
在一个分布式计算系统中,我们作为架构设计者需要为用户隐藏的实现细节有很多,其中就包括了**数据是怎样表达和存储的**。
这个数据可能是来自于内存的数据内部可能只是由一个C++ array存储也有可能是来自外部文件由几个文件存储或者是来自于MySQL数据库由数据库的行来表达
如果没有一个统一的数据抽象的话开发者就需要不停地更改代码。比如在小规模测试的时候用C++ vector等到了真实的生产环境我再换MySQL row。沉溺于这样的实现细节会让开发者无法集中注意力在真正重要的事情上那就是“你想怎样处理数据”。
清楚了这些你就能明白我们需要一层抽象来表达数据而这层抽象就是PCollection。
PCollection的创建完全取决于你的需求。比如在测试中PCollection往往来自于代码生成的伪造数据或者从文件中读取。
Python
```
lines = (p
| beam.Create(['To be, or not to be: that is the question. ']))
lines = p | 'ReadMyFile' &gt;&gt; beam.io.ReadFromText('gs://some/inputData.txt'
```
Java
```
PCollection&lt;String&gt; lines = p.apply(
&quot;ReadMyFile&quot;, TextIO.read().from(&quot;protocol://path/to/some/inputData.txt&quot;));
```
## 为什么PCollection需要Coders
与普通编程相比PCollection的另一个不同之处是你需要为PCollection的元素编写Coder。例如你有一个自己的类MyClass那么PCollection<myclass>一定需要实现Coder<myclass></myclass></myclass>
刚开始使用Beam时你可能会感觉这很不方便。例如你只是要去读取MySQL的一个表也得为此实现Coder。
Coder的作用和Beam的本质紧密相关。因为你的计算流程最终会运行在一个分布式系统。所以所有的数据都有可能在网络上的计算机之间相互传递。而Coder就是在告诉Beam怎样把你的数据类型序列化和逆序列化以方便在网络上传输。
Coder需要注册进全局的CoderRegistry中简单来说是为你自己的数据类型建立与Coder的对应关系。不然每次你都需要手动指定Coder。
Python
```
apache_beam.coders.registry.register_coder(int, BigEndianIntegerCoder)
```
Java
```
PipelineOptions options = PipelineOptionsFactory.create();
Pipeline p = Pipeline.create(options);
CoderRegistry cr = p.getCoderRegistry();
cr.registerCoder(Integer.class, BigEndianIntegerCoder.class);
```
## 为什么PCollection是无序的
讲完为什么PCollection需要Coder之后我们再来看下为什么PCollection是无序的。
PCollection的无序特性其实也和它的分布式本质有关。一旦一个PCollection被分配到不同的机器上执行那么为了保证最大的处理输出不同机器都是独立运行的。所以它的执行顺序就无从得知了。可能是第一个元素先被运行也可能是第二个元素先被运行。所以肯定不会有PCollection[2]这样的运算符。
## 为什么PCollection没有固定大小
无序也就算了为什么PCollection还没有固定大小呢
前面的章节中讲到过Beam想要统一批处理和流处理所以它要统一表达有界数据和无界数据。正因为如此PCollection并没有限制它的容量。如前面所说它可能表达内存上的一个数组也可能表达整个数据库的所有数据。
一个PCollection可以是有界的也可以是无界的。一个有界的PCollection表达了一个已知大小的固定的数据集。一个无界的PCollection表达了一个无限大小的数据集。事实上一个PCollection是否有界往往取决于它是如何产生的。
从批处理的数据源中读取比如一个文件或者是一个数据库就会产生有界的PColleciton。如果从流式的或者是持续更新的数据库中读取比如pub/sub或者kafka会产生一个无界的PCollection。
但是PCollection的有界和无界特性会影响到Beam的数据处理方式。一个批处理作业往往处理有界数据。而无界的PCollection需要流式的作业来连续处理。
在实现中Beam也是用window来分割持续更新的无界数据。所以一个流数据可以被持续地拆分成不同的小块。这样的处理方式我们会在实战部分展开。
## 如何理解PCollection的不可变性
在普通编程语言中,大部分数据结构都是可变的。
Python
```
Alist = []
alist.append(1)
```
C++
```
Std::vector&lt;int&gt; list;
list.push_back(1);
```
但是PCollection不提供任何修改它所承载数据的方式。修改一个PCollection的唯一方式就是去转化(Transform)它下一讲会展开讲Transformation。
但是在这一讲我们需要理解的是Beam的PCollection都是延迟执行deferred execution的模式。也就是说当你下面这样的语句的时候什么也不会发生。
Java
```
PCollection&lt;T1&gt; p1 = ...;
PCollection&lt;T2&gt; p2 = doSomeWork(p1);
```
这样的语句执行完p2这个PCollection仅仅会记录下自己是由doSomeWork这个操作计算而来的和计算自己所需要的数据p1。当你执行写完100行的beam的运算操作最终的结果仅仅是生成了一个有向无环图DAG也就是执行计划execution plan
为什么这么设计呢?如果你记得我们在专栏第一部分讲到的大规模数据框架设计,可能会有印象。这样的有向无环图是框架能够自动优化执行计划的核心。
<img src="https://static001.geekbang.org/resource/image/88/5b/88ce2f3effcf035f838ffcfd382c0b5b.png" alt="">
类似图中这样的数据处理流程在Beam获知了整个数据处理流程后就会被优化成下图所示。
<img src="https://static001.geekbang.org/resource/image/21/49/21d4331dc4574f44ee7914a9c48ae049.png" alt="">
这样的优化在Beam中被称为sibling fusion。类似的操作优化我们后面会继续介绍。在这个小标题下我想用这个优化的例子说明PCollection下的数据不可变是因为改变本身毫无意义。
例如在刚才这个例子中你会发现优化后的执行计划里已经没有了数据A0。因为Beam发现数据A0作为中间结果并不影响最终输出。另外由于Beam的分布式本质即使你想要去修改一个PCollection的底层表达数据也需要在多个机器上查找毫无实现的价值。
## 小结
这一讲我们介绍了整个Beam核心数据结构PCollection的设计思想。尤其是分析了PCollection的几大特性为什么是这么设计的。它的特性包括Coders、无序性、没有固定大小、不可变性等。在每个特性下面我们也介绍了几个例子或者代码示例希望能帮助你加深印象。
## 思考题
PCollection的设计是否能表达你的大规模数据处理场景呢
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@@ -0,0 +1,242 @@
<audio id="audio" title="25 | TransformBeam数据转换操作的抽象方法" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/fd/1d/fd6276a6d184943dcf6b0fabd883521d.mp3"></audio>
你好,我是蔡元楠。
今天我要与你分享的主题是“Beam数据转换操作的抽象方法”。
在上一讲中我们一起学习了Beam中数据的抽象表达——PCollection。但是仅仅有数据的表达肯定是无法构建一个数据处理框架的。那么今天我们就来看看Beam中数据处理的最基本单元——Transform。
下图就是单个Transform的图示。
<img src="https://static001.geekbang.org/resource/image/cc/66/cc1266a6749cdae13426dd9721f66e66.jpg" alt="">
之前我们已经讲过Beam把数据转换抽象成了有向图。PCollection是有向图中的边而Transform是有向图里的节点。
不少人在理解PCollection的时候都觉得这不那么符合他们的直觉。许多人都会自然地觉得PCollection才应该是节点而Transform是边。因为数据给人的感觉是一个实体应该用一个方框表达而边是有方向的更像是一种转换操作。事实上这种想法很容易让人走入误区。
其实区分节点和边的关键是看一个Transform是不是会有一个多余的输入和输出。
每个Transform都可能有大于一个的输入PCollection它也可能输出大于一个的输出PCollection。所以我们只能把Transform放在节点的位置。因为一个节点可以连接多条边而同一条边却只能有头和尾两端。
## Transform的基本使用方法
在了解了Transform和PCollection的关系之后我们来看一下Transform的基本使用方法。
Beam中的PCollection有一个抽象的成员函数Apply。使用任何一个Transform时候你都需要调用这个apply方法。
Java
```
pcollection1 = pcollection2.apply(Transform)
```
Python
```
Pcollection1 = pcollection2 | Transform
```
当然你也可以把Transform级连起来。
```
final_collection = input_collection.apply(Transform1)
.apply(Transform2)
.apply(Transform3)
```
<img src="https://static001.geekbang.org/resource/image/41/ec/41a408a0153844036909e98dd2cefaec.jpg" alt="">
所以说Transform的调用方法是要通过apply()的但是Transform有很多种。
## 常见的Transform
Beam也提供了常见的Transform接口比如ParDo、GroupByKey。最常使用的Transform就是ParDo了。
ParDo就是 Parallel Do的意思顾名思义表达的是很通用的并行处理数据操作。GroupByKey的意思是把一个Key/Value的数据集按Key归并就如下面这个例子。
```
cat, 1
dog, 5
and, 1
jump, 3
tree, 2
cat, 5
dog, 2
and, 2
cat, 9
and, 6
=&gt;
cat, [1,5,9]
dog, [5,2]
and, [1,2,6]
jump, [3]
tree, [2]
```
当然你也可以用ParDo来实现GroupByKey一种简单的实现方法就是放一个全局的哈希表然后在ParDo里把一个一个元素插进这个哈希表里。但这样的实现方法并不能用因为你的数据量可能完全无法放进一个内存哈希表。而且你还要考虑到PCollection会把计算分发到不同机器上的情况。
当你在编写ParDo时你的输入是一个PCollection中的单个元素输出可以是0个、1个或者是多个元素。你只要考虑好怎样处理一个元素。剩下的事情Beam会在框架层面帮你做优化和并行。
使用ParDo时你需要继承它提供的DoFn类你可以把DoFn看作是ParDo的一部分。因为ParDo和DoFn单独拿出来都没有意义。
java
```
static class UpperCaseFn extends DoFn&lt;String, String&gt; {
@ProcessElement
public void processElement(@Element String word, OutputReceiver&lt;String&gt; out) {
out.output(word.toUpperCase());
}
}
PCollection&lt;String&gt; upperCaseWords = words.apply(
ParDo
.of(new UpperCaseFn()));
```
在上面的代码中你可以看出每个DoFn的@ProcessElement标注的函数processElement就是这个DoFn真正的功能模块。在上面这个DoFn中我们把输入的一个词转化成了它的大写形式。之后在调用apply(ParDo.of(new UpperCaseFn()))的时候Beam就会把输入的PCollection中的每个元素都使用刚才的processElement处理一遍。
看到这里你可能会比较迷惑transform、apply、DoFn、ParDo之间到底是什么关系啊怎么突然冒出来一堆名词其实Transform是一种概念层面的说法。具体在编程上面Transform用代码来表达的话就是这样的
```
pcollection.apply(ParDo.of(new DoFn()))
```
这里的apply(ParDo)就是一个Transform。
我们在[第7讲](https://time.geekbang.org/column/article/92928)中讲过数据处理流程的常见设计模式。事实上很多应用场景都可以用ParDo来实现。比如过滤一个数据集、格式转化一个数据集、提取一个数据集的特定值等等。
**1.过滤一个数据集**
当我们只想要挑出符合我们需求的元素的时候我们需要做的就是在processElement中实现。一般来说会有一个过滤函数如果满足我们的过滤条件我们就把这个输入元素输出。
Java
```
@ProcessElement
public void processElement(@Element T input, OutputReceiver&lt;T&gt; out) {
if (IsNeeded(input)) {
out.output(input);
}
}
```
**2.格式转化一个数据集**
给数据集转化格式的场景非常常见。比如我们想把一个来自csv文件的数据转化成TensorFlow的输入数据tf.Example的时候就可以用到ParDo。
Java
```
@ProcessElement
public void processElement(@Element String csvLine, OutputReceiver&lt;tf.Example&gt; out) {
out.output(ConvertToTfExample(csvLine));
}
```
**3.提取一个数据集的特定值**
ParDo还可以提取一个数据集中的特定值。比如当我们想要从一个商品的数据集中提取它们的价格的时候也可以使用ParDo。
Java
```
@ProcessElement
public void processElement(@Element Item item, OutputReceiver&lt;Integer&gt; out) {
out.output(item.price());
}
```
通过前面的几个例子你可以看到ParDo和DoFn这样的抽象已经能处理非常多的应用场景问题。事实正是如此在实际应用中80%的数据处理流水线都是使用基本的ParDo和DoFn。
## Stateful Transform和side input/side output
当然还有一些Transform其实也是很有用的比如GroupByKey不过它远没有ParDo那么常见。所以这一模块中暂时不会介绍别的数据转换操作需要的话我们可以在后面用到的时候再介绍。我想先在这里介绍和ParDo同样是必用的却在大部分教程中被人忽略的技术点——Statefullness和side input/side output。
上面我们所介绍的一些简单场景都是无状态的也就是说在每一个DoFn的processElement函数中输出只依赖于输入。它们的DoFn类不需要维持一个成员变量。无状态的DoFn能保证最大的并行运算能力。因为DoFn的processElement可以分发到不同的机器或者不同的进程也能有多个DoFn的实例。但假如我们的processElement的运行需要另外的信息我们就不得不转而编写有状态的DoFn了。
试想这样一个场景你的数据处理流水线需要从一个数据库中根据用户的id找到用户的名字。你可能会想到用“在DoFn中增加一个数据库的成员变量”的方法来解决。的确实际的应用情况中我们就会写成下面这个代码的样子。
java
```
static class FindUserNameFn extends DoFn&lt;String, String&gt; {
@ProcessElement
public void processElement(@Element String userId, OutputReceiver&lt;String&gt; out) {
out.output(database.FindUserName(userId));
}
Database database;
}
```
但是因为有了共享的状态这里是一个共享的数据库连接。在使用有状态的DoFn时我们需要格外注意Beam的并行特性。
如上面讲到的Beam不仅会把我们的处理函数分发到不同线程、进程也会分发到不同的机器上执行。当你共享这样一个数据库的读取操作时很可能引发服务器的QPS过高。
例如你在处理一个1万个用户id如果beam很有效地将你的DoFn并行化了你就可能观察到数据库的QPS增加了几千。如果你不仅是读取还做了修改的话就需要注意是不是有竞争风险了。这里你可以联想在操作系统中有关线程安全的相关知识。
除了这种简单的增加一个成员变量的方法。如果我们需要共享的状态来自于另外一些Beam的数据处理的中间结果呢这时候为了实现有状态DoFn我们需要应用Beam的Side input/side output计数。
java
```
PCollectionView&lt;Integer&gt; mediumSpending = ...;
PCollection&lt;String&gt; usersBelowMediumSpending =
userIds.apply(ParDo
.of(new DoFn&lt;String, String&gt;() {
@ProcessElement
public void processElement(@Element String userId, OutputReceiver&lt;String&gt; out, ProcessContext c) {
int medium = c.sideInput(mediumSpending);
if (findSpending(userId) &lt;= medium) {
out.output(userId);
}
}
}).withSideInputs(mediumSpending)
);
```
比如在这个处理流程中我们需要根据之前处理得到的结果也就是用户的中位数消费数据找到消费低于这个中位数的用户。那么我们可以通过side input把这个中位数传递进DoFn中。然后你可以在ProcessElement的参数ProcessContext中拿出来这个side input。
## Transform的优化
之前我们也提到过Beam中的数据操作都是lazy execution的。这使得Transform和普通的函数运算很不一样。当你写下面这样一个代码的时候真正的计算完全没有被执行。
```
Pcollection1 = pcollection2.apply(Transform)
```
这样的代码仅仅是让Beam知道了“你想对数据进行哪些操作”需要让它来构建你的数据处理有向图。之后Beam的处理优化器会对你的处理操作进行优化。所以千万不要觉得你写了10个Transform就会有10个Transform马上被执行了。
理解Transform的lazy execution非常重要。很多人会过度地优化自己的DoFn代码想要在一个DoFn中把所有运算全都做了。其实完全没这个必要。
<img src="https://static001.geekbang.org/resource/image/23/55/235e17cc1c3885a39e79217fddbabc55.png" alt="">
你可以用分步的DoFn把自己想要的操作表达出来然后交给Beam的优化器去合并你的操作。比如在FlumeJava论文中提到的MSCR Fusion它会把几个相关的GroupByKey的Transform合并。
## 小结
在这一讲中我们学习了Transform的概念和基本的使用方法。通过文章中的几个简单的例子你要做到的是了解怎样编写Transform的编程模型DoFn类。有状态DoFn在实际应用中尤其常见你可以多加关注。
## 思考题
你可能会发现Beam的ParDo类似于Spark的map()或者是MapReduce的map。它们确实有很多相似之处。那你认为它们有什么不一样之处呢
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@@ -0,0 +1,137 @@
<audio id="audio" title="26 | PipelineBeam如何抽象多步骤的数据流水线" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/14/74/14aa9d734f5d452f9009d179eb14d274.mp3"></audio>
你好,我是蔡元楠。
今天我要与你分享的主题是“PipelineBeam如何抽象多步骤的数据流水线”。
在上两讲中我们一起学习了Beam是如何抽象封装数据以及如何抽象对于数据集的转换操作的。在掌握了这两个基本概念后我们就可以很好地回答Beam编程模型里的4个维度What、Where、When、How中的第一个问题——What了。也就是我们要做什么计算想得到什么样的结果
<img src="https://static001.geekbang.org/resource/image/71/bb/71c8ace006d56d7f6fe93cbc56dc91bb.png" alt="unpreview">
这个时候你可能已经跃跃欲试开始想用PCollection和Transform解决我们平常经常会使用到的批处理任务了。没有问题那我们就先抛开Where、When和How这三个问题由简至繁地讲起。
现在假设我们的数据处理逻辑只需要处理有边界数据集在这个情况下让我们一起来看看Beam是如何运行一套批处理任务的。
## 数据流水线
在Beam的世界里所有的数据处理逻辑都会被抽象成**数据流水线Pipeline**来运行。那么什么是数据流水线呢?
Beam的数据流水线是对于数据处理逻辑的一个封装它包括了从**读取数据集****将数据集转换成想要的结果**和**输出结果数据集**这样的一整套流程。
所以如果我们想要跑自己的数据处理逻辑就必须在程序中创建一个Beam数据流水线出来比较常见的做法是在main()函数中直接创建。
Java
```
PipelineOptions options = PipelineOptionsFactory.create();
Pipeline p = Pipeline.create(options);
```
在创建Beam数据流水线的同时我们必须给这个流水线定义一个**选项**Options。这个选项会告诉Beam用户的Pipeline应该如何运行。例如是在本地的内存上运行还是在Apache Flink上运行关于具体Beam选项的解释我会在第30讲中展开讲解。
## Beam数据流水线的应用
有了数据流水线这个抽象概念之后我们就可以将PCollection和Transform应用在这个流水线里面了。
<img src="https://static001.geekbang.org/resource/image/a5/94/a56f824d0dc8b3c1a777595b42c4b294.jpg" alt="">
上图就是一个Beam的数据流水线整个数据流水线包括了从读取数据到经过了N个Transform之后输出数据的整个过程。
在[第24讲](https://time.geekbang.org/column/article/100666)中我们学习过PCollection的不可变性。也就是说一个PCollection一经生成我们就不能够再增加或者删除它里面的元素了。所以在Beam的数据流水线中每次PCollection经过一个Transform之后流水线都会新创建一个PCollection出来。而这个新的PCollection又将成为下一个Transform的新输入。
<img src="https://static001.geekbang.org/resource/image/47/4b/47e4856cfdcb771c135417741d4d044b.jpg" alt="">
在上图的示例中Beam数据流水线在经过Transform1读取了输入数据集之后会创建出一个新的PCollection1而经过了Transform2之后数据流水线又会创建出新的PCollection2出来同时PCollection1不会有任何改变。也就是说在上面的例子中除去最终的输出结果数据流水线一共创建了3个不同的PCollection出来。
这种特性可以让我们在编写数据处理逻辑的时候对同一个PCollection应用多种不同的Transfrom。
例如下图所示对于PCollection1我们可以使三个不同的Transform应用在它之上从而再产生出三个不同的PCollection2、PCollection3和PCollection4出来。
<img src="https://static001.geekbang.org/resource/image/ee/ef/eeb81605c09e4a6cc684176ef0a9c9ef.jpg" alt="">
## Beam数据流水线的处理模型
在了解完Beam数据流水线高度抽象的概念后紧接着我想和你介绍一下Beam数据流水线的处理模型也就是数据流水线在运行起来之后会发生些什么它是如何处理我们定义好的PCollection和Transform的。
Beam数据流水线的底层思想其实还是动用了MapReduce的原理在分布式环境下整个数据流水线会启动N个Workers来同时处理PCollection。而在具体处理某一个特定Transform的时候数据流水线会将这个Transform的输入数据集PCollection里面的元素分割成不同的Bundle将这些Bundle分发给不同的Worker来处理。
Beam数据流水线具体会分配多少个Worker以及将一个PCollection分割成多少个Bundle都是随机的。但Beam数据流水线会尽可能地让整个处理流程达到**完美并行**Embarrassingly Parallel
我想举个几个例子让你更好地来理解这个概念。
假设在数据流水线的一个Transform里面它的输入数据集PCollection是1、2、3、4、5、6这个6个元素。数据流水线可能会将这个PCollection按下图的方式将它分割成两个Bundles。
<img src="https://static001.geekbang.org/resource/image/1e/1d/1ec163043a8e8e18928ed4771cac671d.jpg" alt="">
当然PCollection也有可能会被分割成三个Bundles。
<img src="https://static001.geekbang.org/resource/image/87/2b/87c924863790f3564949b416a98a6c2b.jpg" alt="">
那数据流水线会启用多少个Worker来处理这些Bundle呢这也是任意的。还是以刚刚的PCollection输入数据集作为例子如果PCollection被分割成了两个Bundles数据流水线有可能会分配两个Worker来处理这两个Bundles。
<img src="https://static001.geekbang.org/resource/image/32/33/32cf33cae5a581b6b5d5739bfe775533.jpg" alt="">
甚至有可能只分配一个Worker来处理这两个Bundles。
<img src="https://static001.geekbang.org/resource/image/d8/29/d8d53d23ea0d507055e003cb2e07cb29.jpg" alt="">
在多步骤的Transforms中一个Bundle通过一个Transform产生出来的结果会作为下一个Transform的输入。
之前刚刚讲过在Beam数据流水线中抽象出来的PCollection经过一个Transform之后流水线都会新创建一个PCollection出来。同样的Beam在真正运行的时候每一个Bundle在一个Worker机器里经过Transform逻辑后也会产生出来一个新的Bundle它们也是具有不可变性的。像这种具有关联性的Bundle必须在同一个Worker上面处理。
我现在来举例说明一下上面的概念。现在假设输入数据集如下图所示它被分成了两个Bundles。
<img src="https://static001.geekbang.org/resource/image/1e/1d/1ec163043a8e8e18928ed4771cac671d.jpg" alt="">
我们现在需要做两个Transforms。第一个Transform会将元素的数值减一第二个Transform会对元素的数值求平方。整个过程被分配到了两个Workers上完成。
<img src="https://static001.geekbang.org/resource/image/57/fd/574e866c6609c6551083d55ff534cffd.jpg" alt="">
过程就如上图所示总共产生了6个不可变的Bundle出来从Bundle1到Bundle3的整个过程都必须放在Worker1上完成因为它们都具有关联性。同样的从Bundle4到Bundle6的整个过程也都必须放在Worker2上完成。
## Beam数据流水线的错误处理
在学习完Beam数据流水线底层的处理模型之后你可能会有个疑问既然Bundle都是放在分布式环境下处理的要是其中一个步骤出错了那数据流水线会做什么样的处理接下来我会给你讲解一下Beam数据流水线的错误处理机制。
### 单个Transform上的错误处理
我们还是以单个Transform开始讲解。在一个Transform里面如果某一个Bundle里面的元素因为任意原因导致处理失败了则这整个Bundle里的元素都必须重新处理。
还是假设输入数据集如下图所示被分成了两个Bundles。
<img src="https://static001.geekbang.org/resource/image/32/33/32cf33cae5a581b6b5d5739bfe775533.jpg" alt="">
Beam数据流水线分配了两个Worker来处理这两个Bundles。我们看到下图中在Worker2处理Bundle2的时候最后一个元素6处理失败了。
<img src="https://static001.geekbang.org/resource/image/e4/91/e4e87019b6e646073a4234348c346091.jpg" alt="">
这个时候即便Bundle2的元素5已经完成了处理但是因为同一个Bundle里面的元素处理失败所以整个Bundle2都必须拿来重新处理。
<img src="https://static001.geekbang.org/resource/image/2c/7b/2c80f7616367535a4bae5d036d75ff7b.jpg" alt="">
重新处理的Bundle也不一定要在原来的Worker里面被处理有可能会被转移到另外的Worker里面处理。如上图所示需要重新被处理的Bundle2就被转移到Worker1上面处理了。
### 多步骤Transform上的错误处理
学习完单个Transform上的错误处理机制我们再来看看在多步骤的Transform上发生错误时是如何处理的。
在多步骤的Transform上如果处理的一个Bundle元素发生错误了则这个元素所在的整个Bundle以及与这个Bundle有关联的所有Bundle都必须重新处理。
我们还是用上面的多步骤Transform来讲解这个例子。
<img src="https://static001.geekbang.org/resource/image/93/25/939e3cf386d5ae416dd878743d98be25.jpg" alt="">
你可以看到在Worker2中处理Transform2逻辑的时候生成Bundle6里面的第一个元素失败了。因为Bundle4、Bundle5和Bundle6都是相关联的所以这三个Bundle都会被重新处理。
## 小结
今天我们一起学习了Beam里对于数据处理逻辑的高度抽象数据流水线以及它的底层处理模型。数据流水线是构建数据处理的基础掌握了它我们就可以根据自身的应用需求构建出一套数据流水线来处理数据。
## 思考题
你能根据自己的理解重述一下在Beam的数据流水线中当处理的元素发生错误时流水线的错误处理机制吗
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@@ -0,0 +1,174 @@
<audio id="audio" title="27 | Pipeline I/O: Beam数据中转的设计模式" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d8/79/d8ba9d0c0f7348f5e00b27591e0afa79.mp3"></audio>
你好,我是蔡元楠。
今天我要与你分享的主题是“Pipeline I/O: Beam数据中转的设计模式”。
在前面的章节中我们一起学习了如何使用PCollection来抽象封装数据如何使用Transform来封装我们的数据处理逻辑以及Beam是如何将数据处理高度抽象成为Pipeline来表达的就如下图所示。
<img src="https://static001.geekbang.org/resource/image/a5/94/a56f824d0dc8b3c1a777595b42c4b294.jpg" alt="">
讲到现在你有没有发现我们还缺少了两样东西没有讲没错那就是最初的输入数据集和结果数据集。那么我们最初的输入数据集是如何得到的在经过了多步骤的Transforms之后得到的结果数据集又是如何输出到目的地址的呢
事实上在Beam里我们可以用Beam的Pipeline I/O来实现这两个操作。今天我就来具体讲讲Beam的Pipeline I/O。
### 读取数据集
一个输入数据集的读取通常是通过Read Transform来完成的。Read Transform从外部源(External Source)中读取数据,这个外部源可以是本地机器上的文件,可以是数据库中的数据,也可以是云存储上面的文件对象,甚至可以是数据流上的消息数据。
Read Transform的返回值是一个PCollection这个PCollection就可以作为输入数据集应用在各种Transform上。Beam数据流水线对于用户什么时候去调用Read Transform是没有限制的我们可以在数据流水线的最开始调用它当然也可以在经过了N个步骤的Transforms后再调用它来读取另外的输入数据集。
以下的代码实例就是从filepath中读取文本。
Java
```
PCollection&lt;String&gt; inputs = p.apply(TextIO.read().from(filepath));
```
当然了Beam还支持从多个文件路径中读取数据集的功能它的文件名匹配规则和Linux系统底下的glob文件路径匹配模式是一样的使用的是“*”和“?”这样的匹配符。
我来为你举个例子解释一下假设我们正运行着一个商品交易平台这个平台会将每天的交易数据保存在一个一个特定的文件路径下文件的命名格式为YYYY-MM-DD.csv。每一个CSV文件都存储着这一天的交易数据。
现在我们想要读取某一个月份的数据来做数据处理,那我们就可以按照下面的代码实例来读取文件数据了。
Java
```
PCollection&lt;String&gt; inputs = p.apply(TextIO.read().from(&quot;filepath/.../YYYY-MM-*.csv&quot;);
```
这样做后所有满足YYYY-MM-前缀和.csv后缀的文件都会被匹配上。
当然了glob操作符的匹配规则最终还是要和你所要使用的底层文件系统挂钩的。所以在使用的时候最好要先查询好你所使用的文件系统的通配符规则。
我来举个Google Cloud Storage的例子吧。我们保存的数据还是上面讲到的商品交易平台数据我们的数据是保存在Google Cloud Storage上面并且文件路径是按照“filepath/…/YYYY/MM/DD/HH.csv”这样的格式来存放的。如果是这种情况下面这样的代码写法就无法读取到一整个月的数据了。
Java
```
PCollection&lt;String&gt; inputs = p.apply(TextIO.read().from(&quot;filepath/.../YYYY/MM/*.csv&quot;);
```
因为在Google Cloud Storage的通配符规则里面“**”只能匹配到“**”自己所在的那一层子目录而已。所以"filepath/…/YYYY/MM/*.csv"这个文件路径并不能找到“filepath/…/YYYY/MM/DD/…”这一层目录了。如果要达到我们的目标,我们就需要用到“**”的通配符,也就是如以下的写法。
Java
```
PCollection&lt;String&gt; inputs = p.apply(TextIO.read().from(&quot;filepath/.../YYYY/MM/**.csv&quot;);
```
如果你想要从不同的外部源中读取同一类型的数据来统一作为输入数据集那我们可以多次调用Read Transform来读取不同源的数据然后利用flatten操作将数据集合并示例如下。
Java
```
PCollection&lt;String&gt; input1 = p.apply(TextIO.read().from(filepath1);
PCollection&lt;String&gt; input2 = p.apply(TextIO.read().from(filepath2);
PCollection&lt;String&gt; input3 = p.apply(TextIO.read().from(filepath3);
PCollectionList&lt;String&gt; collections = PCollectionList.of(input1).and(input2).and(input3);
PCollection&lt;String&gt; inputs = collections.apply(Flatten.&lt;String&gt;pCollections());
```
### 输出数据集
将结果数据集输出到目的地址的操作是通过Write Transform来完成的。Write Transform会将结果数据集输出到外部源中。
与Read Transform相对应只要Read Transform能够支持的外部源Write Transform都是支持的。在Beam数据流水线中Write Transform可以在任意的一个步骤上将结果数据集输出。所以用户能够将多步骤的Transforms中产生的任何中间结果输出。示例代码如下。
Java
```
output.apply(TextIO.write().to(filepath));
```
需要注意的是如果你的输出是写入到文件中的话Beam默认是会写入到多个文件路径中的而用户所指定的文件名会作为实际输出文件名的前缀。
Java
```
output.apply(TextIO.write().to(filepath/output));
```
当输出结果超过一定大小的时候Beam会将输出的结果分块并写入到以“output00”“output01”等等为文件名的文件当中。如果你想将结果数据集保存成为特定的一种文件格式的话可以使用“withSuffix”这个API来指定这个文件格式。
例如如果你想将结果数据集保存成csv格式的话代码就可以这样写
Java
```
output.apply(TextIO.write().to(filepath/output).withSuffix(&quot;.csv&quot;));
```
在Beam里面Read和Write的Transform都是在名为I/O连接器I/O connector的类里面实现的。而Beam原生所支持的I/O连接器也是涵盖了大部分应用场景例如有基于文件读取输出的FileIO、TFRecordIO基于流处理的KafkaIO、PubsubIO基于数据库的JdbcIO、RedisIO等等。
当然了Beam原生的I/O连接器并不可能支持所有的外部源。比如如果我们想从Memcached中读取数据那原生的I/O连接器就不支持了。说到这里你可能会有一个疑问当我们想要从一些Beam不能原生支持的外部源中读取数据时那该怎么办呢答案很简单可以自己实现一个自定义的I/O连接器出来。
### 自定义I/O连接器
自定义的I/O连接器并不是说一定要设计得非常通用而是只要能够满足自身的应用需求就可以了。实现自定义的I/O连接器通常指的就是实现Read Transform和Write Transform这两种操作这两种操作都有各自的实现方法下面我以Java为编程语言来一一为你解释。
### 自定义读取操作
我们知道Beam可以读取无界数据集也可以读取有界数据集而读取这两种不同的数据集是有不同的实现方法的。
如果读取的是有界数据集,那我们可以有以下两种选项:
1. 使用在第25讲中介绍的两个Transform接口ParDo和GroupByKey来模拟读取数据的逻辑。
1. 继承BoundedSource抽象类来实现一个子类去实现读取逻辑。
如果读取的是无界数据集的话那我们就必须继承UnboundedSource抽象类来实现一个子类去实现读取逻辑。
无论是BoundedSource抽象类还是UnboundedSource抽象类其实它们都是继承了Source抽象类。为了能够在分布式环境下处理数据这个Source抽象类也必须是可序列化的也就是说Source抽象类必须实现Serializable这个接口。
如果我们是要读取有界数据集的话Beam官方推荐的是使用第一种方式来实现自定义读取操作也就是将读取操作看作是ParDo和GroupByKey这种多步骤Transforms。
好了下面我来带你分别看看在不同的外部源中读取数据集是如何模拟成ParDo和GroupByKey操作的。
### 从多文件路径中读取数据集
从多文件路径中读取数据集相当于用户转入一个glob文件路径我们从相应的存储系统中读取数据出来。比如说读取“filepath/**”中的所有文件数据我们可以将这个读取转换成以下的Transforms
1. 获取文件路径的ParDo从用户传入的glob文件路径中生成一个PCollection<string>的中间结果,里面每个字符串都保存着具体的一个文件路径。</string>
1. 读取数据集ParDo有了具体PCollection<string>的文件路径数据集从每个路径中读取文件内容生成一个总的PCollection保存所有数据。</string>
### 从NoSQL数据库中读取数据集
NoSQL这种外部源通常允许按照键值范围Key Range来并行读取数据集。我们可以将这个读取转换成以下的Transforms
1. 确定键值范围ParDo从用户传入的要读取数据的键值生成一个PCollection保存可以有效并行读取的键值范围。
1. 读取数据集ParDo从给定PCollection的键值范围读取相应的数据并生成一个总的PCollection保存所有数据。
### 从关系型数据库读取数据集
从传统的关系型数据库查询结果通常都是通过一个SQL Query来读取数据的。所以这个时候只需要一个ParDo在ParDo里面建立与数据库的连接并执行Query将返回的结果保存在一个PCollection里。
### 自定义输出操作
相比于读取操作输出操作会简单很多只需要在一个ParDo里面调用相应文件系统的写操作API来完成数据集的输出。
如果我们的输出数据集是需要写入到文件去的话Beam也同时提供了基于文件操作的FileBasedSink抽象类给我们来实现基于文件类型的输出操作。像很常见的TextSink类就是实现了FileBasedSink抽象类并且运用在了TextIO中的。
如果我们要自己写一个自定义的类来实现FileBasedSink的话也必须实现Serializable这个接口从而保证输出操作可以在分布式环境下运行。
同时自定义的类必须具有不可变性Immutability。怎么理解这个不可变性呢其实它指的是在这个自定义类里面如果有定义私有字段Private Field的话那它必须被声明为final。如果类里面有变量需要被修改的话那每次做的修改操作都必须先复制一份完全一样的数据出来然后再在这个新的变量上做修改。这和我们在第27讲中学习到的Bundle机制一样每次的操作都需要产生一份新的数据而原来的数据是不可变的。
## 小结
今天我们一起学习了在Beam中的一个重要概念Pipeline I/O它使得我们可以在Beam数据流水线上读取和输出数据集。同时我们还学习到了如何自定义一个I/O连接器当Beam自身提供的原生I/O连接器不能满足我们需要的特定存储系统时我们就可以自定义I/O逻辑来完成数据集的读取和输出。
## 思考题
你觉得Beam的Pipeline I/O设计能够满足我们所有的应用需求了吗
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@@ -0,0 +1,202 @@
<audio id="audio" title="28 | 如何设计创建好一个Beam Pipeline" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/86/00/8657ea20e560bd54e4283b08b9c93500.mp3"></audio>
你好,我是蔡元楠。
今天我要与你分享的主题是“如何设计创建好一个Beam Pipeline”。
这一讲我们会用到[第7讲](https://time.geekbang.org/column/article/92928)中介绍过的四种常见设计模式——复制模式、过滤模式、分离模式和合并模式。这些设计模式就像是武功的基本套路一样在实战中无处不在。今天我们就一起来看看我们怎么用Beam的Pipeline来实现这些设计模式。
## 设计Pipeline的基本考虑因素
在设计Pipeline时你需要注意4条基本的考虑因素。
### 1.输入数据存储在哪里?
输入数据是存储在云存储文件系统还是存储在一个关系型数据库里有多大的数据量这些都会影响你的pipeline设计是如何读入数据的。上一讲已经讲到过Pipeline的数据读入是使用Read这个特殊的Transform。而数据读入往往是一个Pipeline的第一个数据操作。
### 2.输入数据是什么格式?
输入数据是纯文本文件还是读取自关系型数据库的行还是结构化好的特殊数据结构这些都会影响你对于PCollection的选择。比如如果输入数据是自带key/value的结构那你用Beam的key/value为元素的PCollection能更好的表示数据。
### 3.这个pipeline你打算对数据进行哪些操作
提前想好要做哪些数据操作可以帮助你设计好Transform。可能你也会使用一些Beam提供的Transform或者是你的团队共用的一些Transform。
### 4.输出数据需要是什么样的格式,需要存储到哪里?
和输入数据相同,对于输出数据,我们也要提前判断好业务的需求。看看需要的数据格式是什么样的,是要存储到本地文本文件?还是存储到另一个数据库?
比如,你在跑一个本地批处理作业,就会需要先存到本地看一看。如果你在生成环境有永久性数据库,或者你有结构化的数据,可能更想存储到你的数据库里。
## 复制模式的Pipeline设计
现在我们就来看看在第7讲中提到的复制模式Copier Pattern的例子是怎么用Beam实现的。这里需要用到[第7讲](https://time.geekbang.org/column/article/92928)的YouTube视频平台的复制模式案例。这里就简单介绍一下以便唤醒你的记忆。如果你完全忘记了我建议你先去做个回顾。
如今的视频平台会提供不同分辨率的视频给不同网络带宽的用户。在YouTube视频平台中将鼠标放在视频缩略图上时它会自动播放一段已经生成好的动画缩略图。平台的自然语言理解NLP的数据处理模块可以分析视频数据自动生成视频字幕。视频分析的数据处理模块也可以通过分析视频数据产生更好的内容推荐系统。这使用的就是复制模式。
要想在在Beam中采用复制模式我们可以用一个PCollection来表示输入的Video data set。将每一种视频处理编写成Transform。最后多重输出各自为一个PCollection。整个过程就如同下图所示。
<img src="https://static001.geekbang.org/resource/image/b2/73/b226e637e8cba5f7c3ef938684526373.jpg" alt="">
你可以从图片中看到在这个工作流系统中每个数据处理模块的输入都是相同的而下面的5个数据处理模块都可以单独并且同步地运行处理。
复制模式通常是将单个数据处理模块中的数据完整地复制到两个或更多的数据处理模块中,然后再由不同的数据处理模块进行处理。当我们在处理大规模数据时,需要对同一个数据集采取多种不同的数据处理转换,我们就可以优先考虑采用复制模式。
比如下面的代码我们用5个不同的pipeline来表示它们的作用分别是生成高画质视频、生成低画质视频、生成GIF动画、生成视频字幕、分析视频。
```
PCollection&lt;Video&gt; videoDataCollection = ...;
// 生成高画质视频
PCollection&lt;Video&gt; highResolutionVideoCollection = videoDataCollection.apply(&quot;highResolutionTransform&quot;, ParDo.of(new DoFn&lt;Video, Video&gt;(){
@ProcessElement
public void processElement(ProcessContext c) {
c.output(generateHighResolution(c.element()));
}
}));
// 生成低画质视频
PCollection&lt;Video&gt; lowResolutionVideoCollection = videoDataCollection.apply(&quot;lowResolutionTransform&quot;, ParDo.of(new DoFn&lt;Video, Video&gt;(){
@ProcessElement
public void processElement(ProcessContext c) {
c.output(generateLowResolution(c.element()));
}
}));
// 生成GIF动画
PCollection&lt;Image&gt; gifCollection = videoDataCollection.apply(&quot;gifTransform&quot;, ParDo.of(new DoFn&lt;Video, Image&gt;(){
@ProcessElement
public void processElement(ProcessContext c) {
c.output(generateGIF(c.element()));
}
}));
// 生成视频字幕
PCollection&lt;Caption&gt; captionCollection = videoDataCollection.apply(&quot;captionTransform&quot;, ParDo.of(new DoFn&lt;Video, Caption&gt;(){
@ProcessElement
public void processElement(ProcessContext c) {
c.output(generateCaption(c.element()));
}
}));
// 分析视频
PCollection&lt;Report&gt; videoAnalysisCollection = videoDataCollection.apply(&quot;videoAnalysisTransform&quot;, ParDo.of(new DoFn&lt;Video, Report&gt;(){
@ProcessElement
public void processElement(ProcessContext c) {
c.output(analyzeVideo(c.element()));
}
}));
```
## 过滤模式的Pipeline设计
过滤模式Filter Pattern也可以用Beam来实现。这里我们先简单回顾一下[第7讲](https://time.geekbang.org/column/article/92928)的例子。在商城会员系统中,系统根据用户的消费次数、消费金额、注册时间划分用户等级。假设现在商城有五星、金牌和钻石这三种会员。而系统现在打算通过邮件对钻石会员发出钻石会员活动的邀请。
<img src="https://static001.geekbang.org/resource/image/47/0f/47498fc9b2d41c59ffb286d84c4f220f.jpg" alt="">
在过滤模式中,一个数据处理模块会将输入的数据集过滤,留下符合条件的数据,然后传输到下一个数据处理模块进行单独处理。
在用Beam实现时我们把输入的用户群组表达成一个PCollection。输出的钻石会员用户群组也表示成一个PCollection。那么中间的过滤步骤就能编写成一个Transform。如下面代码所示我们在一个Beam Pipeline里调用isDiamondUser()方法,从所有的用户中过滤出钻石会员。
```
PCollection&lt;User&gt; userCollection = ...;
PCollection&lt;User&gt; diamondUserCollection = userCollection.apply(&quot;filterDiamondUserTransform&quot;, ParDo.of(new DoFn&lt;User, User&gt;(){
@ProcessElement
public void processElement(ProcessContext c) {
if (isDiamondUser(c.element()) {
c.output(c.element());
}
}
}));
PCollection&lt;User&gt; notifiedUserCollection = userCollection.apply(&quot;notifyUserTransform&quot;, ParDo.of(new DoFn&lt;User, User&gt;(){
@ProcessElement
public void processElement(ProcessContext c) {
if (notifyUser(c.element()) {
c.output(c.element());
}
}
}));
```
## 分离模式的Pipeline设计
分离模式Splitter Pattern与过滤模式不同并不会丢弃里面的任何数据而是将数据分组处理。还是以商城会员系统为例。系统打算通过邮件对不同会员发出与他们身份相应的活动邀请。需要通过**分离模式**将用户按照会员等级分组,然后发送相应的活动内容。
<img src="https://static001.geekbang.org/resource/image/c5/85/c5d84c2aab2e02cc6e1d2e9f7c40e185.jpg" alt="">
用Beam应该怎么实现呢我们可以应用[第25讲](https://time.geekbang.org/column/article/101735)中讲到的side input/output技术。同样的还是把用户群组都定义成不同的PCollection。最终的输出会是三个PCollection。
```
// 首先定义每一个output的tag
final TupleTag&lt;User&gt; fiveStarMembershipTag = new TupleTag&lt;User&gt;(){};
final TupleTag&lt;User&gt; goldenMembershipTag = new TupleTag&lt;User&gt;(){};
final TupleTag&lt;User&gt; diamondMembershipTag = new TupleTag&lt;User&gt;(){};
PCollection&lt;User&gt; userCollection = ...;
PCollectionTuple mixedCollection =
userCollection.apply(ParDo
.of(new DoFn&lt;User, User&gt;() {
@ProcessElement
public void processElement(ProcessContext c) {
if (isFiveStartMember(c.element())) {
c.output(c.element());
} else if (isGoldenMember(c.element())) {
c.output(goldenMembershipTag, c.element());
} else if (isDiamondMember(c.element())) {
c.output(diamondMembershipTag, c.element());
}
}
})
.withOutputTags(fiveStarMembershipTag,
TupleTagList.of(goldenMembershipTag).and(diamondMembershipTag)));
// 分离出不同的用户群组
mixedCollection.get(fiveStarMembershipTag).apply(...);
mixedCollection.get(goldenMembershipTag).apply(...);
mixedCollection.get(diamondMembershipTag).apply(...);
```
比如在上面的代码中我们在processElement()方法中根据过滤函数分拆出五星会员金牌会员和钻石会员。并且把不同的会员等级输出到不同的side output tag中。之后可以在返回的PCollection中用这个side output tag得到想要的输出。
## 合并模式的Pipeline设计
合并模式Joiner Pattern会将多个不同的数据集合成一个总数据集一并进行处理。之前介绍的合并模式案例是用街头美团外卖电动车的数量来预测美团的股价。
数据接入这一处理模块里,我们的输入数据有自己团队在街道上拍摄到的美团外卖电动车图片和第三方公司提供的美团外卖电动车图片。我们需要先整合所有数据然后进行其它数据处理。
<img src="https://static001.geekbang.org/resource/image/1c/ed/1c4bc9aaebc908633da174ba847999ed.jpg" alt="">
使用Beam合并多个PCollection时需要用到Beam自带的Flatten这个Transform函数它的作用是把来自多个PCollection类型一致的元素融合到一个PCollection中去。下面的代码用元素类型为Image的PCollection来表达输入数据和输出数据。
```
PCollectionList&lt;Image&gt; collectionList = PCollectionList.of(internalImages).and(thirdPartyImages);
PCollection&lt;Image&gt; mergedCollectionWithFlatten = collectionList
.apply(Flatten.&lt;Image&gt;pCollections());
mergedCollectionWithFlatten.apply(...);
```
例如在上面的代码示例中我们把internalImages和thirdPartyImages两个PCollection融合到一起。使用apply(Flatten)这样一个Transform实现多个PCollection的平展。
## 小结
今天我们一起学习了怎样在Beam中设计实现第7讲介绍的经典数据处理模式分别是4种设计模式分别是复制模式、过滤模式、分离模式和合并模式。
在实现这四种数据处理模式的过程中我们学到了两种Beam Transform的两个重要技术分别是分离模式中用到的side output和在合并模式中用到的Flatten。正如前文所说第7讲的经典数据处理模式就像是武功的基本套路实战项目中可能80%都是这些基本套路的组合。有了这些小型的模块实现,对我们未来实现大型系统是有很大帮助的。
## 思考题
在你的项目中有没有这四种设计模式的影子呢如果有的话你觉得可以怎样用Beam Pipeline实现呢
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。

View File

@@ -0,0 +1,250 @@
<audio id="audio" title="29 | 如何测试Beam Pipeline" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/74/8e/74237893924c1f23f1b7ae6c574a198e.mp3"></audio>
你好,我是蔡元楠。
今天我要与你分享的主题是“如何测试Beam Pipeline”。
在上一讲中我们结合了第7讲的内容一起学习了在Beam的世界中我们该怎么设计好对应的设计模式。而在今天这一讲中我想要讲讲在日常开发中经常会被忽略的但是又非常重要的一个开发环节——测试。
你知道我们设计好的Beam数据流水线通常都会被放在分布式环境下执行具体每一步的Transform都会被分配到任意的机器上面执行。如果我们在运行数据流水线时发现结果出错了那么想要定位到具体的机器再到上面去做调试是不现实的。
当然还有另一种方法,读取一些样本数据集,再运行整个数据流水线去验证哪一步逻辑出错了。但这是一项非常耗时耗力的工作。即便我们可以把样本数据集定义得非常小,从而缩短运行数据流水线运行所需的时间。但是万一我们所写的是多步骤数据流水线的话,就不知道到底在哪一步出错了,我们必须把每一步的中间结果输出出来进行调试。
基于以上种种的原因,在我们正式将数据流水线放在分布式环境上面运行之前,先完整地测试好整个数据流水线逻辑,就变得尤为重要了。
为了解决这些问题Beam提供了一套完整的测试SDK。让我们可以在开发数据流水线的同时能够实现对一个Transform逻辑的单元测试也可以对整个数据流水线端到端End-to-End地测试。
在Beam所支持的各种Runners当中有一个Runner叫作DirectRunner。DirectRunner其实就是我们的本地机器。也就是说如果我们指定Beam的Runner为DirectRunner的话整个Beam数据流水线都会放在本地机器上面运行。我们在运行测试程序的时候可以利用这个DirectRunner来跑测试逻辑。
在正式讲解之前有一点是我需要提醒你的。如果你喜欢自行阅读Beam的相关技术文章或者是示例代码的话可能你会看见一些测试代码使用了在Beam SDK中的一个测试类叫作DoFnTester来进行单元测试。这个DoFnTester类可以让我们传入一个用户自定义的函数User Defined Function/UDF来进行测试。
通过[第25讲](https://time.geekbang.org/column/article/101735)的内容我们已经知道一个最简单的Transform可以用一个ParDo来表示在使用它的时候我们需要继承DoFn这个抽象类。这个DoFnTester接收的对象就是我们继承实现的DoFn。在这里我们把一个DoFn看作是一个单元来进行测试了。但这并不是Beam所提倡的。
因为在Beam中数据转换的逻辑都是被抽象成Transform而不是Transform里面的ParDo这些具体的实现。**每个Runner具体怎么运行这些ParDo****对于用户来说应该都是透明的。**所以在Beam的2.4.0版本之后Beam SDK将这个类标记成了Deprecated转而推荐使用Beam SDK中的TestPipeline。
所以我在这里也建议你在写测试代码的时候不要使用任何和DoFnTester有关的SDK。
## Beam的Transform单元测试
说完了注意事项那事不宜迟我们就先从一个Transform的单元测试开始看看在Beam是如何做测试的以下所有的测试示例代码都是以Java为编程语言来讲解
一般来说Transform的单元测试可以通过以下五步来完成
1. 创建一个Beam测试SDK中所提供的TestPipeline实例。
1. 创建一个静态Static的、用于测试的输入数据集。
1. 使用Create Transform来创建一个PCollection作为输入数据集。
1. 在测试数据集上调用我们需要测试的Transform上并将结果保存在一个PCollection上。
1. 使用PAssert类的相关函数来验证输出的PCollection是否是我所期望的结果。
假设我们要处理的数据集是一个整数集合处理逻辑是过滤掉数据集中的奇数将输入数据集中的偶数输出。为此我们通过继承DoFn类来实现一个产生偶数的Transform它的输入和输出数据类型都是Integer。
Java
```
static class EvenNumberFn extends DoFn&lt;Integer, Integer&gt; {
@ProcessElement
public void processElement(@Element Integer in, OutputReceiver&lt;Integer&gt; out) {
if (in % 2 == 0) {
out.output(in);
}
}
}
```
那我们接下来就根据上面所讲的测试流程测试这个EvenNumerFn Transform来一步步创建我们的单元测试。
### 创建TestPipeline
第一步创建TestPipeline。创建一个TestPipeline实例的代码非常简单示例如下
Java
```
...
Pipeline p = TestPipeline.create();
...
```
如果你还记得在[第26讲](https://time.geekbang.org/column/article/102182)中如何创建数据流水线的话可以发现TestPipeline实例的创建其实不用给这个TestPipeline定义选项Options。因为TestPipeline中create函数已经在内部帮我们创建好一个测试用的Options了。
### 创建静态输入数据集
Java
```
...
static final List&lt;Integer&gt; INPUTS = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
...
```
第二步创建静态的输入数据集。创建静态的输入数据集的操作就和我们平时所写的普通Java代码一样在示例中我调用了Arrays类的asList接口来创建一个拥有10个整数的数据集。
### 使用Create Transform创建PCollection
在创建完静态数据集后我们进入第三步创建一个PCollection作为输入数据集。在Beam原生支持的Transform里面有一种叫作Create Transform我们可以利用这个Create Transform将Java Collection的数据转换成为Beam的数据抽象PCollection具体的做法如下
Java
```
...
PCollection&lt;Integer&gt; input = p.apply(Create.of(INPUTS)).setCoder(VarIntCoder.of());
...
```
### 调用Transform处理逻辑
第四步调用Transform处理逻辑。有了数据抽象PCollection我们就需要在测试数据集上调用我们需要测试的Transform处理逻辑并将结果保存在一个PCollection上。
Java
```
...
PCollection&lt;String&gt; output = input.apply(ParDo.of(new EvenNumberFn()));
...
```
根据[第25讲](https://time.geekbang.org/column/article/101735)的内容我们只需要在这个输入数据集上调用apply抽象函数生成一个需要测试的Transform并且传入apply函数中就可以了。
### 验证输出结果
第五步验证输出结果。在验证结果的阶段我们需要调用PAssert类中的函数来验证输出结果是否和我们期望的一致示例如下。
Java
```
...
PAssert.that(output).containsInAnyOrder(2, 4, 6, 8, 10);
...
```
完成了所有的步骤我们就差运行这个测试的数据流水线了。很简单就是调用TestPipeline的run函数整个Transform的单元测试示例如下
Java
```
final class TestClass {
static final List&lt;Integer&gt; INPUTS = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
public void testFn() {
Pipeline p = TestPipeline.create();
PCollection&lt;Integer&gt; input = p.apply(Create.of(INPUTS)).setCoder(VarIntCoder.of());
PCollection&lt;String&gt; output = input.apply(ParDo.of(new EvenNumberFn()));
PAssert.that(output).containsInAnyOrder(2, 4, 6, 8, 10);
p.run();
}
}
```
有一点需要注意的是TestPipeline的run函数是在单元测试的结尾处调用的PAssert的调用必须在TestPipeliner调用run函数之前调用。
## Beam的端到端测试
在一般的现实应用中,我们设计的都是多步骤数据流水线,就拿我在[第一讲](https://time.geekbang.org/column/article/90081)中举到的处理美团外卖电动车的图片为例子,其中就涉及到了多个输入数据集,而结果也有可能会根据实际情况有多个输出。
所以我们在做测试的时候往往希望能有一个端到端的测试。在Beam中端到端的测试和Transform的单元测试非常相似。唯一的不同点在于我们要为所有的输入数据集创建测试数据集而不是只针对某一个Transform来创建。对于在数据流水线的每一个应用到Write Transfrom的地方我们都需要用到PAssert类来验证输出数据集。
所以,端到端测试的步骤也分五步,具体内容如下:
1. 创建一个Beam测试SDK中所提供的TestPipeline实例。
1. 对于多步骤数据流水线中的每个输入数据源创建相对应的静态Static测试数据集。
1. 使用Create Transform将所有的这些静态测试数据集转换成PCollection作为输入数据集。
1. 按照真实数据流水线逻辑调用所有的Transforms操作。
1. 在数据流水线中所有应用到Write Transform的地方都使用PAssert来替换这个Write Transform并且验证输出的结果是否我们期望的结果相匹配。
为了方便说明我们就在之前的例子中多加一步Transform和一个输出操作来解释如何写端到端测试。假设我们要处理数据集是一个整数集合处理逻辑是过滤掉奇数将输入数据集中的偶数转换成字符串输出。同时我们也希望对这些偶数求和并将结果输出示例如下
Java
```
final class Foo {
static class EvenNumberFn extends DoFn&lt;Integer, Integer&gt; {
@ProcessElement
public void processElement(@Element Integer in, OutputReceiver&lt;Integer&gt; out) {
if (in % 2 == 0) {
out.output(in);
}
}
}
static class ParseIntFn extends DoFn&lt;String, Integer&gt; {
@ProcessElement
public void processElement(@Element String in, OutputReceiver&lt;Integer&gt; out) {
out.output(Integer.parseInt(in));
}
}
public static void main(String[] args) {
PipelineOptions options = PipelineOptionsFactory.create();
Pipeline p = Pipeline.create(options);
PCollection&lt;Integer&gt; input = p.apply(TextIO.read().from(&quot;filepath/input&quot;)).apply(ParDo.of(new ParseIntFn()));
PCollection&lt;Integer&gt; output1 = input.apply(ParDo.of(new EvenNumberFn()));
output1.apply(ToString.elements()).apply(TextIO.write().to(&quot;filepath/evenNumbers&quot;));
PCollection&lt;Integer&gt; sum = output1.apply(Combine.globally(new SumInts()));
sum.apply(ToString.elements()).apply(TextIO.write().to(&quot;filepath/sum&quot;));
p.run();
}
}
```
从上面的示例代码中你可以看到我们从一个外部源读取了一系列输入数据进来将它转换成了整数集合。同时将我们自己编写的EvenNumberFn Transform应用在了这个输入数据集上。得到了所有偶数集合之后我们先将这个中间结果输出然后再针对这个偶数集合求和最后将这个结果输出。
整个数据流水线总共有一次对外部数据源的读取和两次的输出我们按照端到端测试的步骤为所有的输入数据集创建静态数据然后将所有有输出的地方都使用PAssert类来进行验证。整个测试程序如下所示
Java
```
final class TestClass {
static final List&lt;String&gt; INPUTS =
Arrays.asList(&quot;1&quot;, &quot;2&quot;, &quot;3&quot;, &quot;4&quot;, &quot;5&quot;, &quot;6&quot;, &quot;7&quot;, &quot;8&quot;, &quot;9&quot;, &quot;10&quot;);
static class EvenNumberFn extends DoFn&lt;Integer, Integer&gt; {
@ProcessElement
public void processElement(@Element Integer in, OutputReceiver&lt;Integer&gt; out) {
if (in % 2 == 0) {
out.output(in);
}
}
}
static class ParseIntFn extends DoFn&lt;String, Integer&gt; {
@ProcessElement
public void processElement(@Element String in, OutputReceiver&lt;Integer&gt; out) {
out.output(Integer.parseInt(in));
}
}
public void testFn() {
Pipeline p = TestPipeline.create();
PCollection&lt;String&gt; input = p.apply(Create.of(INPUTS)).setCoder(StringUtf8Coder.of());
PCollection&lt;Integer&gt; output1 = input.apply(ParDo.of(new ParseIntFn())).apply(ParDo.of(new EvenNumberFn()));
PAssert.that(output1).containsInAnyOrder(2, 4, 6, 8, 10);
PCollection&lt;Integer&gt; sum = output1.apply(Combine.globally(new SumInts()));
PAssert.that(sum).is(30);
p.run();
}
}
```
在上面的示例代码中我们用TestPipeline替换了原来的Pipeline创建了一个静态输入数据集并用Create Transform转换成了PCollection最后将所有用到Write Transform的地方都用PAssert替换掉来验证输出结果是否是我们期望的结果。
## 小结
今天我们一起学习了在Beam中写编写测试逻辑的两种方式分别是针对一个Transform的单元测试和针对整个数据流水线的端到端测试。Beam提供的SDK能够让我们不需要在分布式环境下运行程序而是本地机器上运行。测试在整个开发环节中是非常的一环我强烈建议你在正式上线自己的业务逻辑之前先对此有一个完整的测试。
## 思考题
如果让你来利用Beam SDK来测试你日常处理的数据逻辑你会如何编写测试呢
欢迎你把答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。